Skip to content

Commit dfb6942

Browse files
committed
feat: add port-to-next label for forward-porting merged PRs
Adds a generic `port-to-next` label that mirrors the `backport-to-*` labels but with a fixed target: on merge, the PR is cherry-picked onto an accumulating `port-to-next-staging` branch that opens/updates a single PR into `merge-train/spartan` (which flows to `next`). Reuses `scripts/backport_to_staging.sh` via optional STAGING_BRANCH / STAGING_PR_TITLE env overrides (defaults keep existing backport behavior). The backport workflow now resolves either label family; the update-pr-body and auto-merge (8h) automations cover the new staging branch.
1 parent f134965 commit dfb6942

6 files changed

Lines changed: 70 additions & 23 deletions

File tree

.claude/claudebox/backport.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ You will receive a prompt like:
1010
1111
Variables you need to extract from the prompt:
1212
- `PR_NUMBER`: the PR number (e.g., `21829`)
13-
- `TARGET_BRANCH`: the release branch (e.g., `v4-next`)
14-
- `STAGING_BRANCH`: always `backport-to-${TARGET_BRANCH}-staging`
13+
- `TARGET_BRANCH`: the target branch (e.g., `v4-next`, or `merge-train/spartan` for a `port-to-next` port)
14+
- `STAGING_BRANCH`: `backport-to-${TARGET_BRANCH}-staging` for a backport. For a
15+
`port-to-next` port it is `port-to-next-staging`; the prompt states it explicitly
16+
(`... (staging branch port-to-next-staging) ...`) — use the branch named there.
1517

1618
## Constraints
1719

