diff --git a/.github/actions/setup-wright/action.yml b/.github/actions/setup-wright/action.yml new file mode 100644 index 00000000..8197fdd7 --- /dev/null +++ b/.github/actions/setup-wright/action.yml @@ -0,0 +1,81 @@ +name: Set up Wright +description: Install the Wright CLI from the verified release distribution and cache only the executable + +inputs: + version: + description: Exact Wright version (for example 0.28.0) or `latest` + required: false + default: latest + cache: + description: Cache the verified Wright executable with the Actions cache service + required: false + default: 'true' + +outputs: + version: + description: Concrete Wright version that was installed + value: ${{ steps.resolve.outputs.version }} + cache-hit: + description: '`true` when a valid cached executable avoided the release download' + value: ${{ steps.verify.outputs.valid }} + +runs: + using: composite + steps: + - name: Resolve Wright release + id: resolve + shell: bash + env: + WRIGHT_VERSION: ${{ inputs.version }} + run: | + "$GITHUB_ACTION_PATH/setup-wright.sh" resolve + + - name: Restore Wright executable + id: restore + if: inputs.cache == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.resolve.outputs.cache-dir }} + key: ${{ steps.resolve.outputs.cache-key }} + + - name: Verify cached executable + id: verify + shell: bash + env: + WRIGHT_SETUP_DIR: ${{ steps.resolve.outputs.cache-dir }} + WRIGHT_SETUP_VERSION: ${{ steps.resolve.outputs.version }} + WRIGHT_SETUP_TARGET: ${{ steps.resolve.outputs.target }} + CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} + run: | + if [[ "$CACHE_HIT" == true ]]; then + "$GITHUB_ACTION_PATH/setup-wright.sh" verify-cache + else + echo "valid=false" >> "$GITHUB_OUTPUT" + fi + + - name: Download and verify Wright release + if: steps.verify.outputs.valid != 'true' + shell: bash + env: + WRIGHT_SETUP_DIR: ${{ steps.resolve.outputs.cache-dir }} + WRIGHT_SETUP_VERSION: ${{ steps.resolve.outputs.version }} + WRIGHT_SETUP_TARGET: ${{ steps.resolve.outputs.target }} + WRIGHT_SETUP_EXT: ${{ steps.resolve.outputs.archive-ext }} + run: | + "$GITHUB_ACTION_PATH/setup-wright.sh" install + + # Only a freshly verified executable on a true miss is saved. An invalid + # entry under the same immutable key is not overwritten; see docs/setup-wright-action.md. + - name: Save Wright executable + if: inputs.cache == 'true' && steps.restore.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.resolve.outputs.cache-dir }} + key: ${{ steps.resolve.outputs.cache-key }} + + - name: Add Wright to PATH + shell: bash + env: + WRIGHT_SETUP_DIR: ${{ steps.resolve.outputs.cache-dir }} + run: | + "$GITHUB_ACTION_PATH/setup-wright.sh" activate diff --git a/.github/actions/setup-wright/setup-wright.sh b/.github/actions/setup-wright/setup-wright.sh new file mode 100755 index 00000000..06e956cd --- /dev/null +++ b/.github/actions/setup-wright/setup-wright.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Helper for the setup-wright composite Action (#717). +# +# Subcommands: resolve | verify-cache | install | activate +# Environment: +# WRIGHT_VERSION exact version or "latest" (resolve) +# WRIGHT_INSTALL_BASE_URL release distribution base URL (test hook; defaults +# to the R2 contract in docs/release.md) +# WRIGHT_SETUP_OS / WRIGHT_SETUP_ARCH override platform detection (tests) +# WRIGHT_SETUP_DIR cache directory holding the verified executable +# WRIGHT_SETUP_VERSION concrete version (verify-cache, install) +# WRIGHT_SETUP_TARGET target triple (install) +# GITHUB_OUTPUT / GITHUB_PATH standard Actions files + +set -euo pipefail + +BASE_URL="${WRIGHT_INSTALL_BASE_URL:-https://install.wrightkit.dev/wright}" +# Bump when the cached layout (executable plus .sha256 sidecar) changes. +CACHE_LAYOUT="v1" +SEMVER='^[0-9]+\.[0-9]+\.[0-9]+$' + +fail() { + echo "::error::setup-wright: $*" >&2 + exit 1 +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{ print $1 }' + else + shasum -a 256 "$1" | awk '{ print $1 }' + fi +} + +output() { + echo "$1=$2" >> "${GITHUB_OUTPUT:?GITHUB_OUTPUT is not set}" +} + +exe_name() { + case "$WRIGHT_SETUP_TARGET" in *windows*) echo wright.exe ;; *) echo wright ;; esac +} + +cmd_resolve() { + local os="${WRIGHT_SETUP_OS:-${RUNNER_OS:-}}" arch="${WRIGHT_SETUP_ARCH:-${RUNNER_ARCH:-}}" target ext + case "$os/$arch" in + Linux/X64) target=x86_64-unknown-linux-gnu; ext=tar.gz ;; + macOS/X64) target=x86_64-apple-darwin; ext=tar.gz ;; + macOS/ARM64) target=aarch64-apple-darwin; ext=tar.gz ;; + Windows/X64) target=x86_64-pc-windows-msvc; ext=zip ;; + *) fail "unsupported platform $os/$arch (supported: Linux X64, macOS X64, macOS ARM64, Windows X64)" ;; + esac + + local version="${WRIGHT_VERSION:-latest}" + version="${version#v}" + if [[ "$version" == latest ]]; then + version="$(curl -fsSL "$BASE_URL/latest/version" | tr -d '[:space:]')" \ + || fail "could not read $BASE_URL/latest/version; pin an exact version" + version="${version#v}" + fi + [[ "$version" =~ $SEMVER ]] || fail "invalid Wright version '$version' (expected MAJOR.MINOR.PATCH or 'latest')" + + output version "$version" + output target "$target" + output archive-ext "$ext" + output cache-key "setup-wright-$CACHE_LAYOUT-$version-$target" + output cache-dir "${RUNNER_TEMP:?RUNNER_TEMP is not set}/setup-wright/$CACHE_LAYOUT/$version/$target" +} + +# A restored directory is trusted only if its executable matches the sidecar +# SHA-256 written at install time and reports the requested version. +cmd_verify_cache() { + local dir="${WRIGHT_SETUP_DIR:?}" version="${WRIGHT_SETUP_VERSION:?}" exe + exe="$(WRIGHT_SETUP_TARGET="${WRIGHT_SETUP_TARGET:?}" exe_name)" + if [[ -f "$dir/$exe" && -f "$dir/$exe.sha256" ]] \ + && [[ "$(sha256_of "$dir/$exe")" == "$(awk 'NR == 1 { print $1 }' "$dir/$exe.sha256")" ]] \ + && "$dir/$exe" --version 2>/dev/null | grep -Fq "$version"; then + output valid true + return + fi + if [[ -e "$dir" ]]; then + echo "::warning::setup-wright: ignoring invalid cached Wright $version; downloading the release instead" + rm -rf "$dir" + fi + output valid false +} + +cmd_install() { + local work + local dir="${WRIGHT_SETUP_DIR:?}" version="${WRIGHT_SETUP_VERSION:?}" target="${WRIGHT_SETUP_TARGET:?}" ext="${WRIGHT_SETUP_EXT:?}" + local name="wright-$version-$target" archive exe expected actual + archive="$name.$ext" + exe="$(exe_name)" + work="$(mktemp -d)" + WORK_DIR="$work" + trap 'rm -rf "$WORK_DIR"' EXIT + + curl -fsSL -o "$work/$archive" "$BASE_URL/releases/$version/$archive" \ + || fail "failed to download $BASE_URL/releases/$version/$archive (does release $version exist?)" + curl -fsSL -o "$work/$archive.sha256" "$BASE_URL/releases/$version/$archive.sha256" \ + || fail "failed to download the checksum for $archive" + + expected="$(awk 'NR == 1 { print $1 }' "$work/$archive.sha256")" + [[ "$expected" =~ ^[0-9a-fA-F]{64}$ ]] || fail "invalid checksum file for $archive" + actual="$(sha256_of "$work/$archive")" + [[ "$(printf %s "$actual" | tr A-F a-f)" == "$(printf %s "$expected" | tr A-F a-f)" ]] \ + || fail "checksum mismatch for $archive (published $expected, got $actual); nothing was installed" + + if [[ "$ext" == zip ]]; then + # Git Bash's GNU tar cannot read zip; Windows ships bsdtar, which needs native paths. + "${SYSTEMROOT:-C:/Windows}/System32/tar.exe" -xf "$(cygpath -w "$work/$archive")" -C "$(cygpath -w "$work")" \ + || fail "failed to extract $archive" + else + tar -xf "$work/$archive" -C "$work" || fail "failed to extract $archive" + fi + [[ -f "$work/$name/$exe" ]] || fail "unexpected archive layout: $name/$exe missing from $archive" + + rm -rf "$dir" + mkdir -p "$dir" + cp "$work/$name/$exe" "$dir/$exe" + chmod +x "$dir/$exe" + sha256_of "$dir/$exe" > "$dir/$exe.sha256" + "$dir/$exe" --version | grep -Fq "$version" \ + || { rm -rf "$dir"; fail "installed executable does not report version $version"; } +} + +cmd_activate() { + local dir="${WRIGHT_SETUP_DIR:?}" + if command -v cygpath >/dev/null 2>&1; then dir="$(cygpath -w "$dir")"; fi + echo "$dir" >> "${GITHUB_PATH:?GITHUB_PATH is not set}" +} + +case "${1:-}" in + resolve) cmd_resolve ;; + verify-cache) cmd_verify_cache ;; + install) cmd_install ;; + activate) cmd_activate ;; + *) fail "usage: setup-wright.sh resolve|verify-cache|install|activate" ;; +esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6c667a9..0af632eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: cli: ${{ steps.filter.outputs.rust_core }} dist: ${{ steps.filter.outputs.dist }} agent_guide: ${{ steps.filter.outputs.agent_guide }} + setup_action: ${{ steps.filter.outputs.setup_action }} ui: ${{ steps.filter.outputs.ui }} steps: - name: Checkout @@ -49,6 +50,10 @@ jobs: - 'agent-guide/**' - 'schemas/**' - '.github/workflows/**' + setup_action: + - '.github/actions/setup-wright/**' + - 'scripts/test-setup-wright.sh' + - '.github/workflows/**' agent_guide: - 'agent-guide/**' - 'crates/wright-cli/src/agent.rs' @@ -75,6 +80,57 @@ jobs: - 'schemas/**' - 'Cargo.lock' - '.github/workflows/**' + setup-wright-action: + name: setup-wright Action (${{ matrix.os }}) + needs: paths + if: needs.paths.outputs.setup_action == 'true' + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Test the helper script + if: runner.os != 'Windows' + run: scripts/test-setup-wright.sh + + # Consumes the latest published release, so it needs no Wright build. + - name: Install Wright through the Action + id: first + uses: ./.github/actions/setup-wright + + - name: Move the installed copy aside so the cache must restore it + shell: bash + run: mv "$RUNNER_TEMP/setup-wright" "$RUNNER_TEMP/setup-wright-first" + + - name: Install Wright again from the cache + id: second + uses: ./.github/actions/setup-wright + with: + version: ${{ steps.first.outputs.version }} + + # A fork PR has a read-only cache token, so it cannot seed the cache. + - name: Check the second install used the cache + if: github.event.pull_request.head.repo.fork != true + shell: bash + env: + CACHE_HIT: ${{ steps.second.outputs.cache-hit }} + VERSION: ${{ steps.first.outputs.version }} + run: | + [[ "$CACHE_HIT" == true ]] || { echo "::error::expected a cache hit"; exit 1; } + wright --version | grep -Fq "$VERSION" + + - name: Run Wright as an ordinary step + shell: bash + run: | + printf 'rule ("setup smoke") {\n event {\n Ongoing - Global;\n }\n actions {\n Disable Inspector Recording;\n }\n}\n' > "$RUNNER_TEMP/smoke.ws" + wright check "$RUNNER_TEMP/smoke.ws" + grep -Fq 'PASS' "$GITHUB_STEP_SUMMARY" + agent-guide: name: Agent guide sync needs: paths @@ -680,6 +736,7 @@ jobs: - dist-cli-build - dist-validation - agent-guide + - setup-wright-action - ui runs-on: ubuntu-latest timeout-minutes: 5 @@ -706,6 +763,7 @@ jobs: "dist-cli-build": ($p.dist == "true"), "dist-validation": ($p.dist == "true"), "agent-guide": ($p.agent_guide == "true"), + "setup-wright-action": ($p.setup_action == "true"), "ui": ($p.ui == "true") }') for job in $(jq -r 'keys[]' <<<"$required"); do diff --git a/README.md b/README.md index b32739a5..1c1c85d6 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ On Windows, use the PowerShell installer at `https://install.wrightkit.dev/wright/install.ps1`. Archives, the nightly channel, and building from source are covered in [`docs/release.md`](docs/release.md). +In GitHub Actions, use the cached +[`setup-wright` Action](docs/setup-wright-action.md). + Update with `wright update`. ## Usage diff --git a/docs/README.md b/docs/README.md index e6be7da2..93030ef0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -58,6 +58,8 @@ Issue contract. - [Lessons](LESSONS.md): short queue of reusable failure classes from merged work, pruned once promoted into tests or standards. - [Release/distribution](release.md): packaging and publication contract. +- [`setup-wright` Action](setup-wright-action.md): installing and caching the + Wright CLI in GitHub Actions. - [Agent-team governance](agent-team.md): role/authority coordination where still applicable. diff --git a/docs/setup-wright-action.md b/docs/setup-wright-action.md new file mode 100644 index 00000000..ae7a1bce --- /dev/null +++ b/docs/setup-wright-action.md @@ -0,0 +1,109 @@ +# `setup-wright` GitHub Action + +Status: current contract (#717) +Scope: installing and caching the Wright CLI in GitHub Actions workflows + +The Action lives in this repository at `.github/actions/setup-wright`. It only +sets up Wright. Command selection, arguments, and gating stay in ordinary +`run: wright ...` steps, so annotations, step summaries, and exit codes are +Wright's own (see [`cli/presentation.md`](cli/presentation.md)). + +## Usage + +Reference the Action at a Wright release ref, then run Wright as often as +needed: + +```yaml +jobs: + wright: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + - uses: wrightkit/wright/.github/actions/setup-wright@v0.28.0 + with: + version: 0.28.0 + - run: wright check + - run: wright lint + - run: wright analyze +``` + +| Input | Default | Meaning | +| --- | --- | --- | +| `version` | `latest` | Exact Wright version such as `0.28.0` (a leading `v` is accepted), or `latest`. | +| `cache` | `true` | Cache the verified executable. `false` downloads on every run. | + +| Output | Meaning | +| --- | --- | +| `version` | Concrete Wright version installed. | +| `cache-hit` | `true` when a valid cached executable avoided the release download. | + +The Action has no independent version. Use a Wright release tag (or a commit +SHA) as the `@ref`. The ref selects the Action's code; `version` selects the +Wright binary, and the two are independent. + +Supported runners: Linux x86_64, macOS x86_64, macOS arm64, and Windows +x86_64, the targets with published release archives +([`release.md`](release.md#target-matrix-and-artifact-naming)). Any other +platform fails with an `unsupported platform` error. + +## Version pinning + +* An exact `version` is deterministic and, on a cache hit, makes no network + request to the release distribution. +* `latest` first reads the small `latest/version` pointer from the release + distribution (`no-store`, a few bytes), then uses the concrete version for + every later step. A cache hit for that version never re-downloads the archive; + a new pointer value selects a new cache entry and installs the new binary. +* Pin an exact version in workflows that must be reproducible. + +## Cache identity + +Only the `wright` executable and a SHA-256 sidecar are cached, with the Actions +cache service of the consuming repository. The key is exact: + +```text +setup-wright-v1-- +``` + +* There is no restore prefix, so a different version, OS, or architecture can + never restore into the wrong executable. +* `v1` is the cache layout; changing the layout bumps it and abandons old + entries. +* Actions caches are scoped to the consuming repository and ref: a pull request + writes to its own scope, not to the default branch, and a fork pull request + with a read-only token only restores. The Action needs no secrets and no + permissions beyond `contents: read`. + +On a miss the Action downloads the archive and its `.sha256` from the immutable +release route, verifies the checksum before extraction, and only then saves the +executable. + +After every restore the Action recomputes the executable's SHA-256 against the +sidecar and requires `wright --version` to report the requested version. A +failing entry is deleted, a warning is printed, and the release is downloaded +and verified again. An invalid entry keeps its key until the cache service +evicts it, because Actions cache keys are immutable, so each run re-downloads +until then. The sidecar detects corruption and a wrong version. It does not +authenticate the cache against a writer who can alter both files; that trust +boundary is the consuming repository. + +## What is not cached + +Provider downloads, project sources, analysis or lint results, and build output +are never cached. Providers remain managed by Wright itself. Set `cache: false` +to skip the cache service entirely. + +## Verification + +* `scripts/test-setup-wright.sh` runs the helper against a mock release + distribution: cache hit and miss (including zero requests on a hit), version + and `latest` resolution, OS/architecture/version key separation, corrupt and + wrong-version caches, checksum mismatch, a missing release, and unsupported + platforms. +* The `setup-wright Action` job in `ci.yml` uses the Action from the checked-out + ref on Linux, macOS, and Windows against the latest published release, moves + the installed copy aside, installs again and requires `cache-hit == true` (skipped on fork pull requests, which cannot write the cache), + then runs `wright check` as an ordinary step and requires the step summary to + report PASS. diff --git a/scripts/test-setup-wright.sh b/scripts/test-setup-wright.sh new file mode 100755 index 00000000..e6e6bf51 --- /dev/null +++ b/scripts/test-setup-wright.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# Usage: scripts/test-setup-wright.sh +# +# Functional tests for .github/actions/setup-wright/setup-wright.sh (#717) +# against a mock release distribution that logs every request. A "cache" is a +# directory that survives between helper invocations, so cache hit/miss is +# observable as the presence or absence of distribution requests. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HELPER="$ROOT/.github/actions/setup-wright/setup-wright.sh" +WORK="$(mktemp -d)" +LOG="$WORK/requests.log" +PASS=0 +FAIL=0 + +cleanup() { + [[ -n "${SERVER_PID:-}" ]] && kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$WORK" +} +trap cleanup EXIT + +# Serve $2 on loopback port $1, appending each request path to $3. A plain +# threading TCPServer avoids the reverse-DNS stall of `python3 -m http.server`. +serve() { + python3 - "$1" "$2" "$3" >/dev/null 2>&1 <<'PY' & +import functools, http.server, socketserver, sys + +port, root, log = int(sys.argv[1]), sys.argv[2], sys.argv[3] + +class Handler(http.server.SimpleHTTPRequestHandler): + def log_message(self, fmt, *args): + with open(log, "a") as f: + f.write(self.path + "\n") + +socketserver.ThreadingTCPServer( + ("127.0.0.1", port), functools.partial(Handler, directory=root) +).serve_forever() +PY +} + +make_release() { + local version="$1" triple="$2" release="$WORK/mock/wright/releases/$1" + local dir="$release/wright-$version-$triple" + mkdir -p "$dir" + printf '#!/bin/sh\necho "wright %s"\n' "$version" > "$dir/wright" + chmod +x "$dir/wright" + tar -czf "$release/wright-$version-$triple.tar.gz" -C "$release" "wright-$version-$triple" + (cd "$release" && shasum -a 256 "wright-$version-$triple.tar.gz" > "wright-$version-$triple.tar.gz.sha256") + rm -rf "$dir" +} + +report() { + if [[ "$2" == ok ]]; then PASS=$((PASS + 1)); echo "PASS: $1"; else FAIL=$((FAIL + 1)); echo "FAIL: $1"; fi +} + +check() { # name command... + local name="$1"; shift + if "$@" >/dev/null 2>&1; then report "$name" ok; else report "$name" fail; fi +} + +port="$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')" +for triple in x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin; do + make_release 1.2.3 "$triple" + make_release 1.3.0 "$triple" +done +mkdir -p "$WORK/mock/wright/latest" +printf '1.2.3\n' > "$WORK/mock/wright/latest/version" +: > "$LOG" +serve "$port" "$WORK/mock" "$LOG" +SERVER_PID=$! +for _ in $(seq 1 50); do curl -fsS "http://127.0.0.1:$port/" >/dev/null 2>&1 && break; sleep 0.1; done + +: > "$LOG" +export WRIGHT_INSTALL_BASE_URL="http://127.0.0.1:$port/wright" +export RUNNER_TEMP="$WORK/runner-temp" +mkdir -p "$RUNNER_TEMP" + +# resolve OS ARCH VERSION -> sets OUT file; succeeds or fails like the helper. +resolve() { + OUT="$WORK/out.$RANDOM" + : > "$OUT" + GITHUB_OUTPUT="$OUT" WRIGHT_SETUP_OS="$1" WRIGHT_SETUP_ARCH="$2" WRIGHT_VERSION="$3" "$HELPER" resolve +} +out() { grep "^$1=" "$OUT" | cut -d= -f2-; } +requests() { wc -l < "$LOG" | tr -d ' '; } + +# Version resolution +resolve Linux X64 1.2.3 >/dev/null +[[ "$(out version)" == 1.2.3 && "$(out target)" == x86_64-unknown-linux-gnu \ + && "$(out cache-key)" == setup-wright-v1-1.2.3-x86_64-unknown-linux-gnu ]] \ + && report "exact version resolves without a distribution request" ok || report "exact version resolves without a distribution request" fail +[[ "$(requests)" == 0 ]] && report "exact version makes no request" ok || report "exact version makes no request" fail +resolve Linux X64 v1.2.3 >/dev/null +[[ "$(out version)" == 1.2.3 ]] && report "leading v is stripped" ok || report "leading v is stripped" fail +resolve Linux X64 latest >/dev/null +[[ "$(out version)" == 1.2.3 && "$(requests)" == 1 ]] && grep -Fxq /wright/latest/version "$LOG" \ + && report "latest reads only the version pointer" ok || report "latest reads only the version pointer" fail +printf '1.3.0\n' > "$WORK/mock/wright/latest/version" +resolve Linux X64 latest >/dev/null +[[ "$(out version)" == 1.3.0 && "$(out cache-key)" != setup-wright-v1-1.2.3-* ]] \ + && report "new latest version changes the cache key" ok || report "new latest version changes the cache key" fail +printf '1.2.3\n' > "$WORK/mock/wright/latest/version" +check "invalid version is rejected" bash -c '! WRIGHT_VERSION="1.2" GITHUB_OUTPUT=/dev/null WRIGHT_SETUP_OS=Linux WRIGHT_SETUP_ARCH=X64 "$0" resolve' "$HELPER" +check "version with a path is rejected" bash -c '! WRIGHT_VERSION="../x" GITHUB_OUTPUT=/dev/null WRIGHT_SETUP_OS=Linux WRIGHT_SETUP_ARCH=X64 "$0" resolve' "$HELPER" + +# Platform identity and unsupported targets +keys="" +for platform in "Linux X64" "macOS X64" "macOS ARM64" "Windows X64"; do + resolve $platform 1.2.3 >/dev/null + keys="$keys$(out cache-key)"$'\n' +done +[[ "$(printf %s "$keys" | sort -u | wc -l | tr -d ' ')" == 4 ]] \ + && report "each OS/architecture has a distinct cache key" ok || report "each OS/architecture has a distinct cache key" fail +for platform in "Linux ARM64" "Windows ARM64" "macOS X86" "FreeBSD X64"; do + # shellcheck disable=SC2086 + if resolve $platform 1.2.3 >"$WORK/err" 2>&1; then + report "unsupported platform $platform fails" fail + else + grep -Fq "unsupported platform" "$WORK/err" && report "unsupported platform $platform fails" ok || report "unsupported platform $platform fails" fail + fi +done + +# Miss: download, verify, store +resolve Linux X64 1.2.3 >/dev/null +DIR="$(out cache-dir)" +export WRIGHT_SETUP_DIR="$DIR" WRIGHT_SETUP_VERSION=1.2.3 WRIGHT_SETUP_TARGET=x86_64-unknown-linux-gnu WRIGHT_SETUP_EXT=tar.gz +: > "$LOG" +"$HELPER" install >/dev/null +[[ -x "$DIR/wright" && -f "$DIR/wright.sha256" ]] && "$DIR/wright" --version | grep -Fq 1.2.3 \ + && grep -Fq .tar.gz.sha256 "$LOG" && grep -Fq wright-1.2.3-x86_64-unknown-linux-gnu.tar.gz "$LOG" \ + && report "cache miss downloads and installs the verified executable" ok || report "cache miss downloads and installs the verified executable" fail +[[ "$(find "$DIR" -type f | wc -l | tr -d ' ')" == 2 ]] \ + && report "cache holds only the executable and its checksum" ok || report "cache holds only the executable and its checksum" fail + +# Hit: no distribution request at all +: > "$LOG" +OUT="$WORK/verify.out"; : > "$OUT" +GITHUB_OUTPUT="$OUT" "$HELPER" verify-cache >/dev/null +[[ "$(out valid)" == true && "$(requests)" == 0 ]] \ + && report "valid cache hit makes no distribution request" ok || report "valid cache hit makes no distribution request" fail + +# Corrupt executable +printf '#!/bin/sh\necho "wright 1.2.3"\necho tampered\n' > "$DIR/wright" +: > "$OUT" +GITHUB_OUTPUT="$OUT" "$HELPER" verify-cache >/dev/null 2>&1 +[[ "$(out valid)" == false && ! -e "$DIR" ]] \ + && report "corrupt cached executable is rejected and removed" ok || report "corrupt cached executable is rejected and removed" fail +"$HELPER" install >/dev/null +: > "$OUT" +GITHUB_OUTPUT="$OUT" "$HELPER" verify-cache >/dev/null +[[ "$(out valid)" == true ]] && report "recovery reinstalls a verified executable" ok || report "recovery reinstalls a verified executable" fail + +# Missing sidecar, and a matching sidecar over the wrong version +rm "$DIR/wright.sha256" +: > "$OUT"; GITHUB_OUTPUT="$OUT" "$HELPER" verify-cache >/dev/null 2>&1 +[[ "$(out valid)" == false ]] && report "cache without checksum sidecar is rejected" ok || report "cache without checksum sidecar is rejected" fail +mkdir -p "$DIR" +printf '#!/bin/sh\necho "wright 9.9.9"\n' > "$DIR/wright"; chmod +x "$DIR/wright" +shasum -a 256 "$DIR/wright" | awk '{print $1}' > "$DIR/wright.sha256" +: > "$OUT"; GITHUB_OUTPUT="$OUT" "$HELPER" verify-cache >/dev/null 2>&1 +[[ "$(out valid)" == false ]] && report "executable of another version is rejected" ok || report "executable of another version is rejected" fail + +# Distribution checksum mismatch installs nothing +make_release 1.2.3 x86_64-unknown-linux-gnu +printf '%064d wright-1.2.3-x86_64-unknown-linux-gnu.tar.gz\n' 0 \ + > "$WORK/mock/wright/releases/1.2.3/wright-1.2.3-x86_64-unknown-linux-gnu.tar.gz.sha256" +if "$HELPER" install >"$WORK/err" 2>&1; then + report "checksum mismatch fails" fail +else + grep -Fq "checksum mismatch" "$WORK/err" && [[ ! -e "$DIR/wright" ]] \ + && report "checksum mismatch fails and installs nothing" ok || report "checksum mismatch fails and installs nothing" fail +fi + +# Missing release fails explicitly +WRIGHT_SETUP_VERSION=7.7.7 "$HELPER" install >"$WORK/err" 2>&1 \ + && report "missing release fails" fail \ + || { grep -Fq "does release 7.7.7 exist" "$WORK/err" && report "missing release fails" ok || report "missing release fails" fail; } + +# PATH activation +GITHUB_PATH="$WORK/path" "$HELPER" activate +grep -Fxq "$DIR" "$WORK/path" && report "activate adds the executable directory to PATH" ok || report "activate adds the executable directory to PATH" fail + +echo "setup-wright: $PASS passed, $FAIL failed" +[[ "$FAIL" == 0 ]]