smithery.ai

tzurot-git-workflow

Git workflow procedures. Invoke with /tzurot-git-workflow for commit, PR, and release procedures.

First seen Mar 25, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 40,000 B
  • docs SUMMARY.md 171 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Git Workflow Procedures

Invoke with /tzurot-git-workflow for step-by-step git operations.

Safety rules are in .claude/rules/00-critical.md - they apply automatically.

Commit Procedure

1. Stage Changes

git status                    # Review what's changed
git add <specific-files>      # Stage specific files (preferred)
# Or: git add -p              # Interactive staging

2. Create Commit

git commit -m "$(cat <<'EOF'
feat(scope): short description

Longer explanation of what and why.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
EOF
)"

Types: feat, fix, docs, refactor, test, chore, perf, debug (debug = temporary diagnostic instrumentation, added then removed; see .claude/rules/05-tooling.md § "The debug type" for when to use it vs. chore/feat.) Scopes: generated from every packages/+services/ directory, plus tests and a static root set (backlog, ci, deps, docs, hooks, husky, legal, prisma, repo, rules, skills) — source of truth is allScopes in commitlint.config.cjs.

Command-shape rules for commit/push (each class cost multiple cycles in practice):

  • Chain with &&, never ; — a hook-rejected commit must halt the chain; with ; the dead commit flows into a push that no-ops as "Everything up-to-date" and the rejection reason scrolls away.
  • Pass timeout: 600000 on Bash calls that commit or push — lint-staged + the pre-push gate run the full local pipeline (minutes); default timeouts kill mid-hook and leave ambiguous state (commit landed, push didn't).
  • In compound commands that cd into a package, run the git step as git -C <repo-root> … (or with absolute paths) — repo-relative pathspecs break after the cd, failing AFTER the tests already passed.
  • Create a new branch in a SEPARATE Bash call, before staging/committing — never chain git checkout -b <branch> && git add … && git commit …. The develop-code-commit-guard PreToolUse hook evaluates the CURRENT branch before the command runs, so on a compound "branch-then-commit" it still sees develop/main and blocks the commit as an on-long-lived-branch code commit. Run git checkout -b first, confirm the branch, then stage + commit in the next call.
  • Canonical push-verify (don't improvise greps — a pattern starting with - parses as an option flag). Use this ls-remote form when you need a scriptable boolean; the -> branch ref-update line / git status -sb check below is the quick visual form — same goal, pick by context:
test "$(git rev-parse HEAD)" = "$(git ls-remote origin "refs/heads/<branch>" | cut -f1)" && echo PUSH_LANDED

3. Push

pnpm test && git push -u origin <branch>

Verify every push actually landed before proceeding (and before arming the CI Monitor): confirm the -> branch ref-update line in the push output, or git status -sb showing in-sync.

PR Procedure

Create PR

# 1. Ensure on feature branch, up-to-date with develop
git checkout develop && git pull origin develop
git checkout feat/your-feature
git rebase develop

# 2. Push and create PR (--assignee @me is owner policy: every human-authored
#    PR carries its creator as assignee, bots stay unassigned;
#    pr-monitor-reminder.sh backfills the PR author if missed)
git push -u origin feat/your-feature
gh pr create --base develop --title "feat: description" --assignee @me

Before writing a closing reference in the PR body

The moment before typing Closes TASK-N (or "completes doc-N", "finishes the X sweep") is the trigger — re-open the referenced task file and QUOTE its acceptance line verbatim into the PR body, then state per clause whether it is met. Recalling the acceptance from memory is what fails: an overclaim survives paraphrase easily and rarely survives being placed next to the words it contradicts. If any clause is unmet, the PR says partial and names the task that carries the remainder. .claude/hooks/pr-body-ref-gate.sh's claim scan backstops this: it blocks a PR create/body edit once, naming any claim-shaped body line that carries no cite and no hedge.

The same check applies to any exhaustiveness claim in the body ("every call site", "all N modules", "the whole module"): name the enumeration command whose output backs it, or scope the sentence to what was actually swept.

Cite by grep token, not file:line, while the file is still under review. A line number into a file later rounds will edit is stale by construction — five cites on one PR moved three times before merge. grep -n '<distinctive token>' <file> names the same place at every round, and pr-body-ref-gate.sh accepts either form.

A FORWARD reference — "filed as TASK-N", "tracked in doc-N" — is the same claim pointing the other way, and it needs git ls-files, not existsSync. A task file that exists only in the working tree does not exist from any other vantage point: not git log, not another checkout, not the reviewer, not the next session. Before the body claims something was filed:

git ls-files --error-unmatch 'tracker/tasks/task-N*.md'   # non-zero = not tracked

git ls-files answers for the CURRENT branch only. Tracker tasks are committed straight to develop, so one filed mid-PR is absent from a feature branch cut before it — the command reports "not tracked" for a task that is perfectly well tracked on the branch the PR merges into. Check the base ref when the two differ, or the check produces a false alarm exactly when it is being used properly:

git ls-tree -r --name-only origin/develop -- tracker/tasks/ | grep task-N

Commit it first, then write the sentence.

Arm CI monitor (required)

Immediately after gh pr create — and after any subsequent git push to an open PR — start a Monitor that waits for CI to complete and reports new review comments back. Do not skip this step and do not wait for the user to ask about CI status.

One monitor per PR — TaskStop the previous one first (05-tooling.md § PR Monitoring).

Arm it from the template below every time:

Arm a Monitor with description: "CI + reviews for PR <N>", timeout_ms: 1800000, persistent: false (05-tooling.md § PR Monitoring explains the false), and this as its command — verbatim, as plain bash:

pnpm ops gh:ci-gate <N> --sha $(git rev-parse HEAD)

Copy the substitution verbatim — never resolve the SHA and paste the result. The gate rejects an abbreviated SHA and a well-formed one naming no local commit, but the substitution removes the step entirely.

When the monitor fires, run the four-step procedure in 05-tooling.md § PR Monitoring in full — CI state plus the SHA-pinned run-list query, the three review endpoints (gh:pr-comments / gh:pr-reviews / gh:pr-info), one consolidated report that reads every ### section of every claude[bot] entry, then /tzurot-review-response. Do not stop after the CI check: only CI_COMPLETE means CI finished, and a green check list is not proof CI ran.

Merge gate is green-only — every check green before gh pr merge, release PRs included; infrastructure-shaped failures get gh run rerun <run-id> --failed and a re-armed Monitor, never a merge through the red (00-critical.md § Never Merge PRs Without Completed CI).

Before merging: the head branch must be checked out NOWHERE

--delete-branch fails silently when the head branch is checked out anywhere — git refuses to delete a checked-out branch, gh reports the LOCAL failure, and the merge itself still succeeds. The PR closes as merged and nothing says a branch is still there; it surfaces later as a repo-state-sweep finding, or when someone notices the pile.

"Anywhere" is the whole rule, and the easy-to-forget instance is the checkout you are not looking at: a worktree (the orchestration skill mandates one for every file-mutating worker, so every delegated unit lands in this state) or the main checkout still sitting on the branch after a local review.

Before the merge:

git status --short                        # check the main tree BEFORE moving off the branch
git worktree list                         # find any worktree holding the branch
git checkout develop                      # get the main checkout off it

Check every worktree before removing it — --force or not. Run this against each worktree the previous step listed, and do not skip it because you are using plain remove:

git fetch -p                                      # --remotes reads LOCAL tracking refs; refresh them first
git -C <path> status --short                      # empty = no modified or untracked files
git -C <path> log --oneline --not --remotes       # empty = every commit is on a remote
git worktree remove <path>                        # only once BOTH are empty

Plain git worktree remove does not protect you here. It refuses only on a DIRTY worktree — modified or untracked files. A worktree whose work is committed but never pushed is clean by that definition, so plain removal takes it with exit 0, and --delete-branch at the merge step below then deletes the only ref holding those commits, leaving them reachable solely through a local reflog. That is precisely the resumed-worker scenario in § Resuming a worktree-isolated worker in /tzurot-orchestration, which is why the check is unconditional rather than a --force caveat.

--not --remotes, not @{u}.. — measured: @{u} dies with fatal: no upstream configured (exit 128) on a branch that was created but never pushed, which is precisely the state you are checking for, so the check would abort exactly when it matters.

If either is non-empty, do NOT remove the worktree yet — get the work to safety first, then re-run the check and remove:

git -C <path> add -A && git -C <path> commit -m "chore: snapshot before worktree cleanup"
git -C <path> push -u origin HEAD                                       # if commits were unpushed

(chore:, not wip: — wip is not in commitlint's type-enum, so the hook rejects it at exactly the moment you need the commit to land.)

If that push happened, you are no longer ready to merge. It put a commit on the PR's head branch that CI has never seen, so re-arm the Monitor and wait for a fresh green run before the merge below — 00-critical.md § Never Merge PRs Without Completed CI requires green on the LATEST commit, and the green-only gate a few sections above applies here with no exception for a recovery commit. Then merge — this is the feature-PR invocation the sections above build up to, and --delete-branch is correct here precisely because the branch is disposable:

gh pr merge <N> --rebase --delete-branch

After PR Merged

git checkout develop
git pull origin develop
git branch -d feat/your-feature

Then verify the remote branch is actually gone — the ordering step above makes the delete possible, not certain, and this is what turns any other silent-delete failure into a report instead of a discovery:

git ls-remote --exit-code --heads origin "<branch>"; case $? in
  0) echo "SURVIVED — re-delete" ;;
  2) echo "deleted ✓" ;;
  *) echo "UNKNOWN — ls-remote itself failed; the branch's state was NOT checked" ;;
