tzurot-git-workflow

star 8

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

lbds137 By lbds137 schedule Updated 6/9/2026

name: tzurot-git-workflow description: 'Git workflow procedures. Invoke with /tzurot-git-workflow for commit, PR, and release procedures.' lastUpdated: '2026-06-23'

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 <noreply@anthropic.com>
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: ai-worker, api-gateway, bot-client, common-types, ci, deps

3. Push

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

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
git push -u origin feat/your-feature
gh pr create --base develop --title "feat: description"

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.

The .claude/hooks/pr-monitor-reminder.sh PostToolUse hook auto-fires on git push / gh pr create and injects a reminder with the Monitor invocation pre-filled with the current PR number. Use that when you see it; the template below is the fallback shape if you're arming manually:

Monitor({
  description: "CI + reviews for PR <N>",
  command: 'gh pr checks <N> --watch --interval=30 > /dev/null 2>&1; echo "CI_COMPLETE"; gh pr checks <N>',
  timeout_ms: 900000
})

When the monitor fires, all four of the following must happen β€” do not stop after #1 even when every check passed:

  1. Inspect the final gh pr checks <N> output for pass/fail.

  2. Fetch new reviewer feedback via three endpoints (GitHub splits them; the raw /issues/N/comments call silently misses inline line comments):

    • pnpm ops gh:pr-comments <N> β€” conversation + inline line-level review comments
    • pnpm ops gh:pr-reviews <N> β€” review summaries (Approve / Request Changes)
    • pnpm ops gh:pr-info <N> β€” PR-level state

    No bot-only filter β€” human reviewer comments matter too. Dedup by tracking created_at of the last-reported comment.

  3. Report CI status and new reviewer feedback in one concise message. Group findings as blocking vs. non-blocking. If no new reviews since the last push, say so explicitly β€” silence is not a substitute for "no new comments."

    Read the body, not just the summary. Reviewer output is tiered: verdict β†’ strengths β†’ major items β†’ minor items β†’ observations β†’ summary. The trailing "Summary" / "Actionable items" section is a reviewer's shortcut; it frequently under-reports items the body flags in detail. When a review is 100+ lines, treat length as a skimming red flag β€” walk every ### section before calling the report done. Cross-check: if codecov flags missing lines, grep the review body for a corresponding test-gap call-out.

    Each claude[bot] entry is a separate review cycle. If multiple exist (pre-rebase + post-rebase, push + re-push), read every one β€” don't assume only the latest matters. Track created_at of the last-reported comment so future fetches dedup correctly across session boundaries.

  4. Don't fix anything without user approval β€” report only. User decides in-PR vs. backlog.

The #1-without-#2 failure mode is worth guarding against: all-green CI feels complete, but new review comments can still carry blocking findings or non-blocking observations the user wants to triage.

The #2-without-full-body failure mode is the second trap: fetching comments but extracting only the summary section. A review that ends "Summary: two actionable items" almost always has a body with additional items that weren't promoted to the summary.

If the monitor completes without a CI_COMPLETE line in its output, the 15-min timeout fired first β€” re-arm rather than assume CI passed. If CI fails or CodeQL flags something, use PushNotification β€” the user should hear about it before their next turn.

Merge gate is green-only. Per .claude/rules/00-critical.md "Never Merge PRs With Red CI": every check must be green before gh pr merge runs, including release PRs. If a check fails for what looks like infrastructure reasons (binary not found, missing secret, action-setup error), gh run rerun <run-id> --failed and re-arm the Monitor β€” don't merge through the red. The release procedure below assumes a green pipeline.

After PR Merged

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

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.

Workflow-file 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 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.

Consequence: any change to a .github/workflows/*.yml file 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.

Rule: For any .github/workflows/ change, 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 workflow change reach main via the routine develop→main release merge.

This bites most often with dependabot bumps that touch workflow files (e.g. an actions/checkout major bump) β€” dependabot opens them against develop. When a dependabot PR (or any PR) touches .github/workflows/, cherry-pick just the workflow hunk into a fresh main-cut PR and merge that first, rather than letting the change reach develop. (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 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. (Pattern: PR #1318 β€” actions/checkout bump.)
  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

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. Using CURRENT.md caused duplicate entries in beta.94 (items from beta.93 re-listed).

# 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

3. Create Release PR

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

4. Merge Release PR

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

⚠️ Wait for every CI check to be green per .claude/rules/00-critical.md "Never Merge PRs With Red CI". Release PRs are not exempt β€” claude-review is the second-look on the full release delta and infra failures (binary not found, missing secret) need gh run rerun <run-id> --failed before merge, not "merge through it."

# βœ… 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, beta.126 / PR #1120), 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.

5. Run Prisma Migration (if release includes one)

Prod auto-deploys on merge to main (see tzurot-deployment skill), but schema changes do NOT auto-apply. Run the migration immediately after merge to minimize the window where new code runs against the pre-migration schema:

pnpm ops db:migrate --env prod

For backward-compatible migrations (column type widening, additive indexes), the small window is low-risk because old code can still read the new schema. For breaking schema changes, sequence carefully β€” either run the migration first if old code can tolerate the new schema, or coordinate a brief maintenance window.

Skip this step if the release contains no migration. Verify with git log v<previous>..HEAD --no-merges -- prisma/migrations/ β€” empty output means no migration to run.

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. Tag and Push

Git tag + GitHub Release are separate things. The merge does neither β€” you must:

# On main (after the pull above)
git checkout main
git tag -a v3.0.0-beta.XX -m "Release v3.0.0-beta.XX: Description"
git push origin v3.0.0-beta.XX

8. Create GitHub Release

The tag is git metadata; the GitHub Release is the user-facing page with notes. Both are needed. Use the same release notes prepared in step 2.

Release-channel convention: the newest release holds GitHub's latest badge (prerelease=false); every older beta is prerelease=true. The latest badge is how users/tooling find the current build, and a prerelease can't hold it. This is NOT automatic β€” without the two commands below the newest tag stays a plain release and the previous one keeps latest, so both steps are required each release.

# Create the newest release as `latest` (NOT --prerelease β€” they're mutually exclusive)
gh release create v3.0.0-beta.XX \
  --title "v3.0.0-beta.XX" \
  --latest \
  --notes "$(cat <<'EOF'
### Bug Fixes
- ...

### Improvements
- ...

**Full Changelog**: https://github.com/lbds137/tzurot/compare/v3.0.0-beta.YY...v3.0.0-beta.XX
EOF
)"

# Flip the PREVIOUS newest to prerelease. Creating vXX as --latest removes vYY's
# latest badge but leaves it a plain release, so this explicit flip is required.
gh release edit v3.0.0-beta.YY --prerelease

Verify with 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. Older betas (beyond the immediately-previous) are already prerelease from past releases β€” only the immediately-previous tag needs flipping each time. Do NOT mark the newest tag --prerelease; that's the old (wrong) instruction this step replaces.

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. Failing to do this caused duplicate entries in the beta.94 release notes (items from beta.93 were re-listed because CURRENT.md still tracked them).

GitHub CLI (Use ops instead of broken gh pr edit)

pnpm ops gh:pr-info 478        # Get PR info
pnpm ops gh:pr-reviews 478     # Get reviews
pnpm ops gh:pr-comments 478    # Get line comments
pnpm ops gh:pr-edit 478 --title "New title"

References

  • GitHub CLI: docs/reference/GITHUB_CLI_REFERENCE.md
  • Safety rules: .claude/rules/00-critical.md
Install via CLI
npx skills add https://github.com/lbds137/tzurot --skill tzurot-git-workflow
Repository Details
star Stars 8
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator