| name | github |
| description | Complete GitHub workflow — auth, PRs, code review, issues, repo management. Works with gh CLI or git+curl fallback. |
| version | 2.0.0 |
| tags | ["GitHub","Git","Pull-Requests","Code-Review","Issues","Repositories","CI/CD"] |
GitHub
Unified GitHub workflow. Each section shows gh CLI first, then git + curl fallback for environments without gh installed. All sections share the same auth bootstrap (see §Auth).
Detecting Auth Method
At the start of any GitHub workflow, determine what's available:
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
AUTH=*** AUTH=***
fi
if [ -z "$GITHUB_TOKEN" ]; then
if [ -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 | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|')
fi
fi
gh CLI first, Python urllib last
When fetching GitHub data (issue lists, PR lists, commit search, repo metadata): default to gh — gh issue list --search, gh pr list --search, gh search commits, gh api repos/.... Don't reach for python3 -c "import urllib.request; json.load(...)" to hit the GitHub API. Reasons:
gh search <type> ... --json ... --jq ... is one command. Python + urllib is a 5-line subprocess with quoting hazards.
gh handles auth, pagination, rate-limit headers, and error formatting for you.
- Subshelling output into
python3 -c triggers Hermes' smart-approval gate on every call.
Use raw curl/python only when gh cannot do what you need (e.g. custom GraphQL queries, multi-endpoint scripts that need fine-grained control). For "show me upstream issues about X", gh issue list --repo upstream --search X --json ... --jq ... is the right tool.
Within gh, prefer structured output (--json + --jq) over grep-ing human output — same reason, plus it's parseable programmatically.
Extract owner/repo from git remote:
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)
§Auth — Authentication Setup
Git-Only (HTTPS Token)
- User creates a personal access token at https://github.com/settings/tokens with
repo, workflow, read:org scopes
- Set up credential helper:
git config --global credential.helper store
- Do a test operation that triggers auth — username is their GitHub username, password is the token
- Configure identity:
git config --global user.name "Name" and git config --global user.email "email"
Git-Only (SSH)
ssh-keygen -t ed25519 -C "email" -f ~/.ssh/id_ed25519 -N ""
cat ~/.ssh/id_ed25519.pub
ssh -T git@github.com
git config --global url."git@github.com:".insteadOf "https://github.com/"
gh CLI Auth
echo "<token>" | gh auth login --with-token
gh auth setup-git
Helper script
scripts/gh-env.sh — reusable auth detection script.
Troubleshooting
| Problem | Solution |
|---|
git push asks for password | Use personal access token as password, not GitHub password |
gh commands return EOF on WSL with proxy | Prefix with no_proxy='*' |
| Credentials not persisting | Check git config --global credential.helper |
§PR — Pull Request Workflow
Complete PR Lifecycle
git checkout main && git pull origin main
git checkout -b feat/description
git add <files>
git commit -m "feat: short description
- Detail 1
- Detail 2"
git push -u origin HEAD
gh pr create --title "feat: ..." --body "...\nCloses #42"
Branch naming: feat/, fix/, refactor/, docs/, ci/
Commit format: See references/conventional-commits.md
Monitoring CI
gh pr checks
gh pr checks --watch
Auto-fix CI loop: Check status → read failure logs → fix code → commit+push → re-check (max 3x). See references/ci-troubleshooting.md.
Merging
gh pr merge --squash --delete-branch
gh pr merge --auto --squash --delete-branch
WSL Proxy Pitfall
When running gh in WSL with local HTTP proxy (http://127.0.0.1:7890), prefix all gh and curl GitHub API calls with no_proxy='*'. Git operations (push/pull/clone) work fine — only gh's REST/GraphQL API calls need this.
PR Body Templates
templates/pr-body-feature.md
templates/pr-body-bugfix.md
§Review — Code Review
Pre-Push Review (git only)
git diff main...HEAD --stat
git diff main...HEAD | grep -n "print(\|console\.log\|TODO\|FIXME\|debugger"
PR Review Workflow
- Gather context:
gh pr view N, gh pr diff N --name-only, gh pr checks N
- Check out locally:
git fetch origin pull/N/head:pr-N && git checkout pr-N
- Review against the checklist below
- Post review:
gh pr review N --approve --body "LGTM!"
gh pr review N --request-changes --body "See inline comments."
Review Checklist
- Correctness: Does it do what it claims? Edge cases?
- Security: No hardcoded secrets, input validation, no SQLi/XSS
- Code Quality: Clear naming, DRY, single responsibility
- Testing: New paths tested? Happy + error cases?
- Performance: No N+1 queries, appropriate caching
- Documentation: Public APIs documented, README updated
Output format: See references/review-output-template.md
§Issues — Issue Management
View and Create
gh issue list --state open --label "bug"
gh issue view 42
gh issue create --title "..." --body "..." --label "bug,backend" --assignee "username"
Manage
gh issue edit 42 --add-label "priority:high" --add-assignee @me
gh issue comment 42 --body "..."
gh issue close 42 --reason "not planned"
gh issue reopen 42
Templates
templates/bug-report.md
templates/feature-request.md
§Repo — Repository Management
Common Operations
gh repo create my-project --public --clone
git clone https://github.com/owner/repo.git
git clone --depth 1 https://github.com/owner/repo.git
gh repo fork owner/repo --clone
git remote add upstream https://github.com/owner/repo.git
gh secret set API_KEY --body "value"
gh secret list
gh release create v1.0.0 --title "v1.0.0" --generate-notes
gh workflow list
gh run list --limit 10
gh run view <ID> --log-failed
gh run rerun <ID>
Fork Sync
Quick Diagnostics
Before syncing, check the fork's relationship to upstream:
no_proxy='*' gh api repos/$OWNER/$REPO/compare/NousResearch:main...main \
--jq '{status, ahead_by, behind_by}'
no_proxy='*' gh api repos/$OWNER/$REPO/merge-upstream -X POST -f branch=main
Manual Sync (local clone)
git fetch upstream && git rebase upstream/main && git push --force-with-lease origin main
Use rebase + --force-with-lease instead of merge to keep history clean when the fork has its own commits (workflows, patches). The fork's commits stay on top.
Check branch protection first — force push fails on protected branches:
gh api repos/$OWNER/$REPO/branches/main/protection
Diagnosing "Sync fails repeatedly"
Before debugging further, identify whether the failure is:
- Network / auth — fetch log says "could not resolve" or "401". Fix token.
- Workflow-level error — see
references/ci-troubleshooting.md.
- Merge conflict storm — log shows dozens of
CONFLICT (add/add) lines. Read below.
Conflict storm means the fork was many weeks/months behind. Three or fewer conflicts → resolve by hand. Tens to hundreds → reset path. See "Reset path when conflicts are unrecoverable" below.
Verifying a workflow YAML fix
After any change to .github/workflows/*.yaml:
gh workflow run <workflow-file> -R OWNER/REPO
sleep 15 && gh run view $(gh run list -R OWNER/REPO --workflow=<file> --limit 1 --json databaseId -q '.[0].databaseId') --log-failed
This is the fastest loop: edit YAML → push → gh workflow run → log shows real cause. Cheaper than rerunning CI in your head.
Also prettier --check it if the repo has prettier configured — many Next.js / Vercel-deployed repos run prettier --check in CI over the entire repo including .github/workflows/*.yaml. Cron strings must be single-quoted ('0 0 * * *', not "0 0 * * *") to match the project's .prettierrc.json singleQuote: true. Format drift = CI fails despite correct semantics.
npx prettier --check .github/workflows/<file>.yaml
Upstream repo moved / got renamed
Bug-fix path: search upstream first, don't fork-patch based on guesswork
When investigating a fork-only build failure or runtime bug that might be an upstream regression:
- Search upstream issues before proposing a fork patch.
gh issue list -R <upstream> --search "<exact error fragment>", gh pr list --state all -R <upstream> --search "<error fragment>". If a closed issue + merged PR exists, the canonical fix path is to wait for it OR cherry-pick onto the fork.
- Search upstream commit history.
gh search commits "<error fragment>" -R <upstream> --json sha,commit. If 0 results, the issue IS novel — and your fork patch is the first instance, not a duplicate.
- Read the actual error log, not your interpretation.
gh run view <id> --log-failed, curl .../v1/deployments/dpl_<id>/events, or gh pr checks + artifacts. Don't summarize "this is upstream's fault" from a one-line summary field.
- One-line guard on the fork is correct when upstream is silent. Don't add "wait for upstream to fix it" as the default. Upstream maintainers are slow on edge-case schema bugs (Notion block schema edge cases, slow gitignore additions, etc.). A one-line Array.isArray guard on the fork is the lazy fix: small diff, deletable once upstream ships the proper fix.
The wrong reflex: see build error → assume "upstream must be broken, reverting is safest" → tell user to roll back the fork. That removes the bug from sight but loses all the upgrade. The right reflex is the four steps above.
Conflict-storm fork reset (canonical example)
gh api repos/tangly1024/NotionNext 2>&1 | head -3
If moved, edit the workflow's upstream_sync_repo: to the new path and push. One-line semantic fix, but it does NOT resolve existing conflict accumulation — that still requires manual sync (see "Reset path" below).
Reset path when conflicts are unrecoverable
When git rebase upstream/main (or git merge upstream/main) produces 20+ add/add conflicts across themes/, components/, blog.config.js, package.json, the fork and upstream have diverged enough that hand-merging is no longer the right move. Branch protection rules or auto-release workflows on the fork will reject naive merges.
Pattern: identify fork-only commits, then git reset --hard upstream/main.
MERGE_BASE=$(git merge-base origin/main upstream/main)
git log origin/main ^$MERGE_BASE --oneline --no-merges
cp blog.config.js /tmp/blog.config.js.backup
git fetch upstream main
git reset --hard upstream/main
cp /tmp/blog.config.js.backup blog.config.js
git cherry-pick <fork-only-sha1> <fork-only-sha2>
git push --force-with-lease origin main
Before reset — check branch protection as in the prior section.
This is destructive. Get user confirmation before running it against any repo that's not a personal fork. For their own fork with no active downstream consumers, prefer reset over drowning them in 200+ conflict markers.
Safer alternative: reset onto a NEW branch and open a PR — when the fork is the live deployment (e.g. a Vercel deploy hooked to main, or a personal blog with active traffic), force-pushing main is risky even with --force-with-lease. Reset onto a new orphan branch and let the user review-merge.
When the orphan PR also conflicts — promote reset-upstream branch to default
If the resulting PR against the divergent main shows mergeable: CONFLICTING with 1000+ changed files, hand-merging is again not viable. Promote the reset branch to the new default, then replace main via the API in four steps. GitHub's REST API refuses DELETE on the default branch (HTTP 422 "Cannot delete the default branch"), so you cannot delete main first — you must point default elsewhere, delete, recreate.
no_proxy='*' gh api repos/$OWNER/$REPO -X PATCH -f default_branch=reset-upstream
no_proxy='*' gh api repos/$OWNER/$REPO/git/refs/heads/main -X DELETE
SHA=$(no_proxy='*' gh api repos/$OWNER/$REPO/git/refs/heads/reset-upstream --jq '.object.sha')
no_proxy='*' gh api repos/$OWNER/$REPO/git/refs -X POST -f ref=refs/heads/main -f sha="$SHA"
no_proxy='*' gh api repos/$OWNER/$REPO/git/refs/heads/reset-upstream -X DELETE
no_proxy='*' gh api repos/$OWNER/$REPO -X PATCH -f default_branch=main
After this, main HEAD == reset branch HEAD; the conflicting PR auto-closes (base removed). Run the sync workflow once to confirm the reset repo is in a clean sync state:
gh workflow run "Upstream Sync" -R $OWNER/$REPO
sleep 15 && gh run view $(gh run list -R $OWNER/$REPO --workflow="Upstream Sync" --limit 1 --json databaseId -q '.[0].databaseId') -R $OWNER/$REPO --json status,conclusion
Local clone fails or PR diff explodes — drive everything via gh API
For forks that timeout on git fetch (large repos, shallow history, WSL+proxy), or whose PR diff exceeds GitHub's 300-file limit (HTTP 406: PullRequest.diff too_large):
- Skip the local clone. Use
gh api .../git/refs/heads/X --jq '.object.sha' to read SHAs, gh api .../git/refs -X POST to create refs, gh api .../contents/path -X PUT -f base64content to write files.
- For PR review, dump files via
gh pr view N --json files and diff --name-only the reset branch against upstream git diff upstream/main origin/reset-upstream --stat — gives a clean overlay view even when the fork main and reset-upstream have wildly diverged histories.
- For the conflict-storm reset itself, read fork-only files via
git show origin/main:<path> into /tmp, then cp them back on top of git checkout --orphan ... upstream/main. No full clone needed.
merge-base returns nothing with shallow clones against huge upstream
When git fetch --depth=N upstream main fails with Could not read <sha> / Failed to traverse parents (too shallow for that root) but you cannot --unshallow because the upstream is too big, swap who you shallow: clone the fork side with enough history (git fetch --unshallow origin main works because the fork is small), then run git merge-base origin/main upstream/main. The merge-base only needs enough fork-side history to reach a common ancestor.
git checkout --orphan reset-upstream upstream/main
git pull upstream main
cp /tmp/blog.config.js.backup blog.config.js
git add -A && git commit -m "chore(rebase): reapply fork overlays on upstream <version>"
git push origin reset-upstream
gh pr create --repo OWNER/REPO --base main --head reset-upstream \
--title "chore(rebase): reapply fork overlays on upstream <version>" \
--body "$(cat <<'EOF'
## 背景
<why reset was needed — usually 1.5+ months of conflict accumulation>
## Reapplied N fork-only files
- blog.config.js (personal config)
- conf/*.config.js (overrides)
- public/css/custom.css, public/favicon.ico
## 砍掉的 fork 历史
<list dropped commits / sync-merge traces>
> 没动 main。review 完 diff 觉得 OK 再 merge。
EOF
)"
The PR diff will show thousands of files because the fork main and upstream main have diverged histories — that's normal, not a bug. Add a review comment explaining how to see the real overlay:
PR diff 大但是正常。真实 overlay 只有 11 个文件:
git fetch origin reset-upstream && git diff upstream/main origin/reset-upstream --stat
GitHub 跟分叉的 main 三方 diff 会把历史里"消失"的所有文件列成 deletion —— 不影响实际 review。
After merge, manually trigger the sync workflow once to verify the reset-to-upstream state is now sync-clean:
gh workflow run "Upstream Sync" -R OWNER/REPO
Auto-release workflows clash with manual pushes
Many forks enable bump-version-on-main (or similar) auto-bump workflows. Result: between your git fetch and your git push, a release commit lands on origin/main, and your push is rejected for non-fast-forward.
Always pull --rebase before push on any fork with scheduled auto-bump workflows:
git pull --rebase origin main && git push origin main
Or check with git fetch origin main first, then rebase — either way the local branch must fast-forward cleanly.
Automated Sync via GitHub Actions
For forks that should stay continuously synced, use a scheduled workflow. Create it directly on the fork via the API (no local clone needed):
cat > /tmp/sync.yml << 'EOF'
name: Sync Upstream
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
if: github.repository == 'OWNER/REPO'
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: |
git remote add upstream https://github.com/UPSTREAM_OWNER/UPSTREAM_REPO.git
git fetch upstream main
BEHIND=$(git rev-list --count HEAD..upstream/main)
if [ "$BEHIND" -eq 0 ]; then
echo "Already up to date"
exit 0
fi
echo "Syncing $BEHIND commit(s)..."
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git rebase upstream/main
git push --force-with-lease origin main
EOF
CONTENT=$(base64 -w0 /tmp/sync.yml)
no_proxy='*' gh api --method PUT \
repos/OWNER/REPO/contents/.github/workflows/sync-upstream.yml \
-f message="ci: add upstream auto-sync" \
-f content="$CONTENT" \
-f branch="main"
gh workflow run sync-upstream.yml -R OWNER/REPO
sleep 15 && gh run view $(gh run list -R OWNER/REPO --workflow sync-upstream.yml --limit 1 --json databaseId -q '.[0].databaseId') -R OWNER/REPO --log | grep -E "(Already|Syncing)"
Default branch not protected is required on the fork for --force-with-lease. The if: github.repository guard prevents the workflow from accidentally running in the upstream repo.
Creating/Updating Files via gh API
When you need to write a file to a repo without cloning it locally:
CONTENT=$(base64 -w0 file.txt)
gh api --method PUT repos/OWNER/REPO/contents/path/to/file \
-f message="commit message" -f content="$CONTENT" -f branch="main"
SHA=$(gh api repos/OWNER/REPO/contents/path/to/file --jq '.sha')
gh api --method PUT repos/OWNER/REPO/contents/path/to/file \
-f message="update" -f content="$CONTENT" -f sha="$SHA" -f branch="main"
See references/fork-sync-workflow.yml for the ready-to-use template.
GitHub Pages Subdirectory Pitfall
When deploying to https://<user>.github.io/<repo>/, asset paths MUST include the repo name as a prefix. For Next.js: basePath: "/repo-name" in next.config.ts.
API Cheatsheet
See references/github-api-cheatsheet.md for full REST API reference.
Quick Reference
| Action | gh | curl |
|---|
| List PRs | gh pr list | GET /repos/{o}/{r}/pulls |
| View PR diff | gh pr diff N | git diff main...HEAD |
| Create PR | gh pr create ... | POST /repos/{o}/{r}/pulls |
| Merge PR | gh pr merge --squash | PUT /repos/{o}/{r}/pulls/N/merge |
| List issues | gh issue list | GET /repos/{o}/{r}/issues |
| Close issue | gh issue close N | PATCH /repos/{o}/{r}/issues/N |
| Create repo | gh repo create name | POST /user/repos |
| Fork | gh repo fork o/r | POST /repos/o/r/forks |
| Create release | gh release create v1 | POST /repos/o/r/releases |
| List workflows | gh workflow list | GET /repos/o/r/actions/workflows |
| Rerun CI | gh run rerun ID | POST /repos/o/r/actions/runs/ID/rerun |
| Set secret | gh secret set KEY | PUT /repos/o/r/actions/secrets/KEY |