diff --git a/.github/workflows/testbox.yml b/.github/workflows/testbox.yml new file mode 100644 index 000000000..139e8cd7f --- /dev/null +++ b/.github/workflows/testbox.yml @@ -0,0 +1,105 @@ +# A Blacksmith testbox: a warm Linux x64 runner that runs builds and tests against the local tree +# (scripts/goport/testbox.sh). `blacksmith testbox warmup` dispatches this workflow, `testbox run` syncs +# the working tree to it and runs one command. The setup matches the Linux test job of ci.yml, with +# zbook's toolchains. Sticky disks keep target/ and the cargo registry warm between testboxes. +# A pull request that changes this file runs it in validation mode (no testbox_id). +name: Blacksmith Testbox + +on: + workflow_dispatch: + inputs: + testbox_id: + type: string + description: "Testbox session ID" + required: true + pull_request: + paths: + - ".github/workflows/testbox.yml" + +permissions: + contents: read + +concurrency: + group: testbox-${{ inputs.testbox_id || github.ref }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + # The toolchain of shipped bins and --profile goport. Edit-loop builds use the nightly of + # scripts/run-cargo-capped.sh. + RUSTUP_TOOLCHAIN: 1.93.0 + NIGHTLY_TOOLCHAIN: nightly-2026-06-17 + +jobs: + testbox: + runs-on: blacksmith-32vcpu-ubuntu-2404 + timeout-minutes: 360 + steps: + - name: Begin Testbox + uses: useblacksmith/begin-testbox@v2 + with: + testbox_id: ${{ inputs.testbox_id }} + - uses: actions/checkout@v5 + with: + persist-credentials: false + # After checkout: its clean step would empty a disk mounted in the workspace. + - uses: useblacksmith/stickydisk@v1 + with: + key: ts-rust-testbox-target + path: ${{ github.workspace }}/target + - uses: useblacksmith/stickydisk@v1 + with: + key: ts-rust-testbox-cargo + path: ~/.cargo/registry + # main's Go pin is the saved state's batch.upstreamPin.to (AGENTS.md, "Pin"). The Go checkout sits + # outside the workspace, so a sync of the local tree does not touch it. + - name: Resolve the Go pin + id: pin + run: | + key=$(jq -r '.batch.upstreamPin.to // empty' docs/typechecker-state/current.json) + python3 scripts/upstream/pin.py show ${key:+"$key"} > "$RUNNER_TEMP/pin.json" + jq -r '"repo=\(.repo)", "commit=\(.commit)", "subdir=\(.subdir // "")", "go=\(.oracle.go | ltrimstr("go"))"' "$RUNNER_TEMP/pin.json" >> "$GITHUB_OUTPUT" + - name: Check out the pinned Go source + uses: actions/checkout@v5 + with: + repository: ${{ steps.pin.outputs.repo }} + ref: ${{ steps.pin.outputs.commit }} + path: go-pin + sparse-checkout: ${{ steps.pin.outputs.subdir }} + persist-credentials: false + - name: Install the Go repo's typescript package + run: | + url=$(jq -r '.packages["node_modules/typescript"].resolved' go-pin/package-lock.json) + mkdir -p go-pin/node_modules/typescript + curl -sSfL "$url" | tar xz -C go-pin/node_modules/typescript --strip-components=1 + mv go-pin "$HOME/go-pin" + - uses: actions/setup-go@v6 + with: + go-version: ${{ steps.pin.outputs.go }} + cache: false + - run: | + rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal -c rustfmt,clippy + rustup toolchain install "$NIGHTLY_TOOLCHAIN" --profile minimal + # Remote commands run without this job's RUSTUP_TOOLCHAIN. 1.93.0 is the default, as on zbook; + # run-cargo-capped.sh picks the nightly for edit-loop builds. + rustup default "$RUSTUP_TOOLCHAIN" + # testbox.sh sources this file before each command: commands run over SSH, without this job's env. + # TS_CARGO_CGROUP_ACTIVE=1 skips the systemd user scope of run-cargo-capped.sh (no user session here). + - name: Write the testbox env + env: + SUBDIR: ${{ steps.pin.outputs.subdir }} + run: | + cat > "$HOME/testbox.env" < sync this worktree, then run the command with the testbox env +# (e.g. run scripts/run-cargo-capped.sh build --release -p ts_goport --bins). +# Arguments keep their boundaries; use run bash -c '...' for pipes and && +# testbox.sh get [local] copy a file or dir back (remote path relative to the repo root) +# testbox.sh status | stop | list the saved testbox's status, stop it, list active testboxes +# TESTBOX_ID overrides the saved ID. TESTBOX_IDLE: idle minutes before the testbox stops (default 15). +# TESTBOX_REF: the ref the workflow runs from (default main: the workflow must be on the default branch). +# Auth: `blacksmith auth login` once per host (`blacksmith auth status` shows it). +set -euo pipefail +case "${1:-help}" in help | -h | --help) sed -n '2,/^set -euo/p' "$0" | sed '$d'; exit 0 ;; esac + +repo_root="$(git -C "$(dirname -- "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)" +id_file="$repo_root/target/testbox-id" +cd "$repo_root" + +saved_id() { + local id="${TESTBOX_ID:-$(cat "$id_file" 2>/dev/null || true)}" + [[ -n "$id" ]] || { echo "no testbox: run 'testbox.sh warmup' or set TESTBOX_ID" >&2; exit 2; } + echo "$id" +} + +cmd="$1" +shift +case "$cmd" in + warmup) + rc=0 + out="$(blacksmith testbox warmup testbox.yml --ref "${TESTBOX_REF:-main}" --idle-timeout "${TESTBOX_IDLE:-15}" 2>&1)" || rc=$? + echo "$out" + ((rc == 0)) || exit "$rc" + id="$(grep -oE 'tbx_[A-Za-z0-9_-]+' <<<"$out" | head -1)" + [[ -n "$id" ]] || { echo "warmup printed no testbox ID" >&2; exit 1; } + mkdir -p "$(dirname "$id_file")" + echo "$id" >"$id_file" + echo "testbox $id (saved to target/testbox-id)" + ;; + run) + (($#)) || { echo "usage: testbox.sh run " >&2; exit 2; } + printf -v remote_cmd '%q ' "$@" + blacksmith testbox run --id "$(saved_id)" ". ~/testbox.env && $remote_cmd" + ;; + get) blacksmith testbox download --id "$(saved_id)" "$@" ;; + status) blacksmith testbox status --id "$(saved_id)" "$@" ;; + stop) + id="$(saved_id)" + blacksmith testbox stop --id "$id" + # Keep the saved ID when TESTBOX_ID stopped another testbox. + [[ "$(cat "$id_file" 2>/dev/null || true)" != "$id" ]] || rm -f "$id_file" + ;; + list) blacksmith testbox list "$@" ;; + *) echo "unknown command: $cmd (testbox.sh help)" >&2; exit 2 ;; +esac