ci #250
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). | |
| # | |
| # Triggers: pull_request (any target) + push to `main` + manual dispatch. | |
| # The gate has no external runtime dependency beyond Zig 0.16.0 and python3 | |
| # (preinstalled on GitHub runners); `zig build -Dwasm` fetches the embedded | |
| # zwasm engine + the zlinter dev-dep from git over the network. | |
| # | |
| # Tiered (ADR-0107, revised 2026-07-21) so CI verifies exactly what the local | |
| # full gate does on every landed commit — the fast tier is PR-only: | |
| # PR → fast CORE (units + F-012 diff oracle, no e2e shell suite) for iteration. | |
| # push to main → FULL gate (+ every e2e step) == the local full gate, so an | |
| # e2e regression from a shared-code change fails on the push, not a day later. | |
| # nightly schedule / manual dispatch → FULL gate + the non-default-backend | |
| # (tree_walk) sweep (an extra backstop that is not part of the local gate). | |
| # 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: | |
| schedule: | |
| # Nightly full gate (03:17 UTC) — the batched Linux full-e2e coverage that | |
| # ADR-0049 keeps off the per-commit path. | |
| - cron: '17 3 * * *' | |
| 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 }} | |
| - 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 | |
| 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), | |
| # keyed on the manifests. A same-manifest run restores everything; a | |
| # manifest change falls back via restore-keys to the newest prior cache and | |
| # rebuilds incrementally (Zig's cache is content-addressed, so a stale | |
| # restore is safe — it only ever causes a rebuild, never a wrong build). | |
| - name: Cache Zig deps + build | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| .zig-cache | |
| key: ${{ matrix.name }}-zigbuild-${{ hashFiles('build.zig.zon', 'build.zig') }} | |
| restore-keys: | | |
| ${{ matrix.name }}-zigbuild- | |
| # Gate deps not universally preinstalled: ripgrep (check_debt_id_refs.sh) | |
| # 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). | |
| - name: Install gate deps if missing (ripgrep; bash>=4 on macOS) | |
| shell: bash | |
| run: | | |
| if [ "$RUNNER_OS" = "Linux" ]; then | |
| command -v rg >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep; } | |
| else | |
| command -v rg >/dev/null 2>&1 || brew install ripgrep | |
| if [ "$(bash -c 'echo "${BASH_VERSINFO[0]}"')" -lt 4 ]; then | |
| brew install bash | |
| fi | |
| fi | |
| - name: Verify toolchain | |
| shell: bash | |
| run: | | |
| zig 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 | |
| - name: ci_gate.sh | |
| shell: bash | |
| env: | |
| # push-to-main + nightly + dispatch → FULL gate (== the local full | |
| # gate: every e2e step), so a shared-code change that breaks an e2e | |
| # step fails on the push, not a day later on the nightly (ADR-0107 | |
| # revision 2026-07-21). PR → fast core (its merge fires a push event | |
| # that runs the full gate before it lands). | |
| CLJW_CI_FULL: ${{ github.event_name != 'pull_request' && '1' || '0' }} | |
| # The heavier non-default-backend (tree_walk) sweep — a second | |
| # ReleaseSafe rebuild, NOT part of the local full gate — stays | |
| # nightly/dispatch-only as an extra backstop. | |
| CLJW_CI_PARITY: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '1' || '0' }} | |
| run: bash scripts/ci_gate.sh |