vouchington-tooling 0.49.0 → 0.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.1",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
# Sourced by ghcr-package-retention.sh for --main-history. Expects package_root, packages, apply,
|
|
3
3
|
# keep_main and min_age_days from the caller.
|
|
4
4
|
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
. "$(dirname "${BASH_SOURCE[0]}")/ghcr-referenced-digests.sh"
|
|
7
|
+
|
|
5
8
|
prune_main_history() {
|
|
9
|
+
# The registry namespace of the packages: GHCR_OWNER, else the workflow's repository owner.
|
|
10
|
+
local owner="${GHCR_OWNER:-${GITHUB_REPOSITORY_OWNER:-}}"
|
|
11
|
+
if [ -z "$owner" ]; then
|
|
12
|
+
echo '::error::ghcr-package-retention: GHCR_OWNER is required with --main-history' >&2
|
|
13
|
+
exit 2
|
|
14
|
+
fi
|
|
15
|
+
|
|
6
16
|
# A zero keep count would delete every main image in one run.
|
|
7
17
|
case "$keep_main" in
|
|
8
18
|
'' | *[!0-9]*) keep_main=0 ;;
|
|
@@ -49,10 +59,13 @@ prune_main_history() {
|
|
|
49
59
|
| min) as $oldest_image
|
|
50
60
|
| ($versions | map(select(attachment and idle and $oldest_image != null
|
|
51
61
|
and created < $oldest_image - 86400) | . + {reason: "orphan"})) as $orphans
|
|
52
|
-
| ($
|
|
53
|
-
|
|
62
|
+
| {roots: ($versions | map(select((attachment | not) and (.id | IN($doomed[]) | not)) | .name)),
|
|
63
|
+
candidates: ($old_main + $unmerged + $orphans | map({id, name, reason,
|
|
64
|
+
tags: (tags | if length == 0 then "<untagged>" else join(",") end)}))}'
|
|
65
|
+
local filter='.candidates | map(select(.name | IN($referenced[]) | not))[]
|
|
66
|
+
| "\(.id) \(.tags) \(.reason)"'
|
|
54
67
|
|
|
55
|
-
local deleted_total=0 package path error deletions id tags reason
|
|
68
|
+
local deleted_total=0 package path error deletions id tags reason referenced image resolved
|
|
56
69
|
for package in "${packages[@]}"; do
|
|
57
70
|
path="$package_root/$package/versions"
|
|
58
71
|
if ! error=$(gh api --paginate "$path?per_page=100" 2>&1 > "$work/pages"); then
|
|
@@ -67,8 +80,18 @@ prune_main_history() {
|
|
|
67
80
|
exit 1
|
|
68
81
|
fi
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
--argjson cutoff "$cutoff" "$plan" "$work/pages"
|
|
83
|
+
jq -s --rawfile revisions "$work/main-revisions" --argjson keep "$keep_main" \
|
|
84
|
+
--argjson cutoff "$cutoff" "$plan" "$work/pages" > "$work/plan"
|
|
85
|
+
# Never delete a digest that a kept image or index still references (platform manifests,
|
|
86
|
+
# attestations, subjects). An unreadable kept manifest aborts rather than risk a delete.
|
|
87
|
+
referenced='[]'
|
|
88
|
+
if [ "$(jq '.candidates | length' "$work/plan")" -gt 0 ]; then
|
|
89
|
+
jq -r '.roots[]' "$work/plan" > "$work/roots"
|
|
90
|
+
image="$owner/$(printf '%b' "${package//%/\\x}")"
|
|
91
|
+
resolved=$(resolve_referenced "$image" "$owner" "$work" "$work/roots") || exit 1
|
|
92
|
+
referenced=$(printf '%s\n' "$resolved" | jq -R . | jq -s .)
|
|
93
|
+
fi
|
|
94
|
+
deletions=$(jq -r --argjson referenced "$referenced" "$filter" "$work/plan")
|
|
72
95
|
echo "· $package: $(jq -s 'add // [] | length' "$work/pages") version(s)"
|
|
73
96
|
while read -r id tags reason; do
|
|
74
97
|
[ -n "$id" ] || continue
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
# - An attachment: untagged or tagged only sha256-*. Attestations and their referrer indexes are
|
|
13
13
|
# pushed after their image, and platform manifests moments before it, so an attachment created
|
|
14
14
|
# over a day before the oldest surviving image, and idle for the same age, belongs to none.
|
|
15
|
+
# Before deleting, the manifests of every surviving image are read from the registry (GH_TOKEN or
|
|
16
|
+
# GITHUB_TOKEN, else gh auth token) and anything they reference through manifests[] or subject is
|
|
17
|
+
# kept; if one cannot be read, the package is left untouched. The registry namespace is GHCR_OWNER,
|
|
18
|
+
# else GITHUB_REPOSITORY_OWNER (required for user-owned packages too).
|
|
15
19
|
# Packages that do not exist (HTTP 404) are skipped. Defaults to a dry run; pass --apply to delete.
|
|
16
20
|
#
|
|
17
21
|
# Packages come from the arguments, or from GHCR_PACKAGES (space separated) when none are given.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced by ghcr-main-history.sh. Walks OCI manifests through manifests[] and subject.
|
|
3
|
+
|
|
4
|
+
# registry_token <image> <owner> <work-dir>: exchanges the GitHub token for a registry pull token
|
|
5
|
+
# (GHCR's documented /token endpoint, Basic auth) and prints it. Credentials go to curl on stdin.
|
|
6
|
+
registry_token() {
|
|
7
|
+
local image="$1" owner="$2" dir="$3" token registry
|
|
8
|
+
registry="${GHCR_REGISTRY_URL:-https://ghcr.io}"
|
|
9
|
+
token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
10
|
+
if [ -z "$token" ]; then
|
|
11
|
+
token=$(gh auth token) || {
|
|
12
|
+
echo '::error::No GH_TOKEN or GITHUB_TOKEN, and gh auth token failed' >&2
|
|
13
|
+
return 1
|
|
14
|
+
}
|
|
15
|
+
fi
|
|
16
|
+
if ! printf 'user = "%s:%s"\n' "$owner" "$token" |
|
|
17
|
+
curl -fsS -K - "$registry/token?scope=repository:$image:pull" \
|
|
18
|
+
> "$dir/token.json" 2> "$dir/curl-error"; then
|
|
19
|
+
printf '::error::Could not get a registry token for %s: %s\n' "$image" \
|
|
20
|
+
"$(cat "$dir/curl-error")" >&2
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
23
|
+
jq -er '.token | select(type == "string" and . != "")' "$dir/token.json" || {
|
|
24
|
+
printf '::error::Registry token response for %s had no token\n' "$image" >&2
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# resolve_referenced <image> <owner> <work-dir> <roots-file>: prints every digest reachable from
|
|
30
|
+
# the root digests (one per line in the file), roots included. Returns non-zero on any failure, so
|
|
31
|
+
# the caller deletes nothing.
|
|
32
|
+
resolve_referenced() {
|
|
33
|
+
local image="$1" owner="$2" dir="$3" digest registry_bearer registry accept
|
|
34
|
+
registry="${GHCR_REGISTRY_URL:-https://ghcr.io}"
|
|
35
|
+
registry_bearer=$(registry_token "$image" "$owner" "$dir") || return 1
|
|
36
|
+
accept='application/vnd.oci.image.index.v1+json'
|
|
37
|
+
accept="$accept,application/vnd.oci.image.manifest.v1+json"
|
|
38
|
+
accept="$accept,application/vnd.docker.distribution.manifest.list.v2+json"
|
|
39
|
+
accept="$accept,application/vnd.docker.distribution.manifest.v2+json"
|
|
40
|
+
|
|
41
|
+
: > "$dir/seen"
|
|
42
|
+
cp "$4" "$dir/queue" || return 1
|
|
43
|
+
while [ -s "$dir/queue" ]; do
|
|
44
|
+
digest=$(head -n 1 "$dir/queue")
|
|
45
|
+
sed -i.bak 1d "$dir/queue" || return 1
|
|
46
|
+
rm -f "$dir/queue.bak"
|
|
47
|
+
[ -n "$digest" ] || continue
|
|
48
|
+
! grep -qxF "$digest" "$dir/seen" || continue
|
|
49
|
+
echo "$digest" >> "$dir/seen"
|
|
50
|
+
if ! printf 'header = "Authorization: Bearer %s"\n' "$registry_bearer" |
|
|
51
|
+
curl -fsS -K - -H "Accept: $accept" "$registry/v2/$image/manifests/$digest" \
|
|
52
|
+
> "$dir/manifest" 2> "$dir/curl-error"; then
|
|
53
|
+
printf '::error::Could not read manifest %s of %s: %s\n' "$digest" "$image" \
|
|
54
|
+
"$(cat "$dir/curl-error")" >&2
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
if ! jq -r '[(.manifests // [])[].digest, (.subject.digest // empty)] | .[]' \
|
|
58
|
+
"$dir/manifest" >> "$dir/queue" 2> "$dir/jq-error"; then
|
|
59
|
+
printf '::error::Could not parse manifest %s of %s: %s\n' "$digest" "$image" \
|
|
60
|
+
"$(cat "$dir/jq-error")" >&2
|
|
61
|
+
return 1
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
cat "$dir/seen"
|
|
65
|
+
}
|