Repository navigation
581 lines (545 loc) · 30 KB
/
Copy pathci.yml
File metadata and controls
581 lines (545 loc) · 30 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
570
571
572
573
574
575
576
577
578
579
580
581
# ─────────────────────────────────────────────────────────────────────────────
# CI — the gate every change to main passes through.
#
# Four tiers, cheapest first, all required:
# build the parser and the engine driver compile and typecheck
# hygiene repo invariants that need no solver and catch silent breakage
# engine the java / typescript / python / javascript / csharp regression suites, in
# parallel, each compiling its own engine from the rules
# engines every language's engine builds on every platform (the reusable
# build-engines workflow — the same artifacts publish-npm ships)
#
# NO WORKFLOW-LEVEL PATH FILTERS, deliberately. A required check that is skipped
# by a path filter never reports, and a pull request waiting on a check that will
# never report can never merge. Instead the workflow always starts, the `changes`
# job classifies the diff, and the expensive jobs skip THEMSELVES: a docs-only pull
# request runs build and hygiene (which carries the version gate) and nothing else,
# and the platform engine build runs only for main, and only when what it compiles
# changed. The `CI` job reports either way.
#
# dev is the default branch: every pull request lands there and gets build, hygiene
# and the five suites. The every-platform engine build is the slow part and no test
# uses its output, so it runs on the way INTO main (a promotion pull request, a push
# to main) and in the nightly, not on every change to dev.
# ─────────────────────────────────────────────────────────────────────────────
name: CI
on:
push:
# dev takes direct pushes, so they get a result too. A release branch (0.1.6, ...)
# takes merges from pull requests: the run on the merge is what saves the compiled
# engines where the NEXT pull request into it can restore them — a cache a pull
# request saves is visible to that pull request alone.
branches: [main, dev, '0.*']
pull_request:
merge_group:
workflow_dispatch:
# nightly.yml calls this with fresh=true: no restored engine cache, so every engine
# is compiled from the rules as they are, and the platform build always runs.
workflow_call:
inputs:
fresh:
type: boolean
default: false
# One run per ref. A new push to a pull request cancels the previous run, but a
# run on main is always allowed to finish — main's history is the record.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
# bin/axiomcode and the parser need Node ≥ 22.5.
NODE_VERSION: '22'
SOUFFLE_VERSION: '2.5'
SOUFFLE_SHA512: '6b86e554f6aa5abf8a8b55d8312ae37c0957c5bd6c9edeea89246db9406f645ec5e600b84fe6636b1c163da556f0da6c3d2dad46c1083413f2fcf4f95b9ac62c'
jobs:
changes:
name: what changed
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
code: ${{ steps.c.outputs.code }}
engines: ${{ steps.c.outputs.engines }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: c
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
if [ "${{ inputs.fresh }}" = true ]; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=true" >> "$GITHUB_OUTPUT"
echo "nightly: everything runs"; exit 0
fi
if [ "${{ github.event_name }}" = push ] && [ "${{ github.ref }}" = refs/heads/dev ]; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=false" >> "$GITHUB_OUTPUT"
echo "push to dev: suites run, platform engines wait for main"; exit 0
fi
if [ "${{ github.event_name }}" = push ] && [[ "${{ github.ref }}" == refs/heads/0.* ]]; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=false" >> "$GITHUB_OUTPUT"
echo "push to a release branch: suites run (and save its engines for pull requests into it), platform engines wait for main"; exit 0
fi
# A push to main is a release when its version has no tag yet: that commit, and only that one,
# builds every platform and runs the five-platform e2e, and release.yml drafts from it once
# it is green. A push whose version is already tagged released nothing new and builds nothing.
if [ "${{ github.event_name }}" = push ] && [ "${{ github.ref }}" = refs/heads/main ]; then
v="$(node .github/scripts/version.mjs get)"
if git ls-remote --exit-code --tags origin "refs/tags/v$v" >/dev/null; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=false" >> "$GITHUB_OUTPUT"
echo "push to main at v$v, already tagged: suites run, nothing to release"
else
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=true" >> "$GITHUB_OUTPUT"
echo "push to main at v$v, not yet tagged: the release build runs on every platform"
fi
exit 0
fi
if [ "${{ github.event_name }}" != pull_request ]; then
echo "code=true" >> "$GITHUB_OUTPUT"; echo "engines=true" >> "$GITHUB_OUTPUT"
echo "${{ github.event_name }} on ${{ github.ref }}: everything runs"; exit 0
fi
files="$(git diff --name-only "$BASE"...HEAD)"
printf '%s\n' "$files" | sed 's/^/ /'
# DOCS: prose nothing executes. Markdown under graph/ or parser/ is NOT
# docs: graph/bundle/SCHEMA.md is generated, and a suite checks it is current.
code="$(printf '%s\n' "$files" | grep -vE \
-e '^$' \
-e '^(graph|parser)/' -e '^[^/]+\.md$' -e '^docs/' -e '^paper/' \
-e '^\.github/(ISSUE_TEMPLATE/|pull_request_template\.md$|CODEOWNERS$|RELEASING\.md$)' \
-e '^LICENSE' -e '\.(png|jpe?g|gif|svg)$' || true)"
# graph/ and parser/ are code even when the file is markdown
code="$code$(printf '%s\n' "$files" | grep -E '^(graph|parser)/' || true)"
# ENGINES: what the platform build compiles or packages.
engines="$(printf '%s\n' "$files" | grep -E \
-e '\.dl$' -e '^graph/pipeline/' -e '^packaging/' -e 'scripts/dl_program\.py$' -e '^\.github/scripts/query-smoke\.sh$' \
-e '^\.github/workflows/build-engines\.yml$' -e '^package\.json$' -e '^\.github/scripts/e2e-' -e '^\.github/e2e/' || true)"
[ -n "$code" ] && echo "code=true" >> "$GITHUB_OUTPUT" || echo "code=false" >> "$GITHUB_OUTPUT"
# No pull request builds the platform engines: a release builds them once, on the push that
# lands it on main, and publishes exactly that build (#1350).
engines=""
[ -n "$engines" ] && echo "engines=true" >> "$GITHUB_OUTPUT" || echo "engines=false" >> "$GITHUB_OUTPUT"
echo "suites: $([ -n "$code" ] && echo run || echo skip) platform engines: $([ -n "$engines" ] && echo run || echo skip)"
build:
name: build & typecheck
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# No lock file is committed (#476), so this is `npm install`, not `npm ci`,
# and setup-node's npm cache (keyed on a lock file) is not used. The install
# runs the `prepare` script, which builds the parser workspace and the
# driver. Keeping the explicit build step anyway means a prepare-script
# change cannot silently stop compiling this repo.
- run: npm install
- run: npm run typecheck
- run: npm run build
- name: the parser actually built
run: |
test -f parser/dist/index.js \
|| { echo "::error::parser/dist/index.js is missing after build"; exit 1; }
hygiene:
name: repo invariants
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the version gate needs the merge base with the target branch
# A fixture input that .gitignore matches passes on the machine that wrote
# it and fails on every clone. The suites run this too; running it here as
# well means the answer arrives in seconds rather than after the engine.
- name: every fixture input is tracked by git
run: bash graph/test/tools/no-ignored-fixtures.sh
# A relation staged for the client but not for libraries is EMPTY on every
# run and nothing errors — no golden can see it. This is the only check
# that can.
- name: IR staging maps are consistent
run: |
fail=0
for lang in java typescript python javascript csharp; do
echo "── $lang"
python3 graph/test/tools/check_staging.py --lang "$lang" || fail=1
done
exit $fail
# The engine's base declarations are a COPY of the parser's generated
# schema (graph/<lang>/souffle/decls_base.dl ← parser/src/schema/<lang>/).
# A column appended on the parser side and not here is an arity error at
# solve time in every suite at once, with the cause two directories away.
# Compared on the `.decl` lines only: the copies carry their own preambles.
- name: engine declarations match the parser schema
run: |
fail=0
for pair in typescript:decls_base_ts.dl python:decls_base_py.dl javascript:decls_base_js.dl csharp:decls_base_cs.dl; do
lang="${pair%%:*}"; file="${pair#*:}"
if ! diff <(grep '^\.decl' "parser/src/schema/$lang/$file") \
<(grep '^\.decl' "graph/$lang/souffle/decls_base.dl"); then
echo "::error::graph/$lang/souffle/decls_base.dl has drifted from parser/src/schema/$lang/$file"
fail=1
fi
done
exit $fail
# The client->library half is carried by a smaller set of fixtures than the
# client->client half, and it is the half that disappears silently: delete a
# golden and the case still runs, still passes, and simply stops claiming
# anything. A suite can only check the assertions it still has.
- name: client->library coverage has not shrunk
run: bash graph/test/tools/lib-coverage.sh
- name: shell scripts parse
run: |
fail=0
while IFS= read -r f; do
bash -n "$f" || { echo "::error file=$f::does not parse"; fail=1; }
done < <(git ls-files '*.sh')
exit $fail
# npm will not republish a version, so anything inside the tarball reaches
# nobody unless the version moves — README.md included, since `files` names
# it. The inverse is the error worth avoiding too: a bump demanded for a CI
# tweak or a test fixture teaches people to bump without asking why, and a
# version that moves for reasons users cannot observe stops meaning
# anything. The gate reads package.json's own `files` declaration to tell
# the two apart, and prints the files that decided it.
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# package.json's version is repeated in the engine pins, the parser and every
# plugin manifest. Checked on every event, not only pull requests, so main
# can never hold a tree whose manifests disagree about what it is.
- name: every manifest carries the same version
run: node .github/scripts/version.mjs check
- name: a change that reaches a user has a version
if: github.event_name == 'pull_request'
run: bash .github/scripts/version-gate.sh "origin/${{ github.base_ref }}"
# A push to main builds and releases only when its version is new (#1350). The gate above already
# refuses a pull request that changes what users get without a new version; a CI or docs change
# may keep main's version and then builds nothing when it lands. What is left to refuse here is a
# new version that was already released: its tag exists, so the push would build nothing and the
# change would never ship.
- name: a pull request into main does not reuse a released version
if: github.event_name == 'pull_request' && github.base_ref == 'main'
run: |
set -euo pipefail
head="$(node .github/scripts/version.mjs get)"
base="$(git show origin/main:package.json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).version))')"
if [ "$head" = "$base" ]; then
echo "main stays at $base: nothing in this change is released, and landing it builds nothing"; exit 0
fi
if git ls-remote --exit-code --tags origin "refs/tags/v$head" >/dev/null; then
echo "::error::v$head is already released — pick the next version"; exit 1
fi
echo "main $base -> $head: landing this builds every platform and drafts v$head"
engine:
name: engine (${{ matrix.lang }})
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
# the engine is compiled for any x86-64 runner, so a cached one can be restored
# on whichever runner this job lands (see the engine cache step below)
AXIOM_ENGINE_MARCH: portable
strategy:
fail-fast: false
matrix:
include:
# --oracle scores the engine against GROUND TRUTH, not only against the
# goldens. A golden says "the same as last time", which a wrong answer
# satisfies perfectly well as long as it was wrong last time too.
#
# The ground truth is built with the toolchain that defines the language:
# javac and javap for Java, the TypeScript compiler for TypeScript and —
# over allowJs/checkJs — for JavaScript. No third-party analyzer, and no
# third-party library is downloaded to do it. A case whose ground truth would need an external
# classpath reports itself unscored rather than pulling one in.
#
# --no-torture for java ONLY. Those families call java.util.List, Map and
# the functional interfaces, so they need the JVM platform IR staged as a
# library. That IR is 1.8 GB and is built from a JDK source checkout, so it
# cannot live in a repository or a cache. Without it those receivers are
# unresolvable BY CONSTRUCTION — the census goes from 10 missing edges to
# 23 and recall to 0.847 — which measures the staging, not the rules, and
# the resulting red would read as a regression in whatever PR met it.
#
# Java client->library resolution is still covered here: six cases ship
# their own stub library in lib-src/ and are solved with it as --library.
# The torture families remain a local gate until the platform IR can be
# produced reproducibly; the suite prints EXCLUDED so it is never mistaken
# for a family that passed.
- lang: java
oracle: '--oracle --no-torture'
- lang: typescript
oracle: '--oracle'
# Python's ground truth is frozen CPython output, authored by a separate
# harness checkout ($AXIOM_PY_ORACLE) that CI cannot reach yet, so this leg
# is goldens-only for now. That separation is deliberate —
# graph/test/python/run-tests.sh explains why the ability to re-bless
# ground truth must not sit beside the code under test — but it does mean
# the python leg is a weaker check than the other three until the harness
# is reachable from here.
- lang: python
oracle: ''
# JavaScript: 19 cases; the library case ships its dependency under
# src/node_modules and is solved twice. The execution oracles (torture/,
# realapp/) are separate harnesses and stay a local gate — realapp needs
# network for its own npm install.
- lang: javascript
oracle: '--oracle'
# C# has no goldens: every case is scored against the Roslyn oracle
# (graph/test/csharp/ground-truth), which the step below builds with the
# .NET 8 SDK. No flag — scoring against the compiler is all it does.
- lang: csharp
oracle: ''
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# Builds the in-repo parser (parser/dist) through the prepare script, and
# supplies the TypeScript compiler the typescript and javascript oracles run.
# `npm install`, not `npm ci`: no lock file is committed (#476).
- run: npm install
- name: the parser actually built
run: |
test -f parser/dist/index.js \
|| { echo "::error::parser/dist/index.js is missing after npm install"; exit 1; }
# TWO interpreters, because the python suite needs two different things and
# they cannot be the same version.
#
# 3.10 — the tier-1 attribution preflight reads CPython OPCODES, whose
# shapes are not stable across minor versions. It resolves
# `python3.10` by name, so this only has to exist on PATH.
# 3.12 — the torture fixtures are SOURCE that has to import: one of them
# uses `typing.Self`, which is 3.11+ (PEP 673), so on 3.10 the
# tracer dies at import and the family scores nothing.
#
# The later setup-python wins for plain `python3`, so 3.12 must come second.
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: both interpreters are on PATH
run: |
set -euo pipefail
echo "python3 -> $(python3 --version)"
echo "python3.10 -> $(python3.10 --version)"
# JDK 24, not the runner's default. The java torture harness reads class files
# with java.lang.classfile, which is not final before 24 — on an older JDK it
# exits 77 and the whole ten-family oracle silently does not run.
- uses: actions/setup-java@v4
if: matrix.lang == 'java'
with:
distribution: temurin
java-version: '24'
- uses: actions/setup-dotnet@v4
if: matrix.lang == 'csharp'
with:
dotnet-version: '8.0.x'
# Without the oracle binary the suite exits 77, which run-suite.sh turns into
# a failure — so a missing build is loud, never a silent skip.
- name: build the Roslyn oracle
if: matrix.lang == 'csharp'
run: dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle
- name: cache the Soufflé package
uses: actions/cache@v4
with:
path: ~/souffle-pkg
key: souffle-deb-${{ env.SOUFFLE_VERSION }}-ubuntu-2404
# Soufflé is the solver, not a library under test: the engine is compiled
# from .dl to C++ and linked against Soufflé's headers, so a build of it has
# to be present the way a compiler has to be present.
#
# It is pinned to an exact version AND verified against the checksum upstream
# published for that release, so what CI links against is decided in this
# file rather than by whatever the archive happens to serve today. The pin
# the driver reads is graph/pipeline/engine.conf; this must agree with it.
- name: install Soufflé ${{ env.SOUFFLE_VERSION }}
run: |
set -euo pipefail
deb="x86_64-ubuntu-2404-souffle-${SOUFFLE_VERSION}-Linux.deb"
dir="$HOME/souffle-pkg"; mkdir -p "$dir"
if [ ! -f "$dir/$deb" ]; then
curl -fsSL --retry 3 -o "$dir/$deb" \
"https://github.com/souffle-lang/souffle/releases/download/${SOUFFLE_VERSION}/${deb}"
fi
echo "${SOUFFLE_SHA512} ${dir}/${deb}" | sha512sum -c -
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends "$dir/$deb"
souffle --version | head -2
# ── the compiled engine ────────────────────────────────────────────────
# run-souffle.sh caches the compiled solver under a content hash of the
# .dl program text. Mirroring that key here skips a multi-minute C++ build
# on every run whose rules did not change.
# Keyed by this language's ENGINE_ID — the hash the driver itself names its
# compiled binary by (its rules and the Soufflé version) — so a rule change in
# one language recompiles that language only, and an unchanged one never does.
# Two things decide the binary that ENGINE_ID does not cover, so they are in the
# key too: the driver that compiles it (run-souffle.sh holds the compiler flags),
# and the target it is compiled for.
#
# THE TARGET IS PORTABLE, NOT THE RUNNER'S CPU. The driver's default is
# -march=native, and a binary built on one hosted runner died with SIGILL on
# another (runners that report the same model name do not all expose the same
# instruction set). The key used to carry a hash of the runner's CPU flags to
# keep such a binary off other CPUs — and so it missed on almost every run,
# since which CPU a job lands on is luck: a three-minute compile per language,
# per run, for rules nothing had changed. AXIOM_ENGINE_MARCH=portable compiles
# for the compiler's baseline x86-64 target instead, as the published engines
# are, so any runner can run any runner's binary and the CPU leaves the key.
#
# A cache saved by a pull request is visible to that pull request only. The
# ones every pull request can restore are the base branch's and the default
# branch's, which is why a push to a release branch runs the suites too (see
# `on.push` and the `changes` job): it leaves the engine for its rules where
# every pull request into that branch finds it.
- name: this language's engine id
id: eid
run: |
echo "id=$(bash graph/pipeline/run-souffle.sh --language ${{ matrix.lang }} --print-engine-id)" >> "$GITHUB_OUTPUT"
- name: restore the compiled Soufflé engine
id: engine-cache
if: ${{ !inputs.fresh }}
uses: actions/cache/restore@v4
with:
path: .souffle-cache
key: souffle-engine-${{ matrix.lang }}-${{ steps.eid.outputs.id }}-${{ hashFiles('graph/pipeline/run-souffle.sh') }}-march-${{ env.AXIOM_ENGINE_MARCH }}
- name: ${{ matrix.lang }} regression suite
env:
AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js
# The driver's default cache is ~/.cache/axiomcode/souffle; point it at the
# directory the cache step above saves and restores, or it never hits.
AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache
# The cases run concurrently, up to one per CPU (graph/test/tools/case-pool.sh);
# AXIOM_SUITE_JOBS=1 here would run them one at a time, as they used to.
run: bash .github/scripts/run-suite.sh ${{ matrix.lang }} ${{ matrix.oracle }}
# Saved whether or not the suite passed. The binary does not depend on the verdict:
# run-souffle.sh publishes it only whole and verified (a temp name, then a rename),
# so a red run's engine is as good as a green one's, and the run that most needs the
# next push to be fast is the red one. Not when cancelled, and not unless this
# language's engine is actually there: a key saved without it would stay taken, and
# every later run would restore the gap and could never save over it.
- name: save the compiled Soufflé engine
if: >-
${{ !cancelled() && !inputs.fresh && steps.engine-cache.outputs.cache-hit != 'true'
&& hashFiles(format('.souffle-cache/souffle-engine-{0}-{1}', matrix.lang, steps.eid.outputs.id)) != '' }}
uses: actions/cache/save@v4
with:
path: .souffle-cache
key: souffle-engine-${{ matrix.lang }}-${{ steps.eid.outputs.id }}-${{ hashFiles('graph/pipeline/run-souffle.sh') }}-march-${{ env.AXIOM_ENGINE_MARCH }}
# Every language's engine, every platform, built once per release: on the push that
# lands a new version on main (and in the nightly). publish-npm ships these very
# artifacts, so what the e2e below tested is what users install (#1350).
engines:
name: engines build on every platform
needs: [build, changes]
if: needs.changes.outputs.engines == 'true'
uses: ./.github/workflows/build-engines.yml
with:
fresh: ${{ inputs.fresh == true }}
# THE RELEASE GATE: what a user installs, on every platform, answers every verb. The engines
# job only proves each binary compiles and starts; the suites run from the checkout. Neither
# installs the packages, so a missing `files` entry, an engine package the CLI does not find,
# a query program that needs Soufflé, or a verb that only breaks on Windows reached users.
# Runs wherever the platform engines are built: on the push that lands a release on main, and in the nightly.
pack:
name: pack @axiomcode/code-graph
needs: [build, changes]
if: needs.changes.outputs.engines == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install --no-audit --no-fund
# --ignore-scripts: `prepare` already built it; the tarball carries what the build produced
- run: mkdir -p tgz && npm pack --ignore-scripts --pack-destination tgz && ls -la tgz
# kept as long as the engines: publish-npm ships this exact tarball, whenever the draft is published
- uses: actions/upload-artifact@v4
with: { name: code-graph-tgz, path: tgz, retention-days: 90, if-no-files-found: error }
e2e:
name: e2e on ${{ matrix.target.platform }}
needs: [changes, engines, pack]
if: needs.changes.outputs.engines == 'true'
strategy:
fail-fast: false
matrix:
target:
- { os: ubuntu-24.04, platform: linux-x64 }
- { os: ubuntu-24.04-arm, platform: linux-arm64 }
- { os: windows-2025, platform: win32-x64 }
- { os: macos-15, platform: darwin-arm64 }
- { os: macos-15-intel, platform: darwin-x64 }
runs-on: ${{ matrix.target.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/download-artifact@v4
with: { name: 'engines-${{ matrix.target.platform }}', path: artifacts/engines }
- uses: actions/download-artifact@v4
with: { name: code-graph-tgz, path: artifacts/tgz }
- name: installed from the tarballs, no Soufflé, every language, every verb
shell: bash
env:
PLATFORM: ${{ matrix.target.platform }}
run: |
set -euo pipefail
command -v souffle && { echo "::error::this runner has souffle; the gate would not prove anything"; exit 1; }
export CODEGRAPH_TGZ="$(ls "$PWD"/artifacts/tgz/*.tgz)"
for lang in java typescript python javascript csharp; do
# A glob, not ls | head: under pipefail, head exiting early SIGPIPEs ls and fails the step.
cases=(graph/test/"$lang"/cases/*/src)
case_dir="${cases[0]}"
[ -d "$case_dir" ] || { echo "::error::no $lang case under graph/test/$lang/cases"; exit 1; }
echo "::group::$lang ($case_dir)"
bash .github/scripts/e2e-install.sh artifacts/engines "$PLATFORM" "$lang" "$case_dir"
echo "::endgroup::"
done
# A single job the branch ruleset can require. Without it, every new matrix
# entry has to be added to the protection rules by hand, and a matrix job that
# fails to start reports nothing at all — which a ruleset reads as "not
# failing" rather than as "did not run".
ci:
name: CI
runs-on: ubuntu-24.04
needs: [changes, build, hygiene, engine, engines, pack, e2e]
if: always()
steps:
- name: every required job succeeded
run: |
# The expression quotes its separator with SINGLE quotes because that is
# the only string delimiter a GitHub expression has. A double quote there
# is a lex error that invalidates the entire workflow file, and the run
# then fails in zero seconds with no job having started.
# changes, build and hygiene always run and must succeed. engine and
# engines may be SKIPPED, but only when `changes` said so; any other
# skip (a job that never started) is a failure.
always="${{ needs.changes.result }} ${{ needs.build.result }} ${{ needs.hygiene.result }}"
echo "always-run jobs: $always"
for r in $always; do
[ "$r" = "success" ] || { echo "::error::a required job reported '$r'"; exit 1; }
done
check() { # name result expected-to-run
if [ "$3" = true ]; then
[ "$2" = success ] || { echo "::error::$1 reported '$2'"; exit 1; }
else
[ "$2" = skipped ] || { echo "::error::$1 reported '$2' though nothing it tests changed"; exit 1; }
fi
echo "$1: $2"
}
check "engine suites" "${{ needs.engine.result }}" "${{ needs.changes.outputs.code }}"
check "platform engines" "${{ needs.engines.result }}" "${{ needs.changes.outputs.engines }}"
check "pack" "${{ needs.pack.result }}" "${{ needs.changes.outputs.engines }}"
check "e2e on every platform" "${{ needs.e2e.result }}" "${{ needs.changes.outputs.engines }}"
echo "all required jobs passed"