# Github Pr Workflow

> GitHub PR lifecycle: branch, commit, open, CI, merge.

- Skill: `clowlove/github-pr-workflow` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add clowlove/github-pr-workflow`
- Raw SKILL.md: https://api.skillmd.com/api/skills/clowlove/github-pr-workflow/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT
- Author: clowlove (https://skillmd.com/u/clowlove)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/clowlove/github-pr-workflow

---


# 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

