Playbook: Ship one isolated feature/fix straight to main, bypassing a stale rc

Symptom: A feature or fix is fully merged and tested on dev (and usually staging), the user wants it live in production now, but the normal dev → staging → rc → main pipeline would also drag along everything else currently sitting in rc — which can be a large, unrelated, not-necessarily-QA’d backlog (observed: rc was 484 commits / 20+ open issues ahead of main on et-operational-platform, 2026-08-07, completely unrelated to the feature being shipped).

When this pattern applies:

  • The target change is genuinely self-contained (doesn’t functionally depend on the unshipped rc backlog).
  • The user has explicitly chosen “patch, not full release” after being shown the actual gap size — do not default to this path silently; always show git log origin/rc..origin/staging --oneline | wc -l and git log origin/main..origin/rc --oneline | wc -l first and let the human decide. This is a deliberate deviation from the normal release gate, not a shortcut to take unprompted.
  • Matches this repo’s own documented allowance: “Hotfixy krytyczne (produkcja) mogą iść bezpośrednio na main i staging jako cherry-pick.”

Step-by-step

0. Confirm scope and get commit SHAs

git fetch origin main staging rc
git log origin/staging..origin/dev --oneline | wc -l    # small = staging mostly current
git log origin/rc..origin/staging --oneline | wc -l      # gap staging→rc
git log origin/main..origin/rc --oneline | wc -l          # gap rc→main — the one that matters here
gh pr view <PR#> --repo <owner>/<repo> --json mergeCommit --jq '.mergeCommit.oid'

Collect every merge-commit SHA that makes up the feature (usually one per squash-merged PR, in chronological order).

1. Isolated worktree from main

git worktree add tmp/wt-hotfix-<slug> -b hotfix/<slug> origin/main
cd tmp/wt-hotfix-<slug>
git fetch origin dev   # objects for the commits you're about to cherry-pick
git cherry-pick <sha1> <sha2> ...   # one at a time if you expect conflicts; git stops at the first

2. Resolve conflicts — keep ONLY the target feature

main is very likely also missing other features that happen to sit in the same diff hunks as yours, because they all landed on dev around the same time and the cherry-picked commit’s diff carries whatever context lines were adjacent at authorship time. This is the single most error-prone step.

For every conflict:

  1. Read both sides of the <<<<<<< HEAD / ======= / >>>>>>> markers.
  2. Keep HEAD (main’s existing content) as the base.
  3. From the incoming side, add back only the lines that belong to your target feature.
  4. Drop anything else that surfaces in the same hunk (another role, another i18n block, another Playwright project, etc.) — that content will arrive later via the normal release train. Confirmed pattern (et-operational-platform #1562): a roles.ts conflict carried both the target role AND an unrelated already-on-dev role (zarzad) in the same hunk; an i18n JSON conflict carried the target feature’s block AND an entire unrelated dispo top-level section; playwright.config.ts carried 4 unrelated e2e projects alongside the target one.
  5. Watch for structurally-diverged files, not just missing-entry conflicts — e.g. an e2e global-setup.ts on main using a completely different (older) auth-account pattern than dev. Don’t force a hybrid merge of two incompatible architectures. If the file has zero runtime impact (test-only wiring, no app code), it’s usually safest to skip it from the patch entirely:
    git checkout --ours -- path/to/structurally-different-file.ts
    git add path/to/structurally-different-file.ts
    and note the omission explicitly in the PR description. It will arrive correctly, all at once, with the next real release.

3. Verify

npx tsc --noEmit          # expect it to pass for a well-isolated patch; if not...
git diff origin/main...HEAD --stat   # ...confirm any errors are in files NOT in this list (pre-existing
                                       # staleness on `main`, not something your patch introduced)
npx vitest run <relevant test file>

main’s generated files (e.g. Supabase types.ts) are often stale relative to dev by design — this repo’s own migrations run against the live DB independent of the committed type file, so tsc errors about columns/tables that demonstrably exist live are a pre-existing gap, not a blocker. Check next.config.* for typescript.ignoreBuildErrors / eslint.ignoreDuringBuilds before treating a local tsc/lint failure as a deploy blocker — if both are true, the production build itself does not type-check, so a clean local tsc is a nice-to-have signal, not a merge gate.

Also check whether any CI workflow’s on.pull_request.branches even includes main — some app repos (et-operational-platform’s ci.yml) only run the unit-test check on PRs to dev/staging, meaning a PR against main has zero required checks, only whatever Vercel’s git integration runs. Know this before assuming “CI is green” means the same thing it does for a normal PR.

4. Push, PR, merge

git push -u origin hotfix/<slug>
gh pr create --repo <owner>/<repo> --base main --head hotfix/<slug> `
  --title "hotfix: <slug> — direct-to-production cherry-pick (#<issue>)" `
  --body-file <path-to-body.md>   # document exactly what was dropped and why (step 2)
# wait for whatever checks exist (Vercel preview at minimum), then:
gh pr merge <PR#> --repo <owner>/<repo> --merge --delete-branch

Prefer --merge (not --squash) for a multi-commit hotfix so the two original commits stay visible in main’s history rather than collapsing into one.

5. Repeat onto staging if it doesn’t already have the fix

If the feature was already promoted to staging through the normal pipeline before this hotfix, skip this — check first:

git log origin/dev..origin/staging --oneline   # if the feature's commits already appear, staging is current

Otherwise, repeat steps 1–4 with origin/staging as the base and --base staging on the PR. In practice these cherry-picks land cleanly with zero conflicts when targeting staging (unlike main), since staging tracks dev far more closely.

6. Close the loop

Comment on the originating issue listing every branch the fix is now live on (dev / staging / main, with merge-commit SHAs), and close it with the terminal milestone.

Escalation

  • If conflict resolution in step 2 is too tangled to cleanly separate your feature from unrelated work — the feature is not actually independent. Stop, abandon the hotfix, and route through the normal staging-review-to-rcrc-to-main pipeline instead (accepting the full rc payload).

Prevention

  • A 484-commit rc→main gap is itself the underlying problem this playbook works around — it means rc → main promotions aren’t happening regularly. Worth its own follow-up: investigate why and whether /rc-to-main needs to run on a schedule or the QA gate is stuck on something specific.