Skip to content

Commit 74b28b6

Browse files
authored
chore(ci): dispatch ClaudeBox to the v2 webhook (retire abandoned v1 SSH-tunnel path) (#23600)
## Why The merge-train auto-fix (and every other ClaudeBox CI kickoff) goes through `.github/workflows/claudebox.yml`, which SSH-tunneled to the **abandoned v1 ClaudeBox server** on the private build instance (`http://localhost:4001/run` via `ci.aztec-labs.com`). That v1 server is dead — symptom seen in #team-alpha: `create_pr` failing with *"No GitHub access configured"* and the analysis link pointing at a non-resolving v1 URL. ClaudeBox v2 runs as a public service at `https://claudebox.work` (this is the same bot you get when you `@ClaudeBox` in Slack). It exposes the same `/run` API, Bearer-authed with `CLAUDEBOX_API_SECRET`. The fix is to point CI at the v2 webhook and drop the v1 tunnel. ## What changed - **`.github/workflows/claudebox.yml`** — both jobs now `POST` to `${CLAUDEBOX_URL:-https://claudebox.work}/run` instead of tunneling to the v1 server. Removed the `Setup SSH tunnel` steps and the 120-minute synchronous poll loop. Dispatch is now **fire-and-forget**: v2 reports progress to the bound Slack thread and to the GitHub comment IDs we pass through (`comment_id` / `run_comment_id`), so the `/claudebox` PR-comment UX is preserved. - **`ci3/slack_notify_with_claudebox_kickoff`** — forwards the Slack channel ID + thread ts it just posted to (`-f slack_channel`, `-f slack_thread_ts`). v2 threads its status reply under that kickoff message, restoring the Slack feedback loop for merge-train / nightly / healthcheck kickoffs. This one script backs ~12 kickoff workflows, so they need no individual change. - **`backport.yml` / `deploy-network.yml`** — the two direct `gh workflow run claudebox.yml` callers now also forward `slack_channel` / `slack_thread_ts`. - `target_ref` (which v1 checked out server-side) is folded into the prompt — v2's `/run` has no `target_ref` field, so the agent fetches/bases its branch on the ref per the prompt (matches v2's prompt-driven model). `claudebox.yml` keeps `CLAUDEBOX_API_SECRET` in one place, so no per-workflow secret plumbing was needed. ## Operator prerequisites (action required) 1. **Secret**: the `CLAUDEBOX_API_SECRET` GitHub Actions secret in this repo must equal the deployed v2 server's `api_secret`. (`POST /run` is verified live — it returns 401 without a matching bearer.) 2. **Slack membership**: the v2 ClaudeBox bot must be a member of the kickoff channels (`#alerts-next-scenario`, `#backports`, `#honk-team`, `#team-bonobos`, `#team-fairies`, `#alpha-team`, the per-team merge-train channels, and the `#alerts-<network>` deploy channels) for threaded status. If it isn't, the session still runs — it just won't post back into that thread. 3. Optional: set repo variable `CLAUDEBOX_URL` to override the default endpoint. ## Notes - The `claude-review` job is migrated too. v2 *also* handles `claude-review` labels natively via the GitHub App `workflow_run`/`pull_request` webhook, so that job can be retired in a follow-up once the App is confirmed wired for this repo. - No ClaudeBox (`AztecProtocol/claudebox`) code change is required — v2's `/run` already accepts this payload (`prompt`, `user`, `repo`, `run_url`, `link`, numeric `comment_id`/`run_comment_id`, `slack_channel`, `slack_thread_ts`). ## Testing - `bash -n` on the kickoff script and on every `run:` body in `claudebox.yml` (7 steps) — pass. - `jq` payload construction validated for both the issue-comment case (numeric `comment_id`/`run_comment_id` added) and the workflow_dispatch case (omitted) — produces valid JSON matching the v2 `RunRequest`. - All three workflow YAMLs parse. - `POST https://claudebox.work/run` confirmed reachable and auth-protected (401 without bearer). End-to-end with the real secret could not be exercised from this session (the deployed secret is not exposed here). --- *Created by [claudebox](https://claudebox.work/v2/sessions/4b53f2dd8370a83a) · group: `slackbot`*
2 parents a51f60a + 65ebd23 commit 74b28b6

4 files changed

Lines changed: 79 additions & 120 deletions

File tree

.github/workflows/backport.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,6 @@ jobs:
122122
gh workflow run claudebox.yml \
123123
-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." \
124124
-f link="${LINK:-$URL}" \
125-
-f target_ref="origin/backport-to-${BRANCH}-staging"
125+
-f target_ref="origin/backport-to-${BRANCH}-staging" \
126+
-f slack_channel="$CHANNEL_ID" \
127+
-f slack_thread_ts="$TS"

.github/workflows/claudebox.yml

Lines changed: 61 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,27 @@ on:
1616
required: false
1717
type: string
1818
target_ref:
19-
description: 'Git ref to checkout in the container (e.g., origin/backport-to-v4-next-staging)'
19+
description: 'Git ref the session should work against (e.g., origin/merge-train/barretenberg)'
20+
required: false
21+
type: string
22+
slack_channel:
23+
description: 'Slack channel ID to thread status into (set by the kickoff script)'
24+
required: false
25+
type: string
26+
slack_thread_ts:
27+
description: 'Slack thread timestamp to reply under (set by the kickoff script)'
2028
required: false
2129
type: string
2230

31+
# ClaudeBox v2 runs as a public service at https://claudebox.work. CI hands a
32+
# job off by POSTing to its /run webhook (Bearer-authed with
33+
# CLAUDEBOX_API_SECRET) and returns immediately — the session reports progress
34+
# back to the bound Slack thread and to any GitHub comment IDs we pass through.
35+
# The old v1 server lived on a private build instance reached over an SSH
36+
# tunnel; that path is retired.
37+
env:
38+
CLAUDEBOX_URL: ${{ vars.CLAUDEBOX_URL || 'https://claudebox.work' }}
39+
2340
jobs:
2441
claudebox:
2542
if: >-
@@ -49,41 +66,13 @@ jobs:
4966
repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
5067
-f content='eyes' || true
5168
52-
- name: Setup SSH tunnel to ClaudeBox
53-
env:
54-
BUILD_INSTANCE_SSH_KEY: ${{ secrets.BUILD_INSTANCE_SSH_KEY }}
55-
run: |
56-
set -eu
57-
mkdir -p ~/.ssh
58-
echo "${BUILD_INSTANCE_SSH_KEY}" | base64 --decode > ~/.ssh/build_instance_key
59-
chmod 600 ~/.ssh/build_instance_key
60-
61-
# SSH tunnel: CI runner :4001 → bastion :3000 → (reverse tunnel) → claude-box :3001
62-
ssh -f -N -L 4001:localhost:3000 \
63-
-o StrictHostKeyChecking=no \
64-
-o ServerAliveInterval=30 \
65-
-o ServerAliveCountMax=3 \
66-
-o ConnectTimeout=15 \
67-
-i ~/.ssh/build_instance_key \
68-
ubuntu@ci.aztec-labs.com
69-
70-
# Wait for tunnel
71-
for i in $(seq 1 15); do
72-
if curl -s -o /dev/null --max-time 2 http://localhost:4001/ 2>/dev/null; then
73-
echo "SSH tunnel ready"
74-
exit 0
75-
fi
76-
sleep 1
77-
done
78-
echo "ERROR: SSH tunnel failed to connect"
79-
exit 1
80-
8169
- name: Parse command
8270
id: parse
8371
env:
8472
COMMENT_BODY: ${{ github.event.comment.body || '' }}
8573
INPUT_PROMPT: ${{ inputs.prompt || '' }}
8674
INPUT_LINK: ${{ inputs.link || '' }}
75+
INPUT_TARGET_REF: ${{ inputs.target_ref || '' }}
8776
run: |
8877
if [ -n "$INPUT_PROMPT" ]; then
8978
PROMPT="$INPUT_PROMPT"
@@ -93,6 +82,14 @@ jobs:
9382
LINK=""
9483
fi
9584
85+
# ClaudeBox v2 has no separate target_ref input; fold it into the
86+
# prompt so the agent fetches and bases its branch on the right ref.
87+
if [ -n "$INPUT_TARGET_REF" ]; then
88+
PROMPT="$PROMPT
89+
90+
Work against git ref: $INPUT_TARGET_REF. Fetch it and base your branch on it."
91+
fi
92+
9693
echo "link=$LINK" >> "$GITHUB_OUTPUT"
9794
{
9895
echo "prompt<<PROMPT_EOF"
@@ -120,22 +117,24 @@ jobs:
120117
echo "run_comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
121118
echo "Posted status comment: $COMMENT_ID"
122119
123-
- name: Run ClaudeBox
124-
timeout-minutes: 120
120+
- name: Dispatch ClaudeBox v2
125121
env:
126-
CLAUDEBOX_URL: http://localhost:4001
127122
CLAUDEBOX_API_SECRET: ${{ secrets.CLAUDEBOX_API_SECRET }}
128123
CLAUDEBOX_PROMPT: ${{ steps.parse.outputs.prompt }}
129124
CLAUDEBOX_LINK: ${{ steps.parse.outputs.link }}
130-
CLAUDEBOX_TARGET_REF: ${{ inputs.target_ref || '' }}
131125
COMMENT_ID: ${{ github.event.comment.id || '' }}
132126
RUN_COMMENT_ID: ${{ steps.status_comment.outputs.run_comment_id || '' }}
133127
REPO: ${{ github.repository }}
134128
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
135129
AUTHOR: ${{ github.event.comment.user.login || github.actor }}
130+
SLACK_CHANNEL: ${{ inputs.slack_channel || '' }}
131+
SLACK_THREAD_TS: ${{ inputs.slack_thread_ts || '' }}
136132
run: |
137-
AUTH="Authorization: Bearer ${CLAUDEBOX_API_SECRET}"
138-
echo "Creating payload..."
133+
if [ -z "${CLAUDEBOX_API_SECRET:-}" ]; then
134+
echo "ERROR: CLAUDEBOX_API_SECRET is not set; cannot dispatch ClaudeBox v2"
135+
exit 1
136+
fi
137+
139138
PAYLOAD=$(jq -n \
140139
--arg prompt "$CLAUDEBOX_PROMPT" \
141140
--arg user "$AUTHOR" \
@@ -144,41 +143,29 @@ jobs:
144143
--arg repo "$REPO" \
145144
--arg run_url "$RUN_URL" \
146145
--arg link "$CLAUDEBOX_LINK" \
147-
--arg target_ref "$CLAUDEBOX_TARGET_REF" \
148-
'{prompt: $prompt, user: $user, comment_id: $comment_id, run_comment_id: $run_comment_id, repo: $repo, run_url: $run_url, link: $link, target_ref: $target_ref}')
146+
--arg slack_channel "$SLACK_CHANNEL" \
147+
--arg slack_thread_ts "$SLACK_THREAD_TS" \
148+
'{prompt: $prompt, user: $user, repo: $repo, run_url: $run_url, link: $link, slack_channel: $slack_channel, slack_thread_ts: $slack_thread_ts}
149+
+ (if $comment_id != "" then {comment_id: ($comment_id | tonumber)} else {} end)
150+
+ (if $run_comment_id != "" then {run_comment_id: ($run_comment_id | tonumber)} else {} end)')
149151
150-
echo "Sending payload..."
151-
# Start session — returns 202 with log URL
152+
# Fire-and-forget: v2 reports progress to Slack / GitHub comments.
152153
RESPONSE=$(curl -sS -w "\n%{http_code}" \
153-
-H "$AUTH" -H "Content-Type: application/json" \
154+
-H "Authorization: Bearer ${CLAUDEBOX_API_SECRET}" \
155+
-H "Content-Type: application/json" \
154156
-d "$PAYLOAD" "${CLAUDEBOX_URL}/run")
155157
156158
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
157159
BODY=$(echo "$RESPONSE" | head -n -1)
158160
159161
if [ "$HTTP_CODE" -ge 400 ] 2>/dev/null; then
160-
echo "ClaudeBox returned HTTP $HTTP_CODE: $BODY"
162+
echo "ClaudeBox v2 returned HTTP $HTTP_CODE: $BODY"
161163
exit 1
162164
fi
163165
164-
LOG_URL=$(echo "$BODY" | jq -r '.log_url // empty')
165-
SESSION_ID=$(basename "$LOG_URL")
166-
echo "Session started: $LOG_URL"
167-
168-
echo "Session received, polling..."
169-
# Poll until completed
170-
while true; do
171-
sleep 30
172-
STATUS_BODY=$(curl -sS -H "$AUTH" "${CLAUDEBOX_URL}/session/${SESSION_ID}" 2>/dev/null || echo '{}')
173-
STATUS=$(echo "$STATUS_BODY" | jq -r '.status // "unknown"')
174-
echo "$(date -u +%H:%M:%S) status=$STATUS"
175-
if [ "$STATUS" != "running" ]; then
176-
EXIT_CODE=$(echo "$STATUS_BODY" | jq -r '.exit_code // 1')
177-
echo "Session finished: status=$STATUS exit_code=$EXIT_CODE"
178-
echo "Log: $LOG_URL"
179-
exit "$EXIT_CODE"
180-
fi
181-
done
166+
SESSION_ID=$(echo "$BODY" | jq -r '.session_id // empty')
167+
echo "ClaudeBox v2 session: ${CLAUDEBOX_URL}/v2/sessions/${SESSION_ID}"
168+
echo "Status: $(echo "$BODY" | jq -r '.status // "unknown"')"
182169
183170
claude-review:
184171
if: >-
@@ -189,33 +176,6 @@ jobs:
189176
permissions:
190177
contents: read
191178
steps:
192-
- name: Setup SSH tunnel to ClaudeBox
193-
env:
194-
BUILD_INSTANCE_SSH_KEY: ${{ secrets.BUILD_INSTANCE_SSH_KEY }}
195-
run: |
196-
set -eu
197-
mkdir -p ~/.ssh
198-
echo "${BUILD_INSTANCE_SSH_KEY}" | base64 --decode > ~/.ssh/build_instance_key
199-
chmod 600 ~/.ssh/build_instance_key
200-
201-
ssh -f -N -L 4001:localhost:3000 \
202-
-o StrictHostKeyChecking=no \
203-
-o ServerAliveInterval=30 \
204-
-o ServerAliveCountMax=3 \
205-
-o ConnectTimeout=15 \
206-
-i ~/.ssh/build_instance_key \
207-
ubuntu@ci.aztec-labs.com
208-
209-
for i in $(seq 1 15); do
210-
if curl -s -o /dev/null --max-time 2 http://localhost:4001/ 2>/dev/null; then
211-
echo "SSH tunnel ready"
212-
exit 0
213-
fi
214-
sleep 1
215-
done
216-
echo "ERROR: SSH tunnel failed to connect"
217-
exit 1
218-
219179
- name: Post review status comment
220180
id: status_comment
221181
env:
@@ -232,10 +192,8 @@ jobs:
232192
echo "run_comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
233193
echo "Posted review status comment: $COMMENT_ID"
234194
235-
- name: Trigger ClaudeBox review
236-
timeout-minutes: 120
195+
- name: Dispatch ClaudeBox v2 review
237196
env:
238-
CLAUDEBOX_URL: http://localhost:4001
239197
CLAUDEBOX_API_SECRET: ${{ secrets.CLAUDEBOX_API_SECRET }}
240198
PR_NUMBER: ${{ github.event.pull_request.number }}
241199
PR_TITLE: ${{ github.event.pull_request.title }}
@@ -246,7 +204,10 @@ jobs:
246204
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
247205
REPO: ${{ github.repository }}
248206
run: |
249-
AUTH="Authorization: Bearer ${CLAUDEBOX_API_SECRET}"
207+
if [ -z "${CLAUDEBOX_API_SECRET:-}" ]; then
208+
echo "ERROR: CLAUDEBOX_API_SECRET is not set; cannot dispatch ClaudeBox v2"
209+
exit 1
210+
fi
250211
251212
PROMPT="Review PR #${PR_NUMBER}: ${PR_TITLE}
252213
${PR_URL}
@@ -265,36 +226,22 @@ jobs:
265226
--arg repo "$REPO" \
266227
--arg run_url "$RUN_URL" \
267228
--arg link "$PR_URL" \
268-
--arg profile "review" \
269-
'{prompt: $prompt, user: $user, run_comment_id: $run_comment_id, repo: $repo, run_url: $run_url, link: $link, profile: $profile}')
229+
'{prompt: $prompt, user: $user, repo: $repo, run_url: $run_url, link: $link}
230+
+ (if $run_comment_id != "" then {run_comment_id: ($run_comment_id | tonumber)} else {} end)')
270231
271232
RESPONSE=$(curl -sS -w "\n%{http_code}" \
272-
-H "$AUTH" -H "Content-Type: application/json" \
233+
-H "Authorization: Bearer ${CLAUDEBOX_API_SECRET}" \
234+
-H "Content-Type: application/json" \
273235
-d "$PAYLOAD" "${CLAUDEBOX_URL}/run")
274236
275237
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
276238
BODY=$(echo "$RESPONSE" | head -n -1)
277239
278240
if [ "$HTTP_CODE" -ge 400 ] 2>/dev/null; then
279-
echo "ClaudeBox returned HTTP $HTTP_CODE: $BODY"
280-
exit 1
241+
echo "ClaudeBox v2 returned HTTP $HTTP_CODE: $BODY"
242+
# Review dispatch failures are informational — don't fail the workflow.
243+
exit 0
281244
fi
282245
283-
LOG_URL=$(echo "$BODY" | jq -r '.log_url // empty')
284-
SESSION_ID=$(basename "$LOG_URL")
285-
echo "Review session started: $LOG_URL"
286-
287-
# Poll until completed
288-
while true; do
289-
sleep 30
290-
STATUS_BODY=$(curl -sS -H "$AUTH" "${CLAUDEBOX_URL}/session/${SESSION_ID}" 2>/dev/null || echo '{}')
291-
STATUS=$(echo "$STATUS_BODY" | jq -r '.status // "unknown"')
292-
echo "$(date -u +%H:%M:%S) status=$STATUS"
293-
if [ "$STATUS" != "running" ]; then
294-
EXIT_CODE=$(echo "$STATUS_BODY" | jq -r '.exit_code // 1')
295-
echo "Review finished: status=$STATUS exit_code=$EXIT_CODE"
296-
echo "Log: $LOG_URL"
297-
# Don't fail the workflow on review errors — the review itself is informational
298-
exit 0
299-
fi
300-
done
246+
SESSION_ID=$(echo "$BODY" | jq -r '.session_id // empty')
247+
echo "ClaudeBox v2 review session: ${CLAUDEBOX_URL}/v2/sessions/${SESSION_ID}"

.github/workflows/deploy-network.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,9 @@ jobs:
294294
gh workflow run claudebox.yml \
295295
-f prompt="$PROMPT" \
296296
-f link="${LINK:-$RUN_URL}" \
297-
-f target_ref="${{ steps.checkout-ref.outputs.ref }}" || true
297+
-f target_ref="${{ steps.checkout-ref.outputs.ref }}" \
298+
-f slack_channel="$CHANNEL_ID" \
299+
-f slack_thread_ts="$TS" || true
298300
299301
update-irm:
300302
needs: deploy-network

ci3/slack_notify_with_claudebox_kickoff

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,22 @@ if [[ -n "${SLACK_BOT_TOKEN:-}" ]]; then
4747
fi
4848
fi
4949

50-
# Dispatch ClaudeBox workflow (CI=1 guaranteed above; gh auth assumed in CI)
51-
target_ref_args=()
50+
# Dispatch ClaudeBox workflow (CI=1 guaranteed above; gh auth assumed in CI).
51+
# claudebox.yml forwards these inputs to the ClaudeBox v2 /run webhook; passing
52+
# the Slack channel + thread ts lets v2 reply into the message posted above.
53+
extra_args=()
5254
if [[ -n "$target_ref" ]]; then
53-
target_ref_args+=(-f "target_ref=$target_ref")
55+
extra_args+=(-f "target_ref=$target_ref")
56+
fi
57+
if [[ -n "${CHANNEL_ID:-}" ]]; then
58+
extra_args+=(-f "slack_channel=$CHANNEL_ID")
59+
fi
60+
if [[ -n "${TS:-}" ]]; then
61+
extra_args+=(-f "slack_thread_ts=$TS")
5462
fi
5563
gh workflow run claudebox.yml \
5664
-R "$REPO" --ref next \
5765
-f prompt="$prompt" \
5866
-f link="$link" \
59-
"${target_ref_args[@]}" || true
67+
"${extra_args[@]}" || true
6068
echo "ClaudeBox dispatched"

0 commit comments

Comments
 (0)