esac

This is not the push-verify ls-remote from the Commit Procedure above. That one reads the ref's SHA out of stdout to prove a push landed at a specific commit; this one asks only whether the ref exists at all, which is why it wants --exit-code and ignores stdout. Same command, opposite questions — don't swap one form for the other.

Use the case, not && … || …. --exit-code distinguishes the cases (0 found, 2 no match, anything else an error), so read the status rather than its truthiness.

git push origin --delete <branch> re-deletes a survivor. Never for develop or main (00-critical.md § Long-Lived Branch Protection) — the develop→main release PR merges without --delete-branch, so nothing here applies to it.

Dependabot PR Recovery

Dependabot PRs have three distinct cleanup paths — using the wrong one wastes a cycle or produces a forbidden merge-commit state.

Situation Command Effect
Branch is behind develop, dependabot is the only committer @dependabot rebase (PR comment) Dependabot rebases its own branch onto develop and regenerates the lockfile. PR number preserved, CI reruns.
Branch has a non-dependabot commit (e.g., GitHub's "Update branch" button added a merge commit) @dependabot recreate (PR comment) Dependabot closes the existing PR and opens a new one against current develop. PR number changes; any prior review comments are lost.
Need to abandon the bump entirely gh pr close or let it age out Dependabot will re-open on next schedule unless the dep is added to ignore: in dependabot.yml.

Key constraint: @dependabot rebase refuses to run if any commit on the branch is authored by someone other than dependabot. GitHub's "Update branch" UI button appears to rebase, but it actually adds a merge commit authored by github-actions[bot] — which poisons the branch for rebase. Once that happens, recreate is the only in-band recovery.

Rule of thumb: don't hit "Update branch" on dependabot PRs. Use the chat command. If you do hit it by accident, don't waste time on rebase — go straight to recreate.

dependabot.yml is read from the DEFAULT branch (main) — same trap as the claude workflow files below. An ignore: entry merged to develop does nothing until the next release lands it on main; dependabot will keep opening PRs with the "ignored" dep in the meantime. For immediate effect, comment @dependabot ignore <dep-name> major version on the open PR — a server-side ignore, persistent until @dependabot unignore <dep-name> major version, and it works per-dependency inside grouped PRs. Config entry = durable documentation; chat command = the thing that actually stops the PRs. Removing an ignore later requires BOTH the config-entry removal and the unignore comment.

After a dev-tooling bump merges (eslint/tsc/prettier/vitest — especially a major), rebase every open PR before merging it. A green PR's CI ran against its OLD base, so the new tooling never saw its code, and merging on that stale base can redden develop. gh pr merge --rebase rebases-and-merges WITHOUT re-running CI, so it does not protect against this — only a manual pass does: git rebase origin/develop → pnpm install → re-run the affected gate (pnpm focus:lint for eslint/prettier, pnpm typecheck for tsc; changed-package scope is enough, since the bump's own merge proved existing code passes) → push for a confirming CI round → merge.

