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
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
3737jobs :
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
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