|
6 | 6 | workflow_dispatch: |
7 | 7 | inputs: |
8 | 8 | images_to_keep: |
9 | | - description: Keep this many newest non-SHA-only image versions (0 = no count limit, minimum 1 always kept) |
| 9 | + description: Maximum number of tagged images to keep (high water mark, 0 = no limit) |
10 | 10 | required: false |
11 | | - default: '4' |
| 11 | + default: '' |
12 | 12 | retention_days: |
13 | | - description: Delete non-SHA-only image versions older than this many days (0 = no age limit) |
| 13 | + description: Delete images older than this many days (0 = no age limit) |
14 | 14 | required: false |
15 | | - default: '30' |
| 15 | + default: '' |
16 | 16 | delete_sha_only_tags: |
17 | 17 | description: Delete image versions that only have SHA tags |
18 | 18 | required: false |
|
54 | 54 | const packageName = process.env.PACKAGE_NAME; |
55 | 55 | const imagesToKeep = Number(process.env.IMAGES_TO_KEEP || 0); |
56 | 56 | const retentionDays = Number(process.env.RETENTION_DAYS || 0); |
57 | | - const minimumToKeep = Math.max(1, imagesToKeep); |
58 | 57 | const dryRun = process.env.DRY_RUN === 'true'; |
59 | 58 | const cutoff = retentionDays > 0 ? new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000) : null; |
60 | | - let deletedCount = 0; |
61 | 59 |
|
62 | 60 | if (!Number.isFinite(imagesToKeep) || imagesToKeep < 0) { |
63 | 61 | throw new Error(`IMAGES_TO_KEEP must be a non-negative number, received: ${process.env.IMAGES_TO_KEEP}`); |
@@ -135,43 +133,58 @@ jobs: |
135 | 133 | (left, right) => new Date(right.updated_at) - new Date(left.updated_at), |
136 | 134 | ); |
137 | 135 |
|
| 136 | + const nonShaOnlyVersions = sortedVersions.filter(v => !isShaOnlyVersion(v)); |
| 137 | +
|
| 138 | + // Step 1: Filter by retention window (if RETENTION_DAYS > 0) |
| 139 | + let candidates = nonShaOnlyVersions; |
| 140 | + if (retentionDays > 0) { |
| 141 | + candidates = nonShaOnlyVersions.filter(v => new Date(v.updated_at) >= cutoff); |
| 142 | + core.info(`Retention filter: ${candidates.length} of ${nonShaOnlyVersions.length} versions within ${retentionDays} days`); |
| 143 | + } |
| 144 | +
|
| 145 | + // Step 2: Cap by IMAGES_TO_KEEP (high water mark, if > 0) |
| 146 | + if (imagesToKeep > 0 && candidates.length > imagesToKeep) { |
| 147 | + candidates = candidates.slice(0, imagesToKeep); |
| 148 | + core.info(`Count cap: limiting to ${imagesToKeep} newest versions`); |
| 149 | + } |
| 150 | +
|
| 151 | + // Step 3: Safety - ensure at least 1 tagged image |
| 152 | + const taggedCandidates = candidates.filter(hasNonShaTag); |
| 153 | + if (taggedCandidates.length === 0) { |
| 154 | + const newestTagged = nonShaOnlyVersions.find(hasNonShaTag); |
| 155 | + if (newestTagged) { |
| 156 | + candidates.unshift(newestTagged); |
| 157 | + core.info(`Safety: added newest tagged version ${newestTagged.id} to ensure minimum of 1 tagged image`); |
| 158 | + } |
| 159 | + } |
| 160 | +
|
| 161 | + // Build set of IDs to keep |
| 162 | + const keepIds = new Set(candidates.map(v => v.id)); |
| 163 | +
|
| 164 | + let deletedCount = 0; |
138 | 165 | let retainedCount = 0; |
139 | 166 | let retainedTaggedCount = 0; |
140 | 167 |
|
141 | 168 | for (const version of sortedVersions) { |
142 | | - const updatedAt = new Date(version.updated_at); |
143 | 169 | const tags = version.metadata?.container?.tags ?? []; |
144 | 170 | const isShaOnly = isShaOnlyVersion(version); |
145 | | - const isTagged = hasNonShaTag(version); |
146 | | - const withinRetention = retentionDays > 0 && updatedAt >= cutoff; |
147 | | - const needForMinimum = retainedTaggedCount < minimumToKeep; |
148 | 171 |
|
149 | 172 | if (isShaOnly) { |
150 | | - core.info(`Skipping age-based keep-count evaluation for SHA-only version ${version.id} with tags: ${tags.join(', ')}`); |
| 173 | + core.info(`Skipping SHA-only version ${version.id} with tags: ${tags.join(', ')}`); |
151 | 174 | continue; |
152 | 175 | } |
153 | 176 |
|
154 | | - if (withinRetention) { |
155 | | - core.info(`Keeping version ${version.id}; updated ${version.updated_at} is within ${retentionDays} days.`); |
| 177 | + if (keepIds.has(version.id)) { |
| 178 | + core.info(`Keeping version ${version.id} with tags: ${tags.join(', ') || '(none)'}`); |
156 | 179 | retainedCount += 1; |
157 | | - if (isTagged) retainedTaggedCount += 1; |
158 | | - continue; |
159 | | - } |
160 | | -
|
161 | | - if (isTagged && needForMinimum) { |
162 | | - core.info(`Keeping version ${version.id} to maintain minimum of ${minimumToKeep} tagged image(s).`); |
163 | | - retainedCount += 1; |
164 | | - retainedTaggedCount += 1; |
165 | | - continue; |
166 | | - } |
167 | | -
|
168 | | - core.info( |
169 | | - `${dryRun ? 'Would delete' : 'Deleting'} version ${version.id} updated ${version.updated_at} with tags: ${tags.join(', ') || '(none)'}`, |
170 | | - ); |
171 | | - if (!dryRun) { |
172 | | - await deleteVersion(scope, version.id); |
| 180 | + if (hasNonShaTag(version)) retainedTaggedCount += 1; |
| 181 | + } else { |
| 182 | + core.info(`${dryRun ? 'Would delete' : 'Deleting'} version ${version.id} updated ${version.updated_at} with tags: ${tags.join(', ') || '(none)'}`); |
| 183 | + if (!dryRun) { |
| 184 | + await deleteVersion(scope, version.id); |
| 185 | + } |
| 186 | + deletedCount += 1; |
173 | 187 | } |
174 | | - deletedCount += 1; |
175 | 188 | } |
176 | 189 |
|
177 | 190 | return JSON.stringify({ deletedCount, retainedCount, retainedTaggedCount }); |
|
0 commit comments