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
1318permissions :
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