code-freeze
How to Freeze Deployments in GitHub (Step-by-Step)
Freezing deployments in GitHub requires two controls: required status checks to block merges and deployment protection rules to block releases. Here is how to set up both.
Freezing deployments in GitHub requires two separate controls working together: a required status check to block merges into protected branches, and a deployment protection rule to block releases to your environments. Neither one alone covers the full picture. Together, they give you a real freeze.
This guide walks through both, explains why each is necessary, and covers where the native approach breaks down for teams that need scheduling, scoping, or structured override paths.
Why you need two controls, not one
GitHub deployments and GitHub merges are separate events. A deployment can happen without a merge (you can deploy an existing commit), and a merge does not automatically trigger a deployment. Most teams conflate them because in a standard CI/CD pipeline, merging to main kicks off a deployment automatically. But the controls sit at different points in that pipeline.
- Merge blocking stops new code from entering your protected branch. It lives in branch protection rules, enforced through required status checks.
- Deployment blocking stops whatever is in the branch from shipping to an environment. It lives in GitHub deployment protection rules, enforced through environment settings.
If you only block merges, someone can still manually trigger a deployment from an existing commit. If you only block deployments, code can keep accumulating in main while the freeze is active, and every queued change ships the moment the freeze lifts, which creates its own risk. A complete freeze covers both.
Step 1: Block merges with a required status check
GitHub's required status check mechanism is the right lever here. When a status check is marked as required on a branch, GitHub will not allow a merge until that check passes. A freeze tool works by controlling that check: during a freeze, it posts a failing status. During normal operation, it posts a passing status.
To set this up natively:
- Go to your repository's Settings > Branches.
- Under "Branch protection rules," click Add rule (or edit an existing rule for your default branch, usually
main). - Enable "Require status checks to pass before merging."
- Search for the status check context your freeze tool or CI job will post. If you have not set one up yet, you need a check that posts
failurewhen a freeze is active andsuccessotherwise. - Enable "Do not allow bypassing the above settings" if you want the freeze to apply to admins too.
The key detail: the status check must be tied to a real context string that a service actually posts. If no service posts to that context, GitHub will not enforce it. This is where native DIY approaches often fail: someone sets up the rule but nothing ever posts the status, so the check is silently skipped.
Step 2: Block deployments with a protection rule
GitHub's environment protection rules let you add reviewers, wait timers, and custom deployment protection rules to any named environment (such as production, staging, or us-east-1).
To require manual review before each deployment to production:
- Go to Settings > Environments.
- Select or create the environment you want to protect.
- Under "Deployment protection rules," enable "Required reviewers" and add the people or teams who must approve a deployment before it proceeds.
This gives you human-gated deployments. During a freeze, the reviewer simply declines any pending deployment requests. When the freeze lifts, they approve.
For automated blocking (no manual approval needed, just a hard block), you can write a GitHub Action that posts a custom deployment status, or use an app that integrates with the environment protection rule API. GitHub also supports apps that act as deployment protection rule providers, which means the freeze tool can answer "approve or reject?" automatically based on whether a freeze is active.
Step 3: Put them together for a full freeze
A complete freeze looks like this in GitHub's data model:
| What you want to stop | GitHub mechanism | Where it's configured |
|---|---|---|
Merges into main | Required status check (failing during freeze) | Branch protection rule, per-repository |
Deployments to production | Deployment protection rule (reviewer or custom app) | Environment settings, per-environment |
Both rules are repository-level settings, so you configure them per-repository. If you have 30 repositories, that means 30 sets of rules to toggle when a freeze starts and 30 to untoggle when it ends. Manual.
What the native approach can and cannot do
GitHub's native tools enforce the right constraints at the right layer. They are reliable and do not require third-party code to read your source. But they are stateless. GitHub does not have a concept of a freeze window. It has branch protection rules and environment rules, which are always on or always off until someone changes them.
That means every freeze manually involves:
- Updating branch protection rules in each affected repository (or toggling the status check context)
- Configuring environment protection rules for each environment
- Reversing everything when the freeze ends
- No scheduling, no recurring windows, no scoped rules that apply to only some repositories
- No structured override path (someone with admin rights can bypass, but there is no approval workflow or record)
For a small team with one repository, this is manageable. For an org with dozens of repositories and a holiday freeze that happens every December, it becomes a coordination problem.
How to schedule and automate the freeze
If you want the freeze to start and end automatically, you need something that can post status checks and manage deployment rules on a schedule.
One approach is a GitHub Action with a cron trigger that posts a failing status check to all relevant repositories when the freeze window opens, and a passing status when it closes. This works for simple date ranges. It does not easily handle per-repository scoping, override requests, or mid-freeze emergency patches.
For more structured needs, a dedicated tool handles this at the platform level. NoShip is a GitHub App that blocks merges via required status checks and blocks deployments via GitHub's deployment protection rule API. You define a freeze window once (with an RRULE for recurring freezes), specify which repositories and environments are in scope, and the tool manages the status checks and deployment rules automatically. Emergency overrides go through a dual-approval workflow and land in an audit trail. NoShip never requests access to your source code, so it can govern merges and deployments without reading what you ship.
The NoShip concepts guide covers how freeze windows, rules, and environments map to GitHub's enforcement mechanisms in detail.
Which approach is right for your team?
| Team profile | Best approach |
|---|---|
| Small team, 1-3 repos, infrequent freezes | Native branch protection + environment rules, toggled manually |
| Mid-size team, recurring freezes (holidays, Fridays) | Scheduled GitHub Action or a dedicated freeze tool |
| Larger org, multiple repos, compliance requirements | Dedicated tool with scheduling, scoping, override workflow, and audit trail |
The deciding factor is usually the number of repositories and how often you freeze. A single repository and one planned freeze per year: native GitHub is fine. Ten repositories and a policy of freezing every December plus any active incident: the manual overhead compounds quickly and the risk of forgetting to toggle something becomes real.
A note on deployment protection rules vs. required status checks
These two mechanisms are often confused because they both block things in GitHub. The difference:
- Required status checks live on branch protection rules and block the merge event. They work at the source: code cannot enter the branch.
- Deployment protection rules live on environments and block the deployment event. They work at the output: code in the branch cannot reach the environment.
Both can be bypassed by a repository admin unless you explicitly disable the bypass option. If your freeze needs to hold even for admins, make sure the "Do not allow bypassing" option is enabled on your branch protection rule, and configure your environment rule to require approval from a human who is not the person requesting the deploy.
A freeze that blocks merges but not deployments, or deployments but not merges, is a partial freeze. Both enforcement surfaces are necessary for a complete one. If you are reviewing your current setup, the code freeze tools comparison covers how different approaches handle each surface.
Keep reading
Code Freeze vs Change Freeze vs Deployment Freeze
Code freeze, change freeze, and deployment freeze each stop something different. Here is how to tell them apart and choose the right one for your situation.
Merge Freeze Alternatives for GitHub Teams
The best Merge Freeze alternatives for GitHub, compared by enforcement depth, scheduling, override workflows, and migration path from Merge Freeze.
Best Code Freeze Tools for GitHub (2026)
Compare the four main approaches to code freezes on GitHub: branch protection, rulesets, Merge Freeze, and NoShip. Find the right fit for your team.