Repository navigation
Release v1.15.4 from BuddhiLW/main #338
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI — the host-local gate on the supported OSes (macOS arm64 + Linux x86_64), | |
| # mirroring the local gate. Each matrix leg runs `scripts/ci_gate.sh`, so CI | |
| # verifies exactly what the maintainer runs per host (single source of truth). | |
| # ci_gate.sh reaches the suite only through `test/cljw-test`, the one entry | |
| # point: `--only fmt`, then `--only full`. What `full` runs is a row in | |
| # test/units.list, so the gate configuration is defined once, not once per | |
| # launcher. | |
| # | |
| # Triggers: pull_request (any target) + push to `main` + manual dispatch. | |
| # A push to `staging` runs nothing here on purpose: the staging→main PR is its | |
| # gate (see .github/CONTRIBUTING.md "When CI runs"). | |
| # The gate provisions pinned Zig 0.16.0 and Babashka; python3 is preinstalled | |
| # on GitHub runners. `zig build -Dwasm` fetches the embedded zwasm engine + | |
| # the zlinter dev-dep from git over the network. | |
| # | |
| # ONE configuration, everywhere (2026-08-12). PR, push, dispatch and the local | |
| # `scripts/run_gate.sh` all run the identical full gate, so "green" means the | |
| # same run wherever it was said. The tiers this used to have (PR → smoke, | |
| # nightly → full + a tree_walk sweep) were the drift they were meant to catch: | |
| # the last nightly failed on a wall-clock bound in a config nothing else ran, | |
| # which is a finding about the runner, not about cljw. | |
| # `scripts/check_gate_parity.sh` enforces the single configuration mechanically. | |
| # The Zig toolchain, the fetched deps (zwasm/zlinter), and the build cache are | |
| # all preserved across runs (actions/cache), so a warm run rebuilds only what | |
| # changed instead of three cold ReleaseSafe builds — the dominant cost. | |
| name: ci | |
| on: | |
| # Skip CI on doc/scaffolding-only pushes (markdown, .dev/ ledger + ADRs + notes): | |
| # they cannot change build/test outcome, so a ~15-min gate on them is pure waste. | |
| # A commit that also touches src / scripts / build.zig* / flake.nix / .github / | |
| # a root data-yaml still runs (paths-ignore skips only when ALL changed files match). | |
| pull_request: | |
| paths-ignore: ['**/*.md', '.dev/**'] | |
| push: | |
| branches: [main] | |
| paths-ignore: ['**/*.md', '.dev/**'] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| env: | |
| # Homebrew 6.0 (2026-05-30) enforces tap-trust: the macos-15 image ships a | |
| # pre-tapped third-party `aws/tap`, so every `brew` invocation (auto-update | |
| # included) spews a "not trusted / ignoring formulae" block and any future | |
| # third-party formula install would be rejected outright. Our own installs | |
| # are homebrew/core (official) so they still work, but this keeps the log | |
| # clean and future-proofs a third-party dep. Harmless on Linux (no brew). | |
| HOMEBREW_NO_REQUIRE_TAP_TRUST: 1 | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| gate: | |
| name: gate (${{ matrix.name }}) | |
| runs-on: ${{ matrix.runs-on }} | |
| timeout-minutes: 75 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: aarch64-macos | |
| runs-on: macos-15 | |
| zig-dir: zig-aarch64-macos-0.16.0 | |
| - name: x86_64-linux | |
| runs-on: ubuntu-22.04 | |
| zig-dir: zig-x86_64-linux-0.16.0 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.sha }} | |
| # scripts/check_changelog_reachability.sh lists release tags with | |
| # `git tag --merged HEAD`, which needs both the tags and the history | |
| # that connects them to HEAD. A depth-1 clone (even with fetch-tags) | |
| # reaches no tag, so the step would report "no release tag" and pass. | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Cache Zig 0.16.0 | |
| id: cache-zig | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.local/zig-0.16.0 | |
| key: zig-0.16.0-${{ matrix.name }} | |
| - name: Install Zig 0.16.0 | |
| if: steps.cache-zig.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| source .github/versions.lock | |
| mkdir -p ~/.local | |
| curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/${{ matrix.zig-dir }}.tar.xz" -o /tmp/zig.tar.xz | |
| # Clear both paths first so a re-run is idempotent: tar refuses to | |
| # overwrite existing entries ("Cannot open: File exists"), and `mv` into | |
| # an EXISTING directory nests instead of replacing, which would put the | |
| # toolchain at zig-0.16.0/<zig-dir>/ and leave `zig` off PATH. | |
| rm -rf "$HOME/.local/${{ matrix.zig-dir }}" "$HOME/.local/zig-0.16.0" | |
| tar -xJf /tmp/zig.tar.xz -C ~/.local | |
| mv "$HOME/.local/${{ matrix.zig-dir }}" "$HOME/.local/zig-0.16.0" | |
| - name: Add Zig to PATH | |
| shell: bash | |
| run: echo "$HOME/.local/zig-0.16.0" >> "$GITHUB_PATH" | |
| - name: Pin Zig global cache dir | |
| shell: bash | |
| run: echo "ZIG_GLOBAL_CACHE_DIR=$HOME/.cache/zig" >> "$GITHUB_ENV" | |
| # Cache the fetched deps (zwasm git dep + zlinter, under the global cache's | |
| # p/) AND the build artifacts (global cache + the repo-local .zig-cache). | |
| # Zig's cache is content-addressed, so a stale restore is always SAFE — it | |
| # can only cause a rebuild, never a wrong build. That makes "restore the | |
| # newest thing available, always save what this run produced" the correct | |
| # policy, and it is why the key carries the run id. | |
| # | |
| # It did not used to. The key was `hashFiles('build.zig.zon','build.zig')` | |
| # — files that change a few times a year — so it HIT on essentially every | |
| # run, and `actions/cache` only WRITES a cache when its key MISSES. The | |
| # restored `.zig-cache` was therefore frozen at whatever the first run | |
| # with that build.zig produced, and every run since rebuilt everything | |
| # that had changed after that snapshot and then threw the result away. | |
| # Measured 2026-08-19: `zig_build_test_vm` 375 s + `zig_build_test_tree_walk` | |
| # 371 s on the Linux leg, against ~1 s warm locally (D-487) — i.e. cold | |
| # builds, every run, while the step reported a cache hit every time. | |
| # A cache that reports a hit and saves nothing is worse than no cache: | |
| # it makes the waste invisible. | |
| # | |
| # restore/save are split so a FAILED run still saves its build products. | |
| # A red gate is exactly when the next run is a fix for it, seconds later, | |
| # against nearly the same tree — the moment the cache is worth the most. | |
| - name: Restore Zig deps + build | |
| id: zigcache | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| .zig-cache | |
| key: ${{ matrix.name }}-zigbuild-${{ hashFiles('build.zig.zon', 'build.zig') }}-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ matrix.name }}-zigbuild-${{ hashFiles('build.zig.zon', 'build.zig') }}- | |
| ${{ matrix.name }}-zigbuild- | |
| # Gate deps not universally preinstalled: ripgrep (check_debt_id_refs.sh), | |
| # Babashka (native Clojure harness supervision), | |
| # and, on macOS, bash >= 4 — the macos-15 runner resolves `bash` to the | |
| # system 3.2, which cannot parse the e2e harness idioms (heredoc inside | |
| # `$()`, `case` inside `$()`, `declare -A`); 43 gate scripts fail under it | |
| # (first exposed by the 2026-07-02 nightly, the first FULL run on the | |
| # hosted mac runner). Every script is invoked as `bash foo.sh` or via | |
| # `#!/usr/bin/env bash`, so a brew bash 5 on PATH fixes all of them. | |
| # Install only what is absent (fast no-op when the image ships it). | |
| # yq is listed even though both hosted images ship it: `check_compat_members` | |
| # exits 1 without it, so relying on the image means the gate is green here | |
| # for a reason nobody wrote down and red on a developer machine. | |
| - name: Install gate deps if missing (bb, ripgrep, yq; bash>=4 on macOS) | |
| shell: bash | |
| run: | | |
| if ! command -v bb >/dev/null 2>&1; then | |
| source .github/versions.lock | |
| case "$RUNNER_OS:$RUNNER_ARCH" in | |
| Linux:X64) | |
| bb_platform=linux-amd64 | |
| bb_sha="$BABASHKA_LINUX_AMD64_SHA256" | |
| ;; | |
| macOS:ARM64) | |
| bb_platform=macos-aarch64 | |
| bb_sha="$BABASHKA_MACOS_AARCH64_SHA256" | |
| ;; | |
| *) | |
| echo "::error::unsupported Babashka runner $RUNNER_OS/$RUNNER_ARCH" | |
| exit 1 | |
| ;; | |
| esac | |
| bb_archive="/tmp/babashka-${BABASHKA_VERSION}.tar.gz" | |
| curl -fsSL "https://github.com/babashka/babashka/releases/download/v${BABASHKA_VERSION}/babashka-${BABASHKA_VERSION}-${bb_platform}.tar.gz" -o "$bb_archive" | |
| bb_actual=$(shasum -a 256 "$bb_archive" | awk '{print $1}') | |
| if [ "$bb_actual" != "$bb_sha" ]; then | |
| echo "::error::Babashka checksum mismatch: expected $bb_sha, got $bb_actual" | |
| exit 1 | |
| fi | |
| mkdir -p "$HOME/.local/bin" | |
| tar -xzf "$bb_archive" -C "$HOME/.local/bin" bb | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| fi | |
| if [ "$RUNNER_OS" = "Linux" ]; then | |
| command -v rg >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep; } | |
| command -v yq >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq yq; } | |
| else | |
| command -v rg >/dev/null 2>&1 || brew install ripgrep | |
| command -v yq >/dev/null 2>&1 || brew install yq | |
| if [ "$(bash -c 'echo "${BASH_VERSINFO[0]}"')" -lt 4 ]; then | |
| brew install bash | |
| fi | |
| fi | |
| - name: Verify toolchain | |
| shell: bash | |
| run: | | |
| zig version | |
| bb --version | |
| python3 --version | |
| rg --version | head -1 | |
| # Check the PATH-resolved bash (what `bash foo.sh` + env-shebang | |
| # scripts actually get), not this step's own shell. | |
| bash --version | head -1 | |
| if [ "$(bash -c 'echo "${BASH_VERSINFO[0]}"')" -lt 4 ]; then | |
| echo "::error::gate requires bash >= 4 on PATH (PATH bash is $(bash -c 'echo "$BASH_VERSION"'))"; exit 1 | |
| fi | |
| # Warm the build cache in a step that can RECOVER from a poisoned restore. | |
| # A restored `.zig-cache` can reference a file it does not contain (a job | |
| # cancelled mid-build used to save one, see the save step below), and | |
| # Zig treats that as a hard compilation error rather than rebuilding. | |
| # Left alone, `restore-keys` hands that same entry to every later run and | |
| # CI stays red until a human deletes it through the API. | |
| # | |
| # The recovery is UNCONDITIONAL: on any failed warm build, clear the cache | |
| # and build once more. Wiping a content-addressed cache is always safe (it | |
| # costs a rebuild, nothing else), and the retry is a better discriminator | |
| # than any log predicate, because a failure with NO cache present is | |
| # unambiguously about the source. | |
| # | |
| # This step used to gate the recovery on `grep "failed to check cache"`, | |
| # the exact wording of the first poisoning observed. A restored cache has | |
| # at least two spellings for one condition, and the second one landed on | |
| # the v1.14.5 release PR (2026-09-10): | |
| # error: failed to spawn build runner .zig-cache/o/<hash>/build: FileNotFound | |
| # The predicate did not match, so the step printed "build failed for a | |
| # reason unrelated to the cache" and failed the release for the very | |
| # condition it was written to heal. Matching a symptom's wording is how a | |
| # self-heal reports a false negative with confidence; not matching at all | |
| # cannot. | |
| # | |
| # This is not extra work on a healthy run: the gate's own first step is | |
| # this same build, so a healthy run does it here and finds it warm there. | |
| - name: Warm build (self-healing) | |
| shell: bash | |
| run: | | |
| set -uo pipefail | |
| if zig build -Dwasm -Doptimize=ReleaseSafe; then | |
| exit 0 | |
| fi | |
| echo "::warning::warm build failed, clearing the restored .zig-cache and rebuilding once" | |
| rm -rf .zig-cache | |
| zig build -Dwasm -Doptimize=ReleaseSafe | |
| - name: ci_gate.sh | |
| shell: bash | |
| run: bash scripts/ci_gate.sh | |
| # Regression guard: the wasm32-wasi target must keep compiling (ADR-0193). | |
| # Compile-only — a runtime smoke needs a wasm engine the runners do not | |
| # ship. Linux leg only; the wasm target is host-arch independent. | |
| - name: Build wasm32-wasi target (ADR-0193) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: zig build -Dtarget=wasm32-wasi | |
| # Save on success OR failure, but NEVER on cancellation. | |
| # | |
| # Failure is the case worth saving for: the next run is usually a fix for | |
| # that red, minutes later, against nearly the same tree, and a failed job | |
| # ran its BUILD steps to completion — only a test or a check went red, so | |
| # `.zig-cache` is internally consistent. | |
| # | |
| # A CANCELLED job is the opposite, and this condition was originally | |
| # `always()`, which INCLUDES cancellation. Measured 2026-08-19: pushing a | |
| # fix fired `cancel-in-progress`, killing the Linux job in the middle of | |
| # `zig build`. The save captured `.zig-cache` half-written — an | |
| # `o/<hash>` manifest present, `bootstrap_sources_stub.zig` inside it | |
| # never flushed. The next run restored that and Zig refused to build: | |
| # | |
| # error: failed to check cache: '.zig-cache/o/…/bootstrap_sources_stub.zig' | |
| # file_hash FileNotFound | |
| # | |
| # Zig does NOT recover from this on its own — it errors instead of | |
| # rebuilding. And since `restore-keys` matches by PREFIX, a poisoned | |
| # entry is the newest match for every later run, so one cancelled job | |
| # wedges the whole lineage until someone deletes the entry by hand. That | |
| # is a worse failure mode than the stale-cache bug this caching replaced, | |
| # which is why the condition is narrowed rather than the symptom patched. | |
| - name: Bound the Zig cache before saving | |
| if: success() || failure() | |
| shell: bash | |
| run: bash scripts/prune_zig_cache.sh | |
| - name: Save Zig deps + build | |
| if: success() || failure() | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| .zig-cache | |
| key: ${{ steps.zigcache.outputs.cache-primary-key }} |