.claude/skills/merge-train-infra/SKILL.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,25 @@ The merge-train system is fully automated via GitHub Actions in `.github/workflo
1313

1414
1. **PR Creation** (`merge-train-create-pr.yml`): Triggered on push to `merge-train/*` branches. Creates a PR targeting `next` (or `v5-next` for `-v5` trains such as `merge-train/spartan-v5` and `merge-train/fairies-v5`) with the `ci-no-squash` label (plus `private-port-next` for any train that targets `v5-next`, and `ci-full-no-test-cache` for `merge-train/spartan`, `merge-train/spartan-v5`, and `merge-train/ci`). Skips merge commits and commits already in the base branch.
1515

16-
2. **Body Updates** (`merge-train-update-pr-body.yml`): Triggered on push to `merge-train/**` and `backport-to-*-staging` branches. Updates the PR body with meaningful commits (those containing PR references like `(#1234)`). The body wraps the commit list in `BEGIN_COMMIT_OVERRIDE` / `END_COMMIT_OVERRIDE` markers. Backport staging PRs also call `update-pr-body.sh` inline from `scripts/backport_to_staging.sh` to handle the first-push case (where the PR doesn't exist yet when the workflow fires).
16+
2. **Body Updates** (`merge-train-update-pr-body.yml`): Triggered on push to `merge-train/**`, `backport-to-*-staging`, and `port-to-next-staging` branches. Updates the PR body with meaningful commits (those containing PR references like `(#1234)`). The body wraps the commit list in `BEGIN_COMMIT_OVERRIDE` / `END_COMMIT_OVERRIDE` markers. Backport/port staging PRs also call `update-pr-body.sh` inline from `scripts/backport_to_staging.sh` to handle the first-push case (where the PR doesn't exist yet when the workflow fires).
1717

1818
3. **Next Integration** (`merge-train-next-to-branches.yml`): Triggered on push to `next` and `v5-next`. A push to `next` merges `next` into each `next`-based train; a push to `v5-next` merges `v5-next` into the `-v5` trains (`merge-train/spartan-v5`, `merge-train/fairies-v5`). Both go through `scripts/merge-train/merge-next.sh`, which takes an optional second argument for the source branch (defaults to `next`). Uses `continue-on-error: true` so a conflict in one branch does not block others. Skips branches whose PR already has auto-merge enabled.
1919

20-
4. **Auto-Merge** (`merge-train-auto-merge.yml`): Runs hourly via cron (`0 * * * *`). Calls `scripts/merge-train/auto-merge.sh` for both merge-train (4-hour inactivity) and backport-train (8-hour inactivity) branches. Uses separate GitHub tokens: `AZTEC_BOT_GITHUB_TOKEN` for API calls and `MERGE_TRAIN_GITHUB_TOKEN` for approvals. Will not auto-merge if the last merge-queue CI run failed or was cancelled.
20+
4. **Auto-Merge** (`merge-train-auto-merge.yml`): Runs hourly via cron (`0 * * * *`). Calls `scripts/merge-train/auto-merge.sh` for merge-train (4-hour inactivity), backport-train (`BRANCH_PATTERN=backport-to-`, 8-hour), and port-to-next (`BRANCH_PATTERN=port-to-next`, 8-hour) branches. Uses separate GitHub tokens: `AZTEC_BOT_GITHUB_TOKEN` for API calls and `MERGE_TRAIN_GITHUB_TOKEN` for approvals. Will not auto-merge if the last merge-queue CI run failed or was cancelled.
2121

2222
5. **Recreation & Wakeup** (`merge-train-recreate.yml`): Triggered when a PR is closed (merged). If the merged PR's head branch starts with `merge-train/`, recreates the branch from the base branch (usually `next`). Then runs `scripts/merge-train/wakeup-prs.sh` to add the `ci-wakeup-pr-after-merge` label to all open PRs targeting the branch that have passed CI and have automerge enabled. This triggers a CI re-run (typically a no-op via tree-hash cache) so those PRs can proceed through the merge queue. The label is immediately removed by a step in `ci3.yml` so it can be re-applied on subsequent merges.
2323

2424
6. **Failure Notification** (`merge-queue-dequeue-notify.yml`): Triggered when a PR is dequeued from the merge queue. If the PR's head branch starts with `merge-train/` and the PR was NOT merged, sends a Slack notification via `ci3/merge_train_failure_slack_notify`. That script also kicks off a ClaudeBox session to investigate/fix the dequeued PR (`ci3/slack_notify_with_claudebox_kickoff`), passing `--repo "$GITHUB_REPOSITORY"` so the session runs in the mode matching the repo the train lives on. When the train is on a private mirror (`…-private`), `claudebox.yml` selects private mode; otherwise it stays public. Without that repo hint a private-train fix session lands in public mode and cannot read the PR or open the fix.
2525

26+
## Label-Driven Ports (`backport.yml`)
27+
28+
`backport.yml` (triggered on `pull_request_target` labeled/closed) cherry-picks a merged PR onto an accumulating staging branch, then opens/updates one staging PR into a target branch. It handles two label families, both driven by `scripts/backport_to_staging.sh`:
29+
30+
- **`backport-to-<branch>`** (e.g. `backport-to-v5-next`): target is `<branch>` (derived from the label), staging branch `backport-to-<branch>-staging`. Direction `next` → release line.
31+
- **`port-to-next`** (fixed, generic): target is `merge-train/spartan`, staging branch `port-to-next-staging`. Direction: forward-port an already-merged PR into the spartan train (which flows to `next`). The workflow passes `STAGING_BRANCH` / `STAGING_PR_TITLE` env overrides into the script; `port-to-next` takes precedence if both label families are present.
32+
33+
On cherry-pick conflict the workflow comments on the PR, posts to `#backports`, and dispatches ClaudeBox (`.claude/claudebox/backport.md`) with the staging branch to resolve manually. Staging PRs are auto-merged by the 8-hour jobs in `merge-train-auto-merge.yml`.
34+
2635
## CI Integration Details
2736

2837
### CI Mode Selection (`.github/ci3_labels_to_env.sh`)
@@ -94,6 +103,7 @@ When a CI run fails on an EC2 instance, it calls `merge_train_failure_slack_noti
94103
| `.github/workflows/merge-train-update-pr-body.yml` | Updates PR body with commit list (merge-train and backport branches) |
95104
| `.github/workflows/merge-queue-dequeue-notify.yml` | Slack notification on merge-queue dequeue |
96105
| `.github/workflows/squashed-pr-check.yml` | Squash enforcement (skipped for `ci-no-squash`) |
106+
| `.github/workflows/backport.yml` | Cherry-picks merged PRs to staging branches for `backport-to-*` and `port-to-next` labels |
97107

98108
### Scripts
99109

.github/workflows/backport.yml

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Automatic backport action
1+
name: Automatic backport/port action
22

33
on:
44
pull_request_target:
@@ -15,7 +15,7 @@ jobs:
1515
uses: agilepathway/label-checker@825944377ab3bce1269b38c99b718767e2ca6bbc
1616
with:
1717
prefix_mode: true
18-
any_of: backport-to-
18+
any_of: backport-to-,port-to-next
1919
repo_token: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
2020
allow_failure: true
2121
- name: Print status
@@ -24,7 +24,7 @@ jobs:
2424

2525
backport:
2626
needs: [label_checker]
27-
name: Backport PR
27+
name: Backport/port PR
2828
if: github.event.pull_request.merged == true && needs.label_checker.outputs.state == 'success'
2929
runs-on: ubuntu-latest
3030
steps:
@@ -34,30 +34,48 @@ jobs:
3434
fetch-depth: 0
3535
token: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
3636

37-
- name: Extract target branch from labels
37+
- name: Resolve port target from labels
3838
id: extract-branch
3939
env:
4040
LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }}
4141
run: |
4242
LABELS="$LABELS_JSON"
4343
echo "All labels: $LABELS"
4444
45-
# Extract the branch name from backport-to-* label
46-
TARGET_BRANCH=$(echo "$LABELS" | jq -r '.[] | select(startswith("backport-to-")) | sub("backport-to-"; "")')
47-
48-
if [ -z "$TARGET_BRANCH" ]; then
49-
echo "No backport-to-* label found"
50-
exit 1
45+
# port-to-next is a fixed-target port into the spartan merge-train
46+
# (which flows to next). It takes precedence over backport-to-* if
47+
# both are somehow present.
48+
if echo "$LABELS" | jq -e 'index("port-to-next")' >/dev/null; then
49+
TARGET_BRANCH="merge-train/spartan"
50+
STAGING_BRANCH="port-to-next-staging"
51+
STAGING_PR_TITLE="chore: Accumulated ports to next"
52+
VERB="ported"
53+
else
54+
# Extract the branch name from a backport-to-* label
55+
TARGET_BRANCH=$(echo "$LABELS" | jq -r '.[] | select(startswith("backport-to-")) | sub("backport-to-"; "")')
56+
if [ -z "$TARGET_BRANCH" ]; then
57+
echo "No backport-to-* or port-to-next label found"
58+
exit 1
59+
fi
60+
STAGING_BRANCH="backport-to-${TARGET_BRANCH}-staging"
61+
STAGING_PR_TITLE="chore: Accumulated backports to $TARGET_BRANCH"
62+
VERB="backported"
5163
fi
5264
5365
echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT
66+
echo "staging_branch=$STAGING_BRANCH" >> $GITHUB_OUTPUT
67+
echo "staging_pr_title=$STAGING_PR_TITLE" >> $GITHUB_OUTPUT
68+
echo "verb=$VERB" >> $GITHUB_OUTPUT
5469
echo "Target branch: $TARGET_BRANCH"
70+
echo "Staging branch: $STAGING_BRANCH"
5571
5672
- name: Run backport script
5773
id: backport
5874
continue-on-error: true
5975
env:
6076
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
77+
STAGING_BRANCH: ${{ steps.extract-branch.outputs.staging_branch }}
78+
STAGING_PR_TITLE: ${{ steps.extract-branch.outputs.staging_pr_title }}
6179
run: |
6280
# Kludge. We should write this in Python or not checkout history.
6381
# Bash has an ugly footgun with changing history while running the script.
@@ -72,13 +90,14 @@ jobs:
7290
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
7391
run: |
7492
TARGET_BRANCH="${{ steps.extract-branch.outputs.target_branch }}"
75-
STAGING_BRANCH="backport-to-${TARGET_BRANCH}-staging"
93+
STAGING_BRANCH="${{ steps.extract-branch.outputs.staging_branch }}"
94+
VERB="${{ steps.extract-branch.outputs.verb }}"
7695
STAGING_PR=$(gh pr list --base "$TARGET_BRANCH" --head "$STAGING_BRANCH" --json number,url --jq '.[0]')
7796
STAGING_PR_NUMBER=$(echo "$STAGING_PR" | jq -r '.number')
7897
STAGING_PR_URL=$(echo "$STAGING_PR" | jq -r '.url')
7998
8099
gh pr comment "${{ github.event.pull_request.number }}" --body \
81-
"✅ Successfully backported to [$STAGING_BRANCH #$STAGING_PR_NUMBER]($STAGING_PR_URL)."
100+
"✅ Successfully $VERB to [$STAGING_BRANCH #$STAGING_PR_NUMBER]($STAGING_PR_URL)."
82101
83102
- name: Comment on original PR (failure)
84103
if: steps.backport.outcome == 'failure'
@@ -91,7 +110,7 @@ jobs:
91110
gh pr comment "${{ github.event.pull_request.number }}" --body \
92111
"❌ Failed to cherry-pick to \`$TARGET_BRANCH\` due to conflicts. (🤖) [View backport run]($WORKFLOW_URL)."
93112
94-
- name: Notify Slack and dispatch ClaudeBox on backport failure
113+
- name: Notify Slack and dispatch ClaudeBox on failure
95114
if: steps.backport.outcome == 'failure'
96115
env:
97116
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -101,11 +120,13 @@ jobs:
101120
URL: ${{ github.event.pull_request.html_url }}
102121
AUTHOR: ${{ github.event.pull_request.user.login }}
103122
BRANCH: ${{ steps.extract-branch.outputs.target_branch }}
123+
STAGING_BRANCH: ${{ steps.extract-branch.outputs.staging_branch }}
124+
VERB: ${{ steps.extract-branch.outputs.verb }}
104125
run: |
105126
106127
# Post single message to #backports, derive permalink from response
107-
TEXT=$(printf '⚠️ Backport failed: <%s|#%s %s> → `%s` (author: %s) (🤖)' \
108-
"$URL" "$PR" "$TITLE" "$BRANCH" "$AUTHOR")
128+
TEXT=$(printf '⚠️ %s failed: <%s|#%s %s> → `%s` (author: %s) (🤖)' \
129+
"${VERB^}" "$URL" "$PR" "$TITLE" "$BRANCH" "$AUTHOR")
109130
RESP=$(curl -sS -X POST https://slack.com/api/chat.postMessage \
110131
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
111132
-H "Content-type: application/json" \
@@ -120,8 +141,8 @@ jobs:
120141
fi
121142
122143
gh workflow run claudebox.yml \
123-
-f prompt="Backport PR #$PR ($TITLE) to $BRANCH. The automatic cherry-pick failed due to conflicts. Follow .claude/claudebox/backport.md to resolve conflicts and create a PR." \
144+
-f prompt="${VERB^} PR #$PR ($TITLE) to $BRANCH (staging branch $STAGING_BRANCH). The automatic cherry-pick failed due to conflicts. Follow .claude/claudebox/backport.md to resolve conflicts and create a PR." \
124145
-f link="${LINK:-$URL}" \
125-
-f target_ref="origin/backport-to-${BRANCH}-staging" \
146+
-f target_ref="origin/$STAGING_BRANCH" \
126147
-f slack_channel="$CHANNEL_ID" \
127148
-f slack_thread_ts="$TS"

.github/workflows/merge-train-auto-merge.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ jobs:
3131
MERGE_STRATEGY=merge \
3232
INACTIVITY_HOURS=8 \
3333
./scripts/merge-train/auto-merge.sh
34+
35+
- name: Run auto-merge script (port-to-next)
36+
run: |
37+
GH_TOKEN=${{ secrets.AZTEC_BOT_GITHUB_TOKEN }} \
38+
MERGE_TRAIN_GITHUB_TOKEN=${{ secrets.MERGE_TRAIN_GITHUB_TOKEN }} \
39+
BRANCH_PATTERN=port-to-next \
40+
MERGE_STRATEGY=merge \
41+
INACTIVITY_HOURS=8 \
42+
./scripts/merge-train/auto-merge.sh

.github/workflows/merge-train-update-pr-body.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- 'merge-train/**'
77
- 'backport-to-*-staging'
8+
- 'port-to-next-staging'
89

910
jobs:
1011
update-pr-body:

scripts/backport_to_staging.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ if [[ -z "$PR_NUMBER" || -z "$TARGET_BRANCH" ]]; then
6666
usage
6767
fi
6868

69-
STAGING_BRANCH="backport-to-${TARGET_BRANCH}-staging"
69+
# STAGING_BRANCH and STAGING_PR_TITLE may be pre-set in the environment to
70+
# reuse this script for non-backport ports (e.g. the port-to-next label, which
71+
# targets merge-train/spartan). They default to the backport naming.
72+
STAGING_BRANCH="${STAGING_BRANCH:-backport-to-${TARGET_BRANCH}-staging}"
73+
STAGING_PR_TITLE="${STAGING_PR_TITLE:-chore: Accumulated backports to $TARGET_BRANCH}"
7074

7175
# Check for required tools
7276
command -v gh >/dev/null 2>&1 || { echo "Error: 'gh' CLI not found. Install from https://cli.github.com/" >&2; exit 1; }
@@ -197,7 +201,7 @@ if [[ -z "$EXISTING_PR" ]]; then
197201
do_or_dryrun gh pr create \
198202
--base "$TARGET_BRANCH" \
199203
--head "$STAGING_BRANCH" \
200-
--title "chore: Accumulated backports to $TARGET_BRANCH" \
204+
--title "$STAGING_PR_TITLE" \
201205
--body "Backport staging PR. Body will be updated with commit list."
202206
do_or_dryrun echo "Created new backport PR"
203207
else

0 commit comments

Comments
 (0)