mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
ci: keep fork PRs under the macOS limit and the Actions cache under its cap
PLAN 0.10.1 CI item D. - Fork PRs no longer run the persistence-backlog RSS budget and the offline eval inside Test (macos-latest). On a cold hosted Mac those two rebuilds pushed Test to the 90-minute limit (jobs 106749104684, 106235312282). Trusted events keep them in Test on the warm self-hosted build; fork PRs run them in a separate `macos-budget` job with its own 75-minute timeout, so coverage stays and the required Test context can report. - The sccache GitHub Actions backend is installed and enabled only on refs/heads/main, mirroring rust-cache's save-if. It was unconditional in six ci.yml jobs, so every PR run wrote refs/pull/N entries (11.1 GB across 2,896 entries, over the 10 GiB cap). PRs still restore rust-cache's main-saved target. - New cache-janitor.yml with scripts/release/prune-actions-caches.sh: deletes refs/pull/N caches when a PR closes (pull_request_target, base checkout only, no PR code), refs/tags/<tag> caches after a successful tag-push Release, and a daily sweep of closed-PR caches and tag caches idle over a day. Branch caches are never touched; manual dispatch defaults to dry-run. - release-workflows.test.js now evaluates the sccache guards with github.ref, so a PR that re-enables the backend fails the contract. Checks run: - actionlint 1.7.12 with the CI ignore set: 0 findings, all workflows - shellcheck on both new scripts: 0 findings - bash scripts/release/prune-actions-caches.test.sh: passed (5 cases) - node .github/scripts/release-workflows.test.js: passed (11 event cases x 12 steps; 12 hermetic invocations); fails against the pre-change ci.yml - No cargo run: workflow and script change only. Refs #6406 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4c7a1a9023
commit
479db053b0
5 files changed
+355
-9
No files matched your search
@@ -83,15 +83,22 @@ const npmSmokeCases = [
|
||||
["main Ubuntu", "push", true, "ubuntu-latest", true, true, false, false, true],
|
||||
["main macOS", "push", true, "macos-latest", true, true, true, false, false],
|
||||
["main Windows", "push", true, "windows-latest", true, true, true, false, false],
|
||||
["main cache failure", "push", true, "macos-latest", true, false, true, false, false],
|
||||
["light main", "push", false, "ubuntu-latest", true, true, false, false, false],
|
||||
["schedule", "schedule", true, "ubuntu-latest", true, true, false, false, false],
|
||||
];
|
||||
// The sccache GitHub Actions backend is main-only, mirroring rust-cache's
|
||||
// save-if: pull requests never install or enable it (cache bloat, PLAN D).
|
||||
const sccacheInstallStep = "mozilla-actions/[email protected]";
|
||||
for (const [label, event, heavy, os, trusted, cache, execute, linuxDeps, cnb] of npmSmokeCases) {
|
||||
const ref = event === "pull_request" ? "refs/pull/1/merge" : "refs/heads/main";
|
||||
const onMain = ref === "refs/heads/main";
|
||||
const installed = execute && onMain;
|
||||
const context = {
|
||||
needs: { changes: { outputs: { heavy: String(heavy), trusted: String(trusted) } } },
|
||||
github: { event_name: event },
|
||||
github: { event_name: event, ref },
|
||||
matrix: { os },
|
||||
steps: { sccache: { outcome: cache ? "success" : "failure" } },
|
||||
steps: { sccache: { outcome: installed ? (cache ? "success" : "failure") : "skipped" } },
|
||||
};
|
||||
const jobGuard = npmSmokeJob.match(/^ if: (.+)$/m)?.[1];
|
||||
assert.ok(jobGuard, "the wrapper job must retain its event guard");
|
||||
@@ -104,7 +111,8 @@ for (const [label, event, heavy, os, trusted, cache, execute, linuxDeps, cnb] of
|
||||
if (name === "Skip npm wrapper smoke for light change") expected = !heavy;
|
||||
else if (name === "Install Linux system dependencies") expected = linuxDeps;
|
||||
else if (name === "Linux smoke location") expected = cnb;
|
||||
else if (name === "Enable sccache" || name === "sccache stats") expected = execute && cache;
|
||||
else if (name === sccacheInstallStep) expected = installed;
|
||||
else if (name === "Enable sccache" || name === "sccache stats") expected = installed && cache;
|
||||
assert.equal(
|
||||
Boolean(jobEnabled && vm.runInNewContext(guard, context)),
|
||||
expected,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Cache janitor
|
||||
|
||||
# The Actions cache held 11,099,951,127 bytes across 2,896 entries, past the
|
||||
# repo's 10 GiB cap, so GitHub evicted live main entries first. A cache is
|
||||
# readable only from its own ref and the default branch: once a PR closes or
|
||||
# a release finishes, its refs/pull/N or refs/tags/vX entries are dead weight.
|
||||
# This deletes them. Branch caches (main included) are never touched; see
|
||||
# scripts/release/prune-actions-caches.sh.
|
||||
on:
|
||||
# pull_request_target so fork PRs get a token that can delete caches. It
|
||||
# never checks out or runs PR code: the checkout below is the base branch.
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
workflow_run:
|
||||
workflows: [Release]
|
||||
types: [completed]
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: List what the sweep would delete without deleting it
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: cache-janitor-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.workflow_run.id || 'sweep' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prune:
|
||||
name: Prune dead caches
|
||||
if: github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push')
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
# Read PR state for the sweep.
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
# Base-branch script only. Never the PR head.
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
- name: Prune
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
script=scripts/release/prune-actions-caches.sh
|
||||
case "${EVENT_NAME}" in
|
||||
pull_request_target)
|
||||
"${script}" --ref "refs/pull/${PR_NUMBER}/merge" --ref "refs/pull/${PR_NUMBER}/head"
|
||||
;;
|
||||
workflow_run)
|
||||
# A tag-push Release run reports the tag as head_branch. The
|
||||
# script refuses anything that is not a refs/tags/<tag> ref.
|
||||
"${script}" --ref "refs/tags/${RUN_HEAD_BRANCH}"
|
||||
;;
|
||||
workflow_dispatch)
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
"${script}" --dry-run --sweep
|
||||
else
|
||||
"${script}" --sweep
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
"${script}" --sweep
|
||||
;;
|
||||
esac
|
||||
@@ -234,6 +234,7 @@ jobs:
|
||||
bash scripts/release/generate-release-body.test.sh
|
||||
bash scripts/release/install-dogfood.test.sh
|
||||
bash scripts/release/prepare-release.test.sh
|
||||
bash scripts/release/prune-actions-caches.test.sh
|
||||
bash scripts/release/require-release-tag-checkout.test.sh
|
||||
bash scripts/release/validate-crate-publish-order.test.sh
|
||||
python3 scripts/release/publish-crates.test.py
|
||||
@@ -325,6 +326,10 @@ jobs:
|
||||
toolchain: stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# The GitHub Actions cache backend is main-only, mirroring
|
||||
# rust-cache's save-if: PR runs wrote thousands of refs/pull/N
|
||||
# entries that pushed the repo past its 10 GiB cache cap.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
@@ -371,7 +376,8 @@ jobs:
|
||||
# Cache bootstrap failures (e.g. GitHub 504s fetching the sccache
|
||||
# binary) degrade to an uncached build instead of failing product CI.
|
||||
continue-on-error: true
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && github.ref == 'refs/heads/main'
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
@@ -578,6 +584,8 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
@@ -666,7 +674,8 @@ jobs:
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
@@ -732,8 +741,11 @@ jobs:
|
||||
# The Ubuntu lint lane validates non-RSS backlog fields. Run the same
|
||||
# source-bound measurement on macOS so loss or growth of RSS evidence
|
||||
# fails closed instead of becoming an unsupported-field skip.
|
||||
# Trusted events only: here it reuses the warm self-hosted build. Fork
|
||||
# PRs run it in the separate `macos-budget` job with its own timeout,
|
||||
# because on a cold hosted Mac it pushed Test past 90 minutes.
|
||||
- name: Check persistence-backlog RSS budget
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true'
|
||||
run: python3 scripts/check-persistence-backlog-budget.py
|
||||
- name: Lockfile drift guard
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
@@ -742,8 +754,9 @@ jobs:
|
||||
# The eval harness is OS-independent prompt/composition checking;
|
||||
# running it once (on the faster macOS leg, warm from the test build)
|
||||
# instead of once per desktop OS keeps the coverage while taking
|
||||
# ~2min off the Windows critical path.
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
# ~2min off the Windows critical path. Trusted events only; fork PRs
|
||||
# run it in `macos-budget` (see the RSS step above).
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true'
|
||||
run: cargo run -p codewhale-tui --all-features -- eval
|
||||
- name: sccache stats
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
@@ -754,6 +767,31 @@ jobs:
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request'
|
||||
run: echo "Linux workspace tests run on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu."
|
||||
|
||||
macos-budget:
|
||||
# Fork PRs build cold on a GitHub-hosted Mac, and the RSS budget and the
|
||||
# offline eval each rebuild codewhale-tui there. Inside Test that cost
|
||||
# cancelled fork PRs at the 90-minute limit (jobs 106749104684 and
|
||||
# 106235312282) before Test could report. Running both here, in parallel
|
||||
# with Test and under their own timeout, keeps the coverage without
|
||||
# holding the required Test (macos-latest) context hostage. Trusted
|
||||
# events run the same two steps inside Test on the warm build instead.
|
||||
name: macOS budget and eval (fork PR)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.changes.outputs.trusted != 'true'
|
||||
timeout-minutes: 75
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
save-if: false
|
||||
- name: Check persistence-backlog RSS budget
|
||||
run: python3 scripts/check-persistence-backlog-budget.py
|
||||
- name: Run Offline Eval Harness
|
||||
run: cargo run -p codewhale-tui --all-features -- eval
|
||||
|
||||
npm-wrapper-smoke:
|
||||
name: npm wrapper smoke
|
||||
needs: changes
|
||||
@@ -781,7 +819,8 @@ jobs:
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
@@ -847,6 +886,8 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# Delete GitHub Actions caches that no future run can use.
|
||||
#
|
||||
# Usage:
|
||||
# prune-actions-caches.sh [--dry-run] --ref <git-ref> [--ref <git-ref> ...]
|
||||
# prune-actions-caches.sh [--dry-run] --sweep
|
||||
#
|
||||
# --ref deletes every cache saved under that exact ref (for example
|
||||
# refs/pull/123/merge or refs/tags/v1.2.3).
|
||||
#
|
||||
# --sweep deletes caches under refs/pull/N/* whose PR is closed, and caches
|
||||
# under refs/tags/* last used more than a day ago (a finished release run
|
||||
# never reads them again; the day keeps an in-flight release's own cache).
|
||||
# Branch caches, including main, are never touched.
|
||||
#
|
||||
# A cache is only readable from its own ref and the default branch, so a
|
||||
# closed PR's or a released tag's entries are dead weight that pushes live
|
||||
# main entries out once the repo passes its 10 GiB cap.
|
||||
#
|
||||
# Needs GH_REPO=owner/name and gh authenticated with actions:write.
|
||||
# PRUNE_CACHES_GH overrides the gh executable and PRUNE_CACHES_NOW the epoch
|
||||
# clock (tests only).
|
||||
set -euo pipefail
|
||||
|
||||
gh_bin="${PRUNE_CACHES_GH:-gh}"
|
||||
now="${PRUNE_CACHES_NOW:-$(date +%s)}"
|
||||
tag_min_age_seconds=86400
|
||||
dry_run=false
|
||||
sweep=false
|
||||
refs=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=true ;;
|
||||
--sweep) sweep=true ;;
|
||||
--ref)
|
||||
[[ $# -ge 2 ]] || { echo "--ref needs a value" >&2; exit 2; }
|
||||
refs+=("$2")
|
||||
shift
|
||||
;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
repo="${GH_REPO:-}"
|
||||
if ! [[ "${repo}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "GH_REPO must be owner/name, got '${repo}'" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "${sweep}" == false && ${#refs[@]} -eq 0 ]]; then
|
||||
echo "usage: $0 [--dry-run] (--ref <ref>... | --sweep)" >&2
|
||||
exit 2
|
||||
fi
|
||||
for ref in ${refs[@]+"${refs[@]}"}; do
|
||||
# Only PR and tag refs are prunable by name; never a branch.
|
||||
if ! [[ "${ref}" =~ ^refs/pull/[0-9]+/(merge|head)$ || "${ref}" =~ ^refs/tags/[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "refusing to prune caches for '${ref}': only refs/pull/N/{merge,head} and refs/tags/<tag> are allowed" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
deleted=0
|
||||
bytes=0
|
||||
|
||||
delete_cache() {
|
||||
local id="$1" ref="$2" size="$3"
|
||||
if [[ "${dry_run}" == true ]]; then
|
||||
echo "would delete cache ${id} (${ref}, ${size} bytes)"
|
||||
else
|
||||
"${gh_bin}" api -X DELETE "repos/${repo}/actions/caches/${id}" >/dev/null
|
||||
echo "deleted cache ${id} (${ref}, ${size} bytes)"
|
||||
fi
|
||||
deleted=$((deleted + 1))
|
||||
bytes=$((bytes + size))
|
||||
}
|
||||
|
||||
# id, ref, size, last-accessed epoch (fractional seconds stripped for jq).
|
||||
list_caches() {
|
||||
local query="$1"
|
||||
"${gh_bin}" api --paginate "repos/${repo}/actions/caches?per_page=100${query}" \
|
||||
--jq '.actions_caches[] | [.id, .ref, .size_in_bytes, (.last_accessed_at | sub("\\.[0-9]+"; "") | fromdateiso8601)] | @tsv'
|
||||
}
|
||||
|
||||
# Each listing is read in full before deleting, so deletes never shift the
|
||||
# pages still to be fetched, and a failed listing stops the script (set -e).
|
||||
for ref in ${refs[@]+"${refs[@]}"}; do
|
||||
listing="$(list_caches "&ref=${ref}")"
|
||||
while IFS=$'\t' read -r id cache_ref size _; do
|
||||
[[ -n "${id}" ]] || continue
|
||||
[[ "${cache_ref}" == "${ref}" ]] || continue
|
||||
delete_cache "${id}" "${cache_ref}" "${size}"
|
||||
done <<< "${listing}"
|
||||
done
|
||||
|
||||
if [[ "${sweep}" == true ]]; then
|
||||
# "N=state" lines; bash 3.2 (macOS) has no associative arrays.
|
||||
pr_states=""
|
||||
listing="$(list_caches "")"
|
||||
while IFS=$'\t' read -r id cache_ref size accessed; do
|
||||
[[ -n "${id}" ]] || continue
|
||||
if [[ "${cache_ref}" =~ ^refs/pull/([0-9]+)/(merge|head)$ ]]; then
|
||||
pr="${BASH_REMATCH[1]}"
|
||||
state="$(printf '%s' "${pr_states}" | sed -n "s/^${pr}=//p")"
|
||||
if [[ -z "${state}" ]]; then
|
||||
state="$("${gh_bin}" api "repos/${repo}/pulls/${pr}" --jq '.state')"
|
||||
pr_states="${pr_states}${pr}=${state}"$'\n'
|
||||
fi
|
||||
if [[ "${state}" == "closed" ]]; then
|
||||
delete_cache "${id}" "${cache_ref}" "${size}"
|
||||
fi
|
||||
elif [[ "${cache_ref}" =~ ^refs/tags/ ]]; then
|
||||
if (( now - accessed > tag_min_age_seconds )); then
|
||||
delete_cache "${id}" "${cache_ref}" "${size}"
|
||||
fi
|
||||
fi
|
||||
done <<< "${listing}"
|
||||
fi
|
||||
|
||||
verb="deleted"
|
||||
[[ "${dry_run}" == true ]] && verb="would delete"
|
||||
echo "${verb} ${deleted} caches, ${bytes} bytes"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
script="${repo_root}/scripts/release/prune-actions-caches.sh"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmp_dir}"' EXIT
|
||||
|
||||
# Fake gh: serves the cache listing (filtered by &ref= like the real API),
|
||||
# PR states, and records DELETE calls. The caller's --jq filter runs through
|
||||
# the real jq so the timestamp parsing is exercised too.
|
||||
fake_gh="${tmp_dir}/gh"
|
||||
cat > "${fake_gh}" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
[[ "$1" == "api" ]] || { echo "unexpected: $*" >&2; exit 9; }
|
||||
shift
|
||||
if [[ "$1" == "-X" ]]; then
|
||||
[[ "$2" == "DELETE" ]] || exit 9
|
||||
echo "$3" >> "${FIXTURE_DIR}/deleted"
|
||||
exit 0
|
||||
fi
|
||||
[[ "$1" == "--paginate" ]] && shift
|
||||
path="$1"
|
||||
filter="$3"
|
||||
case "${path}" in
|
||||
*/actions/caches\?*)
|
||||
ref=""
|
||||
if [[ "${path}" == *"&ref="* ]]; then ref="${path#*&ref=}"; fi
|
||||
jq --arg ref "${ref}" '{actions_caches: [.actions_caches[] | select($ref == "" or .ref == $ref)]}' \
|
||||
"${FIXTURE_DIR}/caches.json" | jq -r "${filter}"
|
||||
;;
|
||||
*/pulls/*)
|
||||
pr="${path##*/}"
|
||||
jq ".\"${pr}\"" "${FIXTURE_DIR}/pulls.json" | jq -r "${filter}"
|
||||
;;
|
||||
*) echo "unexpected path: ${path}" >&2; exit 9 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${fake_gh}"
|
||||
export PRUNE_CACHES_GH="${fake_gh}"
|
||||
export FIXTURE_DIR="${tmp_dir}"
|
||||
export GH_REPO="owner/repo"
|
||||
# 2026-09-22T12:00:00Z
|
||||
export PRUNE_CACHES_NOW=1790078400
|
||||
|
||||
cat > "${tmp_dir}/caches.json" <<'EOF'
|
||||
{"actions_caches":[
|
||||
{"id":1,"ref":"refs/pull/10/merge","size_in_bytes":100,"last_accessed_at":"2026-09-22T11:00:00.123Z"},
|
||||
{"id":2,"ref":"refs/pull/10/merge","size_in_bytes":200,"last_accessed_at":"2026-09-22T11:30:00Z"},
|
||||
{"id":3,"ref":"refs/pull/11/merge","size_in_bytes":400,"last_accessed_at":"2026-09-22T11:00:00Z"},
|
||||
{"id":4,"ref":"refs/heads/main","size_in_bytes":800,"last_accessed_at":"2026-09-01T00:00:00Z"},
|
||||
{"id":5,"ref":"refs/tags/v0.10.0","size_in_bytes":1600,"last_accessed_at":"2026-09-20T00:00:00.5Z"},
|
||||
{"id":6,"ref":"refs/tags/v0.10.1","size_in_bytes":3200,"last_accessed_at":"2026-09-22T10:00:00Z"}
|
||||
]}
|
||||
EOF
|
||||
echo '{"10":{"state":"closed"},"11":{"state":"open"}}' > "${tmp_dir}/pulls.json"
|
||||
|
||||
deleted_ids() {
|
||||
if [[ -f "${tmp_dir}/deleted" ]]; then
|
||||
sed 's#.*/##' "${tmp_dir}/deleted" | sort -n | tr '\n' ' '
|
||||
fi
|
||||
rm -f "${tmp_dir}/deleted"
|
||||
}
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
# 1. --ref deletes exactly that ref's caches.
|
||||
"${script}" --ref refs/pull/10/merge > "${tmp_dir}/out"
|
||||
[[ "$(deleted_ids)" == "1 2 " ]] || fail "--ref pull/10 deleted the wrong set"
|
||||
grep -q "deleted 2 caches, 300 bytes" "${tmp_dir}/out" || fail "--ref summary"
|
||||
|
||||
# 2. --sweep: closed PR 10 and the day-old tag go; open PR 11, main and the
|
||||
# fresh tag stay.
|
||||
"${script}" --sweep > "${tmp_dir}/out"
|
||||
[[ "$(deleted_ids)" == "1 2 5 " ]] || fail "--sweep deleted the wrong set"
|
||||
|
||||
# 3. --dry-run deletes nothing but reports the same set.
|
||||
"${script}" --dry-run --sweep > "${tmp_dir}/out"
|
||||
[[ -z "$(deleted_ids)" ]] || fail "--dry-run deleted caches"
|
||||
grep -q "would delete 3 caches, 1900 bytes" "${tmp_dir}/out" || fail "--dry-run summary"
|
||||
|
||||
# 4. Branch refs are refused outright.
|
||||
if "${script}" --ref refs/heads/main > "${tmp_dir}/out" 2>&1; then
|
||||
fail "a branch ref was accepted"
|
||||
fi
|
||||
[[ -z "$(deleted_ids)" ]] || fail "a refused ref still deleted caches"
|
||||
|
||||
# 5. A tag ref deletes only that tag.
|
||||
"${script}" --ref refs/tags/v0.10.1 > "${tmp_dir}/out"
|
||||
[[ "$(deleted_ids)" == "6 " ]] || fail "--ref tag deleted the wrong set"
|
||||
|
||||
echo "prune-actions-caches tests passed"
|
||||
Reference in new issue
Block a user