| name | github-pr-workflow |
| description | GitHub PR lifecycle: branch, commit, open, CI, merge. Use when this capability is needed. |
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
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
AUTH="gh"
else
AUTH="git"
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:
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:
git fetch origin
git checkout main && git pull origin main
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:
git add src/auth.py src/models/user.py tests/test_auth.py
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
Force-update prohibition for shared branches
Do not rewrite remote history on shared branches (especially main, release branches, or any branch other people may consume). This includes:
git push --force
git push --force-with-lease
- amended commits already pushed to a shared branch
- rebase/squash followed by a forced update
Only perform a force update when the user explicitly requests a force update in the same message and states the target remote and branch. If a pushed commit needs correction and the user did not explicitly authorize a force update, create a normal follow-up commit instead of amending and force-pushing. Dangerous-command approval is not enough by itself; the user instruction must authorize the force update.
Before any push, inspect the current branch and remotes instead of assuming origin is the correct destination:
git branch --show-current
git remote -v
git log --oneline -1
Use this especially when:
- the repo has more than one writable remote
origin points to upstream, while a different remote points to the writable fork/private repo
- the project intentionally allows direct pushes to
main without a PR
If a push to origin fails with permission errors such as:
remote: Permission to <upstream-owner>/<repo>.git denied to <user>.
fatal: unable to access 'https://github.com/<upstream-owner>/<repo>.git/': The requested URL returned error: 403
then do not retry blindly. Diagnose the destination first:
git remote -v
git branch -vv
git rev-parse HEAD
Interpretation:
origin may be the read-only upstream project
- a separate remote such as
private or fork may be the actual writable destination
- the current branch may still be tracking
origin/main even when the user wants the commit pushed elsewhere
Preferred recovery:
git push <remote> HEAD:main
git push private HEAD:main
When reporting status back to the user, include:
- the local commit SHA
- which remote rejected the push
- which alternate remote(s) exist
- whether the branch is ahead of
origin/main only locally
For direct-to-main repositories, prefer an explicit push target so you do not accidentally update the wrong remote or branch:
git push <remote> HEAD:main
Example:
git push private HEAD:main
Only use the explicit direct-to-main form when the repo workflow really allows it; otherwise follow the normal branch + PR flow below.
Verify push actually advanced the intended remote
A successful shell exit from git push is not enough. Read the push output and verify the target state before continuing to downstream side effects such as restart/deploy.
Required checks when a commit/push is expected:
git status --short --branch
git log --oneline -1
git diff --stat HEAD~1 HEAD
Interpretation:
- Output like
oldsha..newsha HEAD -> main means the remote branch advanced.
- Output like
Everything up-to-date is only acceptable when no new commit was expected. If the user expected a new change to be pushed, treat this as a stop signal: find where the change was made, whether it is outside the git repo, whether it was staged/committed, or whether you are in the wrong repository.
- Do not run downstream side effects (restart, deploy, release, notify) after an unexpected
Everything up-to-date; first resolve the missing commit/push.
- When a user later asks why push did not happen or asks to fix the push, only complete the missing commit/push work. Do not repeat already-completed side effects from the earlier command chain unless the user explicitly requests them again.
For Hermes skills specifically, note that installed skills under ~/.hermes/skills/ are not the same as tracked repo skills under /Users/dev/.hermes/hermes-agent/skills/. If a skill edit should be committed/pushed, ensure the corresponding tracked file in the repo has the diff before committing.
No force updates unless explicitly requested
Do not rewrite remote branch history as a cleanup shortcut. Forbidden unless the user explicitly requests a force update in the same message and states the target branch/remote:
git push --force
git push --force-with-lease
git push <remote> +HEAD:<branch>
If you already pushed a bad commit and need to correct it, create and push a normal follow-up fix commit. Do not commit --amend + force-push shared branches. If history rewriting is genuinely necessary, explain the exact remote/branch/SHA impact and wait for an explicit force-push instruction.
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:
gh pr checks
gh pr checks --watch
With git + curl:
SHA=$(git rev-parse HEAD)
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', '')}\")"
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)
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:
gh run list --branch $(git branch --show-current) --limit 5
gh run view <RUN_ID> --log-failed
With git + curl:
BRANCH=$(git branch --show-current)
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']}\")"
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:
gh pr merge --squash --delete-branch
gh pr merge --auto --squash --delete-branch
With git + curl:
PR_NUMBER=<number>
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)\"
}"
BRANCH=$(git branch --show-current)
git push origin --delete $BRANCH
git checkout main && git pull origin main
git branch -d $BRANCH
Merge methods: "merge" (merge commit), "squash", "rebase"
Enable Auto-Merge (curl)
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
git checkout main && git pull origin main
git checkout -b fix/login-redirect-bug
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."
git push -u origin HEAD
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 |
Source: aiden-lightning/hermes-agent — distributed by TomeVault.