mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(ci): prevent poisoned Rust caches (#6618)
## Summary - roll every `Swatinem/rust-cache` use back from v2.9.2 to the last known-good v2.9.1 digest - give Unit Tests a new `sherpa-cache-v1` key so it cannot restore the existing poisoned artifact - pin Renovate to v2.9.1 and add CI contracts that reject unsafe cache actions or a misplaced generation key ## Why After #5441 upgraded rust-cache to v2.9.2, warm-cache `main` Unit Tests runs began failing while linking `buzz-voice` with `could not find native static library sherpa-onnx-c-api`. The failed run at `db5617dd1` restored the same 1.4 KB cache generation that had already failed at `01091c15a`; the preceding cold run at `26f4c3ed3` downloaded sherpa 1.13.4 and passed. v2.9.2 changed target cleanup, while `sherpa-onnx-sys` treats its prebuilt `lib/` directory as proof that the native archive exists. Rolling back the action and invalidating the affected key removes both sides of that failure state without disabling target caching. ## Validation At `6da0037a0407fc498cd482fcbbb74c7a15907e9f`: - `scripts/test-rust-cache-contract.sh` - `scripts/test-rust-cache-contract-regressions.sh` - negative fixtures reject a bad digest in a newly named `.yaml` workflow and a generation key moved outside the cache action's `with` block - YAML parse for all workflows - release, desktop candidate, mobile release, mobile candidate, and mobile worktree source contracts - `just file-size-check` - pre-commit and pre-push hooks The PR Unit Tests run proves the cold-cache path because pull requests restore but do not save Rust caches. The first successful `main` run after merge will save the new Unit Tests key; the following `main` run will exercise the warm restore. ## Related issue None found. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
This commit is contained in:
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -87,6 +87,10 @@ jobs:
|
|||||||
scripts/test-mobile-release-candidate-publisher.sh
|
scripts/test-mobile-release-candidate-publisher.sh
|
||||||
- name: Mobile worktree identity contract
|
- name: Mobile worktree identity contract
|
||||||
run: scripts/test-mobile-worktree-overrides.sh
|
run: scripts/test-mobile-worktree-overrides.sh
|
||||||
|
- name: Rust cache contract
|
||||||
|
run: |
|
||||||
|
scripts/test-rust-cache-contract.sh
|
||||||
|
scripts/test-rust-cache-contract-regressions.sh
|
||||||
- name: File size policy
|
- name: File size policy
|
||||||
run: just file-size-check
|
run: just file-size-check
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
- name: Format check
|
- name: Format check
|
||||||
@@ -123,8 +127,9 @@ jobs:
|
|||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
|
key: sherpa-cache-v1
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
- name: Install cargo-nextest
|
- name: Install cargo-nextest
|
||||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||||
@@ -147,7 +152,7 @@ jobs:
|
|||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
workspaces: desktop/src-tauri
|
workspaces: desktop/src-tauri
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
@@ -340,7 +345,7 @@ jobs:
|
|||||||
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.github/workflows/ci.yml') }}
|
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.github/workflows/ci.yml') }}
|
||||||
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
||||||
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
||||||
with:
|
with:
|
||||||
workspaces: |
|
workspaces: |
|
||||||
@@ -763,7 +768,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
# Reuse the relay + git-credential-nostr built by Desktop E2E Relay
|
# Reuse the relay + git-credential-nostr built by Desktop E2E Relay
|
||||||
@@ -952,7 +957,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
key: cross-${{ matrix.target }}
|
key: cross-${{ matrix.target }}
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
@@ -993,7 +998,7 @@ jobs:
|
|||||||
# toolchain (1.95.0 + clippy via profile = default) comes from the
|
# toolchain (1.95.0 + clippy via profile = default) comes from the
|
||||||
# repo-root rust-toolchain.toml, which the runner's preinstalled rustup
|
# repo-root rust-toolchain.toml, which the runner's preinstalled rustup
|
||||||
# honors on demand; the host triple already is x86_64-pc-windows-msvc.
|
# honors on demand; the host triple already is x86_64-pc-windows-msvc.
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
workspaces: |
|
workspaces: |
|
||||||
.
|
.
|
||||||
@@ -1070,7 +1075,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
workspaces: desktop/src-tauri
|
workspaces: desktop/src-tauri
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|||||||
2
.github/workflows/mesh-lifecycle.yml
vendored
2
.github/workflows/mesh-lifecycle.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.event_name != 'pull_request' }}
|
save-if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -511,7 +511,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
|
||||||
with:
|
with:
|
||||||
workspaces: desktop/src-tauri
|
workspaces: desktop/src-tauri
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
|
|||||||
@@ -23,6 +23,12 @@
|
|||||||
"separateMinorPatch": true,
|
"separateMinorPatch": true,
|
||||||
"separateMultipleMinor": false
|
"separateMultipleMinor": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "rust-cache v2.9.2 cleanup can preserve sherpa's prebuilt directory after removing its static archive, poisoning warm Unit Tests caches. Keep v2.9.1 until the action or sherpa validates the archive before reuse.",
|
||||||
|
"matchManagers": ["github-actions"],
|
||||||
|
"matchPackageNames": ["Swatinem/rust-cache"],
|
||||||
|
"allowedVersions": "<=2.9.1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "evalexpr v13 relicensed from MIT to AGPL-3.0 — pin below v13 until migrated to an MIT alternative.",
|
"description": "evalexpr v13 relicensed from MIT to AGPL-3.0 — pin below v13 until migrated to an MIT alternative.",
|
||||||
"matchPackageNames": ["evalexpr"],
|
"matchPackageNames": ["evalexpr"],
|
||||||
|
|||||||
161
scripts/test-rust-cache-contract-regressions.sh
Executable file
161
scripts/test-rust-cache-contract-regressions.sh
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
known_good='e18b497796c12c097a38f9edb9d0641fb99eee32'
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
mkdir -p "$tmp/.github/workflows"
|
||||||
|
cp "$root"/.github/workflows/*.yml "$tmp/.github/workflows/"
|
||||||
|
for workflow in "$root"/.github/workflows/*.yaml; do
|
||||||
|
[[ -e "$workflow" ]] || continue
|
||||||
|
cp "$workflow" "$tmp/.github/workflows/"
|
||||||
|
done
|
||||||
|
cp "$root/renovate.json" "$tmp/renovate.json"
|
||||||
|
|
||||||
|
run_contract() {
|
||||||
|
BUZZ_RUST_CACHE_CONTRACT_ROOT="$tmp" "$root/scripts/test-rust-cache-contract.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_failure() {
|
||||||
|
local expected=$1
|
||||||
|
local output
|
||||||
|
if output=$(run_contract 2>&1); then
|
||||||
|
echo "expected rust cache contract failure containing: $expected" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$output" != *"$expected"* ]]; then
|
||||||
|
printf 'unexpected rust cache contract error:\n%s\n' "$output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_cache_workflow() {
|
||||||
|
cat > "$tmp/.github/workflows/new-cache-user.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_contract >/dev/null
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Bad digest
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||||
|
YAML
|
||||||
|
expect_failure 'restored rust-cache v2.9.2'
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Mutable tag
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Expression ref
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Swatinem/rust-cache@${{ matrix.cache-ref }}
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Spaced key
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses : Swatinem/rust-cache@v2
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Flow mapping
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- { uses: Swatinem/rust-cache@v2 }
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Quoted key
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- "uses": Swatinem/rust-cache@v2
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
rm "$tmp/.github/workflows/new-cache-user.yaml"
|
||||||
|
|
||||||
|
mkdir -p "$tmp/.github/actions/cache"
|
||||||
|
cat > "$tmp/.github/actions/cache/action.yml" <<'YAML'
|
||||||
|
name: Unsafe cache wrapper
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- { uses: Swatinem/rust-cache@v2 }
|
||||||
|
YAML
|
||||||
|
write_cache_workflow <<'YAML'
|
||||||
|
name: Local cache wrapper
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: ./.github/actions/cache
|
||||||
|
YAML
|
||||||
|
expect_failure 'rust-cache must stay on the v2.9.1 digest'
|
||||||
|
|
||||||
|
cat > "$tmp/.github/actions/cache/action.yml" <<YAML
|
||||||
|
name: Safe preferred cache wrapper
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: Swatinem/rust-cache@$known_good
|
||||||
|
YAML
|
||||||
|
cat > "$tmp/.github/actions/cache/action.yaml" <<'YAML'
|
||||||
|
name: Unreachable stale cache wrapper
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
YAML
|
||||||
|
run_contract >/dev/null
|
||||||
|
rm -rf "$tmp/.github/actions" "$tmp/.github/workflows/new-cache-user.yaml"
|
||||||
|
|
||||||
|
python3 - "$tmp/.github/workflows/ci.yml" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = pathlib.Path(sys.argv[1])
|
||||||
|
workflow = path.read_text()
|
||||||
|
needle = " key: sherpa-cache-v1\n save-if:"
|
||||||
|
replacement = " save-if:"
|
||||||
|
if workflow.count(needle) != 1:
|
||||||
|
raise SystemExit("expected one Unit Tests cache generation key")
|
||||||
|
workflow = workflow.replace(needle, replacement)
|
||||||
|
job = " unit-tests:\n"
|
||||||
|
if workflow.count(job) != 1:
|
||||||
|
raise SystemExit("expected one Unit Tests job")
|
||||||
|
workflow = workflow.replace(job, job + " env:\n key: sherpa-cache-v1\n", 1)
|
||||||
|
path.write_text(workflow)
|
||||||
|
PY
|
||||||
|
expect_failure 'rust-cache action must keep with.key set to sherpa-cache-v1'
|
||||||
|
|
||||||
|
echo "rust cache contract regressions passed"
|
||||||
122
scripts/test-rust-cache-contract.sh
Executable file
122
scripts/test-rust-cache-contract.sh
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root=${BUZZ_RUST_CACHE_CONTRACT_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
renovate="$root/renovate.json"
|
||||||
|
known_good='e18b497796c12c097a38f9edb9d0641fb99eee32'
|
||||||
|
known_bad='6323deb102c322ba6fcbdcafc7e3dddab59af2b6'
|
||||||
|
|
||||||
|
ruby - "$root" "$renovate" "$known_good" "$known_bad" <<'RUBY'
|
||||||
|
require "json"
|
||||||
|
require "pathname"
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
root_name, renovate_name, known_good, known_bad = ARGV
|
||||||
|
root = Pathname(root_name)
|
||||||
|
workflow_paths = (root / ".github" / "workflows").children
|
||||||
|
.select { |path| path.file? && [".yml", ".yaml"].include?(path.extname) }
|
||||||
|
.sort
|
||||||
|
abort "no GitHub Actions workflows found" if workflow_paths.empty?
|
||||||
|
|
||||||
|
load_workflow = lambda do |path|
|
||||||
|
workflow = YAML.safe_load(path.binread.force_encoding("UTF-8"), aliases: true)
|
||||||
|
abort "#{path}: workflow must be a YAML mapping" unless workflow.is_a?(Hash)
|
||||||
|
workflow
|
||||||
|
rescue Psych::Exception => error
|
||||||
|
abort "#{path}: invalid workflow YAML: #{error.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
uses_values = []
|
||||||
|
walk = nil
|
||||||
|
walk = lambda do |value, path, location|
|
||||||
|
case value
|
||||||
|
when Hash
|
||||||
|
value.each do |key, child|
|
||||||
|
child_location = "#{location}.#{key}"
|
||||||
|
uses_values << [path, child_location, child] if key.to_s == "uses"
|
||||||
|
walk.call(child, path, child_location)
|
||||||
|
end
|
||||||
|
when Array
|
||||||
|
value.each_with_index do |child, index|
|
||||||
|
walk.call(child, path, "#{location}[#{index}]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
workflows = workflow_paths.to_h do |path|
|
||||||
|
workflow = load_workflow.call(path)
|
||||||
|
walk.call(workflow, path, "$")
|
||||||
|
[path, workflow]
|
||||||
|
end
|
||||||
|
|
||||||
|
loaded_paths = workflow_paths.to_h { |path| [path.cleanpath, true] }
|
||||||
|
uses_index = 0
|
||||||
|
while uses_index < uses_values.length
|
||||||
|
coordinate = uses_values[uses_index][2]
|
||||||
|
uses_index += 1
|
||||||
|
next unless coordinate.is_a?(String) && coordinate.start_with?("./")
|
||||||
|
|
||||||
|
clean_root = root.realpath
|
||||||
|
target = (clean_root / coordinate.delete_prefix("./")).cleanpath
|
||||||
|
resolved_target = target.exist? ? target.realpath : target
|
||||||
|
root_prefix = "#{clean_root}#{File::SEPARATOR}"
|
||||||
|
inside_root = resolved_target == clean_root || resolved_target.to_s.start_with?(root_prefix)
|
||||||
|
abort "local action path escapes contract root: #{coordinate}" unless inside_root
|
||||||
|
next unless target.directory?
|
||||||
|
|
||||||
|
manifest = [target / "action.yml", target / "action.yaml"].find(&:file?)
|
||||||
|
abort "local action manifest missing: #{coordinate}" unless manifest
|
||||||
|
clean_manifest = manifest.cleanpath
|
||||||
|
next if loaded_paths[clean_manifest]
|
||||||
|
|
||||||
|
loaded_paths[clean_manifest] = true
|
||||||
|
action = load_workflow.call(clean_manifest)
|
||||||
|
walk.call(action, clean_manifest, "$")
|
||||||
|
end
|
||||||
|
|
||||||
|
cache_uses = uses_values.each_with_object([]) do |(path, location, coordinate), found|
|
||||||
|
next unless coordinate.is_a?(String)
|
||||||
|
|
||||||
|
match = /\ASwatinem\/rust-cache@(.+)\z/i.match(coordinate)
|
||||||
|
found << [path, location, match[1]] if match
|
||||||
|
end
|
||||||
|
abort "no Swatinem/rust-cache actions found" if cache_uses.empty?
|
||||||
|
|
||||||
|
cache_uses.each do |path, location, ref|
|
||||||
|
if ref == known_bad
|
||||||
|
abort "#{path}:#{location}: restored rust-cache v2.9.2, which poisons sherpa caches"
|
||||||
|
end
|
||||||
|
unless ref == known_good
|
||||||
|
abort "#{path}:#{location}: rust-cache must stay on the v2.9.1 digest"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ci_path = root / ".github" / "workflows" / "ci.yml"
|
||||||
|
ci = workflows.fetch(ci_path) { load_workflow.call(ci_path) }
|
||||||
|
jobs = ci["jobs"]
|
||||||
|
unit_tests = jobs.is_a?(Hash) ? jobs["unit-tests"] : nil
|
||||||
|
abort "unit-tests job missing" unless unit_tests.is_a?(Hash)
|
||||||
|
steps = unit_tests["steps"]
|
||||||
|
abort "Unit Tests steps missing" unless steps.is_a?(Array)
|
||||||
|
cache_steps = steps.select do |step|
|
||||||
|
step.is_a?(Hash) && step["uses"].is_a?(String) &&
|
||||||
|
step["uses"].match?(/\ASwatinem\/rust-cache@/i)
|
||||||
|
end
|
||||||
|
abort "Unit Tests must contain exactly one rust-cache action step" unless cache_steps.length == 1
|
||||||
|
|
||||||
|
cache_with = cache_steps.first["with"]
|
||||||
|
unless cache_with.is_a?(Hash) && cache_with["key"] == "sherpa-cache-v1"
|
||||||
|
abort "Unit Tests rust-cache action must keep with.key set to sherpa-cache-v1"
|
||||||
|
end
|
||||||
|
|
||||||
|
renovate = JSON.parse(Pathname(renovate_name).binread.force_encoding("UTF-8"))
|
||||||
|
rules = renovate.fetch("packageRules", []).select do |rule|
|
||||||
|
rule["matchManagers"] == ["github-actions"] &&
|
||||||
|
rule["matchPackageNames"] == ["Swatinem/rust-cache"]
|
||||||
|
end
|
||||||
|
unless rules.length == 1 && rules.first["allowedVersions"] == "<=2.9.1"
|
||||||
|
abort "Renovate must keep Swatinem/rust-cache at v2.9.1 or older"
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
echo "rust cache contract passed"
|
||||||
Reference in New Issue
Block a user