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
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: xberg-io/actions/setup-rust@v1
with:
components: "rustfmt, clippy"
install-llvm-cov: "false"
macos-dynamic-lookup: "false"
- uses: taiki-e/install-action@v2
with:
tool: cargo-deny,cargo-machete,cargo-sort
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings -A clippy::needless_update
- name: Check unused dependencies
run: cargo machete
- name: Security and license audit
run: cargo deny check
- name: Check Cargo.toml sorting
run: cargo sort --check --workspace
poly-validate:
uses: xberg-io/actions/.github/workflows/reusable-validate.yml@v1
with:
setup-rust: true
setup-python: true
test:
name: Test (${{ matrix.os }})
needs: validate
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: xberg-io/actions/setup-rust@v1
with:
install-llvm-cov: "false"
cache-key-prefix: ci-test
# Several `#[test]`s compile alef's own generated output, or alef's own compiler-oracle
# probes, with the real per-language toolchain rather than mocking it (that is the point
# of them), so those toolchains have to actually be on PATH here, not just in the
# `generated-output-gate` job.
#
# `poly` IS installed here (alef#458), unlike most of this job's toolchains, which the
# tests that need them tolerate skipping: `bin_cli::helpers::format_drift`'s alef#436
# drift check has a fast, in-memory `.rs`/`.md` prediction whose precondition is "a real
# `poly fmt --fix` pass ran during the write it predicts" (see that module's doc). Without
# poly on PATH, `alef verify`'s own regression tests in `bin_cli::core_commands::tests`
# (`verify_command_reports_and_fails_on_a_real_orphaned_generated_file` and two siblings)
# ran `alef all` against a tree that never got that pass, then predicted post-pass bytes
# anyway and reported a freshly-generated file as falsely drifted -- three real CI
# failures on every OS this job runs, not a hypothetical. Pinned to the same release the
# `generated-output-gate` job installs, so the two jobs can never disagree about which
# poly version this repo's tests run against. ~keep
#
# Linux and macOS only: poly's installer refuses to run on Windows ("Windows detected.
# Use the PowerShell installer"), and the `Goldziher/poly@v0` action wraps that same
# `install.sh`, so requesting it on the Windows leg fails the job outright. Skipping it
# there is correct rather than a workaround: since alef#458 the drift check degrades to a
# counted, reported skip when poly is absent instead of inventing drift, so the Windows
# leg stays honest, and the real `poly fmt` comparison still executes on two of the three
# OSes every run. ~keep
- name: Install poly
if: runner.os != 'Windows'
uses: Goldziher/poly@v0
with:
version: v0.24.0
- name: Set up JDK
uses: actions/setup-java@v6
with:
distribution: temurin
# Matches `template_versions::toolchain::JAVA_JVM_TARGET`: alef's generated Java
# service bindings use the `java.lang.foreign` FFM API, stable only from JDK 22. This
# JDK is also `javac`, the oracle `ALEF_REQUIRE_JAVAC` below requires.
java-version: "25.0.4+101.0.LTS"
- uses: xberg-io/actions/setup-maven@v1
# Go is NOT preinstalled on every GitHub-hosted image. The arm64 macOS images carry none,
# which is why `Test (macos-latest)` failed nine generated-Go fixtures on every run from
# 2026-08-31 (the commit that added them) onward while `ALEF_REQUIRE_GO` below turned the
# skip into a panic. Installing it explicitly on all three legs is far cheaper than
# weakening the fixtures, and it is what makes `--require go` in the census step below
# honest. Pinned to the minor line alef itself emits into generated `go.mod` files
# (`go 1.26`, see `e2e::codegen::go`), so the toolchain CI compiles the output with can
# never be older than the directive that output declares. ~keep
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27"
- uses: xberg-io/actions/setup-zig@v1
- uses: xberg-io/actions/setup-elixir@v1
with:
elixir-version: "1.19"
otp-version: "28"
- uses: astral-sh/setup-uv@v10.2.0
# The generated magnus Data accessor regression executes Ruby with real Sorbet signatures.
# `ALEF_REQUIRE_RUBY` below turns a missing interpreter into a hard failure, and the census
# step proves the fixture executed rather than skipped. `sorbet-runtime` is pinned to the
# exact gem the fixture was measured against. ~keep
- name: Set up Ruby for generated Data regression
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
with:
ruby-version: "4.0"
- name: Install generated Ruby runtime dependency
run: gem install sorbet-runtime --version 0.6.13506 --no-document
# Exact pin, not a floor: `template_versions::pypi::PYREFLY` sets the floor emitted into
# every generated consumer's own `pyproject.toml` dev dependency, but pyrefly is resolved
# from PATH here (`which::which("pyrefly")` in the pyo3 kwarg-unpack harness and the
# generated-package gate), so this line is the only thing that decides which pyrefly
# actually runs in CI. Alef issue #457: pyrefly's `strict` preset promotes `open-unpacking`
# from `Ignore` to `Error` only from 1.3.0 onward (1.2.0 and 1.2.1 report zero errors for
# the exact sabotaged input `kwarg_unpack_tests` checks), so this pin must never fall below
# `template_versions::pypi::PYREFLY`'s floor. `tests/ci_pyrefly_pin_gate.rs` asserts the two
# agree; bump both together. ~keep
- name: Install generated Python type checker
run: uv tool install pyrefly==1.3.1
- name: Install generated TypeScript compiler
run: npm install --global typescript@5.9.2
# `rust-toolchain.toml` already carries `wasm32-unknown-unknown` on every leg, but the
# wasm-bindgen CLI is a separate artifact and nothing here installed it before this step.
# `backends::wasm::wasm_bindgen_js_oracle` is the only check in this repo that reads what
# wasm-bindgen actually EMITS for alef's output rather than the Rust source alef wrote --
# the defect it guards (a by-value exported-struct argument lowered through
# `__destroy_into_raw()`) compiles cleanly and is invisible to every string assertion.
# No version is pinned here on purpose: the fixture reads this binary's own version and
# pins its generated crate to it, because the CLI aborts on a schema mismatch. ~keep
- name: Install the wasm-bindgen CLI
uses: taiki-e/install-action@v2
with:
tool: wasm-bindgen-cli
# The C# identifier-grammar oracle (`ALEF_REQUIRE_DOTNET` below) needs a real `dotnet` on
# PATH; nothing else in this job installed one before this step existed.
- uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
# The Kotlin identifier-grammar oracle (`ALEF_REQUIRE_KOTLINC` below) needs a real
# `kotlinc` on PATH; nothing else in this job installed one before this step existed.
# `version:` pins the exact kotlinc release `codegen::identifier_grammar`'s docs say the
# Kotlin grammar was measured against, so a `fwilhe2/setup-kotlin` default-version bump
# can't silently change what CI verifies against. ~keep
- name: Install kotlinc
uses: fwilhe2/setup-kotlin@v2
with:
version: "2.4.10"
# Swift is the one toolchain here that genuinely cannot be installed on every leg: it ships
# with Xcode on macOS, while the Windows toolchain is a multi-gigabyte installer with no
# first-party setup action and Linux would need a separate swift.org tarball -- for a single
# fixture. So macOS is where the SwiftPM compile gate has to run, and the census step below
# is what proves it did rather than assuming it. ~keep
- name: Require the Swift toolchain on macOS
if: runner.os == 'macOS'
shell: bash
run: echo "ALEF_REQUIRE_SWIFT=1" >> "$GITHUB_ENV"
# The cargo cache restores `target/`, so a previous run's tallies would otherwise be summed
# into this one and could report fixtures as executed that this run never reached.
- name: Clear the toolchain fixture census
shell: bash
# Derived from `cargo metadata` via the shared helper, not a hard-coded `target/`, so a
# `CARGO_TARGET_DIR` override resets the same directory the census step below reads back
# (`scripts/toolchain-census.sh` resolves its own default the same way).
run: rm -rf "$(scripts/toolchain-census-dir.sh)"
- name: Run tests
env:
ALEF_REQUIRE_ELIXIR: "1"
ALEF_REQUIRE_PYREFLY: "1"
ALEF_REQUIRE_TSC: "1"
# These four make the javac/dotnet/kotlinc identifier-grammar oracle tests
# (`tests/identifier_grammar_compiler_oracle.rs`) and the two dotnet-compile canaries
# scattered across the C# backend tests panic instead of silently skipping when their
# compiler is missing -- a missing toolchain must fail this job loudly, not let the
# gate pass unverified. See `required_javac_mode_fails_when_toolchain_is_unavailable`
# and its dotnet/kotlinc siblings for the proof this panic actually fires.
ALEF_REQUIRE_JAVAC: "1"
ALEF_REQUIRE_DOTNET: "1"
ALEF_REQUIRE_KOTLINC: "1"
# Paired with the `Set up Go` step above, which is not optional: the claim this comment
# used to make -- that GitHub-hosted images all preinstall a Go LTS toolchain, so this
# variable needed no `uses:` step -- is false for the arm64 macOS images, and setting
# the variable without installing Go only converted a silent skip into a permanently red
# leg. `ci_requires_go_for_runtime_regressions` now asserts both halves are present.
ALEF_REQUIRE_GO: "1"
# Paired with the `Set up Ruby` step above: the generated magnus Data accessor
# regression executes real Ruby under `sorbet-runtime`, and a leg that installed Ruby
# must run it rather than count a skip as a pass.
ALEF_REQUIRE_RUBY: "1"
# Paired with the `Install the wasm-bindgen CLI` step above: the JS-glue oracle is the
# only fixture that can see a consumed-handle regression, so a leg that installed the
# CLI must run it rather than count a skip as a pass.
ALEF_REQUIRE_WASM_BINDGEN: "1"
# `--no-fail-fast` is load-bearing, not tidiness: without it cargo stops after the
# first failing *target*, so every integration binary ordered after it never runs and
# its result is indistinguishable from a pass. A stale assertion in
# `backends_wasm_gen_bindings_test` masked a second, unrelated failure in
# `ci_poly_pin_gate` across two releases that way -- the log showed one red test and
# looked complete. Run every target and report every failure. ~keep
run: cargo test --workspace --no-fail-fast
# Every Elixir escaping oracle is #[ignore]d, so the step above runs none of them and
# still exits 0. They evaluate generated Elixir through the real interpreter -- the only
# checks in this repo that do -- so dropping `--ignored` or the filter here retires them
# silently. `elixir_oracle::gate` reads THIS step's actual `run:` line (not the block, and
# not this comment, which contains both needles) and fails if either goes missing.
- name: Run the Elixir escaping oracles
run: cargo test --lib elixir_oracle -- --ignored --nocapture
# Counts what the two steps above actually executed, per toolchain, and fails when a
# toolchain this leg installed executed zero fixtures.
#
# A fixture that shells out to a real toolchain has three possible outcomes and only two of
# them are honest: it can pass, it can fail, or it can quietly verify nothing and be counted
# as a pass. `cargo test`'s own summary cannot tell the third apart from the first -- both
# print `test result: ok` -- so this step prints the ratio instead: "12 of 12 go fixtures
# executed" versus "0 of 12 executed". `if: ${{ !cancelled() }}` so the ratio is still
# reported when the suite failed, which is when it is most useful for telling an
# environmental failure from a real one. ~keep
- name: Toolchain fixture census
if: ${{ !cancelled() }}
shell: bash
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
scripts/toolchain-census.sh --require go --require ruby --require swift --require wasm-bindgen
else
scripts/toolchain-census.sh --require go --require ruby --require wasm-bindgen
fi
# Runs the consumer's own toolchain gate over a tree alef emits, in a scratch directory.
# The `validate` job above lints alef; this one lints alef's *output*, which nothing
# else in this workflow looks at. Kept a separate job because it needs poly and
# cargo-sort — which the three-OS `test` matrix does not install — and because it
# compiles the emitted crates.
generated-output-gate:
name: Generated output gate
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: xberg-io/actions/setup-rust@v1
with:
components: "clippy"
install-llvm-cov: "false"
cache-key-prefix: ci-generated-output-gate
- uses: taiki-e/install-action@v2
with:
tool: cargo-sort
# Pinned to an exact poly release, not `latest`: the `[hooks.pre-commit.commands.
# added-large-files]` entry in poly.toml shells out to `poly hooks check
# --added-large-files`, which poly's own `--help` labels internal/hidden -- not part
# of its user-facing surface, so a future poly could rename or drop it without that
# being a documented breaking change. `Goldziher/poly` is the project's own
# composite action; it forwards `version:` to the project's checksum-verified
# `install.sh`, so this is a true, verified pin, not a presence check. See
# https://github.com/Goldziher/poly#github-actions. ~keep
- name: Install poly
# The `uses:` major tag selects the *action code*, like every other action in this
# workflow. `version:` pins the *poly release the action installs*, which the step
# below re-checks at runtime; bumping it is a deliberate call, because this repo's lint
# output is version-sensitive and has differed between poly patch releases.
# `tests/ci_poly_pin_gate.rs` enforces both. ~keep
uses: Goldziher/poly@v0
with:
version: v0.24.0
# Both tools must be present *before* the lanes run. The lanes fail on a missing
# tool rather than skipping, so this step exists to fail earlier and more legibly,
# not to decide whether the gate runs. The poly assertions are deliberately strict
# rather than a bare `poly --version`: (1) the version must match the pin above
# exactly, so an accidental `latest` resolution or a stale cache is caught here
# instead of silently linting with the wrong poly; (2) `poly hooks check
# --added-large-files --help` must still resolve, because that hidden subcommand is
# exactly what `poly.toml`'s large-file guard depends on -- if a poly upgrade ever
# renames or removes it, this is where that shows up as a loud, named CI failure
# instead of the guard silently no-op'ing on every future commit. ~keep
- name: Verify downstream tooling is present
run: |
cargo sort --version
installed_poly_version="$(poly --version | awk '{print $2}')"
if [ "$installed_poly_version" != "0.24.0" ]; then
echo "::error::expected poly 0.24.0 (pinned for the poly.toml added-large-files hook), got ${installed_poly_version:-<empty>}. Update the Goldziher/poly version: input above together with this check." >&2
exit 1
fi
echo "poly ${installed_poly_version} matches the pinned version."
if ! poly hooks check --added-large-files --help >/dev/null 2>&1; then
echo "::error::'poly hooks check --added-large-files' no longer resolves. This is the hidden/internal subcommand that poly.toml's [hooks.pre-commit.commands.added-large-files] shells out to for the repo's large-file guard -- if poly renamed or dropped it, that guard is now silently not running on every commit. Fix poly.toml (see the 'flag if a public surface can replace the hidden command' note in CHANGELOG.md) before bumping the pinned poly version." >&2
exit 1
fi
echo "poly hooks check --added-large-files is still available."
# Not ignored, needs no tooling: the isolation guard and the wiring check. Run as
# its own step so "the gate refuses to lint alef itself" is a named, visible result
# in the CI log rather than an assertion buried inside a long run.
- name: Prove the gate refuses to lint alef itself
run: cargo test --test generated_output_downstream_gate -- --nocapture
# The lanes and their sabotage proofs. Every test here is #[ignore]d, so dropping
# `--ignored` would run none of them and still exit 0 — which is why
# `ci_workflow_runs_the_generated_output_gate` asserts this flag is still here.
- name: Run downstream gate over the emitted tree
run: cargo test --test generated_output_downstream_gate -- --ignored --nocapture
# Compiles alef at the exact floor `Cargo.toml` declares. Nothing else in this workflow
# does: `rust-toolchain.toml` pins a far newer toolchain, so every other job builds with a
# compiler that accepts features the declared MSRV rejects. 0.68.0 reached crates.io using
# `if let` guards (stable from 1.95) while advertising 1.85, and no CI job could have caught
# it -- an external user did, at install time (issue #262). The toolchain is read from the
# manifest rather than written here, so it cannot drift out of step with the claim.
msrv:
name: Compile at declared MSRV
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Resolve declared MSRV
id: msrv
run: |
version="$(sed -n 's/^rust-version *= *"\(.*\)"/\1/p' Cargo.toml)"
if [ -z "$version" ]; then
echo "Cargo.toml declares no rust-version" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Declared MSRV: $version"
- name: Install the declared toolchain
run: rustup toolchain install "${{ steps.msrv.outputs.version }}" --profile minimal
# `+toolchain` on the command line overrides rust-toolchain.toml, which is the entire
# point of this job.
- name: Compile at the declared MSRV
run: cargo "+${{ steps.msrv.outputs.version }}" check --lib --locked
build:
name: Build release
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: xberg-io/actions/setup-rust@v1
with:
install-llvm-cov: "false"
cache-key-prefix: ci-build
- uses: xberg-io/actions/build-rust-cli@v1
id: build-cli
with:
package-name: alef
binary-name: alef
- name: Verify binary
run: ${{ steps.build-cli.outputs.binary-path }} --help