Skip to content

Commit 940ea5c

Browse files
committed
Make full CI reliable and deterministic
1 parent 14eeb2f commit 940ea5c

11 files changed

Lines changed: 276 additions & 147 deletions

File tree

.github/workflows/canary.yml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,44 @@ jobs:
1111
contract:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4
15-
- uses: actions/setup-node@v4
14+
- uses: actions/checkout@v7
15+
- uses: actions/setup-node@v7
1616
with:
1717
node-version: 22
1818
cache: npm
1919
- run: npm ci
2020
- run: npm install -D @opencode-ai/plugin@latest
21+
- run: npm run lint
2122
- run: npm run typecheck
22-
- run: npx vitest run tests/contract
23+
- run: npm test
24+
- run: npm run build
2325

2426
loader-smoke:
2527
runs-on: ubuntu-latest
2628
steps:
27-
- uses: actions/checkout@v4
28-
- uses: actions/setup-node@v4
29+
- uses: actions/checkout@v7
30+
- uses: actions/setup-node@v7
2931
with:
3032
node-version: 22
3133
cache: npm
3234
- run: npm ci
3335
- run: npm install -g opencode-ai@latest
3436
- run: bash scripts/smoke-loader.sh
37+
38+
notify:
39+
needs: [contract, loader-smoke]
40+
if: >
41+
always() && github.event_name == 'schedule' &&
42+
(needs.contract.result != 'success' || needs.loader-smoke.result != 'success')
43+
runs-on: ubuntu-latest
44+
timeout-minutes: 2
45+
steps:
46+
- name: Notify Google Chat on scheduled failure
47+
env:
48+
WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
49+
run: |
50+
[ -n "$WEBHOOK_URL" ] || { echo "GOOGLE_CHAT_WEBHOOK_URL is missing"; exit 1; }
51+
curl --fail-with-body --retry 3 --retry-all-errors --max-time 20 \
52+
-sS -X POST -H 'Content-Type: application/json' \
53+
-d "{\"text\": \"🚨 opencode-plugin canary FAILED: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
54+
"$WEBHOOK_URL"

.github/workflows/ci.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.

.github/workflows/live-channels.yml

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,46 @@ name: Live — agent channels (email + SMS)
99
# PR is READY (non-draft). The unattended cadence chains off the scheduled
1010
# canary via workflow_run, so live and the host stay in lock-step.
1111
on:
12-
pull_request:
13-
branches: [main]
14-
types: [opened, synchronize, reopened, ready_for_review]
12+
workflow_call:
13+
inputs:
14+
timeout_s:
15+
description: "Seconds to wait for the reply"
16+
required: false
17+
type: string
18+
default: "150"
19+
orchestrated:
20+
description: "True when the full-stack workflow owns the shared live lock"
21+
required: false
22+
type: boolean
23+
default: false
1524
workflow_dispatch:
1625
inputs:
1726
timeout_s:
1827
description: "Seconds to wait for the reply"
1928
default: "150"
20-
workflow_run:
21-
workflows: ["Canary — plugin vs opencode latest"]
22-
types: [completed]
2329

2430
permissions:
2531
contents: read
2632

2733
concurrency:
2834
# Only ONE client may hold the AUT's Inkbox tunnel at a time, so EVERY live
2935
# tunnel workflow MUST use this exact group — they queue behind each other.
30-
group: inkbox-live-aut-tunnel
36+
group: ${{ inputs.orchestrated && format('inkbox-live-child-{0}', github.run_id) || 'inkbox-live-aut-tunnel' }}
3137
cancel-in-progress: false
38+
queue: max
3239

