alef 0.103.6

Opinionated polyglot binding generator for Rust libraries
Documentation
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