Repository navigation
569 lines (519 loc) · 25.5 KB
/
Copy pathci.yml
File metadata and controls
569 lines (519 loc) · 25.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
name: CI
on:
push:
branches: [main]
pull_request:
# `labeled` so adding the `cross-os` label re-runs the PR with the full
# macOS/Windows matrix (see matrix-setup) without a dummy push.
types: [opened, synchronize, reopened, labeled]
schedule:
# WEEKLY full cross-OS matrix (Mondays 06:00 UTC), not nightly.
#
# Every merge to main ALREADY runs the full macOS/Windows matrix (see
# `matrix-setup`), so a nightly run mostly re-tests a commit that was
# already covered when it merged. Its unique value is catching runner-image
# DRIFT — a new macOS/Windows image breaking us with no code change — and
# that does not need daily sampling. macOS bills 10× Linux and Windows 2×,
# which made this the most expensive scheduled job across the repos.
# (2026-08-12 cost pass; PRs are unchanged and still Linux-only.)
- cron: '0 6 * * 1'
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
# Opt into Node.js 24 runtime for GitHub JavaScript actions — silences the
# deprecation warning for actions/checkout@v4, actions/setup-node@v7 etc.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Resolves the cross-OS test matrix by event. PRs get Linux-only (fast, cheap
# signal on every push); merges to main + the nightly cron + manual dispatch get
# the full macOS/Windows coverage. macOS runners bill 10× Linux and Windows 2×,
# so keeping them off every-PR iteration is the single biggest CI-cost lever.
# macOS/Windows run Node 20 only — their OS-specific steps all gate on Node 20,
# so a Node-22 cell there would just burn minutes.
matrix-setup:
name: Resolve cross-OS matrix
runs-on: ubuntu-latest
outputs:
cli: ${{ steps.set.outputs.cli }}
vsc: ${{ steps.set.outputs.vsc }}
steps:
- id: set
run: |
# PRs are Linux-only (cost) UNLESS labelled `cross-os` — use that
# label when a PR touches Windows/macOS-specific paths or fixes a
# cross-OS failure, so it is proven BEFORE it lands on main.
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ contains(github.event.pull_request.labels.*.name, 'cross-os') }}" != "true" ]; then
echo 'cli=[{"os":"ubuntu-latest","node":"20"},{"os":"ubuntu-latest","node":"22"}]' >> "$GITHUB_OUTPUT"
echo 'vsc=["ubuntu-latest"]' >> "$GITHUB_OUTPUT"
else
echo 'cli=[{"os":"ubuntu-latest","node":"20"},{"os":"ubuntu-latest","node":"22"},{"os":"macos-latest","node":"20"},{"os":"windows-latest","node":"20"}]' >> "$GITHUB_OUTPUT"
echo 'vsc=["ubuntu-latest","windows-latest"]' >> "$GITHUB_OUTPUT"
fi
commitlint:
name: Lint PR commit messages
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v6
with:
configFile: commitlint.config.mjs
# Dead-code gate (knip). BLOCKING: the baseline is clean for the
# `files` rule (no orphaned modules), so a new unreferenced file fails
# the PR. `exports` and `dependencies` run at `warn` severity in
# knip.json (surfaced in the log, non-failing) — a deliberate ratchet:
# the pre-existing backlog of intentionally-exported test/DI seams and
# a couple of unused deps is triaged over time, then bumped to `error`.
# One cheap Linux/Node-20 job — no matrix (dead-code analysis is
# platform-independent).
knip:
name: Dead-code check (knip)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '20'
cache: npm
- name: Install workspaces
run: npm ci
- name: knip
run: npm run knip
cli:
name: CLI (${{ matrix.os }} · node ${{ matrix.node }})
needs: matrix-setup
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
# Matrix resolved by `matrix-setup`: PRs run Linux × Node 20/22 only;
# main-push + nightly + manual add macOS/Windows on Node 20. macOS covers
# the darwin code paths (Keychain probes, /opt/homebrew PATH augment,
# Python helper); Node 18 was dropped when vitest 4 / rolldown adopted a
# node:util.styleText API absent on 18 (the published CLI still boots on
# `engines.node >=18`; only the test runner needs 20+).
matrix:
include: ${{ fromJSON(needs.matrix-setup.outputs.cli) }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
- name: Install workspaces
run: npm ci
- name: CLI typecheck
working-directory: apps/cli
run: npm run typecheck
- name: CLI tests (Vitest)
working-directory: apps/cli
run: npm run test
# @codeam/shared's own suite. The CLI bundles shared from source, but
# the shared-only tests (e.g. `point-picker/remix-compat.test.ts`, the
# Remix Worker's picker tests run against single mode) are not in the
# CLI's glob — without this step a PR that breaks them is only caught
# at release time.
- name: Shared package tests (Vitest)
working-directory: packages/shared
run: npm run test
# Explicit STEP-8 gate for the 2026-07 cursor-agent ENOENT fix (bead
# codeagent-ce5). The win32-native integration test reproduces Cursor's
# real installer layout — %LOCALAPPDATA%\cursor-agent\cursor-agent.exe —
# and asserts our resolver returns that absolute path on a REAL Windows
# kernel (path.win32 + real fs): the one thing Docker on a non-Windows
# host can't prove. Already part of `npm run test` above; surfaced here
# as a named, unmissable Windows gate. Self-skips off windows-latest.
- name: Cursor-agent Windows PATH resolution (win32 native)
if: matrix.os == 'windows-latest'
working-directory: apps/cli
run: npx vitest run cursor-agent-binary.win32
- name: CLI build
working-directory: apps/cli
run: npm run build
# Inspector de preview — E2E en un NAVEGADOR REAL.
#
# ⚠️ Es la única capa que puede juzgar las dos mitades juntas. El proxy
# se prueba contra sockets de verdad y el script inyectado se evalúa en
# un DOM, pero ninguno demuestra lo que importa: que una página cargada
# A TRAVÉS del proxy trae el script vivo, que el dashboard puede
# encenderlo por `postMessage` cross-origin, y que un clic devuelve el
# elemento con su caja real medida por el navegador.
#
# Una sola celda (Linux + Node 20): descarga un Chromium, y el
# comportamiento que se prueba es del motor, no del sistema operativo.
# Fuera de `npm test` a propósito, para no obligar a bajar navegadores en
# cada instalación de quien solo quiere correr los unitarios.
- name: CLI Preview inspector E2E (navegador real)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
working-directory: apps/cli
run: |
npx playwright install --with-deps chromium
npm run e2e
# Self-hosted host-agent REAL Docker integration test (Phase 3
# acceptance gate). Boots a Node-20 container running the built
# `codeam host-agent` against an in-test stub backend and asserts the
# real process tree: enroll → control channel → deploy → spawned
# pair-auto child reaches the stub. GitHub-hosted ubuntu runners ship
# a working Docker daemon, so we gate it to Linux + Node 20 (one cell)
# to avoid rebuilding the image 6×. DOCKER_E2E=1 flips it on; on every
# other OS / Node cell the suite self-skips (the spec probes
# `docker info` and skips cleanly when Docker is absent — the default
# `npm run test` step above never depends on Docker).
- name: CLI Docker E2E (self-hosted host-agent)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
working-directory: apps/cli
env:
DOCKER_E2E: '1'
run: npx vitest run host-agent.docker.e2e
# Fleet control plane — REAL Docker integration test (Phase 2 gate).
# Builds the real apps/box image (installs codeam-cli from npm — the
# box is NOT built from this branch's tarball, by design: it tracks
# the published release line the same way the real fleet does) and
# drives the real fleet_* handlers (HostAgentSupervisor +
# defaultDockerRunner, apps/cli/src/commands/host-agent.ts) against the
# Docker daemon: fleet_create_box → `docker inspect` asserts every
# isolation invariant (single named-volume mount, non-root user,
# CapDrop=ALL, no-new-privileges, memory/cpu/pids caps, fleet-net, the
# com.codeagent.* labels); the entrypoint redeems against the in-test
# stub backend; then stop/start/delete(removeVolume) round-trips.
# Gated to Linux + Node 20 (Docker-capable, one cell); RUN_FLEET_INT=1
# flips it on, every other cell self-skips cleanly. Layer-caches the
# box image build across runs (keyed on the Dockerfile hash), like
# the fleet step's own cache key.
# The PINNED integration MCP servers, driven for real over stdio: `npx`
# downloads the exact pin the registry hands the `codeam mcp-run` shim and
# the test asserts `tools/list` serves the tools users reach for. This is
# the gate that would have caught 2026-09-03: the ClickUp pin (0.14.4) had
# gone Proprietary and answered every tool with "LIMITED" behind a licence
# key — and the only thing that noticed was a user. The licence-field
# audit in packages/shared guards the pin; this guards what it SERVES.
# One cell (network + npx download); RUN_MCP_INT=1 flips it on.
- name: CLI pinned MCP servers serve their tools (real npx, tools/list)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
working-directory: apps/cli
env:
RUN_MCP_INT: '1'
run: npx vitest run integration/mcp-clickup-tools --reporter=verbose
# The tool ROUTER, end to end over real processes: the real `codeam
# mcp-router` fronting real `mcp-run` shims, which broker a token from a
# real (local) http server and spawn real MCP servers (the SDK's). Proves
# the agent sees 4 tools, calls reach the right child, and a broken child
# degrades instead of failing. Needs the built CLI (dist/), which the
# earlier build step produced.
- name: CLI tool router end-to-end (real router, real shims, real MCP servers)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
working-directory: apps/cli
env:
RUN_MCP_INT: '1'
run: npx vitest run integration/mcp-router --reporter=verbose
- name: CLI fleet box Docker integration test (create/inspect/lifecycle)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
# The image build runs `npm install -g codeam-cli` and has no timeout of
# its own: on 2026-09-03 a slow npm registry wedged this step for over an
# hour (the job limit is 6 h). Normal duration is ~10 min; fail fast.
timeout-minutes: 25
working-directory: apps/cli
env:
RUN_FLEET_INT: '1'
# The test reads FLEET_BOX_IMAGE_TAG and, when set, skips its own
# docker build and reuses the pre-built image loaded below.
FLEET_BOX_IMAGE_TAG: codeam-box-fleet-e2e:ci
run: |
# Build the box image with GHA layer cache and LOAD it into the
# local daemon so the test's `docker run` can use it without a
# second build.
docker buildx build \
--cache-from type=gha,scope=fleet-box \
--cache-to type=gha,scope=fleet-box,mode=max \
--load \
-t codeam-box-fleet-e2e:ci \
"$GITHUB_WORKSPACE/apps/box"
# PHP projects preview with `php -S` + a Composer pre-flight
# (codeagent-nczt.10): both must run as the runtime `box` user.
docker run --rm --entrypoint sh codeam-box-fleet-e2e:ci \
-c 'php -v && composer --version && php -m | grep -qx mbstring'
npx vitest run fleet-box.int
# ── Session Baton REAL gate — provisioning ───────────────────────────
# Put the SDK-BUNDLED, exact-pinned `claude` on PATH so the native TUI
# half of the baton test can launch it (`buildClaudeLaunch` probes PATH).
#
# ⚠️ Deliberately NOT `npm i -g @anthropic-ai/claude-code`: that floats to
# whatever Claude Code published today — exactly the drift the
# acp-version-freeze contract test exists to prevent — and the ACP half of
# this same test spawns the SDK-bundled binary, so a global install would
# run TWO DIFFERENT claude versions inside one baton session. This is the
# same binary `bundledClaudeBinDir()` resolves on a self-hosted box, and
# it moves only when `@agentclientprotocol/claude-agent-acp` is bumped.
- name: Provision the house agent (pinned claude binary on PATH)
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
run: |
set -euo pipefail
BIN=$(find "$GITHUB_WORKSPACE" -type f \
-path '*/@anthropic-ai/claude-agent-sdk-linux-*/claude' \
-print -quit)
if [ -z "$BIN" ]; then
echo "::error::SDK-bundled claude binary not found under node_modules." \
"Expected @anthropic-ai/claude-agent-sdk-linux-*/claude (an optional" \
"platform dep of @agentclientprotocol/claude-agent-acp). Did npm ci" \
"run with --omit=optional?"
exit 1
fi
chmod +x "$BIN"
mkdir -p "$HOME/.local/bin"
ln -sf "$BIN" "$HOME/.local/bin/claude"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "claude -> $BIN"
"$BIN" --version
# Session Baton — REAL local-session integration test. Drives the actual
# composition root (runBatonSession) with the real native `claude` TUI,
# the real ACP adapter and the real relay against an in-test stub
# backend, and asserts the 2026-08-18 regressions stay fixed:
# take-control BEFORE the first TUI turn reaches MOBILE_DRIVE (never
# wedges in SWITCHING), take-control AGAIN once a transcript exists on
# disk exercises the `session/load` branch, handback returns to
# LOCAL_DRIVE, no raw TUI chrome reaches the chat pipe, and SIGINT posts
# `online:false` before the process exits.
#
# AUTH: the agent runs as a HOUSE AGENT ("CodeAgent Cloud") — Claude Code
# pointed at our managed backend through ANTHROPIC_BASE_URL +
# ANTHROPIC_AUTH_TOKEN with the MiniMax model pins, byte-for-byte the env
# `host-agent.ts` writes for a house `self_hosted_deploy`. No personal
# Anthropic key, no per-developer credential — the same recipe the
# acp-chrome-canary workflow already uses. (The house PROXY hop
# `…/api/v1/agent-proxy` is skipped on purpose: its token is minted
# server-side per (userId, codespaceId) and only adds metering, so there
# is nothing a CI runner could hold long-lived.)
#
# ⚠️ This is a REAL gate: with RUN_BATON_INT=1 and CI=true the test FAILS
# LOUDLY when claude is missing or the backend won't answer — it does not
# skip. It only skips locally (CI unset). Fork PRs cannot read the secret,
# so the step is guarded on it and announces itself as unproven instead.
# `secrets` is not a context you can read from a step-level `if`, so the
# availability is reduced to a step OUTPUT here and branched on below.
- name: Is the house-agent credential available?
id: house_secret
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
env:
HOUSE_AGENT_TOKEN: ${{ secrets.MINIMAX_API_KEY }}
run: |
set -euo pipefail
if [ -n "${HOUSE_AGENT_TOKEN:-}" ]; then
echo 'available=true' >> "$GITHUB_OUTPUT"
else
echo 'available=false' >> "$GITHUB_OUTPUT"
fi
- name: CLI session-baton local integration test (real claude, house agent)
if: >-
matrix.os == 'ubuntu-latest' && matrix.node == '20'
&& steps.house_secret.outputs.available == 'true'
working-directory: apps/cli
env:
RUN_BATON_INT: '1'
ANTHROPIC_BASE_URL: https://api.minimax.io/anthropic
ANTHROPIC_AUTH_TOKEN: ${{ secrets.MINIMAX_API_KEY }}
ANTHROPIC_MODEL: MiniMax-M3
ANTHROPIC_DEFAULT_SONNET_MODEL: MiniMax-M3
ANTHROPIC_DEFAULT_OPUS_MODEL: MiniMax-M3
ANTHROPIC_DEFAULT_HAIKU_MODEL: MiniMax-M3
CLAUDE_CODE_AUTO_COMPACT_WINDOW: '512000'
API_TIMEOUT_MS: '3000000'
# Belt to the version pin's suspenders — the on-disk binary
# self-updates unless told not to (see acp-version-freeze).
DISABLE_AUTOUPDATER: '1'
run: npx vitest run integration/baton-local --reporter=verbose
- name: Session-baton gate NOT run (no house-agent secret)
if: >-
matrix.os == 'ubuntu-latest' && matrix.node == '20'
&& steps.house_secret.outputs.available != 'true'
run: |
echo "::warning::Session-baton real gate did NOT run — MINIMAX_API_KEY is" \
"unavailable (fork PR, or the secret was removed). The baton" \
"regressions are UNPROVEN on this run."
# Smoke from the repo workspace. Confirms the freshly-built
# binary boots on this OS, the argument parser handles every
# `<cmd> --help` we ship, and the live backend at
# api.codeagent-mobile.com still speaks the protocol we expect
# (pair --dry-run, link --dry-run).
- name: CLI smoke (cwd=workspace)
working-directory: apps/cli
run: node scripts/smoke.mjs
# Same smoke, but with cwd = $HOME / %USERPROFILE%. On the Windows
# runner this exercises C:\Users\runneradmin which contains the
# same legacy junctions that crashed Krzysztof in v2.16.1 (#43).
- name: CLI smoke (cwd=home, regression for #43)
working-directory: apps/cli
env:
SMOKE_CWD: home
run: node scripts/smoke.mjs
# macOS-only: pack + global install + --help. Catches the
# bin-shim + postinstall + node-pty prebuild combo we'd
# otherwise only discover when users `npm i -g` on a fresh
# macOS box. Runs on Node 20 only — the other matrix cells
# already verify the typecheck/test surface on this OS.
- name: macOS install-and-help smoke
if: matrix.os == 'macos-latest' && matrix.node == '20'
working-directory: apps/cli
run: |
npm pack
npm install -g ./codeam-cli-*.tgz
codeam --help
codeam --version
cli-coverage:
name: CLI coverage
runs-on: ubuntu-latest
# Run after the cross-OS matrix passes — no point burning coverage
# cycles on a PR that already breaks tests on a target OS.
needs: cli
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- name: Install workspaces
run: npm ci
# Threshold gate lives in vitest.config.ts — vitest exits
# non-zero when the suite drops below the configured floors.
- name: CLI coverage (with threshold gate)
working-directory: apps/cli
run: npm run test:coverage
# Upload as an artifact so PR authors can browse the HTML
# report when the threshold drops. The lcov.info file also
# lets future codecov / coveralls integration light up
# without a second config pass.
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: cli-coverage-${{ github.sha }}
path: apps/cli/coverage/
# Actions storage is billed: a coverage report is read the day it runs.
retention-days: 3
vsc-plugin:
name: VS Code plugin (${{ matrix.os }})
needs: matrix-setup
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
# PRs run Linux only; main-push + nightly + manual add Windows (see
# `matrix-setup`).
matrix:
os: ${{ fromJSON(needs.matrix-setup.outputs.vsc) }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- name: Install workspaces
run: npm ci
- name: VS Code plugin typecheck
working-directory: apps/vsc-plugin
run: npm run typecheck
- name: VS Code plugin tests (Vitest)
working-directory: apps/vsc-plugin
run: npm run test
- name: VS Code plugin build
working-directory: apps/vsc-plugin
run: npm run build
- name: VS Code plugin package (.vsix)
working-directory: apps/vsc-plugin
run: npx --no-install @vscode/vsce package --no-dependencies
jetbrains-plugin:
name: JetBrains plugin — gradle build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 17
# Pure-JVM unit tests first — fast feedback if any util / parser
# regresses. The cross-OS install-command builder is one of these
# (see `BuildInstallCommandTest`), which means a v2.17.x-style
# PowerShell-5 regression now gets caught at PR time instead of
# at the user's terminal in Windows.
- name: Gradle test
working-directory: apps/jetbrains-plugin
run: ./gradlew test
- name: Gradle build
working-directory: apps/jetbrains-plugin
run: ./gradlew buildPlugin
# Plugin Verifier against the newest IDE only (one ~1 GB download):
# the 2024.1 builds predate the deprecated / scheduled-for-removal
# markers, so only this run catches an API the platform is about to
# delete (codeagent-20r: createLocalShellWidget). Internal and
# scheduled-for-removal usages fail the job (build.gradle.kts failureLevel).
- name: Plugin Verifier (latest IDE)
working-directory: apps/jetbrains-plugin
run: ./gradlew verifyPlugin -PverifyLatestOnly=true
# ── CI results → Discord #ci_notifications ───────────────────────────────
# Posts a per-job summary of this run. Always for main pushes / the weekly
# cross-OS run / manual dispatch; for PRs only when something failed (so a
# green PR stays quiet). Reads the run's jobs via the API so matrix cells
# (ubuntu/macos/windows) show individually, not as one aggregate result.
# Owner request 2026-08-19; webhook secret DISCORD_RELEASE_WEBHOOK (shared
# with release.yml). Missing secret → warning only, never a red job.
notify-discord:
name: CI results → Discord
needs: [matrix-setup, commitlint, knip, cli, cli-coverage, vsc-plugin, jetbrains-plugin]
if: always()
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Summarise jobs and post
env:
GH_TOKEN: ${{ github.token }}
WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT: ${{ github.event_name }}
REF: ${{ github.ref_name }}
SHA: ${{ github.sha }}
PR_NUM: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
ACTOR: ${{ github.actor }}
run: |
set -uo pipefail
JOBS=$(gh api --paginate "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
--jq '.jobs[] | select(.name != "CI results → Discord") | [.name, (.conclusion // .status)] | @tsv')
FAILED=0; CANCELLED=0; LINES=""
while IFS=$'\t' read -r NAME RES; do
[ -z "$NAME" ] && continue
case "$RES" in
success) ICON="✅";;
skipped) ICON="⏭️";;
cancelled) ICON="⚪"; CANCELLED=1;;
failure) ICON="❌"; FAILED=1;;
*) ICON="❔";;
esac
LINES="$LINES"$'\n'"$ICON $NAME"
done <<< "$JOBS"
if [ "$CANCELLED" = "1" ]; then
echo "Run was superseded/cancelled — the replacing run will report."
exit 0
fi
if [ "$EVENT" = "pull_request" ] && [ "$FAILED" = "0" ]; then
echo "PR run is green — staying quiet."
exit 0
fi
if [ "$FAILED" = "1" ]; then HEAD="❌ **CI FAILED**"; else HEAD="✅ **CI passed**"; fi
case "$EVENT" in
pull_request) CTX="PR #$PR_NUM — $PR_TITLE";;
schedule) CTX="weekly cross-OS run on $REF";;
*) CTX="$EVENT on $REF @ ${SHA:0:8} by $ACTOR";;
esac
MSG=$(printf '%s · clients · %s%s\n%s' "$HEAD" "$CTX" "$LINES" "$RUN_URL")
echo "$MSG"
if [ -z "${WEBHOOK:-}" ]; then
echo "::warning::DISCORD_RELEASE_WEBHOOK not set — CI summary not posted."
exit 0
fi
jq -n --arg c "$MSG" '{content:$c, flags:4}' \
| curl -sS -o /dev/null -w 'discord %{http_code}\n' -X POST -H 'Content-Type: application/json' -d @- "$WEBHOOK"