forked from fujibee/agmsg
-
Notifications
You must be signed in to change notification settings - Fork 0
512 lines (469 loc) · 22.8 KB
/
Copy pathtests.yml
File metadata and controls
512 lines (469 loc) · 22.8 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
name: tests
# Run the full Bats suite on every PR and on pushes to main. This repo is
# public, so GitHub-hosted runners are free — running everything every time is
# simpler and safer than selectively running "only the changed tests", which
# would miss regressions in shared libs (lib/*.sh) that fan out across many
# scripts. What we do instead is run the whole suite in parallel shards.
#
# Sharding: the suite has grown to ~790 tests, 11 min on ubuntu and 12-17 on
# macOS, which is the dominant wait on every PR. `bats-shard` splits it across
# SHARD_TOTAL runners per OS. The split is computed from the tree by
# .github/scripts/shard-tests.sh, NOT from a list in this file: a static list
# would put a newly added test file in no shard at all, and CI would go on
# reporting green while silently not running it. tests/test_ci_sharding.bats
# pins that the shards stay a partition of tests/*.bats.
#
# Docs-only skip (without the paths-ignore trap):
# the bats check is REQUIRED on main. A naive `paths-ignore` would mean it
# never reports on a docs-only PR, leaving it stuck "pending" forever and
# unmergeable. So instead the shard jobs ALWAYS run and ALWAYS complete green —
# a `changes` job decides whether the diff is docs-only, and only the heavy
# steps (install + run) are skipped when it is. The required context is
# therefore always reported; on docs-only PRs it just reports green in seconds
# without running the suite.
#
# The required context is the `bats` SUMMARY job, not the shards. Shard names
# carry their index, so changing SHARD_TOTAL would rename every required check
# and silently stop enforcing the ones branch protection still names. The
# summary job's name is stable across any future re-shard, and it fails unless
# the whole matrix succeeded — including when the matrix was skipped, which
# must never read as green.
#
# What counts as "docs-only" (= safe to skip the bash suite): the docs tree
# (`docs/**`), the marketing site (`site/**` — a standalone Astro app with no
# bearing on the bash suite), the desktop app (`app/**` — Rust/TS the bats
# suite never reads; it gets its own `app typecheck` job below instead),
# `llms.txt` / `llms-full.txt`, and the top-level prose docs (README,
# CHANGELOG, etc). SKILL.md is deliberately EXCLUDED — the suite asserts on it
# (tests/test_install.bats checks it has no unsubstituted placeholder), so a
# SKILL.md change must run the suite. Anything not on the allowlist runs it too.
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
env:
# Number of parallel shards per OS. Kept in one place because the matrix, the
# job name, and the argument to shard-tests.sh must agree — if they drift, a
# shard silently runs the wrong slice of the suite. Raising it is safe (the
# split is recomputed, not hardcoded); it only changes the shard job names,
# which is exactly why the required check is the summary job below.
SHARD_TOTAL: 4
jobs:
# Cheap gate: is this PR's diff entirely documentation? Pushes to main always
# run the full suite (docs_only=false) — main must stay fully verified.
changes:
name: detect docs-only
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
app_changed: ${{ steps.detect.outputs.app_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need base + head history to diff the PR range
- id: detect
env:
EVENT: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [ "$EVENT" != "pull_request" ]; then
echo "Event is '$EVENT' (not pull_request) — running the full suite."
echo "docs_only=false" >> "$GITHUB_OUTPUT"
echo "app_changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
changed=$(git diff --name-only "$BASE_SHA...$HEAD_SHA")
echo "Changed files:"
echo "$changed"
docs_only=true
app_changed=false
while IFS= read -r f; do
[ -z "$f" ] && continue
case "$f" in
docs/*) ;;
site/*) ;;
app/*) app_changed=true ;;
llms.txt|llms-full.txt) ;;
README.md|CHANGELOG.md|CONTRIBUTING.md|PRIVACY.md|ARCHITECTURE.md|RELEASING.md|LICENSE) ;;
# NOTE: SKILL.md is intentionally not here — the suite tests it.
*) docs_only=false ;;
esac
done <<< "$changed"
echo "Result: docs_only=$docs_only app_changed=$app_changed"
echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"
echo "app_changed=$app_changed" >> "$GITHUB_OUTPUT"
bats-shard:
name: bats (${{ matrix.os }} ${{ matrix.shard }}/4)
needs: changes
# Run even if `changes` somehow failed/was skipped — fail open to the full
# suite rather than leaving the required check unreported (which would
# block the PR). When changes failed, docs_only is empty → heavy steps run.
#
# This is also why the matrix is a literal list rather than something the
# `changes` job computes: a dynamic matrix cannot be evaluated when the job
# that feeds it failed, so a broken `changes` would take the whole suite
# down instead of falling back to running everything.
if: ${{ !cancelled() }}
runs-on: ${{ matrix.os }}
# Bound a hung runner/test so a stall fails fast (and is re-runnable) instead
# of pinning this REQUIRED check for the GitHub default of 6h.
#
# Keep this comfortably above the real suite time, not just above it. The
# cap was 15 when the suite ran in ~5 min; the suite has since grown to 728
# tests and macOS measures 11m57s-14m10s, leaving 1-3 min of headroom. The
# result was that ~47% of macOS runs (9 of 19 sampled) were killed mid-suite
# at ~15m18s, at whatever test happened to be running — a REQUIRED check
# behaving like a coin flip on PRs that changed nothing relevant. 25 matches
# the app-test-windows job below.
#
# If a shard starts firing this cap, re-shard (raise SHARD_TOTAL) rather
# than re-running until it passes: a job that always dies at the same
# elapsed time is a timeout, not a flaky test.
# bats-windows sets its own timeout; this covers ubuntu/macos.
#
# Left at the pre-sharding value on purpose. A shard is a quarter of the
# suite, so this is now generous headroom rather than a tight bound — and
# a cap that only ever fires on a genuine hang is the one that carries no
# false positives. (It was 15 against a 12-14 min macOS suite, and killed
# ~47% of macOS runs mid-suite at whatever test was running.)
timeout-minutes: 25
strategy:
# Cover both GNU (Linux) and BSD (macOS) userlands — the scripts shell out
# to sed/stat/mktemp/ps etc. whose flags differ between them.
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# Must list exactly 1..SHARD_TOTAL. The `bats` summary job verifies
# end-to-end that the shards actually covered every test file, so a
# drift here fails the required check instead of silently dropping a
# slice of the suite.
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Docs-only change — skipping suite
if: needs.changes.outputs.docs_only == 'true'
run: echo "Diff is documentation-only; skipping the bats suite and reporting green to satisfy the required check."
- name: Compute this shard's test files
if: needs.changes.outputs.docs_only != 'true'
env:
SHARD: ${{ matrix.shard }}
run: |
set -euo pipefail
if [ "$SHARD" -gt "$SHARD_TOTAL" ]; then
echo "::error::matrix shard $SHARD exceeds SHARD_TOTAL=$SHARD_TOTAL — the matrix and SHARD_TOTAL have drifted apart"
exit 1
fi
.github/scripts/shard-tests.sh "$SHARD" "$SHARD_TOTAL" > shard-files.txt
if [ ! -s shard-files.txt ]; then
echo "::error::shard $SHARD of $SHARD_TOTAL was assigned no test files"
exit 1
fi
echo "Shard $SHARD/$SHARD_TOTAL runs $(wc -l < shard-files.txt) file(s):"
cat shard-files.txt
- name: Ensure sqlite3 is available (Linux)
if: runner.os == 'Linux' && needs.changes.outputs.docs_only != 'true'
run: sqlite3 --version >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y sqlite3; }
# Install a modern bats-core (the suite uses BATS_TEST_TMPDIR) into a
# HOME-local npm prefix. This avoids both the ubuntu runner's EACCES on
# `npm i -g` under /usr/local and the macOS SIP block on /usr/lib that the
# bats-core action's default install paths hit.
- name: Install bats
if: needs.changes.outputs.docs_only != 'true'
run: |
npm config set prefix "$HOME/.npm-global"
npm install -g bats
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
- name: Show tool versions
if: needs.changes.outputs.docs_only != 'true'
run: |
bash --version | head -1
sqlite3 --version
bats --version
- name: Run bats suite (this shard)
if: needs.changes.outputs.docs_only != 'true'
run: |
set -euo pipefail
# Paths come from shard-tests.sh over tests/*.bats and contain no
# spaces; xargs keeps them as separate arguments to one bats run.
xargs bats --print-output-on-failure < shard-files.txt
# What the summary job checks the partition against. Uploaded only when
# the suite actually ran, so a docs-only PR produces none — and the
# summary skips the check for exactly that case.
- name: Record which files this shard ran
if: needs.changes.outputs.docs_only != 'true'
uses: actions/upload-artifact@v4
with:
name: bats-manifest-${{ matrix.os }}-${{ matrix.shard }}
path: shard-files.txt
retention-days: 1
# The REQUIRED status check. Shard job names carry their index, so making the
# shards required would mean branch protection had to be re-pointed every
# time SHARD_TOTAL changed — and until someone did, protection would be
# enforcing check names that no longer run. This job's name never changes.
#
# It fails unless the whole matrix succeeded. `skipped` is treated as failure
# too: a suite that did not run is not a suite that passed, and the whole
# point of the always-report pattern above is that green means something.
bats:
name: bats
needs: [changes, bats-shard]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Check shard results
env:
RESULT: ${{ needs.bats-shard.result }}
run: |
set -euo pipefail
echo "bats shard matrix result: $RESULT"
case "$RESULT" in
success) ;;
skipped) echo "::error::the bats shards did not run — refusing to report green"; exit 1 ;;
*) echo "::error::the bats shards did not all pass (result: $RESULT)"; exit 1 ;;
esac
- name: Docs-only change — no shard manifests to verify
if: needs.changes.outputs.docs_only == 'true'
run: echo "Diff is documentation-only; the shards reported green without running, so there is no partition to verify."
- name: Download shard manifests
if: needs.changes.outputs.docs_only != 'true'
uses: actions/download-artifact@v4
with:
pattern: bats-manifest-*
path: manifests
# The end-to-end guarantee: every tests/*.bats file was actually run, on
# every OS. A partition bug in shard-tests.sh, a matrix entry someone
# deleted, a new test file that landed nowhere — all of them show up here
# as a named missing file rather than as a green run that quietly tested
# less than it did yesterday.
- name: Verify the shards covered the whole suite
if: needs.changes.outputs.docs_only != 'true'
run: |
set -euo pipefail
expected="$(find tests -maxdepth 1 -name '*.bats' | LC_ALL=C sort)"
status=0
for os in ubuntu-latest macos-latest; do
actual="$(cat manifests/bats-manifest-"$os"-*/shard-files.txt | LC_ALL=C sort)"
if [ "$actual" = "$expected" ]; then
echo "$os: all $(printf '%s\n' "$expected" | wc -l | tr -d ' ') test file(s) ran."
continue
fi
status=1
missing="$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$actual" | uniq))"
extra="$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$actual" | uniq))"
dupes="$(printf '%s\n' "$actual" | uniq -d)"
# Plain `if`s, not `[ -n "$x" ] && echo`: under `set -e` a false
# test at the end of a && list exits the step, which would report
# only the first kind of discrepancy and swallow the rest.
if [ -n "$missing" ]; then
printf '::error::%s: test file(s) run by no shard:\n%s\n' "$os" "$missing"
fi
if [ -n "$extra" ]; then
printf '::error::%s: shard(s) ran file(s) not in tests/:\n%s\n' "$os" "$extra"
fi
if [ -n "$dupes" ]; then
printf '::error::%s: file(s) run by more than one shard:\n%s\n' "$os" "$dupes"
fi
done
exit "$status"
# Windows coverage for the native helpers shipped in #103 (Git Bash runner,
# sqlite3 compatibility shim, cygpath normalization). The POSIX-assuming suite
# is not fully green on Windows yet, so this is staged (#125): a single focused
# leg targeting the Windows-relevant install tests. Not a required check yet —
# promote it to required once it is reliably green. The required checks stay
# bats (ubuntu/macos).
#
# The old informational "full" leg (the whole suite, continue-on-error) was
# removed: it ran POSIX-assuming tests that hang on Windows and hit the job
# timeout. A timed-out job reports "cancelled", and continue-on-error does NOT
# absorb a cancellation (only a failure), so it turned the whole tests run red.
# Windows-relevant tests get fixed in separate PRs and folded into this leg.
bats-windows:
name: bats (windows-latest, ${{ matrix.leg }})
needs: changes
if: ${{ !cancelled() }}
runs-on: windows-latest
# Bound the job so an unexpected hang can't pin a runner. The focused leg is
# fast and green today; this is just a safety net.
timeout-minutes: 12
strategy:
fail-fast: false
matrix:
include:
# Focused leg: the install-side native Windows helpers (#103). These
# are green on windows-latest today and are the candidate to promote
# to a required check.
- leg: install helpers
filter: "[Ww]indows"
target: tests/test_install.bats
# The Windows runtime path, which nothing here covered until #567: a
# change to how liveness is decided on Windows shipped through a CI
# that never decided liveness on Windows. Everything else about that
# class is proved against a `tasklist` stub on a POSIX host, which
# shows what the code does when a probe says "not found" -- not that
# Git Bash says it, and not that a launch survives it.
#
# These two run with the real tasklist and the real MSYS pid space,
# and they assert the EFFECT rather than the premise: the TUI reaches
# its bridged --remote handoff, and a bridge is actually started.
# Reaching the handoff is not delivering a message -- they are
# separate failure points and each has its own test.
- leg: windows runtime (#567)
filter: "windows-native"
target: tests/test_codex_monitor.bats tests/test_codex_bridge_launcher.bats
defaults:
run:
# bats is bash; on Windows runners `shell: bash` is Git Bash, the same
# environment native Windows users run agmsg under.
shell: bash
steps:
- uses: actions/checkout@v4
- name: Docs-only change — skipping suite
if: needs.changes.outputs.docs_only == 'true'
run: echo "Diff is documentation-only; skipping the Windows bats legs."
# chocolatey's community feed intermittently returns 504, which leaves the
# runner with no sqlite3 at all ("Chocolatey installed 0/0 packages").
# Retry rather than let a transient feed outage redden an unrelated PR.
- name: Install sqlite3
if: needs.changes.outputs.docs_only != 'true'
shell: pwsh
run: |
for ($attempt = 1; $attempt -le 3; $attempt++) {
choco install sqlite -y --no-progress
if ($LASTEXITCODE -eq 0) { exit 0 }
Write-Host "::warning::choco install sqlite failed (attempt $attempt of 3); retrying"
Start-Sleep -Seconds 15
}
Write-Host "::error::sqlite3 could not be installed after 3 attempts"
exit 1
- name: Put chocolatey shims on PATH (Git Bash form)
if: needs.changes.outputs.docs_only != 'true'
run: echo "/c/ProgramData/chocolatey/bin" >> "$GITHUB_PATH"
- name: Install bats
if: needs.changes.outputs.docs_only != 'true'
run: |
npm config set prefix "$HOME/.npm-global"
npm install -g bats
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
# Fail HERE if a tool is missing, not three minutes later inside the suite.
# The trailing `|| echo` this used to end with turned a missing sqlite3
# into a passing step, so the real symptom surfaced as unrelated install
# tests failing with "sqlite3 is required but not found" — which reads as
# a defect in whatever PR happened to be running.
- name: Show tool versions
if: needs.changes.outputs.docs_only != 'true'
run: |
bash --version | head -1
sqlite3 --version || sqlite3.exe --version
bats --version
- name: Run tests
if: needs.changes.outputs.docs_only != 'true'
run: |
# matrix.target may name multiple files (space-separated) — leave it
# unquoted so the shell word-splits it into separate bats path args.
# It's a fixed value from the matrix above, not external input.
if [ -n "${{ matrix.filter }}" ]; then
bats --print-output-on-failure --filter "${{ matrix.filter }}" ${{ matrix.target }}
else
bats --print-output-on-failure ${{ matrix.target }}
fi
# The desktop app (app/**) is Rust + TypeScript — the bats suite never
# reads it, and the full Tauri build only runs on app-v* release tags via
# app-release.yml. Without this job an app-only PR would merge with zero
# compile verification (the bats legs skip via the app/* allowlist above).
# cargo check + tsc --noEmit is a few minutes, not the full-bundle cost.
# Same always-report pattern as bats: the job always completes, and only
# the heavy steps are gated, so it can be promoted to a required check
# without ever leaving a PR stuck pending. Pushes to main always run it.
# Fail-open mirrors bats too: steps skip only on an EXPLICIT
# app_changed=false — if the changes job breaks and its output is empty,
# the typecheck runs rather than green-washing a possibly-app diff.
app-check:
name: app typecheck
needs: changes
if: ${{ !cancelled() }}
# macOS: Tauri's crates (wry etc.) compile against system frameworks that
# are already on the runner — ubuntu would need the webkit2gtk/gtk dev
# stack installed first. Also matches the release build environment.
runs-on: macos-latest
timeout-minutes: 20
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- name: No app changes — skipping typecheck
if: needs.changes.outputs.app_changed == 'false'
run: echo "Diff does not touch app/ — reporting green without running the typecheck."
- uses: pnpm/action-setup@v4
if: needs.changes.outputs.app_changed != 'false'
with:
version: 10
- uses: actions/setup-node@v4
if: needs.changes.outputs.app_changed != 'false'
with:
node-version: 20
cache: pnpm
cache-dependency-path: app/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.app_changed != 'false'
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.app_changed != 'false'
with:
workspaces: app/src-tauri
- name: Install frontend deps
if: needs.changes.outputs.app_changed != 'false'
run: pnpm install --frozen-lockfile
- name: TypeScript typecheck
if: needs.changes.outputs.app_changed != 'false'
run: pnpm exec tsc --noEmit
- name: Frontend tests
if: needs.changes.outputs.app_changed != 'false'
run: pnpm test
- name: Bundle pinned agmsg-core
if: needs.changes.outputs.app_changed != 'false'
run: scripts/bundle-core.sh
- name: Rust check
if: needs.changes.outputs.app_changed != 'false'
run: cargo check --manifest-path src-tauri/Cargo.toml
- name: Rust tests
if: needs.changes.outputs.app_changed != 'false'
run: cargo test --manifest-path src-tauri/Cargo.toml
# Windows leg for the app: the command layer's bash resolution and path
# conversion are all behind cfg(windows) and had never run in CI — the very
# 0.1.1→0.1.3 regressions (WSL bash.exe, backslash argv). This runs cargo test
# on windows-latest so those paths are actually exercised. Same fail-open
# convention as app-check: run unless the diff provably doesn't touch app/.
app-test-windows:
name: app test (windows-latest)
needs: changes
if: ${{ !cancelled() }}
runs-on: windows-latest
timeout-minutes: 25
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- name: No app changes — skipping app tests
if: needs.changes.outputs.app_changed == 'false'
run: echo "Diff does not touch app/ — reporting green without running the app tests."
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.app_changed != 'false'
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.app_changed != 'false'
with:
workspaces: app/src-tauri
- name: Bundle pinned agmsg-core
if: needs.changes.outputs.app_changed != 'false'
run: scripts/bundle-core.sh
shell: bash
- name: Rust tests
if: needs.changes.outputs.app_changed != 'false'
run: cargo test --manifest-path src-tauri/Cargo.toml