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
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 deliberately not installed in this job: the tests
# that exercise it already tolerate its absence (see `e2e::format::format_language`'s
# non-`--strict` deferral, and `cli_all_format_gate_covers_every_write_phase`'s own
# `poly_is_available()` branch), so it is not required for this job to pass, only for the
# `generated-output-gate` job's lanes, which install it themselves.
- 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"
- uses: xberg-io/actions/setup-maven@v1
- 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.0.1
- name: Install generated Python type checker
run: uv tool install pyrefly==1.2.0
- name: Install generated TypeScript compiler
run: npm install --global typescript@5.9.2
# 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.
# Pinned to the exact commit for `fwilhe2/setup-kotlin` tag v2.0 -- a third-party action,
# so SHA-pinned rather than trusting a floating tag, matching the `Goldziher/poly` pin in
# the `generated-output-gate` job below. `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@ee9692514da313706b193d808526812102a344e4 # v2.0
with:
version: "2.4.10"
- 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"
# Go needs no dedicated setup step above (GitHub-hosted runner images preinstall a Go
# LTS toolchain), but the two `go test` compile canaries in
# `backends::go::gen_bindings::service_api::tests` silently skipped when `go` was
# missing just the same as the dotnet ones did -- see `required_go` there.
ALEF_REQUIRE_GO: "1"
run: cargo test --workspace
# 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
# 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
# Goldziher/poly@v0 resolved to tag v0.23.0 (commit
# 5f609e9f6913a9e0e9046fc02fe76c0515a8e276) when this pin was written -- pinned to that
# commit SHA below because a major-version tag like `v0` is mutable and can be
# repointed, so the SHA is the real supply-chain pin; `version: v0.23.0` in `with:` is
# kept as the human-readable/functional pin the action itself resolves against. To bump
# poly, update both the SHA below and `version:` together. ~keep
uses: Goldziher/poly@7438037e94efae3487895e0307f20a78a217ffb5 # v0.23.1
with:
version: v0.23.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.23.0" ]; then
echo "::error::expected poly 0.23.0 (pinned for the poly.toml added-large-files hook), got ${installed_poly_version:-<empty>}. Update the pinned Goldziher/poly uses: SHA and 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