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 rcgit log origin/staging..origin/dev --oneline | wc -l # small = staging mostly currentgit log origin/rc..origin/staging --oneline | wc -l # gap staging→rcgit log origin/main..origin/rc --oneline | wc -l # gap rc→main — the one that matters heregh 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/maincd tmp/wt-hotfix-<slug>git fetch origin dev # objects for the commits you're about to cherry-pickgit 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:
Read both sides of the <<<<<<< HEAD / ======= / >>>>>>> markers.
Keep HEAD (main’s existing content) as the base.
From the incoming side, add back only the lines that belong to your target feature.
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.
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:
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-rc → rc-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.