| name | upmerge |
| description | Propagate a php-src bug fix from its lowest fix branch up through every higher maintained branch, applying the per-branch NEWS edit and producing one merge commit per hop. Use when the user says "upmerge", "merge up", "propagate fix to higher branches", "forward-port the fix", "/upmerge", or otherwise wants to mirror a fix that landed on a lower branch onto all newer maintained branches per CONTRIBUTING.md. Defaults to dry-run; `--execute` performs the merges locally. Never pushes, never fetches, never touches the remote. |
upmerge
Final, optional step of the bug-fix flow. Owns no SPEC.md phase; produces its own one-shot living doc UPMERGE.md. Reads the fix commit and target_branch from SPEC.md (or accepts explicit args), computes the merge chain dynamically, plans NEWS edits per branch, and — only on --execute — applies the merges locally.
Inputs
- SPEC.md in the task directory resolved per
_shared/task-dir.md. From it:
type: bug, status: done (Verification phase done — guardrail).
target_branch: PHP-x.y (the source branch — the "from" of the chain).
linked: → GH issue number for the NEWS entry.
- Fix commit SHA recovered from the Progress Log's
bug-fix entry, or via git -C $PHP_SRC_DIR log --grep "Closes GH-<N>" -1 --pretty=%H on target_branch.
- Or explicit flags when SPEC.md is absent:
--from PHP-x.y --commit <sha> --issue <N>.
$PHP_SRC_DIR resolved per _shared/php-src-detection.md.
- Mode flag: default dry-run;
--execute to perform merges.
What it produces
UPMERGE.md in the same task dir as SPEC.md. Schema below.
- On
--execute: one merge commit per branch hop in $PHP_SRC_DIR, with NEWS spliced on each upper branch. Tree left checked out on the topmost branch in the chain.
No git push. Ever.
Workflow
1. Pre-guardrails
Refuse on any of:
2. Resolve inputs
- Resolve
$PHP_SRC_DIR per _shared/php-src-detection.md.
- Resolve task dir per
_shared/task-dir.md. If SPEC.md exists, read frontmatter → target_branch, linked, status.
- Recover fix commit SHA (Progress Log scan, fallback to
git log --grep).
- Without SPEC.md, require
--from, --commit, --issue all three. Refuse if any missing.
3. Compute the merge chain dynamically
Read the maintained-branch list per _shared/branch-detection.md algorithm § 1. Intersect with locally-present branches:
git -C "$PHP_SRC_DIR" for-each-ref --format='%(refname:short)' refs/heads/
Take every entry strictly above <from> ordered low → high. Example with <from>=PHP-8.3:
PHP-8.3 (source) → PHP-8.4 → PHP-8.5 → master
If any branch in the list is missing locally: stop with Branch <X> from matrix.php is not checked out locally. Either: git checkout -b <X> origin/<X>, or pass --skip-missing. Don't auto-fetch (forbidden by the agent's git etiquette).
4. Extract the NEWS entry to propagate
From the fix commit on <from>, isolate the lines it added to NEWS:
git -C "$PHP_SRC_DIR" show "<sha>" -- NEWS \
| awk '/^@@/{in_hunk=1; next} in_hunk && /^\+[^+]/ { sub(/^\+/, ""); print }'
Identify the section header those + lines live under by scanning the same patch hunk upward for the nearest - <Component>: heading. Capture both the section header and the entry body (typically two-three lines starting with . Fixed bug GH-NNNNN ...).
5. Plan — write UPMERGE.md (always)
Write UPMERGE.md to the task dir (schema below). Records:
- Frontmatter:
type: upmerge, status: planned, source_branch, source_commit, issue, created.
- Chain (low → high).
- NEWS entry section + body extracted in step 4.
- One hop block per
(from, to) pair, all status: todo, with the proposed merge subject Merge branch '<from>'.
- Build sanity check entry for the chain head (
master-build), status: todo.
If UPMERGE.md already exists for this same source_commit: re-read it and resume at the first non-done hop (same resume algorithm as SPEC.md). If source_commit differs (re-planning a different fix): back up to UPMERGE.md.bak and rewrite.
Stop here in dry-run mode. Report the planned chain and the NEWS entry. Tell the user to re-run with --execute to apply.
6. Execute (only when --execute)
For each (from, to) pair in the chain, low → high:
git -C "$PHP_SRC_DIR" checkout "<to>"
git -C "$PHP_SRC_DIR" merge --no-ff --no-commit -m "Merge branch '<from>'" "<from>"
--no-ff matches the php-src convention visible in git log --merges --first-parent on master. --no-commit lets us land the NEWS resolution before the merge commit closes.
NEWS conflict resolution (expected)
git -C "$PHP_SRC_DIR" checkout --ours -- NEWS
Then splice the propagated entry under the matching - <Component>: section inside the upper branch's unreleased header (the first line shaped like ?? ??? ????, PHP X.Y.Z). Use awk (not sed — multi-line range matching is fragile):
awk -v section='- Core:' -v entry="$ENTRY_TEXT" '
BEGIN { spliced = 0 }
$0 == section && !spliced { print; in_sec=1; next }
in_sec && /^- / { print entry; print ""; print; in_sec=0; spliced=1; next }
in_sec && /^$/ && !spliced { print entry; print; in_sec=0; spliced=1; next }
{ print }
END {
if (!spliced) {
# section header not present in upper branch — append a new one
print ""; print section; print entry
}
}
' "$PHP_SRC_DIR/NEWS" > /tmp/news.new && mv /tmp/news.new "$PHP_SRC_DIR/NEWS"
git -C "$PHP_SRC_DIR" add NEWS
Non-NEWS conflicts
Stop. Mark the current hop blocked in UPMERGE.md with git status output and exact recovery commands:
Conflict outside NEWS — not auto-resolvable. To abort:
git -C "$PHP_SRC_DIR" merge --abort
Resolve manually, then re-run /upmerge --execute to continue.
Do not attempt content-aware resolution outside NEWS.
Finalise the hop
git -C "$PHP_SRC_DIR" commit --no-edit
git -C "$PHP_SRC_DIR" rev-parse HEAD
Record the new merge-commit SHA under the hop in UPMERGE.md. Mark the hop done. Append one Progress Log line:
YYYY-MM-DD HH:MM — upmerge — hop <from> → <to> done (SHA <short>)
Loop to the next hop with (to) becoming the new (from).
7. Post-guardrail — build at chain head
After every hop is done, run one build at the topmost branch (the tree is already checked out there):
cd "$PHP_SRC_DIR" && make -j$(nproc)
Per-hop make test is not run — too slow (≥15 min × N hops) and /bug-verify already established correctness on the source branch. A build pass at the top catches header / macro drift between branches that would break compile.
If build fails: mark master-build step blocked, leave the tree where it is, do not auto-revert. Print the build error tail.
8. Report
Upmerge complete (dry-run | --execute).
Chain: <from> → <hop1> → <hop2> → ... → master
Hops: <N done>/<N total>
Build: <skipped | passed | failed>
NEWS: spliced on <list of branches>
UPMERGE.md: <task-dir>/UPMERGE.md
To publish (you decide when):
git -C "$PHP_SRC_DIR" push origin <from> <hop1> ... master
Never run the push. Never print the push without the explicit "you decide when" wrapper.
UPMERGE.md schema
---
type: upmerge
status: planned | in-progress | done | blocked
source_branch: PHP-8.3
source_commit: a1b2c3d
issue: GH-12345
created: YYYY-MM-DD
---
# Upmerge: GH-12345
## Chain
PHP-8.3 (source) → PHP-8.4 → PHP-8.5 → master
## NEWS entry to propagate
Section: `- Core:`
Body:
. Fixed bug GH-12345 (one-line description).
(contributor-handle)
## Hops
- [ ] **PHP-8.3 → PHP-8.4** — status: todo
merge subject: `Merge branch 'PHP-8.3'`
merge-commit SHA: (pending)
- [ ] **PHP-8.4 → PHP-8.5** — status: todo
- [ ] **PHP-8.5 → master** — status: todo
## Build check
- [ ] **master-build** — `make -j$(nproc)` at chain head
## Progress Log
- YYYY-MM-DD HH:MM — upmerge — planned chain from .github/matrix.php
Status values mirror SPEC.md (todo / in-progress / done / skipped / blocked). Resume algorithm is identical: re-running /upmerge --execute reads UPMERGE.md, finds the first non-done hop, picks up there.
Boundaries
- No
git push, git pull, git fetch, git remote *. The chain is computed from local refs only. If any branch in the chain is stale, that is the user's call to fix.
- No branch creation or deletion.
- No modification of SPEC.md. Bug-flow state is owned by other skills.
- No
make test per hop. Single build at the chain head only.
- No GitHub interaction. No
gh pr, no gh issue comment, no posting of upmerge status to the issue.
- No conflict resolution outside NEWS. Code conflicts always block.
- No backporting. This skill only goes up the chain. To put a fix on a branch below the one it landed on, the user must redo the fix on that branch — the policy is forward-port, not back-port.
- No proceeding without an explicit
--from when SPEC.md is absent. Don't guess.
- No reversal. If the user wants to undo:
git reset --hard <pre-upmerge-sha> per branch is on them. Document this in the final report when relevant.
Edge cases
| Situation | Handling |
|---|
--from master | Refuse (see § 1). |
| Fix commit landed on master only | Refuse: /upmerge is for forward-port from a lower branch. |
| Upper branch already contains the GH-NNNNN line in its NEWS | The awk splice is effectively idempotent — detect by grepping the upper NEWS for the exact entry first; if present, skip the splice, still produce the merge commit. Note in UPMERGE.md hop body. |
.github/matrix.php missing | Fall back per _shared/branch-detection.md § 1 fallback. Note the fallback in the planning step. |
User aborts mid-chain (Ctrl-C / git merge --abort) | Re-running /upmerge --execute reads UPMERGE.md, resumes at first non-done hop. |
Upper-branch NEWS has no matching - <Component>: section | The awk's END branch appends a new section. Note in the hop body. |
| Fix commit is itself a merge commit (multi-parent) | Refuse: upmerge expects a single linear fix commit. The user must squash/cherry-pick first. |
The chain has only one hop (<from> is just one below master) | Run normally — one hop, one build at the top. |
User asks for partial upmerge (--up-to PHP-8.5) | Honour the flag; truncate the chain at --up-to. UPMERGE.md records the truncation. |
Quality checklist
Out of scope
- Posting upmerge status to GitHub — never auto. The user pushes and announces themselves.
- Squashing or rewriting the fix commit on the source branch.
- Resolving non-NEWS conflicts.
- Detecting that a fix is security-only and shouldn't be upmerged — that judgement is the user's. If they ran
/upmerge, the fix is upmergeable.