From c8a39c05fc40b5046c51500b3261e8883bc4d62e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 21 Oct 2026 00:35:08 +0800 Subject: [PATCH] feat: add cached setup-wright Action Move the composite Action from wrightkit/wright into its own repository so consumers call wrightkit/setup-wright@v1 and get a major tag independent of Wright release refs. This reverses the earlier non-goal of a separate repository. Behavior matches the reviewed in-repo version, except that the installed version check now matches the whole version rather than a substring, with a test. Refs wrightkit/wright#717 --- .github/CODEOWNERS | 4 + .github/workflows/ci.yml | 94 +++++++++++++++++ .github/workflows/release.yml | 37 +++++++ .release-please-manifest.json | 1 + AGENTS.md | 11 ++ README.md | 77 ++++++++++++++ action.yml | 81 ++++++++++++++ release-please-config.json | 11 ++ scripts/setup-wright.sh | 138 ++++++++++++++++++++++++ scripts/test-setup-wright.sh | 191 ++++++++++++++++++++++++++++++++++ 10 files changed, 645 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 action.yml create mode 100644 release-please-config.json create mode 100755 scripts/setup-wright.sh create mode 100755 scripts/test-setup-wright.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..6c0bb5d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# CI, release workflows, and the Action contract define what passes and what publishes. +/.github/ @Teakowa +/action.yml @Teakowa +/AGENTS.md @Teakowa diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96753fb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + # Exact Wright release the smoke jobs install. Bump deliberately. + WRIGHT_SMOKE_VERSION: 0.29.0 + +jobs: + lint: + name: actionlint and shellcheck + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v7 + + # The image bundles shellcheck, so actionlint also checks `run:` scripts. + - name: actionlint + run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12 -color + + - name: shellcheck + run: shellcheck -S warning scripts/*.sh + + test: + name: Helper tests + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Test the helper script + run: scripts/test-setup-wright.sh + + smoke: + name: Smoke (${{ matrix.os }}) + 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: Install Wright through the Action + id: first + uses: ./ + with: + version: ${{ env.WRIGHT_SMOKE_VERSION }} + + - 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: ./ + with: + version: ${{ env.WRIGHT_SMOKE_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 }} + run: | + [[ "$CACHE_HIT" == true ]] || { echo "::error::expected a cache hit"; exit 1; } + + - name: Check the installed version + shell: bash + run: | + wright --version | tee "$RUNNER_TEMP/version.txt" + grep -Fq "$WRIGHT_SMOKE_VERSION" "$RUNNER_TEMP/version.txt" + + - 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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1d57699 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +# release-please maintains the Release PR. Merging it creates the exact vX.Y.Z +# tag and GitHub Release. The same job then moves the major tag (for example +# v1) to that tag, so consumers can use `wrightkit/setup-wright@v1`. The +# built-in token suffices: no downstream workflow starts from these tags. +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + name: Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + - name: Move the major tag + if: steps.release.outputs.release_created == 'true' + env: + GH_TOKEN: ${{ github.token }} + MAJOR_TAG: v${{ steps.release.outputs.major }} + SHA: ${{ steps.release.outputs.sha }} + run: gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$MAJOR_TAG" -f sha="$SHA" -F force=true || gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$MAJOR_TAG" -f sha="$SHA" diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1 @@ +{} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..82ea8e0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# setup-wright + +Cached `setup-wright` GitHub composite Action: installs the Wright CLI and caches the verified executable. Read the organization routing in [`wrightkit/.github`](https://github.com/wrightkit/.github/blob/main/AGENTS.md) first. This file adds only what is specific to this repository. + +- Wright owns the release archives, checksums, and the `latest/version` pointer. This repository only consumes them. Do not change that contract here. +- Keep the Action to setup. No command wrappers, no provider/project/build caching, no write token or secrets. +- Keep the cache key exact. Never add a restore prefix. Verify SHA-256 before cache save and before use, and never execute an unverified cached binary. +- `action.yml` is the public contract (inputs, outputs, `uses: wrightkit/setup-wright@v1`). Change it only with `README.md` and the helper tests in the same PR. +- Run `scripts/test-setup-wright.sh`, `actionlint`, and `shellcheck -S warning scripts/*.sh` before a PR. The `smoke` CI job is the real consuming-workflow check and pins an exact Wright version. +- Never create tags or releases by hand. `.github/workflows/release.yml` owns `vX.Y.Z` and the moving `v1`. +- Pin third-party actions to a full commit SHA. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9968a92 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# setup-wright + +A GitHub Action that installs the [Wright](https://github.com/wrightkit/wright) CLI and caches the verified executable. 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. + +## Usage + +Set up once, then run Wright as often as needed: + +```yaml +jobs: + wright: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + - uses: wrightkit/setup-wright@v1 + with: + version: 0.29.0 + - run: wright check + - run: wright lint + - run: wright analyze +``` + +| Input | Default | Meaning | +| --- | --- | --- | +| `version` | `latest` | Exact Wright version such as `0.29.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. | + +Supported runners: Linux x86_64, macOS x86_64, macOS arm64, and Windows x86_64, the targets with published Wright release archives. Any other platform fails with an `unsupported platform` error. + +## Versioning + +The Action has its own versions, independent of Wright's. Use `@v1` for the moving major tag, an exact tag such as `@v1.0.0`, or a commit SHA. The `@ref` selects the Action's code. The `version` input selects the Wright binary. + +## Version pinning + +* An exact `version` is deterministic and, on a cache hit, makes no request to the release distribution. +* `latest` first reads the small `latest/version` pointer, 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 never restores 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 downloads again 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. + +## Why a separate repository + +The Action started inside `wrightkit/wright` (`uses: wrightkit/wright/.github/actions/setup-wright@`). The owner moved it here because that call path is awkward, and because consumers need a stable major tag that is independent of Wright's release refs. + +## Development + +* `scripts/test-setup-wright.sh` runs the helper against a mock release distribution: cache hit and miss (zero requests on a hit), version and `latest` resolution, key separation, corrupt and wrong-version caches, checksum mismatch, a missing release, and unsupported platforms. +* CI runs `actionlint`, `shellcheck`, those tests, and a smoke job that uses the Action as `uses: ./` on Linux, macOS, and Windows with a published Wright release. It installs twice and requires a cache hit on the second run. +* Releases follow [release-please](.github/workflows/release.yml): merge the Release PR to create the `vX.Y.Z` tag and move the major tag. + +License: MIT. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..82fdb87 --- /dev/null +++ b/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/scripts/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/scripts/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/scripts/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 README.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/scripts/setup-wright.sh" activate diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..83316b5 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "initial-version": "1.0.0", + "packages": { + ".": { + "release-type": "simple", + "include-component-in-tag": false, + "changelog-path": "CHANGELOG.md" + } + } +} diff --git a/scripts/setup-wright.sh b/scripts/setup-wright.sh new file mode 100755 index 0000000..0171be2 --- /dev/null +++ b/scripts/setup-wright.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Helper for the setup-wright composite Action. +# +# 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 Wright release distribution) +# 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 -Eq "(^|[^0-9.])${version//./\\.}([^0-9.]|\$)"; 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 -Eq "(^|[^0-9.])${version//./\\.}([^0-9.]|\$)" \ + || { 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/scripts/test-setup-wright.sh b/scripts/test-setup-wright.sh new file mode 100755 index 0000000..a8c5bb0 --- /dev/null +++ b/scripts/test-setup-wright.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# Usage: scripts/test-setup-wright.sh +# +# Functional tests for scripts/setup-wright.sh +# 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/scripts/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 +mkdir -p "$DIR" +printf '#!/bin/sh\necho "wright 1.2.30"\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 "version that only contains the requested one is rejected" ok || report "version that only contains the requested one 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 ]]