From 4faab3152d4f347f56fd2d34d0f33ac4404fbe31 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:03:40 +1000 Subject: [PATCH 1/6] ci(rs): move all workflows to ubuntu-latest (LAB-3502) Every job in .github/workflows/ now runs on GitHub-hosted ubuntu-latest instead of the self-hosted ARC pool (cachekit / cachekit-lean). Ray ratified moving public-repo lanes off the pool ahead of putting the pool in a runner group public repos cannot reach (LAB-1161 stage 1, option a+c); this repo's slice must merge before stage 2 restricts the pool, or every job here would wait forever for a runner. - ci.yml, release.yml, security.yml: runs-on: cachekit / cachekit-lean -> ubuntu-latest. redis/memcached services blocks work unchanged -- hosted runners have Docker built in, no dind sidecar needed. - Drop the self-hosted-only RUSTUP_HOME/CARGO_HOME/CARGO_BUILD_JOBS overrides: that workaround existed for the pool's 6Gi cgroup and container-overlay filesystem, neither of which applies on a hosted VM. - Add Swatinem/rust-cache to the jobs that do full cargo builds -- the pool never had a hostPath cache wired into these workflows either, so this is a net new speedup, not a lost warm cache. - Add a guard job (ci.yml) that fails the workflow if any runs-on/os/ runner value in .github/workflows/ still names cachekit, cachekit-lean or self-hosted. It's drift protection for maintainers, not a fork-PR control -- a fork runs its own copy of the workflow; the server-side control is stage 2. - Remove .github/actionlint.yaml's self-hosted-runner label registry -- the labels it registered no longer appear anywhere. - Rewrite comments that described the two-pool split (services needs Docker, cachekit-lean's memory cgroup, self-hosted runner workspace) now that the split doesn't exist. --- .github/actionlint.yaml | 7 --- .github/workflows/ci.yml | 96 ++++++++++++++++++++-------------- .github/workflows/release.yml | 8 +-- .github/workflows/security.yml | 15 ++---- 4 files changed, 61 insertions(+), 65 deletions(-) delete mode 100644 .github/actionlint.yaml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml deleted file mode 100644 index 62b8101..0000000 --- a/.github/actionlint.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Self-hosted runner labels (lab ARC scalesets, see lab ADR-0001). -# actionlint (run by CodeRabbit and available locally) treats unknown -# runs-on labels as errors without this registry. -self-hosted-runner: - labels: - - cachekit-lean - - cachekit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d11f2e0..7dc92c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,27 +9,38 @@ on: permissions: contents: read -env: - # Rustup's temp dir must be on the same filesystem as RUSTUP_HOME to avoid - # cross-device link errors on container overlay filesystems. - RUSTUP_HOME: /tmp/rustup - CARGO_HOME: /tmp/cargo - # The cachekit-lean ARC runner has a hard 6Gi memory cgroup (lab ADR-0001). - # cargo defaults -j to the visible core count (~24 via the pod's CPU limit), - # and a cold `cargo test` build of this 246-crate async/TLS/crypto graph with - # full test-profile debuginfo peaks above 6Gi at that fan-out — the kernel - # OOM-kills the linker and the runner "loses communication" (issue #25, - # confirmed by a 6Gi-cgroup repro). Cap parallel compile/link jobs so peak - # RSS stays well under the cap on the cache-less lean pod. - CARGO_BUILD_JOBS: "4" - jobs: + guard-hosted-runners: + name: guard — no self-hosted runner labels + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Fail if any workflow still targets the retired self-hosted pool + # Drift protection for maintainers only, NOT a fork-PR control — a + # fork PR runs its own copy of this workflow file, so this check + # cannot stop an attacker's fork from restoring `runs-on: cachekit`. + # The control that actually blocks that is server-side: stage 2 of + # LAB-1161 moves both scale sets into the `cachekit-private` runner + # group, which excludes public repos. This job only stops a + # maintainer from accidentally merging the drift back in. + run: | + hits=$(grep -rnE '^\s*runs-on:|^\s*(os|runner):|"os":' .github/workflows/ \ + | grep -E '\b(cachekit|self-hosted)\b' || true) + if [ -n "$hits" ]; then + echo "::error::self-hosted runner label found in .github/workflows/ — this repo is hosted-only (LAB-3502)." + echo "$hits" + exit 1 + fi + test: name: ${{ matrix.rust }} - runs-on: cachekit-lean - # Backstop: a wedged/killed runner otherwise hangs ~10min until GitHub's - # heartbeat reckoning (issue #25). Fail fast — a healthy cold -j4 build of - # this tree finishes well inside this window. + runs-on: ubuntu-latest + # Fail fast rather than waiting out a hung build — a healthy cold build + # of this tree finishes well inside this window on a hosted runner. timeout-minutes: 20 continue-on-error: ${{ matrix.rust == 'beta' }} strategy: @@ -44,6 +55,10 @@ jobs: rustup toolchain install ${{ matrix.rust }} --profile minimal --component rustfmt,clippy rustup default ${{ matrix.rust }} + - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 + with: + key: ${{ matrix.rust }} + - name: Check formatting if: matrix.rust != '1.85' run: cargo fmt --all -- --check @@ -57,13 +72,12 @@ jobs: redis-lock: name: redis lock integration - # `services:` containers require Docker. cachekit-lean is a no-dind - # scaleset (lab ADR-0001), so a redis service can only run on the - # with-dind `cachekit` scaleset. Without a live redis the lock - # acquire/release path has zero automated coverage — the + # GitHub-hosted runners have Docker preinstalled, so `services:` works + # unchanged here — no dind sidecar required. Without a live redis the + # lock acquire/release path has zero automated coverage — the # redis_lock_live_semantics test skips when CACHEKIT_TEST_REDIS_URL is # unset (LAB-426: the AC requires locking exercised against a container). - runs-on: cachekit + runs-on: ubuntu-latest timeout-minutes: 20 services: redis: @@ -80,8 +94,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - # This job never pushes; don't leave the token on a self-hosted - # runner workspace (zizmor: artipacked). + # This job never pushes; don't leave the token in the git config + # (zizmor: artipacked). persist-credentials: false - name: Install Rust toolchain @@ -89,12 +103,13 @@ jobs: rustup toolchain install stable --profile minimal rustup default stable + - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 + - name: Run redis lock integration test env: - # The job runs directly on the runner, which shares the pod network - # namespace with the dind sidecar, so the mapped service port is - # reachable on localhost. redis-only, no default features — leanest - # build for the memory-tight (5Gi) with-dind pod. + # The job container shares a network namespace with the redis + # service container, so the mapped port is reachable on localhost. + # redis-only, no default features — leanest build. CACHEKIT_TEST_REDIS_URL: redis://localhost:6379 run: | cargo test --no-default-features --features redis --test redis_tests -- --nocapture 2>&1 | tee redis-test.log @@ -108,12 +123,12 @@ jobs: memcached: name: memcached integration - # Same reasoning as redis-lock: `services:` needs Docker, so this runs on - # the with-dind `cachekit` scaleset. Without a live memcached the + # Same reasoning as redis-lock: hosted runners have Docker built in, so + # `services:` needs no dind sidecar. Without a live memcached the # get/set/delete/ttl path has zero automated coverage — the # memcached_live_semantics test skips when CACHEKIT_TEST_MEMCACHED_URL is # unset (LAB-429: the AC requires memcached exercised against a container). - runs-on: cachekit + runs-on: ubuntu-latest timeout-minutes: 20 services: memcached: @@ -130,8 +145,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - # This job never pushes; don't leave the token on a self-hosted - # runner workspace (zizmor: artipacked). + # This job never pushes; don't leave the token in the git config + # (zizmor: artipacked). persist-credentials: false - name: Install Rust toolchain @@ -139,12 +154,13 @@ jobs: rustup toolchain install stable --profile minimal rustup default stable + - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 + - name: Run memcached integration test env: - # The job runs directly on the runner, which shares the pod network - # namespace with the dind sidecar, so the mapped service port is - # reachable on localhost. memcached-only, no default features — - # leanest build for the memory-tight (5Gi) with-dind pod. + # The job container shares a network namespace with the memcached + # service container, so the mapped port is reachable on localhost. + # memcached-only, no default features — leanest build. CACHEKIT_TEST_MEMCACHED_URL: tcp://localhost:11211 run: | cargo test --no-default-features --features memcached --test memcached_tests -- --nocapture 2>&1 | tee memcached-test.log @@ -158,7 +174,7 @@ jobs: wasm: name: wasm32 check + runtime tests - runs-on: cachekit-lean + runs-on: ubuntu-latest timeout-minutes: 15 env: # Must equal the wasm-bindgen version in Cargo.lock — the test runner @@ -176,6 +192,8 @@ jobs: rustup default stable rustup target add wasm32-unknown-unknown + - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 + - name: Check wasm32 build run: cargo check -p cachekit-rs --target wasm32-unknown-unknown --features workers,encryption --no-default-features diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1321ec..a01f381 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,14 +77,8 @@ jobs: # stops a feature branch from driving a publish) — the required tag input # alone decides what content ships. if: ${{ needs.release-please.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} - runs-on: cachekit-lean + runs-on: ubuntu-latest timeout-minutes: 30 - env: - RUSTUP_HOME: /tmp/rustup - CARGO_HOME: /tmp/cargo - # Same 6Gi cgroup OOM constraint as CI (issue #25): cap parallel jobs so - # the cold pre-publish build doesn't OOM-kill the linker on cachekit-lean. - CARGO_BUILD_JOBS: "4" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index da24f7e..cafb344 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -26,12 +26,6 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - # Same rationale as ci.yml: rustup's temp dir must share a filesystem with - # RUSTUP_HOME, and -j must be capped or cargo reads the node's core count - # instead of the 6Gi pod cgroup and OOM-kills the linker (lab ADR-0001). - RUSTUP_HOME: /tmp/rustup - CARGO_HOME: /tmp/cargo - CARGO_BUILD_JOBS: "4" # Pinned so the gate cannot change meaning without a diff. Renovate bumps # these like any other pin; the cache key below is derived from them. CARGO_DENY_VERSION: "0.19.4" @@ -42,10 +36,7 @@ env: jobs: supply-chain: - # cachekit-lean: neither tool needs Docker — cargo-deny resolves `cargo - # metadata`, cargo-audit reads Cargo.lock — so there is no reason to occupy - # the scarcer with-dind scaleset. - runs-on: cachekit-lean + runs-on: ubuntu-latest timeout-minutes: 20 steps: # Checkout is deliberately LAST. cargo honours a `.cargo/config.toml` @@ -90,8 +81,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - # This job never pushes; don't leave the token on a self-hosted - # runner workspace (zizmor: artipacked). + # This job never pushes; don't leave the token in the git config + # (zizmor: artipacked). persist-credentials: false - name: cargo deny (advisories + bans + licenses + sources) From 32e5158aacf2d1d37650b658bade292b400790ec Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:11:54 +1000 Subject: [PATCH 2/6] ci(rs): fix drift-guard bypass on block-list runs-on (LAB-3502) Expert-panel review (bug-hunter-supreme) found that the drift guard's line-oriented grep only matched a self-hosted label on the same line as `runs-on:`. A block-sequence label (`runs-on:\n - self-hosted`) never starts with `runs-on:`/`os:`/`runner:` itself, so it slipped past undetected while the guard still reported green. - Replace the grep with a yq-based check that resolves the actual runs-on/matrix.os/matrix.runner value regardless of scalar, flow-list, or block-list form, then greps the resolved value for the retired labels. Verified against a synthetic block-list bypass file locally. - release.yml: add a one-line comment explaining why the publish job (holds CARGO_REGISTRY_TOKEN) deliberately skips Swatinem/rust-cache, per code-craftsman's review (the asymmetry with ci.yml's build jobs was correct but undocumented). --- .github/workflows/ci.yml | 21 ++++++++++++++++----- .github/workflows/release.yml | 4 ++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dc92c7..7c31800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,12 +27,23 @@ jobs: # LAB-1161 moves both scale sets into the `cachekit-private` runner # group, which excludes public repos. This job only stops a # maintainer from accidentally merging the drift back in. + # + # Parsed with yq, not a line-oriented grep: `runs-on:` (and + # matrix.os/matrix.runner) are equally valid as a scalar, a + # flow-list (`[self-hosted, cachekit]`), or a block-list (`- cachekit` + # on its own line) — a grep anchored on the `runs-on:` line only + # catches the first form and silently misses the other two. run: | - hits=$(grep -rnE '^\s*runs-on:|^\s*(os|runner):|"os":' .github/workflows/ \ - | grep -E '\b(cachekit|self-hosted)\b' || true) - if [ -n "$hits" ]; then - echo "::error::self-hosted runner label found in .github/workflows/ — this repo is hosted-only (LAB-3502)." - echo "$hits" + bad=0 + for f in .github/workflows/*.yml; do + hits=$(yq eval '(.jobs[].runs-on, .jobs[].strategy.matrix.os[]?, .jobs[].strategy.matrix.runner[]?)' "$f" \ + | grep -oE '\b(cachekit(-lean)?|self-hosted)\b' || true) + if [ -n "$hits" ]; then + echo "::error::$f still targets a self-hosted runner label: $(echo "$hits" | tr '\n' ' ')" + bad=1 + fi + done + if [ "$bad" -eq 1 ]; then exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a01f381..d49aace 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,6 +80,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: + # Deliberately no Swatinem/rust-cache here, unlike ci.yml — this job + # holds CARGO_REGISTRY_TOKEN and only runs on a release (a handful of + # times a year), so a third-party action buys negligible build-time + # savings against a real increase in that secret's trust surface. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: # On dispatch, build and publish the tag's content — never main HEAD. From 2af9493089762c285686b1d638b0597155a8bc1b Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:18:43 +1000 Subject: [PATCH 3/6] test(rs): temporarily reintroduce runs-on: cachekit to prove the drift guard fires (LAB-3502) --- .github/workflows/ci.yml | 1 + .github/workflows/security.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c31800..b92ed97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: # on its own line) — a grep anchored on the `runs-on:` line only # catches the first form and silently misses the other two. run: | + yq --version bad=0 for f in .github/workflows/*.yml; do hits=$(yq eval '(.jobs[].runs-on, .jobs[].strategy.matrix.os[]?, .jobs[].strategy.matrix.runner[]?)' "$f" \ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index cafb344..7d849c2 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -36,7 +36,7 @@ env: jobs: supply-chain: - runs-on: ubuntu-latest + runs-on: cachekit timeout-minutes: 20 steps: # Checkout is deliberately LAST. cargo honours a `.cargo/config.toml` From e1414ef348cf12fb0ce7db13f65b3ffb873fae9a Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:21:36 +1000 Subject: [PATCH 4/6] revert(rs): drop the throwaway self-hosted test edit + yq debug print (LAB-3502) --- .github/workflows/ci.yml | 1 - .github/workflows/security.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b92ed97..7c31800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,6 @@ jobs: # on its own line) — a grep anchored on the `runs-on:` line only # catches the first form and silently misses the other two. run: | - yq --version bad=0 for f in .github/workflows/*.yml; do hits=$(yq eval '(.jobs[].runs-on, .jobs[].strategy.matrix.os[]?, .jobs[].strategy.matrix.runner[]?)' "$f" \ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7d849c2..cafb344 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -36,7 +36,7 @@ env: jobs: supply-chain: - runs-on: cachekit + runs-on: ubuntu-latest timeout-minutes: 20 steps: # Checkout is deliberately LAST. cargo honours a `.cargo/config.toml` From 7d754186e3832ef2f54139fcf7fd3f6fd93ee1d1 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 13 Sep 2026 22:22:27 +1000 Subject: [PATCH 5/6] ci(rs): don't let a yq parse failure hide behind the grep exit status (LAB-3502) --- .github/workflows/ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c31800..732f397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,17 @@ jobs: run: | bad=0 for f in .github/workflows/*.yml; do - hits=$(yq eval '(.jobs[].runs-on, .jobs[].strategy.matrix.os[]?, .jobs[].strategy.matrix.runner[]?)' "$f" \ - | grep -oE '\b(cachekit(-lean)?|self-hosted)\b' || true) + # yq's own parse failure must not be swallowed by the grep on the + # next line: GitHub Actions runs bash steps with `-e` but not + # `-o pipefail`, so `yq ... | grep ... || true` would mask a + # broken/unparseable workflow as "no hits found" instead of + # erroring. Capture yq's output and exit status separately. + if ! values=$(yq eval '(.jobs[].runs-on, .jobs[].strategy.matrix.os[]?, .jobs[].strategy.matrix.runner[]?)' "$f"); then + echo "::error::failed to parse $f with yq" + bad=1 + continue + fi + hits=$(echo "$values" | grep -oE '\b(cachekit(-lean)?|self-hosted)\b' || true) if [ -n "$hits" ]; then echo "::error::$f still targets a self-hosted runner label: $(echo "$hits" | tr '\n' ' ')" bad=1 From 163c0dac72d3931cf75dcd28a3be5ceed6f28a51 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 14 Sep 2026 14:47:19 +1000 Subject: [PATCH 6/6] ci(rs): keep workflow comments to what a public repo should say (LAB-3502) Comment-only, plus one step name. The drift-guard rationale named org runner infrastructure and 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 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732f397..1d6464d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,13 +19,12 @@ jobs: with: persist-credentials: false - - name: Fail if any workflow still targets the retired self-hosted pool + - name: Fail if any workflow still targets a self-hosted runner # Drift protection for maintainers only, NOT a fork-PR control — a # fork PR runs its own copy of this workflow file, so this check # cannot stop an attacker's fork from restoring `runs-on: cachekit`. - # The control that actually blocks that is server-side: stage 2 of - # LAB-1161 moves both scale sets into the `cachekit-private` runner - # group, which excludes public repos. This job only stops a + # The control that actually blocks that lives in repository and org + # runner settings, outside this file. This job only stops a # maintainer from accidentally merging the drift back in. # # Parsed with yq, not a line-oriented grep: `runs-on:` (and @@ -93,7 +92,7 @@ jobs: redis-lock: name: redis lock integration # GitHub-hosted runners have Docker preinstalled, so `services:` works - # unchanged here — no dind sidecar required. Without a live redis the + # unchanged here with no extra Docker setup. Without a live redis the # lock acquire/release path has zero automated coverage — the # redis_lock_live_semantics test skips when CACHEKIT_TEST_REDIS_URL is # unset (LAB-426: the AC requires locking exercised against a container). @@ -144,7 +143,7 @@ jobs: memcached: name: memcached integration # Same reasoning as redis-lock: hosted runners have Docker built in, so - # `services:` needs no dind sidecar. Without a live memcached the + # `services:` needs no extra Docker setup. Without a live memcached the # get/set/delete/ttl path has zero automated coverage — the # memcached_live_semantics test skips when CACHEKIT_TEST_MEMCACHED_URL is # unset (LAB-429: the AC requires memcached exercised against a container).