Skip to content

engine: darwin-arm64 solves in parallel — the residual crash was unfenced node publication #641

engine: darwin-arm64 solves in parallel — the residual crash was unfenced node publication

engine: darwin-arm64 solves in parallel — the residual crash was unfenced node publication #641

Workflow file for this run

# ─────────────────────────────────────────────────────────────────────────────
# 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: 90
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 }}
# THE QUERY LAYER'S CASES (tests/run.py): what impact, path, context, changed and test-impact answer on small
# projects written for one behaviour each. Every case must pass, and a case marked pending that now passes fails
# the run until the mark is removed, so a fix for one shape cannot quietly break another's answer. The suite
# calls the verbs directly, so it checks their own answers, not the front-door rendering (tests/front_door.py).
- name: ${{ matrix.lang }} query cases
env:
AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js
AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache
run: python3 tests/run.py --lang ${{ matrix.lang }} --jobs 4
# the small surface as users and agents get it: find / impact / path / tests through the installed command and
# the MCP server, answered as places with their code, and the direct calls and flags that keep the old answers
- name: the front door answers as places with their code
if: matrix.lang == 'python'
env:
AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js
AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache
run: python3 tests/front_door.py
# The hooks answer impact from SQL (graph_sql.impact_shaped) and fall back to the rules (dl/impact.dl) only when
# it declines, so the two must list the same rows for the same edit: tests/fastpath.py indexes a small case, asks
# both on each target shape, and runs hooks/changes.py on one edit through each path. Same parser and engine
# cache as the suite above. Not typescript: its case needs the TypeScript engine; javascript has no case.
- name: the hooks' fast path agrees with the rules (${{ matrix.lang }})
if: matrix.lang == 'python' || matrix.lang == 'java' || matrix.lang == 'csharp'
env:
AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js
AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache
run: python3 tests/fastpath.py --lang ${{ matrix.lang }}
# 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"