From 5f92e58d88513608f5ed124b8d9ff1dc05fa5daa Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:16:55 +1000 Subject: [PATCH 1/2] ci(ts): move all workflows to ubuntu-latest (LAB-3503) cachekit-ts is public and forkable but every job ran on the org-shared, privileged self-hosted ARC pool (runs-on: cachekit / cachekit-lean) gated only by an in-workflow fork-guard ternary, a control a fork PR can simply delete since it runs the fork's own copy of the workflow. Ray ratified moving all public lanes off the pool onto GitHub-hosted ubuntu-latest (LAB-1161 stage 1, option a+c); the real server-side control (scoping the runner group away from public repos) is staged behind this as stage 2. - Collapse every fork-guard runs-on ternary in ci.yml/build-wasm.yml to a literal ubuntu-latest; fix build-native.yml matrix generator and test job matrix to stop emitting cachekit-lean; delete now-dead self-hosted- only steps and stale comments. - Add a self-hosted-drift-guard job (ci.yml) that fails CI if any runs-on/matrix runner value in .github/workflows/ still names cachekit/cachekit-lean/self-hosted, maintainer drift protection, not a fork-PR control (documented as such; that's stage 2's job). - Add hosted caching (cache: pnpm, Swatinem/rust-cache) since the pool's warm hostPath cache disappears on ephemeral GitHub-hosted runners. - Extract .github/actions/setup-pnpm-node as a composite action for the 10 identical Install-pnpm/Setup-Node call sites this change touched. Reviewed by expert panel (bug-hunter-supreme, security-specialist, code-craftsman, catchphrase-agent): fixed the drift guard's case- sensitivity gap, documented its remaining heuristic limits, dropped a low- value rust-cache step from the rarely-run build-wasm.yml, and collapsed a vestigial single-value matrix axis in build-native.yml test job. --- .github/actions/setup-pnpm-node/action.yml | 20 ++++ .github/workflows/build-native.yml | 43 +++---- .github/workflows/build-wasm.yml | 16 +-- .github/workflows/ci.yml | 133 ++++++++++----------- 4 files changed, 102 insertions(+), 110 deletions(-) create mode 100644 .github/actions/setup-pnpm-node/action.yml diff --git a/.github/actions/setup-pnpm-node/action.yml b/.github/actions/setup-pnpm-node/action.yml new file mode 100644 index 0000000..a19b30d --- /dev/null +++ b/.github/actions/setup-pnpm-node/action.yml @@ -0,0 +1,20 @@ +name: Setup pnpm + Node.js (cached) +description: >- + Install pnpm, then Node.js with the pnpm store cache wired in + (pnpm must be on PATH before actions/setup-node's cache: pnpm probe runs). +inputs: + node-version: + description: Node.js version to install + required: false + default: '22' +runs: + using: composite + steps: + - name: Install pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ inputs.node-version }} + cache: pnpm diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index 010458b..45a0af3 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -47,17 +47,17 @@ jobs: steps: - id: set # PRs build the cheap linux-x64 target only (catches the vast majority - # of compile errors on the self-hosted runner). Full cross-platform - # validation runs on push to main and on manually pushed - # cachekit-core-ts-v* tags — the validation a (currently manual, see - # header) publish relies on — keeping the macOS/Windows cost off the - # PR review loop without losing coverage where it matters. + # of compile errors fast). Full cross-platform validation runs on + # push to main and on manually pushed cachekit-core-ts-v* tags — the + # validation a (currently manual, see header) publish relies on — + # keeping the macOS/Windows cost off the PR review loop without + # losing coverage where it matters. run: | if [ "${{ github.event_name }}" = "pull_request" ]; then - builds='[{"target":"x86_64-unknown-linux-gnu","os":"cachekit-lean"}]' + builds='[{"target":"x86_64-unknown-linux-gnu","os":"ubuntu-latest"}]' else builds='[ - {"target":"x86_64-unknown-linux-gnu","os":"cachekit-lean"}, + {"target":"x86_64-unknown-linux-gnu","os":"ubuntu-latest"}, {"target":"aarch64-unknown-linux-gnu","os":"ubuntu-latest"}, {"target":"x86_64-apple-darwin","os":"macos-latest"}, {"target":"aarch64-apple-darwin","os":"macos-latest"}, @@ -78,19 +78,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Configure Rust paths (self-hosted) - if: matrix.os == 'cachekit-lean' - run: | - echo "RUSTUP_HOME=/tmp/rustup" >> "$GITHUB_ENV" - echo "CARGO_HOME=/tmp/cargo" >> "$GITHUB_ENV" - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Setup Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # stable @@ -98,6 +86,12 @@ jobs: toolchain: stable targets: ${{ matrix.target }} + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: packages/cachekit-core-ts + shared-key: ${{ matrix.target }} + - name: Install aarch64 cross-compiler if: matrix.target == 'aarch64-unknown-linux-gnu' run: | @@ -125,16 +119,14 @@ jobs: needs: build strategy: matrix: - os: [cachekit-lean] node: [22, 24] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: ./.github/actions/setup-pnpm-node with: node-version: ${{ matrix.node }} @@ -144,9 +136,6 @@ jobs: name: bindings-x86_64-unknown-linux-gnu path: packages/cachekit-core-ts - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 - - name: Install dependencies run: pnpm install diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 88a45af..7da4d92 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -37,23 +37,11 @@ env: jobs: build: - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Configure Rust paths (self-hosted) - if: ${{ !github.event.pull_request.head.repo.fork }} - run: | - echo "RUSTUP_HOME=/tmp/rustup" >> "$GITHUB_ENV" - echo "CARGO_HOME=/tmp/cargo" >> "$GITHUB_ENV" - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Setup Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # stable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e572845..8630a07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,17 +11,11 @@ permissions: jobs: lint: - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Install dependencies run: pnpm install --frozen-lockfile @@ -33,7 +27,7 @@ jobs: run: pnpm type-check test: - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: @@ -42,14 +36,10 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js ${{ matrix.node }} - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: ./.github/actions/setup-pnpm-node with: node-version: ${{ matrix.node }} - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 - - name: Install dependencies run: pnpm install --frozen-lockfile @@ -60,13 +50,7 @@ jobs: run: pnpm test test-integration: - # Fork PRs fall back to ubuntu-latest (GHA disables self-hosted runners - # for fork PRs by security policy). Internal/push runs use 'cachekit' - # (NOT 'cachekit-lean') because the redis service container below - # requires Docker on the host: cachekit-lean is a locked-down rootless - # pod with no DOCKER_HOST and no docker socket, so services would fail - # to start. ubuntu-latest has Docker preinstalled. - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit' }} + runs-on: ubuntu-latest services: redis: image: redis:7-alpine @@ -81,13 +65,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Install dependencies run: pnpm install --frozen-lockfile @@ -102,17 +80,11 @@ jobs: CI: true coverage: - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Install dependencies run: pnpm install --frozen-lockfile @@ -130,17 +102,11 @@ jobs: fail_ci_if_error: false security: - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' + - uses: ./.github/actions/setup-pnpm-node - name: Install dependencies run: pnpm install --frozen-lockfile @@ -198,7 +164,7 @@ jobs: workers: name: Workers lane (wasm + workerd) - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest env: # Must match the wasm-bindgen version pinned in the crate's Cargo.lock; # scripts/build.sh fails loudly on drift. @@ -211,19 +177,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Configure Rust paths (self-hosted) - if: ${{ !github.event.pull_request.head.repo.fork }} - run: | - echo "RUSTUP_HOME=/tmp/rustup" >> "$GITHUB_ENV" - echo "CARGO_HOME=/tmp/cargo" >> "$GITHUB_ENV" - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Setup Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # stable @@ -234,6 +188,11 @@ jobs: toolchain: 1.97.1 targets: wasm32-unknown-unknown + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: packages/cachekit-core-wasm + - name: Install wasm-bindgen + wasm-opt (pinned prebuilts, checksummed) run: | mkdir -p "$HOME/.local/bin" @@ -264,18 +223,12 @@ jobs: smoke-test: name: Package smoke test (ESM + CJS) - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} + runs-on: ubuntu-latest needs: [test] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version: '22' - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v2 + - uses: ./.github/actions/setup-pnpm-node - name: Install dependencies run: pnpm install --frozen-lockfile @@ -323,10 +276,51 @@ jobs: console.log('CJS: all exports verified'); " + self-hosted-drift-guard: + name: Self-hosted runner drift guard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + # Drift protection for maintainers only: a fork PR runs its own copy of + # this workflow (a fork can't remove its own guard), so this catches an + # accidental self-hosted `runs-on` creeping back onto main/push, not a + # malicious fork PR. The server-side control (runner group scoped away + # from public repos) is LAB-1161 stage 2. + # + # This is a line-oriented heuristic (case-insensitive grep over + # runs-on:/os:/runner: keys and JSON "os": values), not a schema-aware + # YAML parse — a determined bypass via a renamed matrix key or a YAML + # anchor/multi-line block scalar would slip past it. That tradeoff is + # deliberate: closing it properly means defeating a determined bypass, + # which is what the settings-level runner-group scope (stage 2) is for, + # not this maintainer sanity check. + - name: Fail if any workflow still targets the self-hosted pool + run: | + set -euo pipefail + hits=$(grep -rnE '^[[:space:]]*runs-on:|^[[:space:]]*(os|runner):|"os":' .github/workflows/ \ + | grep -iE 'cachekit|self-hosted' || true) + if [ -n "$hits" ]; then + echo "::error::self-hosted runner reference(s) found in .github/workflows/ (LAB-3503 drift guard):" + echo "$hits" + exit 1 + fi + echo "no self-hosted runner references found in .github/workflows/" + ci-success: name: CI Success - runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'cachekit-lean' }} - needs: [lint, test, test-integration, coverage, security, smoke-test, workers] + runs-on: ubuntu-latest + needs: + [ + lint, + test, + test-integration, + coverage, + security, + smoke-test, + workers, + self-hosted-drift-guard, + ] if: always() steps: - name: Check all jobs succeeded @@ -337,7 +331,8 @@ jobs: [[ "${{ needs.coverage.result }}" != "success" ]] || \ [[ "${{ needs.security.result }}" != "success" ]] || \ [[ "${{ needs.smoke-test.result }}" != "success" ]] || \ - [[ "${{ needs.workers.result }}" != "success" ]]; then + [[ "${{ needs.workers.result }}" != "success" ]] || \ + [[ "${{ needs.self-hosted-drift-guard.result }}" != "success" ]]; then echo "One or more jobs failed" exit 1 fi From 7210b33f07830a7a78bfd685f31c1b88ab519fb6 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 14 Sep 2026 14:47:23 +1000 Subject: [PATCH 2/2] ci(ts): keep workflow comments to what a public repo should say (LAB-3503) Comment-only, plus one step name. The drift-guard rationale named org runner settings that do not belong in a public repository; the public reason is simply that a public, forkable repo runs only on GitHub-hosted runners. No behaviour change. --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8630a07..b7793f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,17 +285,17 @@ jobs: # Drift protection for maintainers only: a fork PR runs its own copy of # this workflow (a fork can't remove its own guard), so this catches an # accidental self-hosted `runs-on` creeping back onto main/push, not a - # malicious fork PR. The server-side control (runner group scoped away - # from public repos) is LAB-1161 stage 2. + # malicious fork PR. The server-side control for fork PRs lives in + # repository and org runner settings, outside this file. # # This is a line-oriented heuristic (case-insensitive grep over # runs-on:/os:/runner: keys and JSON "os": values), not a schema-aware # YAML parse — a determined bypass via a renamed matrix key or a YAML # anchor/multi-line block scalar would slip past it. That tradeoff is # deliberate: closing it properly means defeating a determined bypass, - # which is what the settings-level runner-group scope (stage 2) is for, - # not this maintainer sanity check. - - name: Fail if any workflow still targets the self-hosted pool + # which is what the settings-level runner controls are for, not this + # maintainer sanity check. + - name: Fail if any workflow still targets a self-hosted runner run: | set -euo pipefail hits=$(grep -rnE '^[[:space:]]*runs-on:|^[[:space:]]*(os|runner):|"os":' .github/workflows/ \