Skip to content

Commit 7b08ef8

Browse files
authored
feat: merge-train/spartan (#24815)
BEGIN_COMMIT_OVERRIDE feat: add port-to-next label for forward-porting merged PRs (#24753) END_COMMIT_OVERRIDE
2 parents c2ca55c + e38dbd4 commit 7b08ef8

8 files changed

Lines changed: 309 additions & 26 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 `next` 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: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,29 @@ 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 `next`, staging branch `port-to-next-staging`. Direction: forward-port an already-merged PR straight into `next`. The workflow passes `STAGING_BRANCH` / `STAGING_PR_TITLE` / `STAGING_PR_LABELS` env overrides into the script; the staging PR carries `ci-no-squash` (required because `next` enforces squashed PRs). `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+
35+
## Scheduled Forward-Port (`port-v5-next-to-next.yml`)
36+
37+
A daily bulk sweep (distinct from the per-PR `port-to-next` label) that keeps `next` fed with everything on the `v5-next` release line. `port-v5-next-to-next.yml` runs at 06:30 UTC (and on `workflow_dispatch`) and calls `scripts/port_to_next.sh <source>` (default `v5-next`). The `port-<source>-to-next` branch is long-lived: each run checks it out and merges both `next` and the source into it, then opens/updates one `ci-no-squash` PR into `next`. Accumulating (rather than rebuilding) means any conflict resolution pushed to the branch is preserved across runs. Once the PR is merged (the branch becomes an ancestor of `next`) the next run rebuilds the branch fresh from `next` with a `--force-with-lease` push; while accumulating it fast-forwards. If a run produces no delta over `next` it closes the stale PR. A merge conflict does **not** abandon the run: the conflicted merge is committed with markers (so the PR is still opened/updated as a resolution target), the script emits `conflicts` / `pr_url` step outputs, and the workflow posts the PR link and conflicted files to `#backports`. Resolve by checking out the port branch, fixing the markers, and pushing. This PR is intentionally left for human review — it is not added to the auto-merge patterns.
38+
2639
## CI Integration Details
2740

2841
### CI Mode Selection (`.github/ci3_labels_to_env.sh`)
@@ -94,6 +107,8 @@ When a CI run fails on an EC2 instance, it calls `merge_train_failure_slack_noti
94107
| `.github/workflows/merge-train-update-pr-body.yml` | Updates PR body with commit list (merge-train and backport branches) |
95108
| `.github/workflows/merge-queue-dequeue-notify.yml` | Slack notification on merge-queue dequeue |
96109
| `.github/workflows/squashed-pr-check.yml` | Squash enforcement (skipped for `ci-no-squash`) |
110+
| `.github/workflows/backport.yml` | Cherry-picks merged PRs to staging branches for `backport-to-*` and `port-to-next` labels |
111+
| `.github/workflows/port-v5-next-to-next.yml` | Daily forward-port sweep of `v5-next` into `next` |
97112

98113
### Scripts
99114

@@ -105,6 +120,7 @@ When a CI run fails on an EC2 instance, it calls `merge_train_failure_slack_noti
105120
| `scripts/merge-train/squash-pr.sh` | Squashes PR commits (used by `ci-squash-and-merge` label) |
106121
| `scripts/merge-train/wakeup-prs.sh` | Adds `ci-wakeup-pr-after-merge` label to qualifying PRs after branch recreation |
107122
| `scripts/backport_to_staging.sh` | Cherry-picks a merged PR to a backport staging branch; creates/updates the backport PR |
123+
| `scripts/port_to_next.sh` | Daily forward-port: accumulates `next` + source onto long-lived `port-<source>-to-next`, opens/updates the PR |
108124

109125
### CI Configuration
110126

.github/workflows/backport.yml

Lines changed: 42 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,52 @@ 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 straight into next. Because next
46+
# enforces squashed PRs, the staging PR carries ci-no-squash. It takes
47+
# precedence over backport-to-* if both are somehow present.
48+
if echo "$LABELS" | jq -e 'index("port-to-next")' >/dev/null; then
49+
TARGET_BRANCH="next"
50+
STAGING_BRANCH="port-to-next-staging"
51+
STAGING_PR_TITLE="chore: Accumulated ports to next"
52+
STAGING_PR_LABELS="ci-no-squash"
53+
VERB="ported"
54+
else
55+
# Extract the branch name from a backport-to-* label
56+
TARGET_BRANCH=$(echo "$LABELS" | jq -r '.[] | select(startswith("backport-to-")) | sub("backport-to-"; "")')
57+
if [ -z "$TARGET_BRANCH" ]; then
58+
echo "No backport-to-* or port-to-next label found"
59+
exit 1
60+
fi
61+
STAGING_BRANCH="backport-to-${TARGET_BRANCH}-staging"
62+
STAGING_PR_TITLE="chore: Accumulated backports to $TARGET_BRANCH"
63+
STAGING_PR_LABELS=""
64+
VERB="backported"
5165
fi
5266
5367
echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT
68+
echo "staging_branch=$STAGING_BRANCH" >> $GITHUB_OUTPUT
69+
echo "staging_pr_title=$STAGING_PR_TITLE" >> $GITHUB_OUTPUT
70+
echo "staging_pr_labels=$STAGING_PR_LABELS" >> $GITHUB_OUTPUT
71+
echo "verb=$VERB" >> $GITHUB_OUTPUT
5472
echo "Target branch: $TARGET_BRANCH"
73+
echo "Staging branch: $STAGING_BRANCH"
5574
5675
- name: Run backport script
5776
id: backport
5877
continue-on-error: true
5978
env:
6079
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
80+
STAGING_BRANCH: ${{ steps.extract-branch.outputs.staging_branch }}
81+
STAGING_PR_TITLE: ${{ steps.extract-branch.outputs.staging_pr_title }}
82+
STAGING_PR_LABELS: ${{ steps.extract-branch.outputs.staging_pr_labels }}
6183
run: |
6284
# Kludge. We should write this in Python or not checkout history.
6385
# Bash has an ugly footgun with changing history while running the script.
@@ -72,13 +94,14 @@ jobs:
7294
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
7395
run: |
7496
TARGET_BRANCH="${{ steps.extract-branch.outputs.target_branch }}"
75-
STAGING_BRANCH="backport-to-${TARGET_BRANCH}-staging"
97+
STAGING_BRANCH="${{ steps.extract-branch.outputs.staging_branch }}"
98+
VERB="${{ steps.extract-branch.outputs.verb }}"
7699
STAGING_PR=$(gh pr list --base "$TARGET_BRANCH" --head "$STAGING_BRANCH" --json number,url --jq '.[0]')
77100
STAGING_PR_NUMBER=$(echo "$STAGING_PR" | jq -r '.number')
78101
STAGING_PR_URL=$(echo "$STAGING_PR" | jq -r '.url')
79102
80103
gh pr comment "${{ github.event.pull_request.number }}" --body \
81-
"✅ Successfully backported to [$STAGING_BRANCH #$STAGING_PR_NUMBER]($STAGING_PR_URL)."
104+
"✅ Successfully $VERB to [$STAGING_BRANCH #$STAGING_PR_NUMBER]($STAGING_PR_URL)."
82105
83106
- name: Comment on original PR (failure)
84107
if: steps.backport.outcome == 'failure'
@@ -91,7 +114,7 @@ jobs:
91114
gh pr comment "${{ github.event.pull_request.number }}" --body \
92115
"❌ Failed to cherry-pick to \`$TARGET_BRANCH\` due to conflicts. (🤖) [View backport run]($WORKFLOW_URL)."
93116
94-
- name: Notify Slack and dispatch ClaudeBox on backport failure
117+
- name: Notify Slack and dispatch ClaudeBox on failure
95118
if: steps.backport.outcome == 'failure'
96119
env:
97120
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -101,11 +124,13 @@ jobs:
101124
URL: ${{ github.event.pull_request.html_url }}
102125
AUTHOR: ${{ github.event.pull_request.user.login }}
103126
BRANCH: ${{ steps.extract-branch.outputs.target_branch }}
127+
STAGING_BRANCH: ${{ steps.extract-branch.outputs.staging_branch }}
128+
VERB: ${{ steps.extract-branch.outputs.verb }}
104129
run: |
105130
106131
# 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")
132+
TEXT=$(printf '⚠️ %s failed: <%s|#%s %s> → `%s` (author: %s) (🤖)' \
133+
"${VERB^}" "$URL" "$PR" "$TITLE" "$BRANCH" "$AUTHOR")
109134
RESP=$(curl -sS -X POST https://slack.com/api/chat.postMessage \
110135
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
111136
-H "Content-type: application/json" \
@@ -120,8 +145,8 @@ jobs:
120145
fi
121146
122147
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." \
148+
-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." \
124149
-f link="${LINK:-$URL}" \
125-
-f target_ref="origin/backport-to-${BRANCH}-staging" \
150+
-f target_ref="origin/$STAGING_BRANCH" \
126151
-f slack_channel="$CHANNEL_ID" \
127152
-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:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Port v5-next to next
2+
3+
# Daily forward-port: accumulate everything new from `v5-next` onto the
4+
# long-lived `port-v5-next-to-next` branch (merging `next` and `v5-next` into
5+
# it) and open/update one large PR against `next`. See scripts/port_to_next.sh.
6+
7+
on:
8+
schedule:
9+
# Daily at 06:30 UTC.
10+
- cron: "30 6 * * *"
11+
workflow_dispatch:
12+
inputs:
13+
source_branch:
14+
description: "Source branch to port into next"
15+
required: false
16+
default: v5-next
17+
type: string
18+
19+
jobs:
20+
port:
21+
name: Forward-port v5-next into next
22+
if: ${{ github.event_name != 'schedule' || github.repository == 'AztecProtocol/aztec-packages' }}
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
27+
with:
28+
fetch-depth: 0
29+
token: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
30+
31+
- name: Configure Git
32+
run: |
33+
git config --global user.name AztecBot
34+
git config --global user.email tech@aztecprotocol.com
35+
36+
- name: Run port script
37+
id: port
38+
continue-on-error: true
39+
env:
40+
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
41+
run: ./scripts/port_to_next.sh "${{ github.event.inputs.source_branch || 'v5-next' }}"
42+
43+
- name: Notify Slack of conflicts
44+
if: steps.port.outputs.conflicts != ''
45+
env:
46+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
47+
SOURCE_BRANCH: ${{ github.event.inputs.source_branch || 'v5-next' }}
48+
PR_URL: ${{ steps.port.outputs.pr_url }}
49+
CONFLICTS: ${{ steps.port.outputs.conflicts }}
50+
run: |
51+
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
52+
TEXT=$(printf '⚠️ Daily port of `%s` → `next` hit merge conflicts. A PR was opened/updated for manual resolution: %s\n*Conflicted files:*\n```\n%s```\n<%s|View run>' \
53+
"$SOURCE_BRANCH" "${PR_URL:-(none)}" "$CONFLICTS" "$WORKFLOW_URL")
54+
curl -sS -X POST https://slack.com/api/chat.postMessage \
55+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
56+
-H "Content-type: application/json" \
57+
-d "$(jq -n --arg c "#backports" --arg t "$TEXT" '{channel:$c, text:$t}')"
58+
59+
- name: Notify Slack on unexpected failure
60+
if: steps.port.outcome == 'failure'
61+
env:
62+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
63+
SOURCE_BRANCH: ${{ github.event.inputs.source_branch || 'v5-next' }}
64+
run: |
65+
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
66+
TEXT=$(printf '❌ Daily port of `%s` → `next` failed unexpectedly (not a merge conflict). (🤖) <%s|View run>' \
67+
"$SOURCE_BRANCH" "$WORKFLOW_URL")
68+
curl -sS -X POST https://slack.com/api/chat.postMessage \
69+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
70+
-H "Content-type: application/json" \
71+
-d "$(jq -n --arg c "#backports" --arg t "$TEXT" '{channel:$c, text:$t}')"

0 commit comments

Comments
 (0)