-
Notifications
You must be signed in to change notification settings - Fork 2
444 lines (385 loc) · 17.2 KB
/
Copy pathci.yml
File metadata and controls
444 lines (385 loc) · 17.2 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
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
# Build the shared dev/CI image (.devcontainer/Dockerfile) and push it to
# GHCR tagged to this commit. Every Linux leg below runs *inside* this image
# via `container:`, so the environment Codespaces opens and the environment
# CI tests in are byte-for-byte the same toolchain — they cannot drift.
# gha-cached, so an unchanged Dockerfile rebuilds in seconds.
dev-image:
name: build dev/ci image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.ref.outputs.image }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
# Fork PRs run with a read-only GITHUB_TOKEN (no packages:write), so
# they cannot push a ci-<sha> image — and with no image, every Linux
# leg used to skip, leaving fork PRs with no Linux CI at all. Fork
# PRs instead run inside the rolling `ci-main` image that every main
# push publishes below: same toolchain, just not rebuilt from the
# PR's Dockerfile. (The rare fork PR that EDITS
# .devcontainer/Dockerfile still tests with the base image — push
# its branch to origin to exercise the Dockerfile change.)
- name: Log in to GitHub Container Registry
if: github.event.pull_request.head.repo.fork != true
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# GHCR requires a lowercase owner; compute the ref once and hand it to
# the downstream jobs through a job output. `image` is what the Linux
# legs run in; `tags` is what this job pushes (empty on fork PRs —
# nothing is pushed; main pushes also advance the rolling ci-main tag).
- name: Compute image ref
id: ref
env:
OWNER: ${{ github.repository_owner }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
EVENT: ${{ github.event_name }}
run: |
BASE="ghcr.io/${OWNER,,}/eigenscript-dev"
if [ "$IS_FORK" = "true" ]; then
echo "image=$BASE:ci-main" >> "$GITHUB_OUTPUT"
echo "tags=" >> "$GITHUB_OUTPUT"
elif [ "$EVENT" = "push" ]; then
echo "image=$BASE:ci-${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "tags=$BASE:ci-${{ github.sha }},$BASE:ci-main" >> "$GITHUB_OUTPUT"
else
echo "image=$BASE:ci-${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "tags=$BASE:ci-${{ github.sha }}" >> "$GITHUB_OUTPUT"
fi
- name: Build and push dev image
if: github.event.pull_request.head.repo.fork != true
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: .devcontainer/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.ref.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Linux build + full suite, inside the shared image, for gcc and clang.
# gcc and clang shake out different warnings and codegen.
build-and-test-linux:
name: linux / ${{ matrix.cc }}
needs: dev-image
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
strategy:
fail-fast: false
matrix:
cc: [gcc, clang]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build
run: CC=${{ matrix.cc }} ./build.sh
- name: Verify binary
run: ./src/eigenscript --version
- name: Run test suite
run: cd tests && bash run_all_tests.sh
# Cross-repo observer corpus (#262): real observer-using programs
# vendored from downstream consumers (dynamics solvers, iLambdaAi) plus
# this repo's examples, each pinned to a value-path golden. The unit
# suite proves the predicates in isolation; this guards that an observer
# (or any) change doesn't silently shift real consumer behavior. Linux
# only: the goldens are float-formatted on Linux.
- name: Observer cross-repo corpus
run: bash tests/observer_corpus/run.sh
# Trace-on-fail (#394): prove a failing test's tape replays byte-for-byte,
# then record one and archive it — the artifact a CI post-mortem replays
# with `EIGS_REPLAY=<tape> eigenscript <file>` (same-binary; see TRACE.md).
- name: Trace-on-fail replay + tape artifact
if: matrix.cc == 'gcc'
run: |
bash tests/test_trace_on_fail.sh
mkdir -p failure-tapes
printf 'r is random of null\nprint of ("roll: " + (str of r))\nassert of [0, "demo red test — replay this tape"]\n' > /tmp/red_demo.eigs
./src/eigenscript --trace failure-tapes/red_demo.tape /tmp/red_demo.eigs || true
- name: Upload trace-on-fail tape
if: matrix.cc == 'gcc'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: trace-on-fail-tape
path: failure-tapes/
if-no-files-found: ignore
# Freestanding symbol gate: compiles the runtime with the freestanding
# profile (-DEIGENSCRIPT_FREESTANDING=1) and asserts its undefined-symbol
# set stays within tools/freestanding_allowlist.txt — the HAL roots +
# mini-libc/libm surface from docs/FREESTANDING.md. Catches any new libc
# dependency leaking into the EigenOS-bound profile. Also smoke-runs a
# hosted build of the profile to prove the core language still executes.
freestanding:
name: freestanding profile (symbol gate + smoke)
needs: dev-image
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Symbol gate (stage 1 profile, stage 2 mini-libc residue)
run: make freestanding-check
- name: Hosted smoke of the profile
run: bash tools/freestanding_smoke.sh
- name: Embedded-stack soak (REPL soak in a 64 KiB stack rlimit)
run: bash tools/embed_stack_soak.sh
- name: Mini-libc differential vs glibc oracle
run: make freestanding-libc-diff
# macOS exercises the BSD/clang toolchain and (on arm64) the non-x86 JIT
# fallback. Runs natively — a Linux container image can't host these.
build-and-test-macos:
name: macos / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-15-intel]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build
run: CC=clang ./build.sh
- name: Verify binary
run: ./src/eigenscript --version
- name: Run test suite
run: cd tests && bash run_all_tests.sh
# Linux LSP coverage lives in the extensions job; this leg exists
# because the LSP build had macOS-only link failures nothing surfaced.
- name: Compile-check LSP (macOS)
run: make lsp CC=clang
# Build the http+model variant and run the suite against it: the suite's
# probe-gated sections ([44]-[45] HTTP, [47] model roundtrip/overflow,
# transformer smoke) silently skip on the minimal build, so without this
# job ~5000 lines of extension C never execute in CI. Also build the gfx
# variant and run the suite against it (the audio-synthesis section [62] is
# pure DSP — no display needed — and only executes against a gfx build; the
# windowed gfx_* demos still auto-skip), exercise the embedding API
# (eigs_embed.c is otherwise 0% — embed-smoke is the only thing that runs
# it), compile-check the LSP (bitrots invisibly otherwise), and run the
# standalone JIT emitter smoke tests.
extensions:
name: extensions (http+model+gfx suite; embed/lsp/jit-smoke)
needs: dev-image
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: JIT emitter smoke tests
run: make jit-smoke
- name: Embedding API smoke test (linked against the amalgamation)
run: make embed-smoke
- name: Static embed library (make lib)
run: make lib
- name: Two-file amalgamation drop-in (cc alone, fresh dir)
run: bash tests/test_amalgamation.sh
- name: Build http+model variant
run: make http
- name: Run full suite against http+model build
run: cd tests && bash run_all_tests.sh
- name: Build gfx variant
run: make gfx
- name: Run full suite against gfx build (executes audio section [62])
run: cd tests && bash run_all_tests.sh
- name: Compile-check LSP
run: make lsp
# 44 deterministic adversarial inputs against the ASan-instrumented
# stdin harness (same compile->vm pipeline as main.c). Seconds to run;
# any exit other than 0/1 is a crash.
- name: Fuzz smoke (ASan harness, curated corpus)
run: make fuzz && bash fuzz/run_fuzz.sh
# Build the `full` variant (http+model+db) against a real PostgreSQL service
# and run the suite with DATABASE_URL set. Without this, ext_db.c never
# executes anywhere: the suite's DB section [46] is probe-gated on the build,
# and test_db.eigs's live-path checks (DB09-DB15) additionally gate on an
# actual connection. When the job is containerized the service is reachable
# by its label (`db`), not localhost.
database:
name: db extension (postgres service)
needs: dev-image
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
services:
db:
image: postgres:16
env:
POSTGRES_USER: eigs
POSTGRES_PASSWORD: eigs_test
POSTGRES_DB: eigs_test
options: >-
--health-cmd "pg_isready -U eigs"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build full variant (http+model+db)
run: make full
- name: Run full suite with live DATABASE_URL
env:
DATABASE_URL: postgres://eigs:eigs_test@db:5432/eigs_test
run: cd tests && bash run_all_tests.sh
# Run the entire suite under AddressSanitizer + UndefinedBehaviorSanitizer.
# The net that catches use-after-free, buffer overflow, and UB the normal
# -O2 build silently tolerates. Per-iteration leak regressions are covered
# separately by tests/test_leak_guard.sh.
sanitizers:
name: asan + ubsan (full suite)
needs: dev-image
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build with AddressSanitizer + UBSan
run: make asan
- name: Run full suite under sanitizers
env:
# Leak detection is ON: the suite runs leak-clean since the exit-time
# teardown + birth-ref adoption fixes, so any new leak fails this job.
ASAN_OPTIONS: detect_leaks=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
run: cd tests && bash run_all_tests.sh
- name: Behavior-test the LSP under sanitizers
# `make asan` builds the interpreter, not eigenlsp — so the LSP would
# otherwise never run under a sanitizer in CI. This builds it with
# ASan/UBSan and drives the JSON-RPC harness; test_lsp.py fails on any
# sanitizer report from the LSP process (leaks, UB).
run: cd tests && bash test_lsp_asan.sh
# Valgrind/Memcheck over a representative spread of programs — the
# uninitialised-read / use-after-free / definite+indirect-leak net at the
# system-allocator granularity, complementing the ASan+UBSan job (and the
# TSan-class race coverage). Runs on the plain uninstrumented binary, so no
# instrumented libc is needed; JIT is forced off (its runtime-emitted native
# code needs --smc-check=all and muddies the signal). A smoke set, not the
# full suite, to fit the runtime budget under Valgrind's ~20-30x slowdown.
# (Follow-up: annotate the arena/freelist allocators so it also sees uninit
# reads *inside* the custom arena, not just at malloc granularity.)
valgrind:
name: valgrind (memcheck smoke, JIT off)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Valgrind
run: sudo apt-get update && sudo apt-get install -y valgrind
- name: Build (-O1 -g, minimal)
run: make valgrind
- name: Valgrind memcheck smoke
run: cd tests && bash valgrind_smoke.sh
# ThreadSanitizer concurrency race gate (#401): the spawn/channel slice must
# be data-race-free (the #297 property, now regression-gated), and a seeded
# race must be caught so the gate can't rot. `setarch -R` disables ASLR, which
# ThreadSanitizer needs here (see CLAUDE.md).
tsan:
name: tsan (concurrency race gate)
runs-on: ubuntu-latest
# Backstop for the per-run timeouts in test_tsan.sh: the seeded-race UB
# hung ~1/3 of runs overnight (2026-07-05) and burned the 6h default.
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: ThreadSanitizer build
run: make tsan
- name: Concurrency race gate (setarch -R)
run: bash tests/test_tsan.sh
# First-run tooling unification (#400): the VS Code tree is canonical and has
# the LSP client, and a fresh install.sh leaves BOTH binaries on PATH.
install-smoke:
name: install.sh (interpreter + eigenlsp on PATH)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Canonical VS Code tree has the LSP client (no drifted duplicate)
run: |
test -f editors/vscode/extension.js
test ! -e editor
python3 - <<'PY'
import json
p = json.load(open("editors/vscode/package.json"))
assert p.get("main") == "./extension.js", p.get("main")
assert "vscode-languageclient" in p.get("dependencies", {}), "missing LSP client dep"
assert "onLanguage:eigenscript" in p.get("activationEvents", []), "missing activation"
print("editors/vscode/ package.json wires the LSP client")
PY
- name: Fresh install.sh puts eigenscript AND eigenlsp on PATH
run: |
./install.sh
test -x "$HOME/.local/bin/eigenscript"
test -x "$HOME/.local/bin/eigenlsp"
"$HOME/.local/bin/eigenscript" --version
# Performance regression gate (#398). Wall-clock flakes on shared runners, so
# the gate compares deterministic cachegrind instruction counts (Ir) of THIS
# commit against origin/main — both built in the same runner, so the diff is a
# pure code-change signal, never machine noise. A self-test proves a 2x-work
# pessimization is caught. Published numbers (wall-clock) live in
# docs/PERFORMANCE.md, regenerated from bench/run_bench.sh.
bench:
name: bench (instruction-count regression gate)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Valgrind
run: sudo apt-get update && sudo apt-get install -y valgrind
- name: Build this commit
run: ./build.sh
- name: Build origin/main in a worktree (same environment)
run: |
git fetch --no-tags --depth=1 origin main
git worktree add /tmp/main-ref origin/main
( cd /tmp/main-ref && ./build.sh )
- name: Gate self-test (a 2x pessimization must fail the gate)
run: EIGENSCRIPT="$PWD/src/eigenscript" bash bench/check_regression.sh --selftest
- name: Regression gate — Ir of this commit vs origin/main
run: EIGENSCRIPT="$PWD/src/eigenscript" bash bench/check_regression.sh --vs /tmp/main-ref/src/eigenscript