Skip to content

Commit 09784cf

Browse files
committed
#369 Clean up untagged packages also
and enable optional dry-run (default true)
1 parent 19a95a3 commit 09784cf

File tree

1 file changed

+68
-22
lines changed

1 file changed

+68
-22
lines changed

.github/workflows/cleanup-ghcr.yml

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ on:
99
description: 'Delete images older than this many days'
1010
required: false
1111
default: '14'
12+
dry_run:
13+
description: 'If true, only print which versions would be deleted (no deletion performed)'
14+
required: false
15+
type: boolean
16+
default: true
1217

1318
permissions:
1419
contents: read
@@ -30,11 +35,18 @@ jobs:
3035
const package_name = 'hsc';
3136
const per_page = 100;
3237
const tsTagRegex = /^\d{14}$/; // yyyyMMddHHmmss
38+
const sha256Regex = /^[a-f0-9]{64}$/i; // sha256 digest-like tag
39+
3340
const daysInput = (context.payload && context.payload.inputs && context.payload.inputs.retention_days) || '14';
3441
const daysParsed = parseInt(daysInput, 10);
3542
const retentionDays = Number.isFinite(daysParsed) && daysParsed > 0 ? daysParsed : 14;
3643
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); // retentionDays days ago
37-
core.info(`Using retention period: ${retentionDays} day(s).`);
44+
45+
const dryRunInput = context.payload && context.payload.inputs ? context.payload.inputs.dry_run : undefined;
46+
const dryRun = (typeof dryRunInput === 'boolean') ? dryRunInput
47+
: (dryRunInput === undefined ? true : String(dryRunInput).toLowerCase() === 'true');
48+
49+
core.info(`Using retention period: ${retentionDays} day(s). Dry-run: ${dryRun}.`);
3850
3951
function parseTimestampTag(tag) {
4052
// tag format: yyyyMMddHHmmss, interpreted as UTC
@@ -49,54 +61,88 @@ jobs:
4961
5062
let page = 1;
5163
let totalDeleted = 0;
64+
let wouldDelete = 0;
5265
let scanned = 0;
5366
5467
while (true) {
5568
const { data: versions } = await github.request(
5669
'GET /orgs/{org}/packages/{package_type}/{package_name}/versions',
5770
{ org, package_type, package_name, per_page, page }
5871
);
59-
72+
6073
if (!versions || versions.length === 0) break;
6174
6275
for (const v of versions) {
6376
scanned++;
6477
const tags = (v.metadata && v.metadata.container && v.metadata.container.tags) || [];
65-
if (!tags || tags.length === 0) continue;
78+
const createdAt = new Date(v.created_at || v.updated_at || 0);
79+
80+
core.debug (`Checking version '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);
6681
6782
// Skip protected tags to avoid removing latest or release tags that share the same version
68-
const hasProtected = tags.some(t => t === 'latest' || /^v\d+/.test(t));
69-
if (hasProtected) {
83+
const isProtected = tags.some(t => t === 'latest' || /^v\d[\.\d]*/.test(t));
84+
if (isProtected) {
7085
core.info(`Skipping protected version ${v.id} with tags: ${tags.join(', ')}`);
7186
continue;
7287
}
7388
7489
const tsTags = tags.filter(t => tsTagRegex.test(t));
75-
if (tsTags.length === 0) continue;
90+
const shaTags = tags.filter(t => sha256Regex.test(t));
91+
const nonShaTags = tags.filter(t => !sha256Regex.test(t));
92+
93+
let shouldDelete = false;
7694
7795
// If any timestamp tag is older than cutoff, delete the entire version
78-
let shouldDelete = tsTags.some(t => parseTimestampTag(t) < cutoff);
96+
if (tsTags.length > 0) {
97+
shouldDelete = tsTags.some(t => parseTimestampTag(t) < cutoff);
98+
}
99+
100+
// Additionally handle versions that are tagged only by sha256 values.
101+
// Delete if older than retention (by created_at/updated_at) unless there is any non-sha256 tag.
102+
if (!shouldDelete && shaTags.length > 0 && nonShaTags.length === 0) {
103+
if (createdAt instanceof Date && !isNaN(createdAt) && createdAt < cutoff) {
104+
shouldDelete = true;
105+
}
106+
}
107+
108+
// Handle versions with no tags at all: delete if older than retention by created/updated timestamp
109+
if (!shouldDelete && (!tags || tags.length === 0)) {
110+
if (createdAt instanceof Date && !isNaN(createdAt) && createdAt < cutoff) {
111+
shouldDelete = true;
112+
}
113+
}
79114
80115
if (shouldDelete) {
81-
try {
82-
await github.request(
83-
'DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}',
84-
{
85-
org,
86-
package_type,
87-
package_name,
88-
package_version_id: v.id,
89-
}
90-
);
91-
totalDeleted++;
92-
core.info(`Deleted version ${v.id} with tags: ${tags.join(', ')}`);
93-
} catch (err) {
94-
core.warning(`Failed to delete version ${v.id}: ${err.message}`);
116+
if (dryRun) {
117+
wouldDelete++;
118+
core.info(`[DRY-RUN] Would delete version '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);
119+
} else {
120+
try {
121+
await github.request(
122+
'DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}',
123+
{
124+
org,
125+
package_type,
126+
package_name,
127+
package_version_id: v.id,
128+
}
129+
);
130+
totalDeleted++;
131+
core.info(`Deleted version '${v.id}' (${v.name}) of '${createdAt}' with tags: ${tags.join(', ')}`);
132+
} catch (err) {
133+
core.warning(`Failed to delete version ${v.id}: ${err.message}`);
134+
}
95135
}
136+
} else {
137+
core.debug (`Not deleting '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);
96138
}
97139
}
98140
99141
page++;
100142
}
101143
102-
core.info(`Scanned versions: ${scanned}. Deleted versions: ${totalDeleted}.`);
144+
if (dryRun) {
145+
core.info(`Scanned versions: ${scanned}. Would delete versions: ${wouldDelete}.`);
146+
} else {
147+
core.info(`Scanned versions: ${scanned}. Deleted versions: ${totalDeleted}.`);
148+
}

0 commit comments

Comments
 (0)