From b717d7469ec2f510452aba111a2e5915ef6d0656 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Thu, 6 Aug 2026 23:50:08 -0400 Subject: [PATCH] Add actions/setup: consumer tool bootstrap at the Cargo.lock rev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every consumer hand-rolled the same bootstrap (derive the pin from Cargo.lock, cargo install, pins gate), and the four copies diverged. The composite action owns the CI half — rev-keyed cache included — and actions/README.md now carries the one canonical local recipe so the justfile copies can converge textually. The ci.yml smoke job dogfoods the action from source against a synthetic consumer lock pinned to the commit under test, asserting the outputs and bin names. Fixes #56. --- .github/workflows/ci.yml | 46 ++++++++++++++ actions/README.md | 53 ++++++++++++++++ actions/setup/action.yml | 132 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 actions/setup/action.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc8824..a9a52bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,3 +103,49 @@ jobs: fail_on_failure: false require_tests: true annotate_only: true + + # Dogfoods actions/setup (the #56 consumer bootstrap): a synthetic + # consumer Cargo.lock pins this very commit, the action installs the + # tools from the checkout (source-path), and its pins gate must hold + # the fixture to that rev. Asserts the advertised outputs and bin + # names on top. + actions-setup-smoke: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + + - name: Install Rust toolchain + run: rustup show active-toolchain || rustup toolchain install + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: actions-setup-smoke + cache-all-crates: true + + - name: Synthesize a consumer Cargo.lock pinned to this commit + run: | + mkdir -p "$RUNNER_TEMP/consumer" + cat > "$RUNNER_TEMP/consumer/Cargo.lock" <`. +## `setup` + +Installs the component-test tools at the revision the consumer's +Cargo.lock pins (crate-name-anchored on `component-test-sdk`, so a +renamed repository still resolves), caches the install keyed on +`(os, rev, rust-toolchain.toml)`, and runs the `component-test pins` +gate over every declared lockfile — the one-rev-everywhere check. The +Cargo.lock is the single source of truth; the action cannot be pointed +at a different rev than the workspace builds against. + +```yaml +- uses: polymorph-components/polymorph-test/actions/setup@ + id: ct + with: + cargo-lock: Cargo.lock + js-locks: | + conformance/driver-ct/jco/package-lock.json + # tools: component-test-cli component-test-runner (default) + # install-root: target/ct-tools (default) +# steps.ct.outputs.rev — the pinned revision +# steps.ct.outputs.bin — directory holding component-test / ct-runner +``` + +`source-path: .` installs from a checkout instead of `--git` (used by +this repository's own CI smoke, and the local-override analogue). + +### The local half: one canonical `_ct-tools` recipe + +CI is only half the bootstrap; local `just` runs need the same tools. +Consumers should carry exactly this recipe (module-path-adjusted), so +the copies diff empty against each other: + +```just +# Install the component-test CLI and host runner at the revision the +# workspace pins (read from Cargo.lock, so a bump cannot half-apply), +# then hold every other appearance of the pin to it. +_ct-tools: + #!/usr/bin/env bash + set -euo pipefail + cd {{root}} + src=$(grep -m1 -A2 '^name = "component-test-sdk"' Cargo.lock | grep '^source = "git+') + url=$(printf '%s' "$src" | sed -E 's/^source = "git\+([^?]+)\?.*/\1/') + rev=$(printf '%s' "$src" | grep -oE '[0-9a-f]{40}' | head -n1) + if [ ! -x target/ct-tools/bin/component-test ] || [ "$(cat target/ct-tools/.rev 2>/dev/null)" != "$rev" ]; then + cargo install --locked --root target/ct-tools --git "$url" --rev "$rev" component-test-cli component-test-runner + printf '%s' "$rev" > target/ct-tools/.rev + fi + target/ct-tools/bin/component-test pins --cargo-lock Cargo.lock --expect "$rev" > /dev/null +``` + +(Append `--js-lock ` for each JS lockfile the repo carries; +drop `component-test-runner` if the runner is embedded as a library.) + ## `aggregate` Validates per-target results-JSONL against a lockfile + target diff --git a/actions/setup/action.yml b/actions/setup/action.yml new file mode 100644 index 0000000..5f0a884 --- /dev/null +++ b/actions/setup/action.yml @@ -0,0 +1,132 @@ +name: component-test setup +description: > + Install the component-test tools (`component-test`, `ct-runner`) at + the revision the consumer's Cargo.lock pins, with a rev-keyed cache, + and hold every declared pin to that revision with `component-test + pins`. The Cargo.lock is the single source of truth: the action + derives the revision from the `component-test-sdk` entry + (crate-name-anchored, so a renamed repository still resolves), so a + pin bump cannot half-apply. + +inputs: + cargo-lock: + description: Path to the consumer's Cargo.lock. + default: Cargo.lock + js-locks: + description: > + JS lockfiles to include in the pins gate (package-lock.json / + pnpm-lock.yaml), one path per line. Optional. + default: "" + tools: + description: > + Space-separated crates to install from this repository. + default: component-test-cli component-test-runner + install-root: + description: > + `cargo install --root`; binaries land under `/bin`. + default: target/ct-tools + source-path: + description: > + When set, install from this checkout path (`cargo install + --path /crates/`) instead of `--git` at the + pinned revision, and skip the cache. For testing the action and + for local-override parity with the consumers' justfile recipes. + The pins gate still runs against the Cargo.lock revision. + default: "" + +outputs: + rev: + description: The revision derived from the Cargo.lock. + value: ${{ steps.pin.outputs.rev }} + bin: + description: Directory holding the installed binaries. + value: ${{ steps.pin.outputs.bin }} + +runs: + using: composite + steps: + - name: Derive the pinned revision from the Cargo.lock + id: pin + shell: bash + env: + INPUT_CARGO_LOCK: ${{ inputs.cargo-lock }} + INPUT_INSTALL_ROOT: ${{ inputs.install-root }} + run: | + set -euo pipefail + src=$(grep -m1 -A2 '^name = "component-test-sdk"' "$INPUT_CARGO_LOCK" | grep '^source = "git+' || true) + if [ -z "$src" ]; then + echo "::error title=component-test setup::no component-test-sdk git pin in $INPUT_CARGO_LOCK" >&2 + exit 1 + fi + url=$(printf '%s' "$src" | sed -E 's/^source = "git\+([^?]+)\?.*/\1/') + rev=$(printf '%s' "$src" | grep -oE '[0-9a-f]{40}' | head -n1) + { + echo "rev=$rev" + echo "url=$url" + echo "bin=$INPUT_INSTALL_ROOT/bin" + } >> "$GITHUB_OUTPUT" + + - name: Cache the installed tools + if: inputs.source-path == '' + uses: actions/cache@v4 + with: + path: ${{ inputs.install-root }} + key: component-test-tools-${{ runner.os }}-${{ steps.pin.outputs.rev }}-${{ hashFiles('rust-toolchain.toml') }} + + - name: Install the tools + shell: bash + env: + INPUT_TOOLS: ${{ inputs.tools }} + INPUT_INSTALL_ROOT: ${{ inputs.install-root }} + INPUT_SOURCE_PATH: ${{ inputs.source-path }} + PIN_URL: ${{ steps.pin.outputs.url }} + PIN_REV: ${{ steps.pin.outputs.rev }} + run: | + set -euo pipefail + # Bin name per crate (cargo install exposes the bin, not the + # crate name). + bin_of() { + case "$1" in + component-test-cli) echo component-test ;; + component-test-runner) echo ct-runner ;; + *) echo "$1" ;; + esac + } + missing=() + for tool in $INPUT_TOOLS; do + [ -x "$INPUT_INSTALL_ROOT/bin/$(bin_of "$tool")" ] || missing+=("$tool") + done + if [ "${#missing[@]}" -eq 0 ]; then + echo "tools already present in $INPUT_INSTALL_ROOT/bin (cache hit)" + exit 0 + fi + if [ -n "$INPUT_SOURCE_PATH" ]; then + for tool in "${missing[@]}"; do + cargo install -q --locked --root "$INPUT_INSTALL_ROOT" \ + --path "$INPUT_SOURCE_PATH/crates/$tool" + done + else + cargo install -q --locked --root "$INPUT_INSTALL_ROOT" \ + --git "$PIN_URL" --rev "$PIN_REV" "${missing[@]}" + fi + + - name: Hold every declared pin to the Cargo.lock revision + shell: bash + env: + INPUT_CARGO_LOCK: ${{ inputs.cargo-lock }} + INPUT_JS_LOCKS: ${{ inputs.js-locks }} + INPUT_INSTALL_ROOT: ${{ inputs.install-root }} + PIN_REV: ${{ steps.pin.outputs.rev }} + run: | + set -euo pipefail + cli="$INPUT_INSTALL_ROOT/bin/component-test" + if [ ! -x "$cli" ]; then + echo "::error title=component-test setup::the pins gate needs the component-test CLI; keep component-test-cli in the tools input" >&2 + exit 1 + fi + args=(pins --cargo-lock "$INPUT_CARGO_LOCK" --expect "$PIN_REV") + while IFS= read -r line; do + line="${line#"${line%%[![:space:]]*}"}" + [ -n "$line" ] && args+=(--js-lock "$line") + done <<< "$INPUT_JS_LOCKS" + "$cli" "${args[@]}" > /dev/null