3340
jobs:
3441
live:
3542
runs-on: ubuntu-latest
36-
# Skip fork PRs (no secrets), draft PRs (expensive), and canary chains
37-
# where the canary FAILED (never take the tunnel against a broken host).
38-
if: >-
39-
(github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false)) &&
40-
(github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')
4143
strategy:
4244
fail-fast: false
4345
max-parallel: 1 # legs share the AUT identity → one at a time
4446
matrix:
4547
mode: [mock, real]
4648

4749
steps:
48-
- uses: actions/checkout@v4
49-
- uses: actions/setup-node@v4
50+
- uses: actions/checkout@v7
51+
- uses: actions/setup-node@v7
5052
with:
5153
node-version: 22
5254
cache: npm
@@ -79,7 +81,7 @@ jobs:
7981
REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }}
8082
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
8183
LIVE_REAL_MODEL: ${{ matrix.mode == 'real' && '1' || '' }}
82-
LIVE_REPLY_TIMEOUT_S: ${{ github.event.inputs.timeout_s || '150' }}
84+
LIVE_REPLY_TIMEOUT_S: ${{ inputs.timeout_s || '150' }}
8385
run: |
8486
npx vitest run --config vitest.live.config.ts \
8587
tests/live/email-reply.test.ts tests/live/sms.test.ts
@@ -102,7 +104,7 @@ jobs:
102104
103105
- name: Upload artifacts (on failure only)
104106
if: failure()
105-
uses: actions/upload-artifact@v4
107+
uses: actions/upload-artifact@v7
106108
with:
107109
name: live-logs-${{ matrix.mode }}
108110
retention-days: 5
@@ -111,18 +113,3 @@ jobs:
111113
${{ env.AUT_SERVE_LOG }}
112114
${{ runner.temp }}/mock.log
113115
if-no-files-found: ignore
114-
115-
# Alert only when an unattended (canary-chained) run fails; PRs and manual
116-
# dispatch stay silent — the check is visible inline there.
117-
notify:
118-
needs: [live]
119-
if: always() && needs.live.result == 'failure' && github.event_name == 'workflow_run'
120-
runs-on: ubuntu-latest
121-
steps:
122-
- name: Notify chat webhook on scheduled failure
123-
# Non-blocking: a flaky webhook must never flip the suite result.
124-
run: |
125-
[ -z "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" ] && exit 0
126-
curl -sS --max-time 10 --retry 3 -X POST "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" \
127-
-H 'Content-Type: application/json' \
128-
-d '{"text": "⚠️ FAILED — Live channels (email + SMS) suite\n\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' || true

.github/workflows/live-external-events.yml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ name: Live — external events (webhook → agent acts)
66
# send, so this runs only on ready (non-draft) PRs + dispatch, and shares the
77
# AUT tunnel lock with the other live suites.
88
on:
9-
pull_request:
10-
branches: [main]
11-
types: [opened, synchronize, reopened, ready_for_review]
9+
workflow_call:
10+
inputs:
11+
timeout_s:
12+
description: "Seconds to wait for the external-event model turn"
13+
required: false
14+
type: string
15+
default: "200"
16+
orchestrated:
17+
description: "True when the full-stack workflow owns the shared live lock"
18+
required: false
19+
type: boolean
20+
default: false
1221
workflow_dispatch:
1322
inputs:
1423
timeout_s:
@@ -19,19 +28,17 @@ permissions:
1928
contents: read
2029

2130
concurrency:
22-
group: inkbox-live-aut-tunnel
31+
group: ${{ inputs.orchestrated && format('inkbox-live-child-{0}', github.run_id) || 'inkbox-live-aut-tunnel' }}
2332
cancel-in-progress: false
33+
queue: max
2434

2535
jobs:
2636
external-events:
2737
runs-on: ubuntu-latest
28-
# Skip fork PRs (no secrets) and draft PRs (expensive).
29-
if: >-
30-
(github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false))
3138

3239
steps:
33-
- uses: actions/checkout@v4
34-
- uses: actions/setup-node@v4
40+
- uses: actions/checkout@v7
41+
- uses: actions/setup-node@v7
3542
with:
3643
node-version: 22
3744
cache: npm
@@ -55,7 +62,7 @@ jobs:
5562
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
5663
INKBOX_WEBHOOK_SECRET_GITHUB: ${{ secrets.LIVE_GITHUB_WEBHOOK_SECRET }}
5764
LIVE_REAL_MODEL: "1"
58-
LIVE_REPLY_TIMEOUT_S: ${{ github.event.inputs.timeout_s || '200' }}
65+
LIVE_REPLY_TIMEOUT_S: ${{ inputs.timeout_s || '200' }}
5966
run: |
6067
npx vitest run --config vitest.live.config.ts tests/live/external-event.test.ts
6168
@@ -73,7 +80,7 @@ jobs:
7380
7481
- name: Upload artifacts (on failure only)
7582
if: failure()
76-
uses: actions/upload-artifact@v4
83+
uses: actions/upload-artifact@v7
7784
with:
7885
name: external-events-logs
7986
retention-days: 5

