Skip to content

Commit 6bf7fd6

Browse files
committed
ci(cleanup): expand image cleanup to include untagged images
Rename delete_sha_only_tags to delete_unversioned and update logic to handle both SHA-only tagged and completely untagged container images. This provides comprehensive cleanup of all images lacking proper semantic version tags.
1 parent 775f311 commit 6bf7fd6

File tree

1 file changed

+47
-59
lines changed

1 file changed

+47
-59
lines changed

.github/workflows/cleanup-ghcr-images.yml

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ on:
1313
description: Delete images older than this many days (0 = no age limit)
1414
required: false
1515
default: '15'
16-
delete_sha_only_tags:
17-
description: Delete image versions that only have SHA tags
16+
delete_unversioned:
17+
description: Delete unversioned images (SHA-only or untagged)
1818
required: false
1919
type: boolean
2020
default: true
@@ -31,7 +31,7 @@ env:
3131
PACKAGE_NAME: opencode-cli
3232
IMAGES_TO_KEEP: ${{ inputs.images_to_keep || vars.GHCR_IMAGES_TO_KEEP }}
3333
RETENTION_DAYS: ${{ inputs.retention_days || vars.GHCR_RETENTION_DAYS }}
34-
DELETE_SHA_ONLY_TAGS: ${{ github.event_name == 'workflow_dispatch' && inputs.delete_sha_only_tags || vars.GHCR_DELETE_SHA_ONLY_TAGS || 'true' }}
34+
DELETE_UNVERSIONED: ${{ github.event_name == 'workflow_dispatch' && inputs.delete_unversioned || vars.GHCR_DELETE_UNVERSIONED || 'true' }}
3535
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
3636

