Skip to content

Commit 57473fb

Browse files
elysia-bestQwen
andcommitted
feat(actions): add workflow to clean up untagged Docker images
Signed-off-by: elysia <a.elysia@proton.me> Co-Authored-By: Qwen <qwen@alibabacloud.com>
1 parent f869afc commit 57473fb

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
name: Container Registry Cleanup
2+
3+
permissions:
4+
contents: read
5+
packages: write
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
package:
11+
description: 'The name of the package to clean up'
12+
required: true
13+
default: 'openlist-git'
14+
owner:
15+
description: 'The owner of the package (organization or user)'
16+
required: false
17+
default: 'openlistteam'
18+
older_than:
19+
description: 'Delete untagged images older than this many days'
20+
required: false
21+
default: '-1'
22+
untagged_timestamp_tolerance:
23+
description: 'Tolerance in milliseconds for untagged images close to tagged ones'
24+
required: false
25+
# We do this because multi-arch docker containers will be pushed as separate untagged images
26+
# to the container registry and GitHub cannot recognize them as part of a tagged image.
27+
# Setting this option large enough prevents accidental deletion of one of these images.
28+
# Here we set it to 30 seconds
29+
default: '10000'
30+
token:
31+
description: 'GitHub token with permissions to delete packages (leave empty to use default GITHUB_TOKEN)'
32+
required: false
33+
default: ''
34+
schedule:
35+
- cron: '30 2 * * MON'
36+
37+
jobs:
38+
cleanup:
39+
runs-on: ubuntu-slim
40+
41+
steps:
42+
- name: Check inputs and tools
43+
id: setup
44+
shell: bash
45+
run: |
46+
PACKAGE="${{ github.event.inputs.package || 'openlist-git' }}"
47+
OLDER="${{ github.event.inputs.older_than || '7' }}"
48+
TOLERANCE_VAL="${{ github.event.inputs.untagged_timestamp_tolerance || '0' }}"
49+
TOKEN_INPUT="${{ github.event.inputs.token || '' }}"
50+
OWNER="${{ github.event.inputs.owner || '' }}"
51+
52+
echo "package_name=$PACKAGE" >> "$GITHUB_OUTPUT"
53+
echo "older_than=$OLDER" >> "$GITHUB_OUTPUT"
54+
echo "tolerance=$TOLERANCE_VAL" >> "$GITHUB_OUTPUT"
55+
echo "owner=$OWNER" >> "$GITHUB_OUTPUT"
56+
57+
if [[ -z "$TOKEN_INPUT" ]]; then
58+
echo "::notice:: No token provided, using GITHUB_TOKEN instead."
59+
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
60+
else
61+
echo "::add-mask::$TOKEN_INPUT"
62+
echo "token=$TOKEN_INPUT" >> "$GITHUB_OUTPUT"
63+
fi
64+
65+
if [[ -z "$PACKAGE" || -z "$OWNER" || ( -z "$TOKEN_INPUT" && -z "${{ secrets.GITHUB_TOKEN }}" ) ]]; then
66+
echo "::error:: Missing required inputs (package, token, or owner context)."
67+
exit 1
68+
fi
69+
70+
command -v curl >/dev/null 2>&1 || { echo "::error::curl is required but not installed."; exit 1; }
71+
command -v jq >/dev/null 2>&1 || { echo "::error::jq is required but not installed."; exit 1; }
72+
73+
- name: Access all versions from GitHub
74+
id: fetch
75+
shell: bash
76+
run: |
77+
PACKAGE_NAME="${{ steps.setup.outputs.package_name }}"
78+
OWNER="${{ steps.setup.outputs.owner }}"
79+
TOKEN="${{ steps.setup.outputs.token }}"
80+
81+
BASE_URL="https://api.github.com/orgs/${OWNER}/packages/container/${PACKAGE_NAME}/versions"
82+
URL="${BASE_URL}?per_page=100"
83+
TEMP_FILE="/tmp/all_versions.ndjson"
84+
85+
> "$TEMP_FILE"
86+
87+
while [[ -n "$URL" ]]; do
88+
echo "Fetching: $URL"
89+
90+
curl -s -H "Authorization: token $TOKEN" \
91+
-H "Accept: application/vnd.github.v3+json" \
92+
-D headers.txt \
93+
"$URL" > page.json
94+
95+
jq -c '.[]' page.json >> "$TEMP_FILE"
96+
97+
URL=$(grep -i '^link:' headers.txt | grep -o '<[^>]*>;\s*rel="next"' | sed 's/<\(.*\)>.*/\1/' || true)
98+
done
99+
rm -f headers.txt page.json
100+
101+
echo "temp_file=$TEMP_FILE" >> "$GITHUB_OUTPUT"
102+
103+
- name: Filter untagged versions
104+
id: filter
105+
shell: bash
106+
run: |
107+
OLDER_THAN="${{ steps.setup.outputs.older_than }}"
108+
TOLERANCE="${{ steps.setup.outputs.tolerance }}"
109+
TEMP_FILE="${{ steps.fetch.outputs.temp_file }}"
110+
111+
TO_DELETE=$(jq -c -s \
112+
--arg older_than "$OLDER_THAN" \
113+
--arg tolerance "$TOLERANCE" \
114+
'
115+
def abs: if . < 0 then -. else . end;
116+
def to_ms: sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime * 1000;
117+
118+
. as $all |
119+
($older_than | tonumber) as $older_than_val |
120+
($tolerance | tonumber) as $tolerance_val |
121+
now as $now |
122+
(if $older_than_val > 0 then ($now - ($older_than_val * 86400)) * 1000 else 0 end) as $cutoff_ms |
123+
124+
[ $all[] | select((.metadata.container.tags // []) | length > 0) | .created_at | to_ms ] as $tagged_times |
125+
126+
$all[] |
127+
select((.metadata.container.tags // []) | length == 0) |
128+
. as $item |
129+
(.created_at | to_ms) as $created_ms |
130+
131+
(if $older_than_val > 0 then $created_ms < $cutoff_ms else true end) as $pass_age |
132+
133+
(if $tolerance_val <= 0 then true
134+
else [ $tagged_times[] | select(($created_ms - .) | abs < $tolerance_val) ] | length == 0
135+
end) as $pass_tolerance |
136+
137+
select($pass_age and $pass_tolerance) |
138+
{id, name}
139+
' "$TEMP_FILE")
140+
141+
rm -f "$TEMP_FILE"
142+
143+
if [[ -z "$TO_DELETE" ]]; then
144+
DELETE_COUNT=0
145+
else
146+
DELETE_COUNT=$(echo "$TO_DELETE" | wc -l | tr -d ' ')
147+
fi
148+
149+
echo "Found $DELETE_COUNT untagged images no longer necessary"
150+
151+
{
152+
echo 'to_delete<<EOF'
153+
echo "$TO_DELETE"
154+
echo 'EOF'
155+
} >> "$GITHUB_OUTPUT"
156+
157+
echo "delete_count=$DELETE_COUNT" >> "$GITHUB_OUTPUT"
158+
159+
- name: Delete untagged versions
160+
id: delete
161+
shell: bash
162+
run: |
163+
PACKAGE_NAME="${{ steps.setup.outputs.package_name }}"
164+
OWNER="${{ steps.setup.outputs.owner }}"
165+
TOKEN="${{ steps.setup.outputs.token }}"
166+
DELETE_COUNT="${{ steps.filter.outputs.delete_count }}"
167+
TO_DELETE='${{ steps.filter.outputs.to_delete }}'
168+
169+
if [[ "$DELETE_COUNT" -gt 0 && -n "$TO_DELETE" ]]; then
170+
while IFS= read -r item; do
171+
[[ -z "$item" ]] && continue
172+
173+
# $item 是完整的单行 JSON,如 {"id":123,"name":"sha256:..."}
174+
ID=$(echo "$item" | jq -r '.id')
175+
NAME=$(echo "$item" | jq -r '.name')
176+
177+
DELETE_URL="https://api.github.com/orgs/${OWNER}/packages/container/${PACKAGE_NAME}/versions/${ID}"
178+
179+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
180+
-H "Authorization: token $TOKEN" \
181+
-H "Accept: application/vnd.github.v3+json" \
182+
"$DELETE_URL")
183+
184+
if [[ "$HTTP_CODE" == "204" ]]; then
185+
echo "Deleted untagged container image '${NAME}'"
186+
else
187+
echo "::warning:: Failed to delete ${NAME} (ID: ${ID}). HTTP Status: ${HTTP_CODE}"
188+
fi
189+
done <<< "$TO_DELETE"
190+
else
191+
echo "No images to delete."
192+
fi
193+
194+
echo "Cleanup completed."

0 commit comments

Comments
 (0)