.github/workflows/live-stack.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: Full stack e2e
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
workflow_dispatch:
7+
workflow_run:
8+
workflows: ["Canary — plugin vs opencode latest"]
9+
types: [completed]
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: inkbox-live-aut-tunnel
16+
cancel-in-progress: false
17+
queue: max
18+
19+
jobs:
20+
channels:
21+
if: >
22+
(github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
23+
github.event.pull_request.head.repo.full_name == github.repository) ||
24+
github.event_name == 'workflow_dispatch' ||
25+
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
26+
github.event.workflow_run.head_branch == 'main')
27+
uses: ./.github/workflows/live-channels.yml
28+
with:
29+
orchestrated: true
30+
secrets: inherit
31+
32+
voice:
33+
needs: channels
34+
if: >
35+
!cancelled() &&
36+
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
37+
github.event.pull_request.head.repo.full_name == github.repository) ||
38+
github.event_name == 'workflow_dispatch' ||
39+
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
40+
github.event.workflow_run.head_branch == 'main'))
41+
uses: ./.github/workflows/live-voice.yml
42+
with:
43+
orchestrated: true
44+
secrets: inherit
45+
46+
external-events:
47+
needs: voice
48+
if: >
49+
!cancelled() &&
50+
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
51+
github.event.pull_request.head.repo.full_name == github.repository) ||
52+
github.event_name == 'workflow_dispatch' ||
53+
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
54+
github.event.workflow_run.head_branch == 'main'))
55+
uses: ./.github/workflows/live-external-events.yml
56+
with:
57+
orchestrated: true
58+
secrets: inherit
59+
60+
full-stack:
61+
name: full-stack
62+
needs: [channels, voice, external-events]
63+
if: >
64+
!cancelled() &&
65+
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
66+
github.event.pull_request.head.repo.full_name == github.repository) ||
67+
github.event_name == 'workflow_dispatch' ||
68+
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
69+
github.event.workflow_run.head_branch == 'main'))
70+
runs-on: ubuntu-latest
71+
steps:
72+
- name: Require every live suite to pass
73+
env:
74+
CHANNELS_RESULT: ${{ needs.channels.result }}
75+
VOICE_RESULT: ${{ needs.voice.result }}
76+
EXTERNAL_EVENTS_RESULT: ${{ needs.external-events.result }}
77+
run: |
78+
failed=0
79+
for suite in CHANNELS VOICE EXTERNAL_EVENTS; do
80+
result_var="${suite}_RESULT"
81+
result="${!result_var}"
82+
echo "$suite: $result"
83+
if [ "$result" != "success" ]; then failed=1; fi
84+
done
85+
exit "$failed"
86+
87+
notify:
88+
needs: [channels, voice, external-events, full-stack]
89+
if: >
90+
always() && github.event_name == 'workflow_run' &&
91+
github.event.workflow_run.event == 'schedule' &&
92+
github.event.workflow_run.conclusion == 'success' &&
93+
github.event.workflow_run.head_branch == 'main' &&
94+
(needs.channels.result != 'success' || needs.voice.result != 'success' ||
95+
needs.external-events.result != 'success' || needs.full-stack.result != 'success')
96+
runs-on: ubuntu-latest
97+
timeout-minutes: 2
98+
steps:
99+
- name: Notify Google Chat
100+
env:
101+
WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
102+
run: |
103+
[ -n "$WEBHOOK_URL" ] || { echo "GOOGLE_CHAT_WEBHOOK_URL is missing"; exit 1; }
104+
curl --fail-with-body --retry 3 --retry-all-errors --max-time 20 \
105+
-sS -X POST -H 'Content-Type: application/json' \
106+
-d "{\"text\": \"🚨 opencode-plugin full-stack e2e FAILED (chained off the canary): ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
107+
"$WEBHOOK_URL"

0 commit comments

Comments
 (0)