3737
jobs:
@@ -45,7 +45,7 @@ jobs:
4545
PACKAGE_NAME: ${{ env.PACKAGE_NAME }}
4646
IMAGES_TO_KEEP: ${{ env.IMAGES_TO_KEEP }}
4747
RETENTION_DAYS: ${{ env.RETENTION_DAYS }}
48-
DELETE_SHA_ONLY_TAGS: ${{ env.DELETE_SHA_ONLY_TAGS }}
48+
DELETE_UNVERSIONED: ${{ env.DELETE_UNVERSIONED }}
4949
DRY_RUN: ${{ env.DRY_RUN }}
5050
with:
5151
result-encoding: string
@@ -55,7 +55,7 @@ jobs:
5555
const packageName = process.env.PACKAGE_NAME;
5656
const imagesToKeep = Number(process.env.IMAGES_TO_KEEP || 0);
5757
const retentionDays = Number(process.env.RETENTION_DAYS || 0);
58-
const deleteShaOnly = process.env.DELETE_SHA_ONLY_TAGS === 'true';
58+
const deleteUnversioned = process.env.DELETE_UNVERSIONED === 'true';
5959
const dryRun = process.env.DRY_RUN === 'true';
6060
const cutoff = retentionDays > 0 ? new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000) : null;
6161
@@ -121,13 +121,13 @@ jobs:
121121
};
122122
123123
const isShaTag = (value) => /^sha-[0-9a-f]{7,}$/i.test(value);
124-
const isShaOnlyVersion = (version) => {
124+
const isUnversioned = (version) => {
125125
const tags = version.metadata?.container?.tags ?? [];
126-
return tags.length > 0 && tags.every(isShaTag);
126+
return tags.length === 0 || tags.every(isShaTag);
127127
};
128-
const hasNonShaTag = (version) => {
128+
const isTagged = (version) => {
129129
const tags = version.metadata?.container?.tags ?? [];
130-
return tags.length > 0 && tags.some(tag => !isShaTag(tag));
130+
return tags.length > 0 && !tags.every(isShaTag);
131131
};
132132
133133
// Semantic version handling
@@ -185,18 +185,18 @@ jobs:
185185
return new Date(right.updated_at) - new Date(left.updated_at);
186186
});
187187
188-
// Separate SHA-only from regular versions
189-
const shaOnlyVersions = sortedVersions.filter(isShaOnlyVersion);
190-
const nonShaOnlyVersions = sortedVersions.filter(v => !isShaOnlyVersion(v));
188+
// Separate unversioned (SHA-only or untagged) from properly tagged versions
189+
const unversionedVersions = sortedVersions.filter(isUnversioned);
190+
const taggedVersions = sortedVersions.filter(isTagged);
191191
192-
core.info(`${shaOnlyVersions.length} SHA-only versions${deleteShaOnly ? ' (will be deleted)' : ''}`);
193-
core.info(`${nonShaOnlyVersions.length} non-SHA-only versions to evaluate for retention`);
192+
core.info(`${unversionedVersions.length} unversioned versions (SHA-only or untagged)`);
193+
core.info(`${taggedVersions.length} tagged versions to evaluate for retention`);
194194
195-
// Apply retention filter to non-SHA-only versions
196-
let candidates = nonShaOnlyVersions;
195+
// Apply retention filter to tagged versions
196+
let candidates = taggedVersions;
197197
if (retentionDays > 0) {
198-
candidates = nonShaOnlyVersions.filter(v => new Date(v.updated_at) >= cutoff);
199-
core.info(`Retention filter: ${candidates.length} of ${nonShaOnlyVersions.length} versions within ${retentionDays} days`);
198+
candidates = taggedVersions.filter(v => new Date(v.updated_at) >= cutoff);
199+
core.info(`Retention filter: ${candidates.length} of ${taggedVersions.length} versions within ${retentionDays} days`);
200200
}
201201
202202
// Apply count cap
@@ -206,56 +206,49 @@ jobs:
206206
}
207207
208208
// Safety: ensure at least 1 tagged image
209-
const taggedCandidates = candidates.filter(hasNonShaTag);
210-
if (taggedCandidates.length === 0) {
211-
const newestTagged = nonShaOnlyVersions.find(hasNonShaTag);
212-
if (newestTagged) {
213-
candidates.unshift(newestTagged);
214-
core.info(`Safety: added newest tagged version ${newestTagged.id} to ensure minimum of 1 tagged image`);
215-
}
209+
if (candidates.length === 0 && taggedVersions.length > 0) {
210+
candidates.push(taggedVersions[0]);
211+
core.info(`Safety: added newest tagged version ${taggedVersions[0].id} to ensure minimum of 1 tagged image`);
216212
}
217213
218214
// Build set of IDs to keep
219215
const keepIds = new Set(candidates.map(v => v.id));
220-
core.info(`Will keep ${keepIds.size} non-SHA-only versions`);
216+
core.info(`Will keep ${keepIds.size} tagged versions`);
221217
222-
// Process all versions in a single pass
223-
let shaOnlyDeleted = 0;
224-
let shaOnlyRetained = 0;
218+
// Process all versions
219+
let unversionedDeleted = 0;
225220
let ageBasedDeleted = 0;
226221
let retainedCount = 0;
227-
let retainedTaggedCount = 0;
228222
229223
core.info('');
230-
if (deleteShaOnly) {
231-
core.info('=== SHA-only versions (will be deleted) ===');
232-
for (const version of shaOnlyVersions) {
224+
core.info('=== Unversioned (SHA-only or untagged) ===');
225+
if (unversionedVersions.length === 0) {
226+
core.info('None found.');
227+
} else if (deleteUnversioned) {
228+
for (const version of unversionedVersions) {
233229
const tags = version.metadata?.container?.tags ?? [];
234-
core.info(`${dryRun ? '[DRY RUN] Would delete' : 'Deleting'} SHA-only version ${version.id} with tags: ${tags.join(', ')}`);
230+
core.info(`${dryRun ? '[DRY RUN] Would delete' : 'Deleting'} unversioned version ${version.id} with tags: ${tags.join(', ') || '(none)'}`);
235231
if (!dryRun) {
236232
await deleteVersion(scope, version.id);
237233
}
238-
shaOnlyDeleted += 1;
234+
unversionedDeleted += 1;
239235
}
240236
} else {
241-
core.info('=== SHA-only versions (skipped - DELETE_SHA_ONLY_TAGS is false) ===');
242-
for (const version of shaOnlyVersions) {
237+
for (const version of unversionedVersions) {
243238
const tags = version.metadata?.container?.tags ?? [];
244-
core.info(`Keeping SHA-only version ${version.id} with tags: ${tags.join(', ')}`);
245-
shaOnlyRetained += 1;
239+
core.info(`Keeping unversioned version ${version.id} with tags: ${tags.join(', ') || '(none)'} (DELETE_UNVERSIONED is false)`);
246240
}
247241
}
248242
249243
core.info('');
250-
core.info('=== Non-SHA-only versions ===');
251-
for (const version of nonShaOnlyVersions) {
244+
core.info('=== Tagged versions ===');
245+
for (const version of taggedVersions) {
252246
const tags = version.metadata?.container?.tags ?? [];
253247
if (keepIds.has(version.id)) {
254-
core.info(`Keeping version ${version.id} with tags: ${tags.join(', ') || '(none)'}`);
248+
core.info(`Keeping version ${version.id} with tags: ${tags.join(', ')}`);
255249
retainedCount += 1;
256-
if (hasNonShaTag(version)) retainedTaggedCount += 1;
257250
} else {
258-
core.info(`${dryRun ? '[DRY RUN] Would delete' : 'Deleting'} version ${version.id} with tags: ${tags.join(', ') || '(none)'}`);
251+
core.info(`${dryRun ? '[DRY RUN] Would delete' : 'Deleting'} version ${version.id} with tags: ${tags.join(', ')}`);
259252
if (!dryRun) {
260253
await deleteVersion(scope, version.id);
261254
}
@@ -265,35 +258,32 @@ jobs:
265258
266259
core.info('');
267260
core.info('=== Summary ===');
268-
core.info(`SHA-only deleted: ${shaOnlyDeleted}`);
269-
if (!deleteShaOnly) {
270-
core.info(`SHA-only retained: ${shaOnlyRetained}`);
261+
if (deleteUnversioned) {
262+
core.info(`Unversioned deleted: ${unversionedDeleted}`);
271263
}
272264
core.info(`Age/count-based deleted: ${ageBasedDeleted}`);
273-
core.info(`Retained: ${retainedCount} (${retainedTaggedCount} tagged)`);
265+
core.info(`Retained: ${retainedCount} (all tagged)`);
274266
275267
return JSON.stringify({
276268
totalBefore: versions.length,
277-
shaOnlyDeleted,
278-
shaOnlyRetained,
269+
unversionedDeleted,
279270
ageBasedDeleted,
280271
retainedCount,
281-
retainedTaggedCount,
282272
});
283273
284274
- name: Generate cleanup report
285275
if: always()
286276
uses: actions/github-script@v8
287277
env:
288278
CLEANUP_RESULT: ${{ steps.cleanup.outputs.result }}
289-
DELETE_SHA_ONLY_TAGS: ${{ env.DELETE_SHA_ONLY_TAGS }}
279+
DELETE_UNVERSIONED: ${{ env.DELETE_UNVERSIONED }}
290280
DRY_RUN: ${{ env.DRY_RUN }}
291281
IMAGES_TO_KEEP: ${{ env.IMAGES_TO_KEEP }}
292282
RETENTION_DAYS: ${{ env.RETENTION_DAYS }}
293283
with:
294284
script: |
295285
const dryRun = process.env.DRY_RUN === 'true';
296-
const deleteShaOnly = process.env.DELETE_SHA_ONLY_TAGS === 'true';
286+
const deleteUnversioned = process.env.DELETE_UNVERSIONED === 'true';
297287
const imagesToKeep = process.env.IMAGES_TO_KEEP || '0';
298288
const retentionDays = process.env.RETENTION_DAYS || '0';
299289
@@ -308,7 +298,7 @@ jobs:
308298
summary += '|---------|-------|\n';
309299
summary += `| Images to keep | ${imagesToKeep} |\n`;
310300
summary += `| Retention days | ${retentionDays} |\n`;
311-
summary += `| Delete SHA-only tags | ${deleteShaOnly} |\n\n`;
301+
summary += `| Delete unversioned | ${deleteUnversioned} |\n\n`;
312302
313303
const result = process.env.CLEANUP_RESULT;
314304
if (result && result !== 'null') {
@@ -317,13 +307,11 @@ jobs:
317307
summary += '| Metric | Count |\n';
318308
summary += '|--------|-------|\n';
319309
summary += `| Total versions before | ${data.totalBefore || 0} |\n`;
320-
summary += `| SHA-only deleted | ${data.shaOnlyDeleted || 0} |\n`;
321-
if (data.shaOnlyRetained > 0) {
322-
summary += `| SHA-only retained | ${data.shaOnlyRetained || 0} |\n`;
310+
if (deleteUnversioned) {
311+
summary += `| Unversioned deleted | ${data.unversionedDeleted || 0} |\n`;
323312
}
324313
summary += `| Age/count-based deleted | ${data.ageBasedDeleted || 0} |\n`;
325-
summary += `| **Retained images** | **${data.retainedCount || 0}** |\n`;
326-
summary += `| **Retained tagged images** | **${data.retainedTaggedCount || 0}** |\n`;
314+
summary += `| **Retained tagged images** | **${data.retainedCount || 0}** |\n`;
327315
}
328316
329317
core.summary.addRaw(summary).write();

0 commit comments

Comments
 (0)