Freezing deploys with branch protection Jump to heading

Deploy freezes happen for good reasons: a peak trading weekend, a major customer migration, an end-of-year period when on-call cover is thin. The usual implementation is a message in the team channel asking people not to merge, which works until someone misses it and their merge auto-deploys on the busiest day of the year. A freeze is far more reliable when the repository enforces it: a required check that fails while a freeze is active, or a ruleset that blocks merges to the deploy branch, with a documented route for approved emergency fixes. This page sets up an enforceable freeze, an exception path, announcements and a clean lift, within environment and deployment branches.

When to use this approach Jump to heading

  • Merges to main or a deploy branch deploy automatically, and you need to stop that for a period.
  • Past freezes were announced but broken by merges from people who missed the message.
  • Emergency fixes must still be possible during a freeze, with approval.
  • You run scheduled releases, as in running a scheduled release train.

Step 1 — Decide what the freeze blocks Jump to heading

Be explicit. A freeze can block merges to the deploy branch, block deployments while allowing merges, or both. Blocking deployments only lets work continue integrating on main while production stays still, which reduces the pile-up when the freeze lifts.

Freezing merges versus freezing deploymentsFreezing merges stops changes reaching the deploy branch, which is simple but lets unmerged work pile up. Freezing deployments lets merges continue while the deploy job refuses to run, so integration continues and only production stays still. Many teams freeze deployments and keep merging.Freeze mergesFreeze deploymentswork keeps integratingnoyesbacklog when liftedlargeonly the deployenforcement pointbranch rules / checkdeploy job / environmentrisk of driftbranches agenone extrafreeze the thing that is actually risky — usually production, not main

Step 2 — Store the freeze state in one place Jump to heading

Keep the freeze window in a file in the repository or a repository variable, so checks, deploy jobs and announcements read the same value.

# Repository variables read by workflows
gh variable set DEPLOY_FREEZE_START --body "2026-11-26T18:00:00Z"
gh variable set DEPLOY_FREEZE_END   --body "2026-12-01T08:00:00Z"
gh variable set DEPLOY_FREEZE_REASON --body "Peak trading weekend"

Step 3 — Enforce it with a required check or deploy guard Jump to heading

Add a job that fails during the window. Make it a required status check to block merges, or a step at the start of the deploy job to block deployments.

jobs:
  freeze-check:
    runs-on: ubuntu-latest
    steps:
      - name: Fail during a deploy freeze unless an exception is approved
        env:
          START: ${{ vars.DEPLOY_FREEZE_START }}
          END: ${{ vars.DEPLOY_FREEZE_END }}
          REASON: ${{ vars.DEPLOY_FREEZE_REASON }}
          EXCEPTION: ${{ contains(github.event.pull_request.labels.*.name, 'freeze-exception') }}
        run: |
          now=$(date -u +%s); s=$(date -u -d "$START" +%s 2>/dev/null || echo 0); e=$(date -u -d "$END" +%s 2>/dev/null || echo 0)
          if [ "$now" -ge "$s" ] && [ "$now" -lt "$e" ] && [ "$EXCEPTION" != true ]; then
            echo "::error::Deploy freeze until $END: $REASON. Add the freeze-exception label with approval to proceed."
            exit 1
          fi

Re-run the check when the label changes by adding labeled and unlabeled to the pull request trigger types. The required-check mechanics are covered in merge queues and required checks.

Step 4 — Define the exception path Jump to heading

Emergency fixes must still be possible. Require an approval from a named group to apply the exception label, and record why.

# Who may apply the label: restrict via a ruleset requiring review from @org/release-managers
gh pr edit 1234 --add-label freeze-exception
gh pr comment 1234 --body "Freeze exception approved by @release-manager: fixes checkout failures for EU customers (INC-4821)."

On deploy-job freezes, use a protected environment with required reviewers instead: the deploy waits for an approval from the release managers.

Can this change go out during the freeze?If it fixes an active incident affecting customers, it can go out with a release manager's approval and the exception label. If it is a fix for a known issue that is not causing harm now, it waits for the freeze to lift. Anything else waits, though it can still be reviewed and merged if only deployments are frozen.What is the change?fixes an active incidentExceptionapproved + labelledfix, no current harmWaitdeploy after the liftfeature / routineWaitmerge if only deploys frozenthe exception path should be used a handful of times per freeze, not dozens

Step 5 — Announce the freeze from the same data Jump to heading

Generate the announcement and a banner from the stored window, so the message never disagrees with the enforcement.

start=$(gh variable get DEPLOY_FREEZE_START); end=$(gh variable get DEPLOY_FREEZE_END); why=$(gh variable get DEPLOY_FREEZE_REASON)
printf 'Deploy freeze: %s → %s (UTC). Reason: %s.\nEmergency fixes: add the freeze-exception label and get release-manager approval.\n' "$start" "$end" "$why"

Post it a week ahead, the day before, and when the freeze starts.

Step 6 — Lift the freeze deliberately Jump to heading

When the freeze ends, do not release everything at once. Deploy the accumulated changes in small batches with monitoring between them, starting with low-risk ones.

git log --oneline --first-parent "$(git describe --tags --abbrev=0 --match 'deploy-*')..origin/main" | wc -l   # changes waiting
gh variable delete DEPLOY_FREEZE_START; gh variable delete DEPLOY_FREEZE_END

Rolling back a problem batch is covered in rolling back a deployment with Git.

A deploy freeze from announcement to liftThe freeze window is set and announced a week ahead. The day before, a reminder goes out. The freeze starts and the check blocks deploys except approved exceptions. When it ends, the variables are removed and the backlog is deployed in small batches.Window setannounced−7 daysReminderexception path restated−1 dayFreeze activecheck failsstartVariables removedfreeze liftedendBacklog deployedin small batches+1 daythe lift is a release in its own right — plan it

Step 7 — Review exceptions afterwards Jump to heading

After each freeze, list the exceptions granted and why. Many exceptions for the same kind of change point to something that should be fixed before the next freeze.

gh pr list --state merged --label freeze-exception --search "merged:2026-11-26..2026-12-01" --json number,title,mergedAt \
  --jq '.[] | "\(.mergedAt[:10]) #\(.number) \(.title)"'

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why not just disable the deploy workflow? Jump to heading

Disabling it blocks emergency fixes too, and depends on someone remembering to re-enable it. A guard with an exception path keeps the deploy available for what matters.

Should hotfix branches bypass the freeze? Jump to heading

They should take the exception path like everything else. A separate bypass for hotfix branches tends to become the way everything ships during freezes.

How long should a freeze be? Jump to heading

As short as the risk allows. Long freezes build large backlogs, and large backlogs make the lift itself risky.