Claude workflow changes target main, not develop

GitHub Actions that validate against the default branch (main) — notably claude-review and the @claude responder — refuse to run on a PR unless their own workflow file is byte-identical to the version on main. A "security skip": it stops an untrusted PR from altering the very workflow that reviews it.

Scope — the validation is file-scoped. Only the self-validating claude workflow files (claude-code-review.yml, claude.yml) trigger the skip; a PR carrying drift in any OTHER workflow file still gets a real review. Non-claude workflows (ci.yml) also execute from the PR's own branch, so routine ci.yml edits ride normal develop PRs like any code change — no main-cut ceremony.

Consequence: a change to one of the claude workflow files that lands on develop first silently disables those reviews on every PR — they pass as a green ~10-15s no-op ("Skipping action due to workflow validation", no review posted) — until the change reaches main. Under the normal flow that's only at the next release, and the release PR's own review skips too, so it compounds across the whole cycle.

Local runs before the PR exists need --base main. The guard reads the branch's real merge target from GitHub (gh pr view) rather than guessing it from git shape — the shape is genuinely ambiguous, because release:finalize puts main's HEAD on develop's history and a stale develop-cut branch then looks identical to a main-cut one. Consequence: on a main-cut branch with no PR open yet, there is nothing to ask, and the guard fails CLOSED. That is the correct direction for a guard, but it means pnpm quality / pre-push goes red until the PR exists. Pass pnpm ops guard:workflow-sync --base main (or just open the PR first).

