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 [ -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
The gh pr merge Command — What Works and What Doesn't
# ✓ This works: squash + delete branch (non-interactive on most gh versions)
gh pr merge N --squash --delete-branch
# ✗ This flag DOES NOT EXIST — don't use it:
gh pr merge N --no-editor # unknown flag — will error
# ✓ For fully non-interactive programmatic merge, use the REST API directly:
gh api --method PUT repos/$OWNER/$REPO/pulls/$PR_NUMBER/merge \
-f merge_method=squash -f delete_branch=true
# Returns: {"merged": true, "message": "Pull Request successfully merged"}
Key insight: When scripting batch merges (e.g., merging all dependabot CI bumps), prefer the REST API over
gh pr mergeCLI. The API call is deterministic, returns JSON, and has no flag compatibility issues across gh versions.
Merge with gh (CLI)
# 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
# Merge when PR is already approved from a reviewer who is also admin
gh pr merge --admin --merge
Note: When the PR already has approval and you want to merge immediately without prompts, use
--admin --merge. Faster than--squash --delete-branchwhich may prompt depending on gh version.
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
Merge methods: "merge" (merge commit), "squash", "rebase"
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)
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 |
Pitfalls
Branch Protection: Never git push to Protected Branches
Even with admin privileges and gh pr merge --admin --merge access, git push origin main will always fail on a protected branch (e.g., main in clowlove/hermes-house). The rejection happens at the git protocol level before any auth check.
Correct pattern:
# WRONG — will be rejected
git checkout main
git merge feature/xxx
git push origin main # ✗ rejected
# RIGHT — go through PR
git checkout -b feature/xxx
git add . && git commit -m "..."
git push -u origin HEAD
gh pr create --title "feat: ..." --body "..."
gh pr review N -a # if you need to approve your own PR
gh pr merge N --admin --merge
GitHub App Creation Is UI-Only
GitHub Apps cannot be created via API. The createApp GraphQL mutation does not exist. You must use the web UI at https://github.com/settings/apps/new. After creation you get:
- App ID (number) — used in JWT
issclaim - Client ID / Client Secret — for OAuth
- Private key (.pem) — generated in the UI, used to sign JWTs
The webhook URL can be a placeholder during creation and updated later in App settings.
GitHub Actions Workflow Debugging — Empty Commits and PR Failures
When a workflow step fails on "Create Pull Request" and logs are unavailable or uninformative:
Common root cause: The workflow script made no actual content changes. git diff --staged --quiet succeeds (no diff), so git commit is skipped, and the branch has no new commits. When github-script tries to create a PR for an empty branch, it silently fails.
Diagnosis via API:
# Get run → job → step details
curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs?per_page=5" | \
python3 -c "import sys,json; d=json.load(sys.stdin); [print(f\"{r['id']} | {r['conclusion'] or r['status']} | {r['name']}\") for r in d['workflow_runs'][:3]]"
# Get jobs for a specific run
curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs/$RUN_ID/jobs" | \
python3 -c "import sys,json; [print(f\" {j['name']}: {j['conclusion']}\") for j in json.load(sys.stdin)['jobs']]"
# Get step-level details to find the exact failing step
curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs/$RUN_ID/jobs" | \
python3 -c "import sys,json; jobs=json.load(sys.stdin)['jobs']; [print(f\" Step {s['number']}: {s['name']} - {s['conclusion']}\") for j in jobs for s in j['steps'] if s['conclusion'] != 'success']"
Prevention pattern — check for empty commits before pushing:
git add -A
if git diff --staged --quiet; then
echo "No changes to commit"
exit 1 # Don't proceed to PR step — branch would be empty
fi
git commit -m "Auto-update journal"
git push -u origin "$BRANCH"
⚠️ Critical: exit 1 vs exit 0 depends on workflow type:
- Interactive / on-demand workflows (
workflow_dispatch,push):exit 1is correct — if a human triggers the workflow and nothing changed, that's a failure worth surfacing. - Automated scheduled workflows (
schedulecron): useexit 0— a "no changes to commit" result is normal and expected behavior (nothing new happened today). Usingexit 1here will cause the CI step to fail even when there's no actual problem.
# Example: automated weekly job (use exit 0 for no-change)
- name: Create Branch and Commit
run: |
BRANCH="auto/journal-update-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH"
git add -A
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0 # ✓ Correct for scheduled workflows
fi
git commit -m "Auto-update journal"
git push -u origin "$BRANCH"
# Example: interactive fix job (exit 1 is fine here)
if git diff --staged --quiet; then
echo "No changes to commit"
exit 1 # ✓ Correct for on-demand workflows
fi
Failure symptom of wrong exit code in scheduled workflows: the "Create Branch and Commit" step fails even though the prior "Generate Weekly Summary" step succeeded. Check step timing — if "Generate Weekly Summary" completed in <1s, it likely produced no file changes. The fix is exit 0, not exit 1.
Fix for workflows that only update timestamps: If a workflow only modifies timestamps (e.g., *最后更新:2026-05-12 16:40 UTC*), the file content may be identical after regex substitution if the regex doesn't match. The actual file must change — fetch real data (from GitHub API, external sources) and add new content before committing.
Use gh CLI Over github-script for PR Operations
actions/github-script is unreliable for PR creation and merge — it can fail without clear error messages. Prefer gh pr create + gh pr merge in a run step:
Git Push with Token-in-URL Fails
When the git remote URL embeds a token (e.g., https://github.com/sponsors@github.com/owner/repo.git), git push may fail with:
fatal: could not read Password for 'https://github.com/sponsors@github.com': No such device or address
Workarounds (pick one):
# Option 1: Remove token from remote URL — let gh handle auth
git remote set-url origin https://github.com/owner/repo.git
git push -u origin HEAD
# Option 2: Skip git push entirely — use gh pr create directly (preferred)
git checkout -b evolution/2026-05-18
git add . && git commit -m "docs: update evolution log"
gh pr create --repo owner/repo --base main --head evolution/2026-05-18 \
--title "docs: update evolution log" --body "..."
Rule: Prefer gh pr create over git push + separate PR creation when the remote requires token auth. gh manages its own credential flow reliably; embedding tokens in URLs is fragile.
- name: Create and Merge Pull Request
run: |
gh pr create
--title "Auto-update journal $(date +%Y-%m-%d)"
--body "🤖 Automated journal update"
--head "$BRANCH"
--base main
--admin
&& gh pr merge --squash --auto || echo "PR merge failed or already merged" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: ${{ env.BRANCH }}
Also ensure explicit job permissions:
```yaml
jobs:
update-journal:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write