Promote Branch → Pull Request
Compare two branches in a GitHub repo, create a promotion PR if the target is behind,
and optionally hand off to babysit-github-pr for monitoring.
Parse inputs from the user's message
repo_url(required) — full GitHub repo URL, e.g.https://github.com/mocaverse/air-agent-servicessource[default:staging] — branch to promote fromtarget[default:sandbox] — branch to promote intoauto_babysit[default:false] — iftrue, skip the confirmation prompt and immediately invokebabysit-github-pr
Any missing optional param falls back to the default above.
Prerequisites — check before starting
ghCLI authenticated (gh auth status). If not available, fall back togit+curlwithGITHUB_TOKEN.- The local repo does not need to be cloned. All operations work against the remote via
ghAPI orcurl.
Extract owner/repo
OWNER_REPO=$(echo "$repo_url" | sed -E 's|.*github\.com[:/]||; s|\.git$||')
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
Step 1: Compare branches
With gh:
gh api repos/$OWNER/$REPO/compare/$target...$source --jq '.commits[] | "\(.sha[:7]) \(.commit.message | split("\n")[0])"' 2>/dev/null
COMMIT_COUNT=$(gh api repos/$OWNER/$REPO/compare/$target...$source --jq '.total_commits' 2>/dev/null)
With curl (fallback):
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$OWNER/$REPO/compare/$target...$source" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
commits = data.get('commits', [])
print(f'Total commits: {len(commits)}')
for c in commits:
msg = c['commit']['message'].split('\n')[0]
print(f\" {c['sha'][:7]} {msg}\")
" 2>/dev/null
COMMIT_COUNT=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$OWNER/$REPO/compare/$target...$source" | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))")
Outcomes
COMMIT_COUNT |
Action |
|---|---|
0 or empty |
Report "$target is already up to date with $source. No PR needed." and exit. |
> 0 |
Continue to Step 2. |
Step 2: Check for existing open PR
Search for an open PR with head=$source and base=$target.
With gh:
EXISTING=$(gh pr list --repo $OWNER/$REPO --head "$source" --base "$target" --state open --json number,url --jq '.[0]')
With curl:
EXISTING=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/pulls?state=open&head=$OWNER:$source&base=$target" \
| python3 -c "import sys,json; prs=json.load(sys.stdin); print(prs[0]['html_url'] if prs else '')")
If EXISTING is non-empty:
- Report the existing PR URL.
- Ask the user:
"An open PR already exists. Babysit it? (yes/no)" - If yes → invoke
babysit-github-prwithpr_url=$EXISTING. - Exit without creating a new PR.
Step 3: Build PR body
Generate a promotion body that includes the commit list and a machine-readable footer.
## Promote `source` → `target`
| Metric | Value |
|---|---|
| Commits ahead | COMMIT_COUNT |
| Source | `source` |
| Target | `target` |
**Commits included:**
- `abc1234` feat: add X
- `def5678` fix: resolve Y
- ...
---
*Generated by promote-branch-pr*
To build the commit list dynamically, reuse the output from Step 1.
Step 4: Create the promotion PR
With gh:
gh pr create \
--repo "$OWNER/$REPO" \
--base "$target" \
--head "$source" \
--title "promote: $source → $target" \
--body "$PR_BODY"
Capture the returned PR URL.
With curl:
PR_RESPONSE=$(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\": \"promote: $source → $target\",
\"body\": \"$PR_BODY\",
\"head\": \"$source\",
\"base\": \"$target\"
}")
PR_URL=$(echo "$PR_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('html_url',''))")
If creation fails, report the error and exit.
Step 5: Report and ask about babysitting
Report to the user:
✅ Created promotion PR: PR_URL
source → target | COMMIT_COUNT commits
Then ask:
"PR created. Run babysit-github-pr to monitor it? (yes/no)"
- If user says yes (or
auto_babysit=true): - Invoke
babysit-github-prwithpr_url=$PR_URL. - Pass sensible defaults:
duration=2h,interval=10m. - NEVER merge the PR yourself — leave it merge-ready for the user to merge.
- If user says no:
- Report
"Done. PR is open at PR_URL."and exit.
- Report
Agent autonomy boundary
- Create the PR if it doesn't exist.
- Monitor it if asked (via
babysit-github-pr). - Resolve merge conflicts, review comments, and CI failures.
- Report when the PR is merge-ready.
- NEVER merge the PR into the target branch. That action belongs to the user.
Standalone guarantee
This skill never depends on babysit-github-pr being present. If the user declines babysitting, or the skill is unavailable, promotion still completes successfully. The handoff is strictly optional.
Edge cases
| Scenario | Behavior |
|---|---|
source == target |
Report error, exit |
source or target branch does not exist |
Report error from GitHub API, exit |
| Open PR already exists | Report it, ask to babysit, do not duplicate |
COMMIT_COUNT == 0 |
Report up-to-date, exit |
| PR creation fails (permissions, rules) | Report raw error, exit |
PR is CONFLICTING/DIRTY |
Trivial/format drift only: Resolve locally (see below), then re-check. Non-trivial: Halt and ask user for guidance. |
gh missing and no GITHUB_TOKEN |
Fail fast with auth setup instructions |
Resolving trivial merge conflicts (format drift)
Promotion PRs often become CONFLICTING due to biome/format/style drift on target (e.g. sandbox had formatting-only changes that diverged from source). These are safe to resolve mechanically without human review.
Detection
After finding an existing PR, check its merge state:
gh pr view $PR_NUM --repo $OWNER/$REPO --json mergeStateStatus,mergeable
If mergeable: CONFLICTING and the repo uses automated formatters (Biome, Prettier, ESLint), the conflicts are likely trivial.
Resolution workflow
Ensure the repo is cloned locally and
originpoints to the remote:git fetch origin $source $targetCheckout the source branch and merge target into it:
git checkout $source git merge origin/$target --no-editAccept the source branch versions for all conflicted files:
git checkout --ours <file1> <file2> ... # or accept all conflicts programmatically: git diff --name-only --diff-filter=U | xargs git checkout --oursCommit and push:
git add -A git commit -m "chore: resolve $target→$source promotion conflicts" git push origin $sourceRe-check the PR — it should flip to
MERGEABLE.
Pitfalls
--oursvs--theirsis counterintuitive during a merge:--oursmeans the branch you checked out (source),--theirsmeans the branch you merged in (target). For promotion PRs, always accept source (--ours) because source is the source of truth.- Do NOT use
--forcewhen pushing the resolved source branch. - If conflicts are in non-formatting files (logic, types, schemas), stop and ask the user before resolving.
See references/resolve-promotion-conflicts.md for a worked example from the mocaverse/air-agent-services repo.