Rule: For any change to claude-code-review.yml or claude.yml, open a PR cut from main and targeting main — never branch from develop for this (a develop-based branch targeting main drags all of develop's unmerged commits into the diff). The moment it merges, run pnpm ops release:finalize to resync develop onto main — do this before other work piles onto develop, since every commit added there (and every open feature branch) then needs rebasing onto the resynced develop. Do NOT let a claude-workflow change reach main via the routine develop→main release merge.

This bites most often with dependabot bumps that touch the claude workflow files (e.g. an actions/checkout major bump usually edits every workflow, claude ones included) — dependabot opens them against develop. When a dependabot PR (or any PR) touches claude-code-review.yml/claude.yml, cherry-pick just those workflow hunks into a fresh main-cut PR and merge that first, rather than letting the change reach develop; the ci.yml hunk of the same bump can ride develop normally. (There's no @dependabot retarget command; re-pointing a develop-based PR's base at main via the GitHub UI would drag all of develop's unmerged commits into the diff, so cherry-picking the hunk is the clean path.)

Recovery — a claude-workflow change already landed on develop (the disruptive case; infrequent but real):

  1. Branch off main, sync just the affected workflow file(s) to develop's state (git checkout origin/develop -- .github/workflows/<file>), commit, PR against main.
  2. Merge to main (needs explicit approval — main always does).
  3. Rebase develop onto main so the two don't diverge on the workflow file (pnpm ops release:finalize, or manual git rebase origin/main + --force-with-lease).
  4. Order matters — do step 3 first. For each open PR: rebase the feature branch onto the updated develop (git rebase develop) and push. The push itself re-triggers the review on the new HEAD, which now carries the updated workflow in its ancestry — so the validation passes. Do not reach for gh run rerun: it re-runs the old commit's checkout, whose workflow bytes still mismatch main, so it keeps skipping. The rebase-push is the only reliable trigger (the PR's review validates the PR branch's own HEAD workflow against main).

Rebase Procedure

git checkout develop && git pull origin develop
git checkout feat/your-feature
git rebase develop

# If conflicts:
# 1. Edit files to resolve
# 2. git add <resolved>
# 3. git rebase --continue
# Repeat until done

git push --force-with-lease origin feat/your-feature

Release Procedure

0. Risk & Coverage Appraisal (produce UNPROMPTED with any release proposal)

Answer these before being asked, as part of proposing the cut:

  • The proposal itself is three parts, always (owner directive):

a full summary of everything included (every PR, grouped by theme), an explicit justification judgment ("is there enough here to justify a release?"), and a riders check ("any other small items worth including?"). A bare "ready to cut?" prompt forces the owner to reconstruct the contents themselves.

  • Cite the release plan — the 🚢 Next Release section in backlog/now.md

is the primary cut trigger (10-working-posture.md § Ship in bounded units): state that its waiting-on list is empty, or name the backstop that fired instead and what the plan still lists as waiting. A cut that diverges from the plan is fine — but the divergence is stated, not silent.

  • Risk level (low/medium/high) with the one-line basis (what's runtime-unverified,

what's blast-radius-bounded).

  • Smoke scope derived from the release diff — risk-scoped and minimal, not a

fixed checklist; name the specific user-visible paths this release touched.

  • Coverage gaps that would raise confidence — name them and offer to close

before cutting (or explicitly note why post-hoc observability suffices).

  • For minor features, offer observability-instead-of-smoke ("logging is in

place to check the first real use") as a first-class alternative to another manual round.

  • The smoke request IS the CURRENT.md write — never ask the owner to

smoke-test anything that isn't already a CURRENT.md checklist item with its own status line. Asking in chat shorthand forces "remind me what to test?"

  • Each smoke item carries a confidence tier (canonical definition in

/tzurot-testing § Human-Verification Requests) — offer the owner only the needs-smoke tier, never the high tier CI + review already cover.

  • Never cite "soaked in dev" as safety evidence (see /tzurot-deployment).

1. Version Bump

# Option A: Changesets (recommended)
pnpm changeset
pnpm changeset:version
git add . && git commit -m "chore: version packages"

# Option B: Manual
pnpm bump-version 3.0.0-beta.XX
git commit -am "chore: bump version to 3.0.0-beta.XX"

2. Write Release Notes

Write release notes following the Conventional Changelog format defined in .claude/rules/05-tooling.md.

Source of truth: git log v<previous-tag>..HEAD --no-merges — NOT CURRENT.md. CURRENT.md tracks session work; release notes track what shipped between tags.

Any count or list of "PRs merged since the last release" — in a release proposal, a PR body, or a status message — comes from pnpm ops release:range, never a hand-rolled gh pr list/date-window query. It classifies each PR runtime/non-runtime and prints both cut triggers (runtime-PR count ~10, range diff size ~250 files); read both, per 10-working-posture.md § Ship in bounded units. If the size cannot be measured the command says so on stderr; a SKIPPED check is not a passing one.

Backlog sweep — same pass, same commit as the notes. The release range enumerates every shipped PR anyway, which is the one deterministic moment the full shipped-list exists. For each PR in the range, grep backlog/ (recursive, incl. cold/) for the item's topic and strike/remove the shipped entries.

# 0. Sync tags FIRST — a stale local tag store silently spans two releases
git fetch --tags origin

# 1. Find the previous release tag
git tag --list "v3.0.0-beta.*" --sort=-version:refname | head -1

# 2. List actual commits for this release
git log v<previous>..HEAD --no-merges --oneline

# 3. Cross-check: every release note item must map to a commit in that range
# 4. Cross-check: no item should appear in the previous release's notes

User-facing doc sweep (required before the release PR): the drafted notes enumerate exactly what shipped — walk each Breaking Changes, Features, and Improvements item (breaking renames/removals are the stalest-doc risk) against every user-facing doc surface, and fix what's stale in the same sitting:

  • README.md — the derivable half (project tree, prerequisites, fenced

scripts, slash-command list, links) is gated by pnpm ops guard:readme; this sweep is the Highlights and Features prose: with the drafted notes in hand, ask whether they still describe what the range shipped, and fix in the same cut.

  • docs/commands.md — command table. Rendered live at tzurot.org/docs/commands.
  • docs/guides/*.md — the getting-started guide (and future guides).

Rendered live under tzurot.org/docs. A new user-visible feature or a changed command flow belongs here, not just in the command table.

  • docs/legal/PRIVACYPOLICY.md / TERMSOF_SERVICE.md — check whenever the

release changes data collection, retention windows, notification behavior, or third-party processors; the retention table and behavior claims must match the shipped code. Rendered live at /privacy and /terms.

The website glob-loads these files, so staleness is now PUBLIC the moment the release deploys — and conversely the fix ships itself with the release. New website-rendered markdown sources must ALSO be COPY'd in services/website/Dockerfile (a missing base dir fails the docker build loudly via the pages' getEntry throw — by design). Prose docs have no mechanical drift guard; the release-notes draft is the one moment the full delta is already enumerated, so the sweep is nearly free here and expensive anywhere else.

3. Create Release PR

Security preflight first — a fixable vuln is cheaper to ride along than to hotfix after:

pnpm ops security:advisories        # each advisory + severity + fix version + direct/transitive + action
pnpm ops guard:repo-settings        # deletion of main/develop must be UNREACHABLE (see below)
gh pr list --author "app/dependabot" --state open   # any auto-PRs to ride along

guard:repo-settings belongs in the preflight specifically, because the release merge is the one merge whose head branch is develop. A CRITICAL finding means the next release merge will delete develop — fix it before cutting (00-critical.md § Long-Lived Branch Protection).

security:advisories is the primary check. The ride-along candidate is a transitive-with-fix advisory — Dependabot can never PR one, so widen/add the pnpm.overrides entry, pnpm install, and verify the lockfile resolves the patched version (05-tooling.md § Security Advisories).

gh pr create --base main --head develop --title "Release v3.0.0-beta.XX: Description" --assignee @me

4. Pre-Merge Migration (if release includes one)

Run migrations before merging — Railway auto-deploys every service the moment the release PR merges to main, so migrating after leaves new code on the old schema for the deploy window (the beta.140 column ... does not exist incident). Migrate first, while prod still runs the old code:

pnpm ops release:premigrate --dry-run   # preview the new migrations in the release range
pnpm ops release:premigrate             # apply to prod, THEN proceed to merge

Skip if the release has no migration — release:premigrate detects this and exits cleanly. It refuses a destructive migration without --allow-destructive and an -- tzurot:apply-after-deploy one without --allow-marked; both cases, and the maintenance-window sequence, are in .claude/rules/03-database.md § Deployment.

5. Merge Release PR

⚠️ NEVER use --delete-branch for release PRs. develop is a long-lived branch.

⚠️ Wait for every CI check to be green — release PRs are not exempt (00-critical.md § Never Merge PRs Without Completed CI); claude-review is the second look on the full release delta.

⚠️ When assessing release safety, do NOT cite "soaked in dev". Dev has no organic traffic — a dev deploy proves boot, not behavior (see /tzurot-deployment § "What a dev deploy proves"). The honest safety basis is per-PR CI + reviews, the holistic release review, and blast-radius analysis of runtime-unverified paths.

⚠️ CodeQL "new alert" on a large release PR is usually a re-surfaced dismissed alert, not a real one. The release PR's diff is huge (hundreds of files); CodeQL's PR-diff analysis can't diff it cleanly and the check's own summary says so verbatim: "Alerts not introduced by this pull request might have been detected because the code changes were too large." A constituent PR that relocated code (e.g. a file/function move) carries any previously-dismissed alert to the new path, where the release PR re-flags it as "new." Before treating it as a blocker: (1) read the alert's rule + file/line from the failed check-run's annotations (gh api repos/{owner}/{repo}/check-runs/<id>/annotations); (2) check the repo's open-alert count (gh api …/code-scanning/alerts?state=open) — 0 open means it's not a real default-branch alert; (3) find the matching dismissed alert (…/code-scanning/alerts?state=dismissed) and confirm same rule + relocated code. If it's a confirmed re-surface, the durable fix is to make the code stop tripping the rule (so it can't re-surface on the next relocation) rather than re-dismissing — then the release CodeQL greens on the fixed tree.

# ✅ CORRECT - Merge without deleting develop (only after all checks green)
gh pr merge <number> --rebase

# ❌ FORBIDDEN - Would delete develop!
gh pr merge <number> --rebase --delete-branch

Fallback for large PRs: fast-forward when rebase-merge chokes

GitHub's "Rebase and merge" replays every PR commit onto main as new commits. On a release PR with a large commit range (observed failing at ~200 commits), the API rejects the merge and the web UI falsely reports merge conflicts — even though gh pr view <N> --json mergeable,mergeStateStatus returns MERGEABLE / CLEAN. --admin does not help; this is a mechanical rebase failure, not a branch-protection block. The error to grep this skill for when you hit it:

GraphQL: This branch can't be rebased (mergePullRequest)

When this happens, fast-forward main to develop instead. Because every release leaves main an ancestor of develop (step 6 rebases develop onto main, and all new work piles onto develop), this is a clean fast-forward — and it's actually cleaner than the button: it keeps develop's original SHAs, so main and develop end byte-identical and step 6's release:finalize becomes a no-op (no SHA divergence to repair).

Two guardrails are mandatory — do not skip either:

  1. Attempt gh pr merge <N> --rebase FIRST, even when you expect it to fail. That command fires the pr-merge-review-check.sh PreToolUse gate (00-critical.md), which forces the latest claude-review into context before any merge. Distinguish the two failure modes: the gate blocks once by injecting the review into stderr and exiting non-zero — engage with the review and retry the same command; if that retry also fails with the can't be rebased error above, the merge has failed mechanically and you proceed to the FF. A bare git push to main does not trigger that gate, so the FF is only safe after the gate has been satisfied by a real gh pr merge attempt in the same session. (If the session restarts between the failed attempt and the FF push, re-attempt gh pr merge --rebase once more first — the acked comment-id persists, so the hook won't re-block, but the re-attempt re-establishes that the review is in context.)
  2. Verify main is an ancestor of develop — git merge --ff-only refuses (loudly, no side effects) if main has diverged (e.g. a hotfix landed directly on main). If it refuses, do NOT force anything: rebase develop onto main first (git checkout develop && git rebase origin/main && git push --force-with-lease), then retry the FF.
# Only after `gh pr merge --rebase` has fired the review gate AND failed mechanically:
git fetch --all                            # REQUIRED: refresh origin/develop — `git pull origin main`
                                           # below does NOT fetch it, so the FF could land a stale develop
git checkout main && git pull origin main
git merge --ff-only origin/develop         # fast-forward; refuses if main diverged
git push origin main                       # FF push — NOT a force-push
# GitHub auto-closes the PR as MERGED once its head commits land on main.

This is a permitted, documented merge path for the large-PR case — not a workaround to reach for casually. For normal-sized release PRs, gh pr merge --rebase remains the default (it's contributor-agnostic and fires the gate directly). Reserve the FF for when rebase-merge mechanically fails.

6. After Merge to Main

Rebase develop onto main so their SHAs stay aligned. Skipping this step causes the next release PR to show apparent "conflicts with main" that aren't real (content is identical, just different SHAs).

Preferred — automated:

pnpm ops release:finalize           # Interactive: prompts before force-push
pnpm ops release:finalize --yes     # Skip the prompt (non-TTY safe)
pnpm ops release:finalize --dry-run # Preview the steps without executing

The command runs the full fetch → checkout main → pull → checkout develop → pull → rebase origin/main → push --force-with-lease sequence with safety rails: refuses on dirty working tree, no-op exit when already aligned, aborts rebase cleanly on conflicts.

Manual fallback (if the tool is broken or you need step-by-step debugging):

git fetch --all
git checkout main && git pull origin main
git checkout develop && git pull origin develop
git rebase origin/main
git push origin develop --force-with-lease

7–8. Tag + Create the GitHub Release — pnpm ops release:publish

Git tag and GitHub Release are separate things and the merge does neither. The flag dance around them (newest holds latest; a prerelease-channel version also demotes the previous tag) is error-prone from memory, so it's automated — use the command, not the raw steps:

# Prepare notes first (step 2 output), then one shot:
pnpm ops release:publish 3.0.0-beta.XX --notes-file /tmp/notes.md
pnpm ops release:publish 3.0.0-beta.XX --notes-file /tmp/notes.md --dry-run  # preview

What it does (release-flow steps 7–8):

  1. Creates + pushes an annotated tag on main (idempotent — reuses an existing tag).
  2. Creates the GitHub Release holding the latest badge (never --prerelease

— the newest release always holds latest, stable or beta).

  1. Only for a prerelease-channel version (-alpha/-beta/-rc): demotes the

immediately-previous release to --prerelease (found via gh release list, the authoritative GitHub state — NOT local git tag, which drifts because gh release create mints the tag server-side). A stable X.Y.Z release skips the demote — a GA release doesn't demote its predecessor.

Release-channel convention (what the command enforces): the newest release holds latest (prerelease=false); every older beta is prerelease=true. Do NOT mark the newest tag --prerelease — that's mutually exclusive with latest.

Known-benign race — publish right after the merge. Publishing while Railway is still swapping prod containers can 502 the release webhooks at Railway's edge (GitHub delivers once, never retries). This is expected and self-healing: the hourly reconcile sweep picks the release up and sends the DM blast with ≤1h lag. Don't re-publish, don't debug the webhook delivery, and don't wait for the deploy to settle before publishing — the lag is the designed absorption path.

Verify: gh release list --limit 5 --json tagName,isPrerelease,isLatest --jq '.[] | {tagName, isPrerelease, isLatest}' — the newest must read prerelease=false / latest=true, every older beta prerelease=true / latest=false. (If gh/tooling is unavailable, the raw fallback is git tag -a vXX -m … && git push origin vXX, then gh release create vXX --title vXX --latest --notes-file …, then — betas only — gh release edit v<PREV> --prerelease.)

9. Reset CURRENT.md Unreleased Section

After a release merges to main, reset the "Unreleased on Develop" section in CURRENT.md to only track items since the new release tag.

Also flag that a fresh session is available as an alternative to compacting onward when the session has spanned one or more releases — long-lived sessions accumulate compaction churn ("again" re-asks, re-explained context) and the owner has named session-per-release as the preferred cadence. This is a technical-breakpoint flag per 09-interaction-style.md § Don't Suggest Stopping (a fresh session continues the work — it is not a stop); one sentence at close-out is enough, and the call is theirs.

10. Draft the Next Release Plan

Close the release by planning the next one (10-working-posture.md § Ship in bounded units). Rewrite the 🚢 Next Release section in backlog/now.md for vNext (~10 minutes, direct-commit doc change):

  • Theme — what this release IS, in a phrase. Pull candidates from Current

Focus, active-epic.md, cold/queue.md, and the digest's oldest-20.

  • In already — empty at cut time; grows as PRs merge.
  • Waiting on — the named items whose landing defines the cut.
  • Explicitly NOT in — what's deliberately deferred to the next train,

with the gating reason.

  • Deploy notes — migration timing, maintenance windows, owner decisions

that must survive until cut time (CURRENT.md is session state; this is the durable home for release-scoped facts).

  • Cut when — the criterion, plus the standing backstops (~10 runtime PRs /

~250 files) with current values from release:range.

  • vNext+1 sketch — one line, so the horizon extends past a single train.

Hotfix release cut from main

Three gotchas the standard develop→main flow never exercises: (1) the pre-push branch-name pattern has no release type — use a valid prefix such as chore/release-vX.Y.Z-beta.N; (2) gh pr merge --rebase --delete-branch switches the local checkout to the default branch and tries to fast-forward it, so expect "not possible to fast-forward" noise and a stale local main — git pull origin main after; (3) pnpm ops release:finalize --yes rebases develop cleanly (duplicate cherry-picks drop via patch-id), but its force-push can still fail the pre-push gate on semantic divergence the textual rebase resolved (a symbol that moved between packages, a develop-side manifest route the conformance gate wants fixtures for) — budget one fix-commit riding the rebased push, and note the force-push to develop needs explicit per-instance user approval.

GitHub CLI

gh pr edit is broken — use pnpm ops gh:pr-edit. The read commands (gh:pr-info, gh:pr-reviews, gh:pr-comments) and their flags are in 05-tooling.md § GitHub and docs/reference/tooling/OPSCLIREFERENCE.md.

References

  • GitHub CLI: docs/reference/GITHUBCLIREFERENCE.md
  • Safety rules: .claude/rules/00-critical.md