@@ -16,7 +16,7 @@ const parseBoolean = (input, falsyValues = ["0", "no", "n", "false"]) => {
1616 return ! falsyValues . includes ( normalized ) ;
1717} ;
1818/**
19- * Split a comma-separated pattern into trimmed items.
19+ * Split a comma- or pipe- separated pattern into trimmed items.
2020 * If pattern is empty/undefined returns an empty array.
2121 * @param {string|undefined } pattern
2222 * @returns {string[] }
@@ -37,7 +37,7 @@ async function deleteRuns(runs, context, dryRun, octokit, owner, repo) {
3737 core . debug ( `[${ context } ] No runs to delete.` ) ;
3838 return ;
3939 }
40- const tasks = runs . map ( ( run ) => async ( ) => {
40+ const tasks = runs . map ( run => async ( ) => {
4141 if ( dryRun ) {
4242 core . info ( `[dry-run] 🚀 Simulate deletion: Run ${ run . id } (${ context } )` ) ;
4343 return { status : "skipped" , runId : run . id } ;
@@ -51,7 +51,7 @@ async function deleteRuns(runs, context, dryRun, octokit, owner, repo) {
5151 return { status : "failed" , runId : run . id , error : err } ;
5252 }
5353 } ) ;
54- const results = await Promise . allSettled ( tasks . map ( ( t ) => t ( ) ) ) ;
54+ const results = await Promise . allSettled ( tasks . map ( t => t ( ) ) ) ;
5555 const summary = results . reduce (
5656 ( acc , res ) => {
5757 const status = res . status === "fulfilled" ? res . value ?. status : null ;
@@ -75,8 +75,7 @@ async function deleteRuns(runs, context, dryRun, octokit, owner, repo) {
7575 * @returns {boolean }
7676 */
7777function shouldDeleteRun ( run , options ) {
78- const { checkPullRequestExist, checkBranchExistence, branchNames, allowedConclusions, retainDays } = options ;
79- // Only completed runs are considered.
78+ const { checkPullRequestExist, checkBranchExistence, branchNames, allowedConclusions, retainDays = 0 , skipAgeCheck = false } = options ;
8079 if ( run . status !== "completed" ) {
8180 core . debug ( `💬 Skip: Run ${ run . id } status=${ run . status } ` ) ;
8281 return false ;
@@ -93,62 +92,87 @@ function shouldDeleteRun(run, options) {
9392 return false ;
9493 }
9594 // Conclusion filter (if provided). If allowedConclusions is empty, that means "ALL".
96- if ( allowedConclusions . length > 0 && ! allowedConclusions . includes ( run . conclusion ) ) {
97- core . debug ( `💬 Skip: Run ${ run . id } conclusion="${ run . conclusion } " not allowed` ) ;
98- return false ;
95+ if ( allowedConclusions . length > 0 ) {
96+ const runConclusion = String ( run . conclusion ?? "" ) . toLowerCase ( ) ;
97+ if ( ! allowedConclusions . includes ( runConclusion ) ) {
98+ core . debug ( `💬 Skip: Run ${ run . id } conclusion="${ run . conclusion } " not allowed` ) ;
99+ return false ;
100+ }
99101 }
100- // Age filter
101- const ageDays = ( Date . now ( ) - new Date ( run . created_at ) . getTime ( ) ) / 86400000 ;
102- if ( ageDays < retainDays ) {
103- core . debug ( `💬 Skip: Run ${ run . id } is ${ ageDays . toFixed ( 1 ) } days old (< ${ retainDays } days)` ) ;
104- return false ;
102+ // Age filter only when requested
103+ if ( ! skipAgeCheck && retainDays > 0 ) {
104+ if ( ! run . created_at ) {
105+ core . debug ( `💬 Skip age check: Run ${ run . id } has no created_at` ) ;
106+ return false ;
107+ }
108+ const ageDays = ( Date . now ( ) - new Date ( run . created_at ) . getTime ( ) ) / 86400000 ;
109+ if ( ageDays < retainDays ) {
110+ core . debug ( `💬 Skip: Run ${ run . id } is ${ ageDays . toFixed ( 1 ) } days old (< ${ retainDays } days)` ) ;
111+ return false ;
112+ }
105113 }
106- // All checks passed → delete
107114 return true ;
108115}
109116/**
110117 * Group runs by date and filter runs to retain per day
111118 * @param {Array } runs
112- * @param {number } keepMinimumRunsPerDay
119+ * @param {number } keepMinimumRuns
120+ * @param {number } retainDays
113121 * @returns {Object } { runsToDelete: Array, runsToRetain: Array }
114122 */
115- function filterRunsByDailyRetention ( runs , keepMinimumRunsPerDay ) {
116- if ( keepMinimumRunsPerDay <= 0 ) {
117- return { runsToDelete : runs , runsToRetain : [ ] } ;
123+ function filterRunsByDailyRetention ( runs , keepMinimumRuns , retainDays ) {
124+ if ( keepMinimumRuns <= 0 || retainDays <= 0 ) {
125+ return {
126+ runsToDelete : runs ,
127+ runsToRetain : [ ]
128+ } ;
118129 }
119- // Group runs by date (YYYY-MM-DD)
130+ const cutoffDate = new Date ( ) ;
131+ cutoffDate . setDate ( cutoffDate . getDate ( ) - retainDays ) ;
132+ const cutoffTime = cutoffDate . getTime ( ) ;
120133 const runsByDate = { } ;
134+ const expiredRuns = [ ] ; // older than retainDays → delete
121135 runs . forEach ( run => {
122- const date = new Date ( run . created_at ) . toISOString ( ) . split ( 'T' ) [ 0 ] ; // Get YYYY-MM-DD
123- if ( ! runsByDate [ date ] ) {
124- runsByDate [ date ] = [ ] ;
136+ if ( ! run ?. created_at ) {
137+ // If no created_at treat as expired to be safe
138+ expiredRuns . push ( run ) ;
139+ return ;
140+ }
141+ const runTime = new Date ( run . created_at ) . getTime ( ) ;
142+ if ( isNaN ( runTime ) || runTime < cutoffTime ) {
143+ expiredRuns . push ( run ) ;
144+ return ;
125145 }
126- runsByDate [ date ] . push ( run ) ;
146+ // Normalize date key via ISO to avoid locale variations
147+ const dateKey = new Date ( run . created_at ) . toISOString ( ) . split ( "T" ) [ 0 ] ; // YYYY-MM-DD
148+ if ( ! runsByDate [ dateKey ] )
149+ runsByDate [ dateKey ] = [ ] ;
150+ runsByDate [ dateKey ] . push ( run ) ;
127151 } ) ;
128- const runsToDelete = [ ] ;
129152 const runsToRetain = [ ] ;
130- // For each date, keep the latest keepMinimumRunsPerDay runs
153+ const runsToDelete = [ ... expiredRuns ] ;
131154 Object . values ( runsByDate ) . forEach ( dateRuns => {
132- // Sort by creation time (newest first)
133- dateRuns . sort ( ( a , b ) => new Date ( b . created_at ) - new Date ( a . created_at ) ) ;
134- // Keep the latest N runs for this date
135- const retainedRuns = dateRuns . slice ( 0 , keepMinimumRunsPerDay ) ;
136- const deletedRuns = dateRuns . slice ( keepMinimumRunsPerDay ) ;
137- runsToRetain . push ( ...retainedRuns ) ;
138- runsToDelete . push ( ...deletedRuns ) ;
155+ dateRuns . sort ( ( a , b ) => new Date ( b . created_at ) - new Date ( a . created_at ) ) ; // newest first
156+ const retain = dateRuns . slice ( 0 , keepMinimumRuns ) ;
157+ const del = dateRuns . slice ( keepMinimumRuns ) ;
158+ runsToRetain . push ( ...retain ) ;
159+ runsToDelete . push ( ...del ) ;
139160 } ) ;
140161 return { runsToDelete, runsToRetain } ;
141162}
142163async function run ( ) {
143164 try {
144165 // ---------------------- 1. Parse Input Parameters ----------------------
145166 const token = core . getInput ( "token" ) ;
167+ if ( ! token )
168+ throw new Error ( "Missing required input: token" ) ;
146169 const baseUrl = core . getInput ( "baseUrl" ) ;
147170 const repositoryInput = core . getInput ( "repository" ) ;
171+ if ( ! repositoryInput )
172+ throw new Error ( 'Missing required input: repository (expected "owner/repo")' ) ;
148173 const [ repoOwner , repoName ] = repositoryInput . split ( "/" ) ;
149- if ( ! repoOwner || ! repoName ) {
174+ if ( ! repoOwner || ! repoName )
150175 throw new Error ( `Invalid repository: "${ repositoryInput } ". Use "owner/repo".` ) ;
151- }
152176 const retainDays = Number ( core . getInput ( "retain_days" ) || "30" ) ;
153177 const keepMinimumRuns = Number ( core . getInput ( "keep_minimum_runs" ) || "6" ) ;
154178 const useDailyRetention = parseBoolean ( core . getInput ( "use_daily_retention" ) ) ;
@@ -168,9 +192,7 @@ async function run() {
168192 core . warning ( `Rate limit: ${ options . method } ${ options . url } — wait ${ retryAfter } s` ) ;
169193 return retryAfter < 5 ;
170194 } ,
171- onSecondaryRateLimit : ( retryAfter , options ) => {
172- core . warning ( `Secondary rate limit: ${ options . method } ${ options . url } ` ) ;
173- } ,
195+ onSecondaryRateLimit : ( ) => core . warning ( "Secondary rate limit hit" ) ,
174196 } ,
175197 } ) ;
176198 // ---------------------- 3. Fetch Workflows ----------------------
@@ -179,7 +201,7 @@ async function run() {
179201 repo : repoName ,
180202 per_page : 100 ,
181203 } ) ;
182- const workflowIds = workflows . map ( ( w ) => w . id ) ;
204+ const workflowIds = workflows . map ( w => w . id ) ;
183205 // ---------------------- 4. Fetch Branches (if needed) ----------------------
184206 let branchNames = [ ] ;
185207 if ( checkBranchExistence ) {
@@ -188,8 +210,7 @@ async function run() {
188210 owner : repoOwner ,
189211 repo : repoName ,
190212 per_page : 100 ,
191- } )
192- ) . map ( ( b ) => b . name ) ;
213+ } ) ) . map ( b => b . name ) ;
193214 core . info ( `💬 Found ${ branchNames . length } branches` ) ;
194215 }
195216 // ---------------------- 5. Delete Orphan Runs ----------------------
@@ -198,32 +219,38 @@ async function run() {
198219 repo : repoName ,
199220 per_page : 100 ,
200221 } ) ;
201- const orphanRuns = allRuns . filter ( ( run ) => ! workflowIds . includes ( run . workflow_id ) ) ;
222+ const orphanRuns = allRuns . filter ( run => ! workflowIds . includes ( run . workflow_id ) ) ;
202223 if ( orphanRuns . length > 0 ) {
203224 core . info ( `👻 Found ${ orphanRuns . length } orphan runs` ) ;
204225 await deleteRuns ( orphanRuns , "orphan runs" , dryRun , octokit , repoOwner , repoName ) ;
205226 }
206227 // ---------------------- 6. Filter Workflows ----------------------
207228 let filteredWorkflows = workflows ;
208229 if ( deleteWorkflowPattern ) {
209- const patterns = splitPattern ( deleteWorkflowPattern ) ;
230+ const patterns = splitPattern ( deleteWorkflowPattern ) . map ( p => p . toLowerCase ( ) ) ;
210231 if ( patterns . length > 0 ) {
211232 core . info ( `🔍 Filtering by patterns: ${ patterns . join ( ", " ) } ` ) ;
212- filteredWorkflows = filteredWorkflows . filter ( ( { name, path } ) => {
213- const filename = path . replace ( / ^ \. g i t h u b \/ w o r k f l o w s \/ / , "" ) ;
214- return patterns . some ( ( p ) => name . includes ( p ) || filename . includes ( p ) ) ;
233+ filteredWorkflows = filteredWorkflows . filter ( ( {
234+ name,
235+ path
236+ } ) => {
237+ const filename = ( path || "" ) . replace ( / ^ \. g i t h u b \/ w o r k f l o w s \/ / , "" ) ;
238+ const nameLower = String ( name || "" ) . toLowerCase ( ) ;
239+ const filenameLower = String ( filename || "" ) . toLowerCase ( ) ;
240+ return patterns . some ( p => nameLower . includes ( p ) || filenameLower . includes ( p ) ) ;
215241 } ) ;
216242 }
217243 }
218244 if ( deleteWorkflowByStatePattern . toUpperCase ( ) !== "ALL" ) {
219- const states = splitPattern ( deleteWorkflowByStatePattern ) ;
245+ const states = splitPattern ( deleteWorkflowByStatePattern ) . map ( s => s . toLowerCase ( ) ) ;
220246 core . info ( `🔍 Filtering by state: ${ states . join ( ", " ) } ` ) ;
221- filteredWorkflows = filteredWorkflows . filter ( ( { state } ) => states . includes ( state ) ) ;
247+ filteredWorkflows = filteredWorkflows . filter ( ( {
248+ state
249+ } ) => states . includes ( String ( state ?? "" ) . toLowerCase ( ) ) ) ;
222250 }
223251 core . info ( `Processing ${ filteredWorkflows . length } workflow(s)` ) ;
224252 // ---------------------- 7. Process Each Workflow ----------------------
225- const allowedConclusionsAll = deleteRunByConclusionPattern . toUpperCase ( ) === "ALL" ;
226- const allowedConclusions = allowedConclusionsAll ? [ ] : splitPattern ( deleteRunByConclusionPattern ) ;
253+ const allowedConclusions = deleteRunByConclusionPattern . toUpperCase ( ) === "ALL" ? [ ] : splitPattern ( deleteRunByConclusionPattern ) . map ( c => c . toLowerCase ( ) ) ;
227254 for ( const workflow of filteredWorkflows ) {
228255 core . startGroup ( `Processing: ${ workflow . name } (ID: ${ workflow . id } )` ) ;
229256 const runs = await octokit . paginate ( octokit . rest . actions . listWorkflowRuns , {
@@ -232,32 +259,29 @@ async function run() {
232259 workflow_id : workflow . id ,
233260 per_page : 100 ,
234261 } ) ;
235- const candidates = runs . filter ( ( run ) =>
262+ // Pre-filter (branch, PR, conclusion, etc.)
263+ const candidates = runs . filter ( run =>
236264 shouldDeleteRun ( run , {
237265 checkPullRequestExist,
238266 checkBranchExistence,
239267 branchNames,
240268 allowedConclusions,
241- retainDays,
242- } ) ,
243- ) ;
269+ retainDays : useDailyRetention ? 0 : retainDays , // age handled later in daily mode
270+ skipAgeCheck : useDailyRetention ,
271+ } ) , ) ;
244272 let runsToDelete = [ ] ;
245273 let runsToRetain = [ ] ;
246274 if ( useDailyRetention ) {
247- // Use daily retention strategy
248- const { runsToDelete : dailyRunsToDelete , runsToRetain : dailyRunsToRetain } =
249- filterRunsByDailyRetention ( candidates , keepMinimumRuns ) ;
250- runsToDelete = dailyRunsToDelete ;
251- runsToRetain = dailyRunsToRetain ;
252- core . info ( `📅 Daily retention: Keeping ${ keepMinimumRuns } runs per day, retaining ${ runsToRetain . length } runs total` ) ;
275+ const { runsToDelete : del , runsToRetain : ret } = filterRunsByDailyRetention ( candidates , keepMinimumRuns , retainDays ) ;
276+ runsToDelete = del ;
277+ runsToRetain = ret ;
278+ core . info ( `🔄 Daily retention: Keeping up to ${ keepMinimumRuns } runs/day for last ${ retainDays } days` ) ;
253279 } else {
254- // Use original strategy (keep latest N runs overall)
255280 candidates . sort ( ( a , b ) => new Date ( a . created_at ) - new Date ( b . created_at ) ) ;
256281 runsToRetain = keepMinimumRuns > 0 ? candidates . slice ( - keepMinimumRuns ) : [ ] ;
257- runsToDelete = keepMinimumRuns > 0 ? candidates . slice ( 0 , candidates . length - keepMinimumRuns ) : candidates ;
258- if ( runsToRetain . length > 0 ) {
282+ runsToDelete = keepMinimumRuns > 0 ? candidates . slice ( 0 , candidates . length - runsToRetain . length ) : candidates ;
283+ if ( runsToRetain . length > 0 )
259284 core . info ( `🔄 Retaining latest ${ runsToRetain . length } run(s)` ) ;
260- }
261285 }
262286 if ( runsToDelete . length > 0 ) {
263287 core . info ( `🚀 Deleting ${ runsToDelete . length } run(s)` ) ;
0 commit comments