Imported from jochst/dotfiles (
.hermes/skills/github/github-pr-workflow/SKILL.md). Install upstream withnpx skills add jochst/dotfiles --skill github-pr-workflow. Copyright stays with the author (MIT).
GitHub Pull Request Workflow
Complete guide for managing the PR lifecycle. Each section shows the gh way first, then the git + curl fallback for machines without gh.
Prerequisites
- Authenticated with GitHub (see
github-authskill) - Inside a git repository with a GitHub remote
Quick Auth Detection
# Determine which method to use throughout this workflow
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
AUTH="gh"
else
AUTH="git"
# Ensure we have a token for API calls
if [ -z "$GITHUB_TOKEN" ]; then
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|')
fi
fi
fi
echo "Using: $AUTH"
Extracting Owner/Repo from the Git Remote
Many curl commands need owner/repo. Extract it from the git remote:
# Works for both HTTPS and SSH remote URLs
REMOTE_URL=$(git remote get-url origin)
OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's|.*github\.com[:/]||; s|\.git$||')
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
echo "Owner: $OWNER, Repo: $REPO"
1. Branch Creation
This part is pure git — identical either way:
# Make sure you're up to date
git fetch origin
git checkout main && git pull origin main
# Create and switch to a new branch
git checkout -b feat/add-user-authentication
Branch naming conventions:
feat/description— new featuresfix/description— bug fixesrefactor/description— code restructuringdocs/description— documentationci/description— CI/CD changes
2. Making Commits
Use the agent's file tools (write_file, patch) to make changes, then commit:
# Stage specific files
git add src/auth.py src/models/user.py tests/test_auth.py
# Commit with a conventional commit message
git commit -m "feat: add JWT-based user authentication
- Add login/register endpoints
- Add User model with password hashing
- Add auth middleware for protected routes
- Add unit tests for auth flow"
Commit message format (Conventional Commits):
type(scope): short description
Longer explanation if needed. Wrap at 72 characters.
Types: feat, fix, refactor, docs, test, ci, chore, perf
3. Pushing and Creating a PR
Push the Branch (same either way)
git push -u origin HEAD
Create the PR
With gh:
gh pr create \
--title "feat: add JWT-based user authentication" \
--body "## Summary
- Adds login and register API endpoints
- JWT token generation and validation
## Test Plan
- [ ] Unit tests pass
Closes #42"
Options: --draft, --reviewer user1,user2, --label "enhancement", --base develop
With git + curl:
BRANCH=$(git branch --show-current)
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/$OWNER/$REPO/pulls \
-d "{
\"title\": \"feat: add JWT-based user authentication\",
\"body\": \"## Summary\nAdds login and register API endpoints.\n\nCloses #42\",
\"head\": \"$BRANCH\",
\"base\": \"main\"
}"
The response JSON includes the PR number — save it for later commands.
To create as a draft, add "draft": true to the JSON body.
4. Monitoring CI Status
Check CI Status
With gh:
# One-shot check
gh pr checks
# Watch until all checks finish (polls every 10s)
gh pr checks --watch
With git + curl:
# Get the latest commit SHA on the current branch
SHA=$(git rev-parse HEAD)
# Query the combined status
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/status \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
print(f\"Overall: {data['state']}\")
for s in data.get('statuses', []):
print(f\" {s['context']}: {s['state']} - {s.get('description', '')}\")"
# Also check GitHub Actions check runs (separate endpoint)
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/check-runs \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for cr in data.get('check_runs', []):
print(f\" {cr['name']}: {cr['status']} / {cr['conclusion'] or 'pending'}\")"
Poll Until Complete (git + curl)
# Simple polling loop — check every 30 seconds, up to 10 minutes
SHA=$(git rev-parse HEAD)
for i in $(seq 1 20); do
STATUS=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/status \
| python3 -c "import sys,json; print(json.load(sys.stdin)['state'])")
echo "Check $i: $STATUS"
if [ "$STATUS" = "success" ] || [ "$STATUS" = "failure" ] || [ "$STATUS" = "error" ]; then
break
fi
sleep 30
done
5. Auto-Fixing CI Failures
When CI fails, diagnose and fix. This loop works with either auth method.
Step 1: Get Failure Details
With gh:
# List recent workflow runs on this branch
gh run list --branch $(git branch --show-current) --limit 5
# View failed logs
gh run view <RUN_ID> --log-failed
With git + curl:
BRANCH=$(git branch --show-current)
# List workflow runs on this branch
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs?branch=$BRANCH&per_page=5" \
| python3 -c "
import sys, json
runs = json.load(sys.stdin)['workflow_runs']
for r in runs:
print(f\"Run {r['id']}: {r['name']} - {r['conclusion'] or r['status']}\")"
# Get failed job logs (download as zip, extract, read)
RUN_ID=<run_id>
curl -s -L \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/actions/runs/$RUN_ID/logs \
-o /tmp/ci-logs.zip
cd /tmp && unzip -o ci-logs.zip -d ci-logs && cat ci-logs/*.txt
Step 2: Fix and Push
After identifying the issue, use file tools (patch, write_file) to fix it:
git add <fixed_files>
git commit -m "fix: resolve CI failure in <check_name>"
git push
Step 3: Verify
Re-check CI status using the commands from Section 4 above.
Auto-Fix Loop Pattern
When asked to auto-fix CI, follow this loop:
- Check CI status → identify failures
- Read failure logs → understand the error
- Use
read_file+patch/write_file→ fix the code git add . && git commit -m "fix: ..." && git push- Wait for CI → re-check status
- Repeat if still failing (up to 3 attempts, then ask the user)
6. Merging
With gh:
# Squash merge + delete branch (cleanest for feature branches)
gh pr merge --squash --delete-branch
# Enable auto-merge (merges when all checks pass)
gh pr merge --auto --squash --delete-branch
With git + curl:
PR_NUMBER=<number>
# Merge the PR via API (squash)
curl -s -X PUT \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER/merge \
-d "{
\"merge_method\": \"squash\",
\"commit_title\": \"feat: add user authentication (#$PR_NUMBER)\"
}"
# Delete the remote branch after merge
BRANCH=$(git branch --show-current)
git push origin --delete $BRANCH
# Switch back to main locally
git checkout main && git pull origin main
git branch -d $BRANCH
6.1 Resolving PR Merge Conflicts Locally
When GitHub reports "This branch has conflicts that must be resolved", use the local working tree to resolve them cleanly — do not use the GitHub web editor for anything beyond a one-line conflict. The web editor has no context on which version is semantically correct, and picking either side blindly is a common source of broken merges.
Step-by-step
# 1. Fetch the latest target branch and switch to the conflicted feature branch
git fetch origin
git checkout <feature-branch>
# 2. Merge the target branch into the feature branch (no commit yet)
git merge origin/<target> --no-commit
# 3. Identify conflicted files
git diff --name-only --diff-filter=U
# 4. Resolve each conflict using the agent's file tools (patch/write_file), then
# verify no conflict markers remain
grep -R "<<<<<<<" <conflicted-file> && echo "Still has conflict markers" || echo "Clean"
# 5. Stage the resolved file(s) and complete the merge
git add <conflicted-file>
git commit -m "Merge branch '<target>' into <feature-branch>"
# 6. Push the updated branch (force-with-lease is safe if the previous push was
# the tip of this PR branch)
git push origin <feature-branch> --force-with-lease
Deciding which side to keep
- Keep the feature branch's version when the conflict is in code the feature branch created or fundamentally reworked (e.g., the schema-fix entry point, the new DB query, the refactored feature builder).
- Keep the target branch's version when the target branch introduced a new canonical pattern or infrastructure that must not be overridden (e.g., CI workflow files, new shared abstractions that other branches already depend on).
- Merge both when each side contributed a valid piece of the same function, class, or import block. Remove the conflict markers and combine the useful imports and logic, then run the relevant tests.
- Drop unused imports after resolving; merge conflicts frequently leave
behind imports that are no longer referenced (e.g.,
from datetime import dateadded by one side but never used after the resolution).
Post-resolution verification
After resolving, always run the verification set that covers the changed files:
uv run pytest <relevant-test-files> -q
uv run ruff check <changed-files>
If the conflict was in a file that is mostly structural (e.g., __init__.py,
registry.py, a CLI entry point), also run a quick import smoke test:
uv run python -c "from <module.path> import <changed_symbol>"
Avoiding the merge-conflict trap
- Do not accept GitHub's web UI buttons ("Accept current change" / "Accept incoming change") for non-trivial conflicts. They are fine for a single-line conflict where the semantics are obvious, but dangerous for multi-line imports, function bodies, or data pipeline entry points.
- Do not rebase the whole branch unless the branch is intentionally short and you want a linear history. Replaying every commit against a changed target can introduce conflicts in commits that predate the actual PR diff.
- Do not leave conflict markers in the committed file. A
grepfor<<<<<<<is a cheap final safety check.
Merge methods: "merge" (merge commit), "squash", "rebase"
Case Study: Import-Block Conflict with a Canonical Entry Point
A common conflict pattern is when the target branch imports lower-level
registry/builder functions directly, while the feature branch imports a
higher-level canonical entry point (e.g., build_mlb_features) that already
uses the registry internally.
Example:
# Target branch
from sports_juice_ml.ml.features.registry import get_builder as get_feature_builder
from sports_juice_ml.ml.features.mlb import _load_batting_logs, _load_pitching_logs
from sports_juice_ml.ml.training.trainer import train_pipeline
# Feature branch
from sports_juice_ml.ml.features.build_mlb_features import build_mlb_features
from sports_juice_ml.ml.training.trainer import train_pipeline
Resolution rule:
- Keep the canonical entry point (
build_mlb_features) when it already delegates to the registry internally. This preserves the feature branch's semantics and avoids re-introducing the old lower-level pattern. - Drop the duplicate path setup and any imports that become unused after the
resolution (e.g.,
from datetime import datethat one side added but the merged code doesn't reference). - Verify the merge result with
ruff checkand the targeted test suite, then amend the merge commit if cleanup was needed before pushing.
After resolution, the imports should be clean and minimal:
from sports_juice_ml.ml.features.build_mlb_features import build_mlb_features
from sports_juice_ml.ml.training.trainer import train_pipeline
This pattern shows up frequently in ML/ingestion repos where the feature branch introduces a new orchestration entry point while master has direct builder usage.
Enable Auto-Merge (curl)
# Auto-merge requires the repo to have it enabled in settings.
# This uses the GraphQL API since REST doesn't support auto-merge.
PR_NODE_ID=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER \
| python3 -c "import sys,json; print(json.load(sys.stdin)['node_id'])")
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/graphql \
-d "{\"query\": \"mutation { enablePullRequestAutoMerge(input: {pullRequestId: \\\"$PR_NODE_ID\\\", mergeMethod: SQUASH}) { clientMutationId } }\"}"
7. Complete Workflow Example
# 1. Start from clean main
git checkout main && git pull origin main
# 2. Branch
git checkout -b fix/login-redirect-bug
# 3. (Agent makes code changes with file tools)
# 4. Commit
git add src/auth/login.py tests/test_login.py
git commit -m "fix: correct redirect URL after login
Preserves the ?next= parameter instead of always redirecting to /dashboard."
# 5. Push
git push -u origin HEAD
# 6. Create PR (picks gh or curl based on what's available)
# ... (see Section 3)
# 7. Monitor CI (see Section 4)
# 8. Merge when green (see Section 6)
8. Branch Audit & Merge Safety
Before merging any branch, especially one created by another agent or after a long gap, audit it for destructive changes.
Quick Audit Checklist
# Fetch all branches
git fetch origin
# List commits ahead of master/main
BRANCH="feature/some-branch"
git log --oneline origin/master..origin/$BRANCH
# List files changed (with status: A=Added, M=Modified, D=Deleted, R=Renamed)
git diff --name-status origin/master..origin/$BRANCH
# Check for critical file deletions
git diff --name-status origin/master..origin/$BRANCH | grep '^D'
# Check for API/server file changes
git diff --name-status origin/master..origin/$BRANCH | grep -E 'main\.py|app\.py|server|api|Dockerfile|railway|docker-compose'
Red Flags — Do Not Merge Without Review
| Pattern | Risk | Action |
|---|---|---|
D\tsrc/app/api/main.py or similar |
BREAKING — serving layer deleted | Stop. Create PROPOSED.md for user review. |
D\tDockerfile or D\trailway.json |
BREAKING — deployment config deleted | Stop. Cherry-pick only safe commits. |
M\tpyproject.toml with deps removed |
HIGH — production dependencies stripped | Verify fastapi, uvicorn, asyncpg, redis still present. |
Large binary files added (.parquet, .joblib) |
MEDIUM — repo bloat, LFS issues | Check if tracked by Git LFS; reject if not. |
.env files added |
MEDIUM — secrets exposure risk | Reject. Secrets belong in runtime env vars only. |
A\tscripts/sgo_*.py or similar |
LOW — new subsystem | Verify it doesn't conflict with existing modules. |
When to Write PROPOSED.md Instead of Merging
If any red flag is found, stop the merge and create a PROPOSED.md document:
# Create analysis branch for safety
git checkout -b review/audit-$BRANCH origin/master
# Write findings to PROPOSED.md
# (Use write_file tool — see template in references/branch-audit-template.md)
Contents of PROPOSED.md:
- Executive Summary — merge or don't merge
- Critical Breaking Changes — table of deleted/modified critical files
- Branch-Specific Analysis — what each branch does, its theme
- What Would Be Lost on Merge — files that would disappear
- Recommended Path Forward — cherry-pick, new branch, or reconcile
Cherry-Picking Safe Commits
If a branch has both valuable and dangerous commits, cherry-pick the safe ones:
# Start from clean master
git checkout master && git pull origin master
git checkout -b cherrypick/safe-ml-improvements
# Cherry-pick specific commits by hash
# (Only commits that add new files or modify non-critical files)
git cherry-pick -x <commit-hash>
# If a cherry-pick conflicts, abort and skip it
git cherry-pick --abort
# Move to next commit
Comparing Two Branches Against Master
When multiple branches exist (e.g., claude/branch-a and claude/branch-b), check if they are mutually compatible:
# Check if branches touch the same files
git diff --name-only origin/master..origin/branch-a > /tmp/files-a.txt
git diff --name-only origin/master..origin/branch-b > /tmp/files-b.txt
comm -12 <(sort /tmp/files-a.txt) <(sort /tmp/files-b.txt)
# Any output = overlapping files = potential merge conflicts
Preserving Production vs. Training Branches
When a branch removes the serving layer (FastAPI, Dockerfile, etc.) but adds valuable ML/training code, the correct pattern is:
- Keep
masteras production — serving API, deployment configs - Create
training/orchestrationbranch — cherry-pick ML improvements - Merge back only when serving layer is restored
This prevents "training branch accidentally kills production" scenarios.
Useful PR Commands Reference
| Action | gh | git + curl |
|---|---|---|
| List my PRs | gh pr list --author @me |
curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$OWNER/$REPO/pulls?state=open" |
| View PR diff | gh pr diff |
git diff main...HEAD (local) or curl -H "Accept: application/vnd.github.diff" ... |
| Add comment | gh pr comment N --body "..." |
curl -X POST .../issues/N/comments -d '{"body":"..."}' |
| Request review | gh pr edit N --add-reviewer user |
curl -X POST .../pulls/N/requested_reviewers -d '{"reviewers":["user"]}' |
| Close PR | gh pr close N |
curl -X PATCH .../pulls/N -d '{"state":"closed"}' |
| Check out someone's PR | gh pr checkout N |
git fetch origin pull/N/head:pr-N && git checkout pr-N |