@@ -128,10 +128,61 @@ jobs:
128128 return tags.length > 0 && tags.some(tag => !isShaTag(tag));
129129 };
130130
131+ // Semantic version handling
132+ const specialTags = new Set(['latest', 'stable', 'next', 'canary', 'edge', 'dev', 'alpha', 'beta', 'rc', 'release']);
133+ const semverPattern = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-._]?(.+))?$/;
134+
135+ const parseSemver = (tag) => {
136+ const match = tag.match(semverPattern);
137+ if (!match) return null;
138+ return {
139+ major: parseInt(match[1], 10),
140+ minor: match[2] !== undefined ? parseInt(match[2], 10) : 0,
141+ patch: match[3] !== undefined ? parseInt(match[3], 10) : 0,
142+ prerelease: match[4] || null,
143+ original: tag
144+ };
145+ };
146+
147+ const compareSemver = (a, b) => {
148+ if (a.major !== b.major) return a.major - b.major;
149+ if (a.minor !== b.minor) return a.minor - b.minor;
150+ if (a.patch !== b.patch) return a.patch - b.patch;
151+ // Prerelease versions are lower than release
152+ if (a.prerelease && !b.prerelease) return -1;
153+ if (!a.prerelease && b.prerelease) return 1;
154+ if (a.prerelease && b.prerelease) return a.prerelease.localeCompare(b.prerelease);
155+ return 0;
156+ };
157+
158+ const extractSemver = (version) => {
159+ const tags = version.metadata?.container?.tags ?? [];
160+ const semverTags = tags
161+ .filter(tag => !isShaTag(tag) && !specialTags.has(tag.toLowerCase()))
162+ .map(parseSemver)
163+ .filter(Boolean)
164+ .sort((a, b) => compareSemver(b, a)); // Highest version first
165+ return semverTags[0] || null;
166+ };
167+
131168 const { scope, versions } = await paginateVersions();
132- const sortedVersions = [...versions].sort(
133- (left, right) => new Date(right.updated_at) - new Date(left.updated_at),
134- );
169+
170+ // Sort by semantic version DESC, then by updated_at DESC as tiebreaker
171+ const sortedVersions = [...versions].sort((left, right) => {
172+ const leftSemver = extractSemver(left);
173+ const rightSemver = extractSemver(right);
174+
175+ if (leftSemver && rightSemver) {
176+ const cmp = compareSemver(rightSemver, leftSemver);
177+ if (cmp !== 0) return cmp;
178+ } else if (leftSemver && !rightSemver) {
179+ return -1; // Has version > no version
180+ } else if (!leftSemver && rightSemver) {
181+ return 1;
182+ }
183+ // Tiebreaker: newer update time first
184+ return new Date(right.updated_at) - new Date(left.updated_at);
185+ });
135186
136187 const nonShaOnlyVersions = sortedVersions.filter(v => !isShaOnlyVersion(v));
137188
@@ -265,20 +316,30 @@ jobs:
265316 (left, right) => new Date(right.updated_at) - new Date(left.updated_at),
266317 );
267318
319+ const shaOnlyVersions = sortedVersions.filter(v => {
320+ const tags = v.metadata?.container?.tags ?? [];
321+ return tags.length > 0 && tags.every(isShaTag);
322+ });
323+
324+ core.info(`Found ${shaOnlyVersions.length} SHA-only versions to evaluate`);
325+
268326 let shaOnlyDeletedCount = 0;
269327
270- for (const version of sortedVersions ) {
328+ for (const version of shaOnlyVersions ) {
271329 const tags = version.metadata?.container?.tags ?? [];
272- const isShaOnly = tags.length > 0 && tags.every(isShaTag);
273- if (!isShaOnly) continue;
274-
275330 core.info(`${dryRun ? 'Would delete' : 'Deleting'} version ${version.id} with SHA-only tags: ${tags.join(', ')}`);
276331 if (!dryRun) {
277332 await deleteVersion(scope, version.id);
278333 }
279334 shaOnlyDeletedCount += 1;
280335 }
281336
337+ if (shaOnlyDeletedCount > 0) {
338+ core.info(`${dryRun ? 'Would delete' : 'Deleted'} ${shaOnlyDeletedCount} SHA-only version(s)`);
339+ } else {
340+ core.info('No SHA-only versions to delete');
341+ }
342+
282343 return JSON.stringify({
283344 totalBefore: sortedVersions.length,
284345 ageBasedDeleted: ageBasedResult.deletedCount || 0,
0 commit comments