返回 CodeWhale
prune-actions-caches.sh
根目录 / scripts / release / prune-actions-caches.sh
1 #!/usr/bin/env bash
2 # Delete GitHub Actions caches that no future run can use.
3 #
4 # Usage:
5 # prune-actions-caches.sh [--dry-run] --ref <git-ref> [--ref <git-ref> ...]
6 # prune-actions-caches.sh [--dry-run] --sweep
7 #
8 # --ref deletes every cache saved under that exact ref (for example
9 # refs/pull/123/merge or refs/tags/v1.2.3).
10 #
11 # --sweep deletes caches under refs/pull/N/* whose PR is closed, and caches
12 # under refs/tags/* last used more than a day ago (a finished release run
13 # never reads them again; the day keeps an in-flight release's own cache).
14 # Branch caches, including main, are never touched.
15 #
16 # A cache is only readable from its own ref and the default branch, so a
17 # closed PR's or a released tag's entries are dead weight that pushes live
18 # main entries out once the repo passes its 10 GiB cap.
19 #
20 # Needs GH_REPO=owner/name and gh authenticated with actions:write.
21 # PRUNE_CACHES_GH overrides the gh executable and PRUNE_CACHES_NOW the epoch
22 # clock (tests only).
23 set -euo pipefail
24
25 gh_bin="${PRUNE_CACHES_GH:-gh}"
26 now="${PRUNE_CACHES_NOW:-$(date +%s)}"
27 tag_min_age_seconds=86400
28 dry_run=false
29 sweep=false
30 refs=()
31
32 while [[ $# -gt 0 ]]; do
33 case "$1" in
34 --dry-run) dry_run=true ;;
35 --sweep) sweep=true ;;
36 --ref)
37 [[ $# -ge 2 ]] || { echo "--ref needs a value" >&2; exit 2; }
38 refs+=("$2")
39 shift
40 ;;
41 *) echo "unknown argument: $1" >&2; exit 2 ;;
42 esac
43 shift
44 done
45
46 repo="${GH_REPO:-}"
47 if ! [[ "${repo}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
48 echo "GH_REPO must be owner/name, got '${repo}'" >&2
49 exit 2
50 fi
51 if [[ "${sweep}" == false && ${#refs[@]} -eq 0 ]]; then
52 echo "usage: $0 [--dry-run] (--ref <ref>... | --sweep)" >&2
53 exit 2
54 fi
55 for ref in ${refs[@]+"${refs[@]}"}; do
56 # Only PR and tag refs are prunable by name; never a branch.
57 if ! [[ "${ref}" =~ ^refs/pull/[0-9]+/(merge|head)$ || "${ref}" =~ ^refs/tags/[A-Za-z0-9._-]+$ ]]; then
58 echo "refusing to prune caches for '${ref}': only refs/pull/N/{merge,head} and refs/tags/<tag> are allowed" >&2
59 exit 2
60 fi
61 done
62
63 deleted=0
64 bytes=0
65
66 delete_cache() {
67 local id="$1" ref="$2" size="$3"
68 if [[ "${dry_run}" == true ]]; then
69 echo "would delete cache ${id} (${ref}, ${size} bytes)"
70 else
71 "${gh_bin}" api -X DELETE "repos/${repo}/actions/caches/${id}" >/dev/null
72 echo "deleted cache ${id} (${ref}, ${size} bytes)"
73 fi
74 deleted=$((deleted + 1))
75 bytes=$((bytes + size))
76 }
77
78 # id, ref, size, last-accessed epoch (fractional seconds stripped for jq).
79 list_caches() {
80 local query="$1"
81 "${gh_bin}" api --paginate "repos/${repo}/actions/caches?per_page=100${query}" \
82 --jq '.actions_caches[] | [.id, .ref, .size_in_bytes, (.last_accessed_at | sub("\\.[0-9]+"; "") | fromdateiso8601)] | @tsv'
83 }
84
85 # Each listing is read in full before deleting, so deletes never shift the
86 # pages still to be fetched, and a failed listing stops the script (set -e).
87 for ref in ${refs[@]+"${refs[@]}"}; do
88 listing="$(list_caches "&ref=${ref}")"
89 while IFS=$'\t' read -r id cache_ref size _; do
90 [[ -n "${id}" ]] || continue
91 [[ "${cache_ref}" == "${ref}" ]] || continue
92 delete_cache "${id}" "${cache_ref}" "${size}"
93 done <<< "${listing}"
94 done
95
96 if [[ "${sweep}" == true ]]; then
97 # "N=state" lines; bash 3.2 (macOS) has no associative arrays.
98 pr_states=""
99 listing="$(list_caches "")"
100 while IFS=$'\t' read -r id cache_ref size accessed; do
101 [[ -n "${id}" ]] || continue
102 if [[ "${cache_ref}" =~ ^refs/pull/([0-9]+)/(merge|head)$ ]]; then
103 pr="${BASH_REMATCH[1]}"
104 state="$(printf '%s' "${pr_states}" | sed -n "s/^${pr}=//p")"
105 if [[ -z "${state}" ]]; then
106 state="$("${gh_bin}" api "repos/${repo}/pulls/${pr}" --jq '.state')"
107 pr_states="${pr_states}${pr}=${state}"$'\n'
108 fi
109 if [[ "${state}" == "closed" ]]; then
110 delete_cache "${id}" "${cache_ref}" "${size}"
111 fi
112 elif [[ "${cache_ref}" =~ ^refs/tags/ ]]; then
113 if (( now - accessed > tag_min_age_seconds )); then
114 delete_cache "${id}" "${cache_ref}" "${size}"
115 fi
116 fi
117 done <<< "${listing}"
118 fi
119
120 verb="deleted"
121 [[ "${dry_run}" == true ]] && verb="would delete"
122 echo "${verb} ${deleted} caches, ${bytes} bytes"
123
123 lines BASH