GitHub PR lifecycle: branch, commit, open, CI, merge.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add clowlove/Hermes-House --skill github-pr-workflow --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Github Pr Workflow?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/clowlove-github-pr-workflow)More formats (shields.io, HTML) on the badges page.
---
name: github-pr-workflow
description: "GitHub PR lifecycle: branch, commit, open, CI, merge."
version: 1.1.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [GitHub, Pull-Requests, CI/CD, Git, Automation, Merge]
related_skills: [github-auth, github-code-review]
---
# 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-auth` skill)
- Inside a git repository with a GitHub remote
### Quick Auth Detection
```bash
# 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:
```bash
# 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:
```bash
# 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 features
- `fix/description` — bug fixes
- `refactor/description` — code restructuring
- `docs/description` — documentation
- `ci/description` — CI/CD changes
## 2. Making Commits
Use the agent's file tools (`write_file`, `patch`) to make changes, then commit:
```bash
# 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)
```bash
git push -u origin HEAD
```
### Create the PR
**With gh:**
```bash
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:**
```bash
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:**
```bash
# One-shot check
gh pr checks
# Watch until all checks finish (polls every 10s)
gh pr checks --watch
```
**With git + curl:**
```bash
# 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)
```bash
# 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:**
```bash
# 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:**
```bash
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:
```bash
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:
1. Check CI status → identify failures
2. Read failure logs → understand the error
3. Use `read_file` + `patch`/`write_file` → fix the code
4. `git add . && git commit -m "fix: ..." && git push`
5. Wait for CI → re-check status
6. 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
```bash
# ✓ 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 merge` CLI. The API call is deterministic, returns JSON, and has no flag compatibility issues across gh versions.
### Merge with gh (CLI)
```bash
# 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-branch` which may prompt depending on gh version.
**With git + curl:**
```bash
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)
```bash
# 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
```bash
# 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:**
```bash
# 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 `iss` claim
- **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:**
```bash
# 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:**
```bash
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 1` is correct — if a human triggers the workflow and nothing changed, that's a failure worth surfacing.
- **Automated scheduled workflows** (`schedule` cron): **use `exit 0`** — a "no changes to commit" result is normal and expected behavior (nothing new happened today). Using `exit 1` here will cause the CI step to fail even when there's no actual problem.
```yaml
# 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):**
```bash
# 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
```
### Branch Protection: Never `git push` to Protected Branches
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!