-
Notifications
You must be signed in to change notification settings - Fork 5
519 lines (436 loc) · 21.5 KB
/
Copy pathci.yml
File metadata and controls
519 lines (436 loc) · 21.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
name: CI
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
workflow_dispatch:
# Least-privilege default. The `dependency-submission` job overrides
# to `contents: write` because it submits the dependency graph back
# to GitHub; every other job runs read-only.
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# SonarCloud needs full history to attribute blame and compute
# "new code" metrics correctly. Shallow clones break both.
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
cache: true
# uv pins the Python runtime for eval-harness tests (and the weekly eval
# job). The harness invokes `uv run --python <PinnedPythonVersion>` so the
# version is guaranteed across every runner regardless of what system
# Python is installed. Update PinnedPythonVersion in
# internal/eval_harness/python.go if the target moves.
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Install dependencies
run: make deps
# Prefetch UNPOISONED so the poisoned `go test` step below cannot fail on a
# module-cache miss. This MUST run BEFORE the binaries are built: `go mod
# download all` writes new entries into the TRACKED go.sum, and the binary
# staleness detector (internal/testutil/ailangbin.go) compares binary mtime
# against the newest Go source file — so downloading afterwards makes every
# ailang binary look stale and silently SKIPS every binary-gated integration
# test. Measured on PR #599's first run: binaries built 21:21:03Z, go.sum
# touched 21:21:17Z, TestRunSmokeInTempDir_Pass / TestPromptCommand_Piping /
# TestZ3VerifyEndToEnd all skipped, and the no-silent-skip gate below
# correctly red-lighted the job. build.yml was unaffected precisely because
# its download already precedes `go install`.
- name: Download all Go modules (including test dependencies)
run: go mod download all
- name: Build binaries
run: make install
- name: Consumer regression guardrail (M-RIG-RELIABILITY)
# Fail the PR if an AILANG change breaks real-world consumer .ail code (the escape/
# parser/type/effect constructs the motoko harness relies on). The v0.27.0 \uXXXX
# lexer break stalled the rig ~35h with zero CI signal — this is that missing gate.
# (Cloud CI has no motoko fork/model, so it checks the vendored corpus; the rig also
# checks the real motoko core via MOTOKO_REPO.)
run: tools/ci/motoko_smoke.sh
# Without z3, every SMT/contract-verification integration test
# (internal/smt TestSolve_*_Z3, internal/bestof TestZ3VerifyEndToEnd, …)
# t.Skip()s — they had silently never run in CI until 2026-07-10.
- name: Install z3 (SMT solver for contract-verification tests)
run: sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends z3
# jq drives scripts/hooks/format_ail.sh, which TestFormatAilHookSinkRoundTrip
# executes end-to-end. It ships on the ubuntu-latest image today, but the
# no-silent-skip gate below now turns a missing jq into a hard failure
# instead of an invisible skip — so pin it explicitly rather than rely on
# the image (docusaurus-deploy.yml already installs jq for the same reason).
- name: Install jq (drives the .ail format PostToolUse hook under test)
run: sudo apt-get install -y -qq --no-install-recommends jq && jq --version
# -timeout is PER TEST BINARY (package). cmd/ailang legitimately runs
# 35-50s (CLI subprocess tests), so 60s was boundary-flaky on loaded
# runners (red dev 2026-07-10: package panicked at exactly 1m0s).
# 300s still catches genuine hangs (M-DX11's intent) without the flake.
- name: Run tests with timeout (M-DX11)
env:
HTTP_PROXY: http://127.0.0.1:9
HTTPS_PROXY: http://127.0.0.1:9
NO_PROXY: localhost,127.0.0.1
GOPROXY: off
run: |
[[ "$HTTP_PROXY" == "http://127.0.0.1:9" ]] || { echo "::error::HTTP_PROXY poison is not configured"; exit 1; }
[[ "$HTTPS_PROXY" == "http://127.0.0.1:9" ]] || { echo "::error::HTTPS_PROXY poison is not configured"; exit 1; }
[[ "$NO_PROXY" == "localhost,127.0.0.1" ]] || { echo "::error::NO_PROXY loopback bypass is not configured"; exit 1; }
[[ "$GOPROXY" == "off" ]] || { echo "::error::GOPROXY is not disabled"; exit 1; }
go test -timeout 300s ./...
# `go test ./...` output is non-verbose: a t.Skip is invisible and CI
# stays green while integration coverage silently evaporates (e.g. if
# the "Build binaries" or "Install z3" steps above are ever removed or
# reordered after the test step). Re-run one fast representative test
# per external-binary gate (ailang via bin/, ailang via PATH, z3)
# verbosely and require an explicit PASS — a skip or rename fails here.
- name: Assert binary-gated integration tests ran (no silent skips)
run: |
go test -count=1 -v -run '^(TestRunSmokeInTempDir_Pass|TestPromptCommand_Piping|TestZ3VerifyEndToEnd|TestFormatAilHookSinkRoundTrip|TestGateLint_SelfTest)$' \
./internal/pkg ./cmd/ailang ./internal/bestof ./internal/eval_harness ./internal/testutil/gatelint 2>&1 | tee gated_integration.log
for t in TestRunSmokeInTempDir_Pass TestPromptCommand_Piping TestZ3VerifyEndToEnd TestFormatAilHookSinkRoundTrip TestGateLint_SelfTest; do
grep -q -- "--- PASS: $t" gated_integration.log || {
echo "::error::$t did not PASS — a binary-gated integration test is being skipped (missing/stale ailang, z3, jq or bash?) or was renamed (then update this step)."
exit 1
}
done
- name: Run parser tests
run: make test-parser
# Added iteration 183: until now NO make target and NO CI job ran `ailang test` over any
# .ail suite, so every .ail test in the repo was decoration. The target carries its own
# anti-vacuity floor — an empty glob fails loudly rather than passing silently.
- name: Run stdlib .ail test suites
run: make test-stdlib-ail
- name: Check file sizes (>800 lines)
run: make check-file-sizes
- name: Check architecture boundaries
run: make check-boundaries
- name: Check changelog index hygiene
run: make check-changelog
- name: Check skill frontmatter
run: make check-skills
- name: Check coverage gate (M-P2)
run: make test-coverage-gate
- name: Check golden drift protection (M-P2)
run: make check-golden-drift
env:
ALLOW_GOLDEN_UPDATES: ${{ github.event_name == 'push' && contains(github.event.head_commit.message, '[golden-update]') && '1' || '' }}
- name: Fuzz parser (short)
run: make fuzz-parser
- name: Run operator lowering tests
run: make test-lowering
- name: Test import system (success cases)
run: make test-imports-success
- name: Test import errors (golden file verification)
run: make test-import-errors
- name: Verify no shim usage (CI gate)
run: make verify-no-shim
env:
AILANG_REQUIRE_LOWERING: "true"
- name: Validate builtin registry (M-DX1.3)
run: make doctor
- name: Run regression guard tests (v0.3.10 prevention)
run: make test-regression-guards
- name: Run nightly classifier variance-guard tests
run: |
set -o pipefail
make test-nightly-classifier 2>&1 | tee nightly_classifier.log
- name: Assert nightly classifier tests ran (no silent skips)
run: |
# Floor tracks the measured suite size (88 on 2026-08-01) with a four-test rename buffer.
# It drifted to 20-vs-40 before iteration 119 caught it: a floor set
# once and never revisited is the same vacuous-check class this suite
# exists to prevent. Raise it whenever you add tests.
PASSES=$(grep -c -- '--- PASS:' nightly_classifier.log || true)
if [ "$PASSES" -lt 84 ]; then
echo "::error::nightly classifier emitted only $PASSES PASS lines; tests were skipped, renamed, or did not run"
exit 1
fi
- name: Check compilation with timeout (M-DX11)
run: |
# Use timeout to detect compilation hangs
# ailang check only supports individual files, so use xargs
find examples/runnable -name '*.ail' -type f -exec ailang check --timeout 60s {} \; || true
- name: Detect cyclic types (M-DX11)
id: cycles
run: |
# Run cycle detection on example files
ailang debug cycles --json examples/complex_types.ail > cycles.json || true
# Check if there are suspicious cycles
SUSPICIOUS=$(jq -r '.summary.suspicious // 0' cycles.json)
echo "suspicious=$SUSPICIOUS" >> $GITHUB_OUTPUT
# Show results
echo "=== Cycle Detection Results ==="
cat cycles.json | jq .
# Note: We don't fail on suspicious cycles for now (informational only)
# To enforce: uncomment the following line
# if [ "$SUSPICIOUS" -gt 0 ]; then exit 1; fi
- name: Upload cycle analysis (M-DX11)
if: always()
uses: actions/upload-artifact@v7
with:
name: cycle-analysis
path: cycles.json
- name: Generate test coverage
run: |
make test-coverage
# Extract coverage percentage for badge
COVERAGE=$(go tool cover -func=coverage/coverage.out | grep "^total:" | awk '{print $3}' | sed 's/%//')
echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV
echo "Coverage: $COVERAGE%"
# Generate coverage badge color (using awk for POSIX compatibility)
if awk "BEGIN {exit !($COVERAGE < 20)}"; then
COLOR="red"
elif awk "BEGIN {exit !($COVERAGE < 40)}"; then
COLOR="orange"
elif awk "BEGIN {exit !($COVERAGE < 60)}"; then
COLOR="yellow"
elif awk "BEGIN {exit !($COVERAGE < 80)}"; then
COLOR="green"
else
COLOR="brightgreen"
fi
echo "COVERAGE_COLOR=$COLOR" >> $GITHUB_ENV
# Tie Sonar's "new code" window to GitHub release tags. The PREVIOUS_VERSION
# new-code period (set project-side via /api/settings/set) resets its
# baseline whenever `sonar.projectVersion` changes. Using `git describe`
# means: commits on a tagged release share that projectVersion, and the
# first commit that bumps the tag (release-manager adds a new tag) starts
# a fresh "new code" window. Falls back to the short SHA for ad-hoc runs.
- name: Compute Sonar projectVersion from git tag
id: sonar-version
run: |
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || git rev-parse --short HEAD)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Sonar projectVersion: $VERSION"
# Runs only when the SONAR_TOKEN repo secret is configured. Until then,
# SonarCloud's Automatic Analysis handles scans (without coverage data).
# continue-on-error keeps CI green if the secret is missing or the scan
# service has a hiccup — Sonar is a reporting layer, not a gate.
- name: SonarCloud scan
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v7
continue-on-error: true
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sonarcloud.io
with:
args: >
-Dsonar.projectVersion=${{ steps.sonar-version.outputs.version }}
- name: Verify examples
id: verify
run: |
# Real gate: `make verify-examples` exits non-zero on any red example or
# manifest modules drift, while still writing examples_report.json +
# examples_status.md. Tee keeps the full log visible AND lets the step's
# own exit code fail the job (no `|| true`, no unmatchable grep).
make verify-examples 2>&1 | tee examples_output.txt
exit "${PIPESTATUS[0]}"
- name: Verify example determinism (traces)
id: verify-traces
if: always() # still print the trace signal even when the gate above is red
run: |
make verify-examples-trace > trace_output.txt 2>&1 || true
cat trace_output.txt
# Windows runtime signal (M-CLI-ARGS-WIN, v0.14.2). Sibling to `test`
# rather than a matrix because the main `test` job uses many POSIX-only
# patterns (heredocs, find -exec, awk/sed in make targets, uv, SonarCloud).
# This job runs go test ./... directly and a PowerShell smoke for the
# -args-file / -args-json - paths that motivated this work. Future work:
# expand to make verify-examples / make test-imports once stable.
test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
# Avoid CRLF rewrites on test fixtures and golden files.
# PowerShell + git autocrlf can otherwise mutate bytes between
# checkout and `go test`, producing spurious diffs.
fetch-depth: 1
- name: Disable autocrlf
shell: pwsh
run: git config --global core.autocrlf false
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
cache: true
- name: Set up uv (provides Python for eval-harness Python runner tests)
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
# Same ordering constraint as the Linux leg: prefetch BEFORE the binary is
# installed, because `go mod download all` touches the tracked go.sum and the
# staleness detector would then treat the freshly-installed ailang as stale,
# silently skipping every binary-gated test.
- name: Download all Go modules (including test dependencies)
run: go mod download all
- name: Install ailang to PATH (mirrors `make install`)
shell: pwsh
# Use go install so the binary lands in $(go env GOPATH)/bin, which
# setup-go puts on PATH. Tests like tests/golden/codegen invoke
# exec.Command("ailang", ...) and need it discoverable by name.
run: |
go install ./cmd/ailang
$bin = & go env GOPATH
Write-Host "Installed to $bin/bin"
& "$bin/bin/ailang.exe" --version
- name: PowerShell smoke — -args-file (regression fixture for #199)
shell: pwsh
# Run BEFORE go test so the regression fixture for the original bug
# always exercises, even if some other test fails. This is the whole
# reason the test-windows job exists.
run: |
Set-Content -Path args.json -Value '"World"' -NoNewline
$output = & ailang run --caps IO --entry greet --args-file args.json examples/docs/module_greet.ail
if ($LASTEXITCODE -ne 0) { Write-Error "ailang exited $LASTEXITCODE"; exit 1 }
if ($output -notcontains 'World') { Write-Error "expected 'World' in output, got: $output"; exit 1 }
Write-Host "OK: -args-file"
- name: PowerShell smoke — -args-json - via stdin (regression fixture for #199)
shell: pwsh
run: |
$output = '"World"' | & ailang run --caps IO --entry greet --args-json - examples/docs/module_greet.ail
if ($LASTEXITCODE -ne 0) { Write-Error "ailang exited $LASTEXITCODE"; exit 1 }
if ($output -notcontains 'World') { Write-Error "expected 'World' in output, got: $output"; exit 1 }
Write-Host "OK: -args-json - (stdin)"
- name: Run Go test suite
shell: pwsh
# 5min timeout (vs 60s on Linux): windows-latest runners are noticeably
# slower for IO-heavy work (Git worktree ops, process spawn). Real hangs
# still get caught — a 5min ceiling is well above any healthy test.
# If a test is genuinely Windows-incompatible (not just slow), fix it or
# skip it with a `// TODO(windows-ci): <reason>` + tracked GH issue.
env:
HTTP_PROXY: http://127.0.0.1:9
HTTPS_PROXY: http://127.0.0.1:9
NO_PROXY: localhost,127.0.0.1
GOPROXY: off
run: |
if ($env:HTTP_PROXY -ne 'http://127.0.0.1:9') { Write-Error "HTTP_PROXY poison is not configured"; exit 1 }
if ($env:HTTPS_PROXY -ne 'http://127.0.0.1:9') { Write-Error "HTTPS_PROXY poison is not configured"; exit 1 }
if ($env:NO_PROXY -ne 'localhost,127.0.0.1') { Write-Error "NO_PROXY loopback bypass is not configured"; exit 1 }
if ($env:GOPROXY -ne 'off') { Write-Error "GOPROXY is not disabled"; exit 1 }
go test -timeout 300s ./...
# Same anti-silent-skip gate as the Linux job (minus z3, which isn't
# installed here): tests that shell out to ailang t.Skip when it's
# missing, invisibly in non-verbose `go test ./...`. Require an explicit
# PASS from one representative test per discovery path (bin/ vs PATH).
- name: Assert binary-gated integration tests ran (no silent skips)
shell: pwsh
run: |
go test -count=1 -v -run '^(TestRunSmokeInTempDir_Pass|TestPromptCommand_Piping|TestFormatAilHookSinkRoundTrip|TestGateLint_SelfTest)$' ./internal/pkg ./cmd/ailang ./internal/eval_harness ./internal/testutil/gatelint 2>&1 | Tee-Object -FilePath gated_integration.log
foreach ($t in 'TestRunSmokeInTempDir_Pass','TestPromptCommand_Piping','TestFormatAilHookSinkRoundTrip','TestGateLint_SelfTest') {
if (-not (Select-String -Path gated_integration.log -SimpleMatch "--- PASS: $t" -Quiet)) {
Write-Error "$t did not PASS - a binary-gated integration test is being skipped (missing/stale ailang, jq or bash?) or was renamed (then update this step)."
exit 1
}
}
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
cache: true
- name: Build binary
run: make build
- name: Upload binary artifact
uses: actions/upload-artifact@v7
with:
name: ailang-binary
path: bin/ailang
docs:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps:
- uses: actions/checkout@v7
- name: Sync prompts/ to docs/prompts/
run: ./docs/scripts/sync-prompts.sh
- name: Generate llms.txt
run: ./tools/generate-llms-txt.sh
# Disabled: Auto-commit causes race condition with test job
# Run sync-prompts.sh and generate-llms-txt.sh manually before releases
# - name: Commit documentation updates
# uses: stefanzweifel/git-auto-commit-action@v7
# with:
# commit_message: 'Docs: Auto-sync prompts and generate llms.txt [skip ci]'
# file_pattern: 'docs/docs/prompts/*.md docs/llms.txt llms.txt'
# commit_user_name: github-actions[bot]
# commit_user_email: github-actions[bot]@users.noreply.github.com
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
cache: true
- name: Check code formatting
run: make fmt-check
- name: Run go vet
run: make vet
- name: Install and run golangci-lint
run: |
make install-lint
make lint
launchd-drivers:
name: launchd drivers (bash 3.2)
# macOS DELIBERATELY, and it is the whole point of this job: the rig runs bash 3.2.57 and
# GitHub's macOS runners still ship 3.2 as /bin/bash, while ubuntu ships bash 5. A suite
# green on bash 5 proves nothing about the constraint that actually bites here — every
# `declare -A` or ${v,,} that reaches tools/launchd/ breaks the rig and passes on ubuntu.
# No Go, no cache: these are shell + git tests and nothing else.
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Confirm /bin/bash really is 3.2 (guard against a silent runner upgrade)
run: |
/bin/bash --version | head -1
/bin/bash -c '[ "${BASH_VERSINFO[0]}" -eq 3 ]' \
|| { echo "::error::/bin/bash is no longer 3.x — this job no longer tests the rig's shell"; exit 1; }
- name: Run launchd driver tests
run: make test-launchd-drivers
govulncheck:
name: govulncheck (vuln gate)
runs-on: ubuntu-latest
# Reads .govulncheck-allow.yml. Fails on any new finding or any
# allowlist entry past its expires date — forces re-review rather
# than letting suppressions float forever.
steps:
- uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
- name: Install govulncheck
# Pinned (was @latest): govulncheck v1.4.0 started crashing on `./...`
# (full goroutine dump → exit 2), failing the gate with no code change —
# @latest makes a CI gate non-reproducible. v1.3.0 is the prior stable.
# Bump deliberately after confirming a newer release is healthy.
run: go install golang.org/x/vuln/cmd/govulncheck@v1.3.0
- name: Build filter
run: go build -o ./bin/govulncheck-filter ./tools/govulncheck-filter
- name: Run govulncheck against allowlist
run: |
set -o pipefail
govulncheck -format json ./... | ./bin/govulncheck-filter
dependency-submission:
runs-on: ubuntu-latest
permissions:
contents: write
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
steps:
- uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.26.5'
- name: Go Dependency Submission
uses: actions/go-dependency-submission@v2.0.3
continue-on-error: true # Known upstream bug: duplicate namespace assertion with multi-module deps