From af44653f928fc53b21019abe348b803c2f9f5fe4 Mon Sep 17 00:00:00 2001 From: Ed L Date: Wed, 23 Sep 2026 20:27:52 -0700 Subject: [PATCH] chore: setup nixos runners --- .github/actionlint.yaml | 6 ++ .github/workflows/ci.yml | 132 ++++++++++++++++---------------------- flake.nix | 58 +++++++++++------ harness/package-lock.json | 48 +++++--------- harness/package.json | 2 +- scripts/checkers-test.sh | 12 +++- 6 files changed, 126 insertions(+), 132 deletions(-) create mode 100644 .github/actionlint.yaml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..bce99740 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +# Labels the self-hosted NixOS microVM runners register with. +self-hosted-runner: + labels: + - nixos + - microvm + - audited diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4646ac2d..9b1cd222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,15 @@ on: env: CARGO_TERM_COLOR: always +# Every job runs on the self-hosted NixOS microVM runners, and every `run:` +# step inside the flake's `ci` dev shell. Generic-Linux downloads (rustup, +# setup-node's node, Playwright's browsers) cannot run on NixOS, so the +# toolchain comes from flake.nix, the same one `nix develop` gives locally. +# Each step re-enters the shell; after the first, the eval cache makes it cheap. +defaults: + run: + shell: nix develop .#ci --command bash --noprofile --norc -eo pipefail {0} + jobs: # --------------------------------------------------------------------------- # The one place wasm-pack runs (improvise CI dedup). @@ -35,7 +44,7 @@ jobs: # would serialise a 109s job behind a 126s one for no artifact at all. build: name: Build all bundles - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 # First, because it needs nothing and takes a second: the gate that stops @@ -44,9 +53,10 @@ jobs: # below, so it fails under this job's name rather than another's. - name: The stamp gate refuses what it claims to run: ./scripts/checkers.sh assemble-stamp - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown + # rust-cache is a JS action: it runs outside the dev shell, and needs + # cargo and rustc on the job's PATH to compute its key. + - name: Put the dev shell's toolchain on PATH for actions + run: dirname "$(command -v cargo)" >> "$GITHUB_PATH" # Two workspaces, because `harness/opfs-contract` carries an empty # `[workspace]` table and so has its own target dir, which the default # single-workspace configuration never cached (it recompiled its whole @@ -56,34 +66,21 @@ jobs: workspaces: | . -> target harness/opfs-contract -> target - # One home for the two pins, so the release capture - # (scripts/build-web-release.sh, improvise-oujb) provisions the same - # versions rather than a second copy free to go stale. The grep keeps a - # comment or a blank line out of $GITHUB_ENV, which takes NAME=VALUE only. - - name: Pinned web toolchain versions - run: grep -E '^[A-Z_]+=' scripts/web-toolchain-versions.env >> "$GITHUB_ENV" - # Pinned release binary rather than `cargo install` (a three-minute - # source build) or a third-party action. wasm-pack runs its own bundled - # `wasm-opt`, which is why improvise-wasm-client's Cargo.toml names - # `--enable-bulk-memory`: without it the optimizer rejects the - # `memory.copy` LLVM emits regardless. - # v0.15.0 is what `nix develop` supplies, and the bundle was built and - # measured against it locally (346 KB, 120 KB gzipped). Pinning the same - # version is what makes that local run evidence about this one. - - name: Install wasm-pack + # scripts/web-toolchain-versions.env stays the one home for the pins, so + # the release capture (scripts/build-web-release.sh, improvise-oujb) + # provisions the same versions. CI no longer installs them — the dev + # shell supplies wasm-pack and node — so this checks the shell against + # the pins instead: a nixpkgs bump that moves either fails here, by name. + # wasm-pack runs its own bundled `wasm-opt`, which is why + # improvise-wasm-client's Cargo.toml names `--enable-bulk-memory`. + - name: The dev shell supplies the pinned web toolchain run: | - set -eu - url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz" - curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \ - "wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack" + . scripts/web-toolchain-versions.env + test "$(wasm-pack --version)" = "wasm-pack ${WASM_PACK_VERSION#v}" + test "$(node -p 'process.versions.node.split(".")[0]')" = "$NODE_VERSION" wasm-pack --version - - # Both assemblies below generate the prose site with node. The runner - # image ships one, but pinning it is what makes a local run evidence. - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + node --version # The same script a developer runs locally, so there is one answer to # "how is the bundle built" rather than two that can drift. It builds all @@ -133,12 +130,11 @@ jobs: test: name: Test and lint (host) - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt + - name: Put the dev shell's toolchain on PATH for actions + run: dirname "$(command -v cargo)" >> "$GITHUB_PATH" - uses: Swatinem/rust-cache@v2 # NOT `cargo fmt --all`, which reaches only ONE of the two workspace # roots. Exit 2 here is a usage error, never a verdict (improvise-b4ks). @@ -187,12 +183,11 @@ jobs: wasm: name: wasm32 check (browser bundle crates) - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown + - name: Put the dev shell's toolchain on PATH for actions + run: dirname "$(command -v cargo)" >> "$GITHUB_PATH" - uses: Swatinem/rust-cache@v2 # Only the crates that go into the browser bundle. The root `improvise` # crate is the terminal binary and depends on crossterm, which does not @@ -229,15 +224,12 @@ jobs: # here is a decision `web/shell.js` or `web/serial.js` got wrong, and # reporting that as "Browser harness" would put it behind an 80 MB browser # download and two wasm-pack builds. It needs neither — `node --test` is in - # the runtime, and this job installs nothing at all. + # the runtime, which the dev shell supplies; this job installs nothing. js: name: Browser JS unit tests (node --test) - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v4 - with: - node-version: 22 # The glob is the SCRIPT's, not the shell's and not node's. Under bash an # unmatched glob survives as a literal, node globs it, matches nothing and # exits 0 having run no test -- so this step gated nothing the moment @@ -252,23 +244,23 @@ jobs: kb: name: Knowledge base consistency - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: cachix/install-nix-action@v31 # kb.pl is executable knowledge, so it gets checked like code: rules # must reject what they should, domains must close, and functional keys # must not carry conflicting values. See kb_report/0 in the file. + # trealla comes from the dev shell, as it does locally. - name: kb_consistent run: | - nix shell nixpkgs#trealla --command tpl prolog-sessions/kb.pl \ + tpl prolog-sessions/kb.pl \ -g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))" # scripts/kb is the only thing that writes kb.pl, so its refusals are as # load-bearing as kb_consistent itself. Without this the replacement # writer could regress green -- the review that found the directive # bypass also found that nothing ran this suite. - name: The kb tool refuses what it claims to - run: nix shell nixpkgs#trealla --command ./scripts/checkers.sh kb + run: ./scripts/checkers.sh kb # Every suite goes through scripts/checkers.sh, which pins a bash >= 5 # and scrubs the environment, so the shell and the variables a suite # passes under here are the ones it passes under locally (improvise-f9qf). @@ -301,13 +293,11 @@ jobs: # failed the *Browser harness* job. `-p opfs-contract` scopes it to 1 file. harness-lint: name: Lint the harness crate - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - components: clippy, rustfmt + - name: Put the dev shell's toolchain on PATH for actions + run: dirname "$(command -v cargo)" >> "$GITHUB_PATH" - uses: Swatinem/rust-cache@v2 with: workspaces: harness/opfs-contract -> target @@ -335,37 +325,25 @@ jobs: browser: name: Browser harness (OPFS contract + shell) needs: build - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v4 - with: - node-version: 22 - name: Install the harness working-directory: harness run: npm ci - # Keyed on the resolved @playwright/test version, so a version bump is a - # miss and anything else is a hit. The download was 50s on run - # 35535225637 and is pure I/O — nothing about it depends on this commit. - - name: Playwright version - id: pw + # No browser install step: the dev shell points PLAYWRIGHT_BROWSERS_PATH + # at nixpkgs' Chromium, Firefox and WebKit, since Playwright's own + # downloads cannot run on NixOS. All three, because improvise-xf1's + # acceptance criterion names Chrome, Firefox and Safari and + # `createSyncAccessHandle` is implemented separately by each engine. + # WebKit's OPFS is absent today; the spec skips on a runtime probe. + # @playwright/test must equal nixpkgs' playwright-driver version, or it + # looks for browser builds that are not there; this says which. + - name: Playwright matches the dev shell's browsers working-directory: harness - run: echo "v=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" - - uses: actions/cache@v4 - id: pw-cache - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ steps.pw.outputs.v }} - # All three, because improvise-xf1's acceptance criterion names Chrome, - # Firefox and Safari and `createSyncAccessHandle` is implemented - # separately by each engine. WebKit is installed even though its OPFS is - # absent today: the spec skips on a runtime probe, so the day that build - # gains OPFS the check starts running without a CI change. - # `--with-deps` installs apt packages OUTSIDE the cached directory, so it - # runs on a cache hit too; only the browser download is skipped. - - name: Install browsers - working-directory: harness - run: npx playwright install --with-deps chromium firefox webkit + run: | + npx playwright --version + ls "$PLAYWRIGHT_BROWSERS_PATH" # The tree `build` assembled, not one this job rebuilds. Same bytes as # the Pages artifact, modulo the cache-bust token `web/assemble.sh` # stamps — the harness tree is stamped `harness` and the site the commit @@ -434,7 +412,7 @@ jobs: name: Deploy to GitHub Pages needs: [test, wasm, js, browser, harness-lint, kb, build] if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest + runs-on: [self-hosted, nixos, microvm] permissions: contents: read pages: write diff --git a/flake.nix b/flake.nix index 0db23e81..5f323551 100644 --- a/flake.nix +++ b/flake.nix @@ -36,12 +36,47 @@ cargoNix = import ./Cargo.nix { inherit nixpkgs pkgs; }; + # Everything CI runs, and nothing else: `devShells.ci` is exactly this, + # and the default shell is this plus editor and release tooling, so a + # local `nix develop` runs the toolchain the checks run. + ciInputs = [ + rustToolchain + pkgs.pkg-config + + # `scripts/kb` and the CI knowledge-base check both run trealla. It + # was reached through `nix shell nixpkgs#trealla` per invocation, + # which cost more than the work: the tool's first real use took past + # two minutes for a write that takes half a second on PATH. + pkgs.trealla + + # `web/build.sh` — and so the Pages job's local dry run — needs this. + # The `wasm` CI job only runs `cargo check`, which does not, so the + # shell was missing the one tool that turns a passing check into a + # loadable page (improvise-d31). Must match WASM_PACK_VERSION in + # scripts/web-toolchain-versions.env; CI checks that it does. + pkgs.wasm-pack + + # NODE_VERSION's major, same check. The js and browser jobs ran 22 + # while `build` ran the pinned 24; one shell means one node. + pkgs.nodejs_24 + pkgs.python3 + pkgs.jq + ]; + + # Playwright's downloaded browsers are generic-Linux builds that cannot + # start on NixOS, so nixpkgs' build is used. harness/package.json pins + # @playwright/test to this playwright-driver's version: they must match. + playwrightEnv = { + PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = "true"; + RUST_BACKTRACE = "1"; + }; in { inherit cargoNix; - devShells.default = pkgs.mkShell { - nativeBuildInputs = [ - rustToolchain - pkgs.pkg-config + devShells.ci = pkgs.mkShell (playwrightEnv // {nativeBuildInputs = ciInputs;}); + devShells.default = pkgs.mkShell (playwrightEnv // { + nativeBuildInputs = ciInputs ++ [ pkgs.rust-analyzer crate2nix.packages.${system}.default pkgs.cargo-expand @@ -50,18 +85,6 @@ pkgs.universal-ctags beads.packages.${system}.default - # `scripts/kb` and the CI knowledge-base check both run trealla. It - # was reached through `nix shell nixpkgs#trealla` per invocation, - # which cost more than the work: the tool's first real use took past - # two minutes for a write that takes half a second on PATH. - pkgs.trealla - - # `web/build.sh` — and so the Pages job's local dry run — needs this. - # The `wasm` CI job only runs `cargo check`, which does not, so the - # shell was missing the one tool that turns a passing check into a - # loadable page (improvise-d31). - pkgs.wasm-pack - # Demo recording and release tooling pkgs.asciinema pkgs.vhs @@ -69,8 +92,7 @@ # nixpkgs cargo-dist installs as "dist"; alias so `cargo dist` works (pkgs.writeShellScriptBin "cargo-dist" ''exec ${pkgs.cargo-dist}/bin/dist "$@"'') ]; - RUST_BACKTRACE = "1"; - }; + }); packages = { improvise = cargoNix.workspaceMembers.improvise.build; diff --git a/harness/package-lock.json b/harness/package-lock.json index 2904809a..2862707d 100644 --- a/harness/package-lock.json +++ b/harness/package-lock.json @@ -6,70 +6,52 @@ "": { "name": "improvise-harness", "devDependencies": { - "@playwright/test": "1.56.1" + "@playwright/test": "1.63.0" } }, "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.1" + "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=20" } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=20" } }, "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } } } diff --git a/harness/package.json b/harness/package.json index 2923ee0f..bfc4fc72 100644 --- a/harness/package.json +++ b/harness/package.json @@ -8,6 +8,6 @@ "test": "playwright test" }, "devDependencies": { - "@playwright/test": "1.56.1" + "@playwright/test": "1.63.0" } } diff --git a/scripts/checkers-test.sh b/scripts/checkers-test.sh index 01995e07..ba7ab935 100755 --- a/scripts/checkers-test.sh +++ b/scripts/checkers-test.sh @@ -133,9 +133,15 @@ EOF chmod +x "$work/fakebin/bash" # The runner is invoked through a REAL bash, so the fake one below is only # what its own `command -v bash` probe finds -- not its interpreter. -# /usr/bin:/bin carries the utilities the runner itself needs and carries no -# nix, so the fake bash is the only candidate and the nix fallback is absent. -env PATH="$work/fakebin:/usr/bin:/bin" HOME="$HOME" \ +# utilbin carries only the utilities the runner itself needs, and no nix, so +# the fake bash is the only candidate and the nix fallback is absent. Linked +# one by one rather than taking /usr/bin:/bin, which on NixOS hold only `env` +# and `sh` -- the runner died on `dirname` there, exit 1 instead of 3. +mkdir -p "$work/utilbin" +for tool in dirname; do + ln -sf "$(command -v "$tool")" "$work/utilbin/$tool" +done +env PATH="$work/fakebin:$work/utilbin" HOME="$HOME" \ "${BASH:-bash}" "$d/checkers.sh" any > "$work/out" 2>&1 got=$? [ "$got" = 3 ] && ok "an old bash with no nix is exit 3" \