@@ -4,7 +4,7 @@ import axios from "axios";
44import * as core from "@actions/core" ;
55
66interface PaginationResponse {
7- next_page_token ?: string ;
7+ nextToken ?: string ;
88}
99
1010interface GitStatus {
@@ -83,6 +83,11 @@ interface DeletedEnvironmentInfo {
8383 inactiveDays : number ;
8484}
8585
86+ /**
87+ * Sleep function to add delay between API calls
88+ */
89+ const sleep = ( ms : number ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
90+
8691/**
8792 * Formats a date difference in days
8893 */
@@ -116,6 +121,30 @@ function isStale(lastStartedAt: string, days: number): boolean {
116121 return lastStarted < cutoffDate ;
117122}
118123
124+ async function getRunner ( runnerId : string , gitpodToken : string ) : Promise < boolean > {
125+ const baseDelay = 2000 ;
126+ try {
127+ const response = await axios . post (
128+ "https://app.gitpod.io/api/gitpod.v1.RunnerService/GetRunner" ,
129+ {
130+ runner_id : runnerId
131+ } ,
132+ {
133+ headers : {
134+ "Content-Type" : "application/json" ,
135+ Authorization : `Bearer ${ gitpodToken } ` ,
136+ } ,
137+ }
138+ ) ;
139+
140+ await sleep ( baseDelay ) ;
141+ return response . data . runner . kind === "RUNNER_KIND_REMOTE" ;
142+ } catch ( error ) {
143+ core . debug ( `Error getting runner ${ runnerId } : ${ error } ` ) ;
144+ return false ;
145+ }
146+ }
147+
119148/**
120149 * Lists and filters environments that should be deleted
121150 */
@@ -126,59 +155,87 @@ async function listEnvironments(
126155) : Promise < DeletedEnvironmentInfo [ ] > {
127156 const toDelete : DeletedEnvironmentInfo [ ] = [ ] ;
128157 let pageToken : string | undefined = undefined ;
158+ const baseDelay = 2000 ;
159+ let retryCount = 0 ;
160+ const maxRetries = 3 ;
161+ let totalEnvironmentsChecked = 0 ;
129162
130163 try {
131164 do {
132- const response : { data : ListEnvironmentsResponse } = await axios . post < ListEnvironmentsResponse > (
133- "https://app.gitpod.io/api/gitpod.v1.EnvironmentService/ListEnvironments" ,
134- {
135- organization_id : organizationId ,
136- pagination : {
137- page_size : 100 ,
138- page_token : pageToken
139- }
140- } ,
141- {
142- headers : {
143- "Content-Type" : "application/json" ,
144- Authorization : `Bearer ${ gitpodToken } ` ,
165+ try {
166+ const response : { data : ListEnvironmentsResponse } = await axios . post < ListEnvironmentsResponse > (
167+ "https://app.gitpod.io/api/gitpod.v1.EnvironmentService/ListEnvironments" ,
168+ {
169+ organization_id : organizationId ,
170+ pagination : {
171+ page_size : 100 ,
172+ page_token : pageToken
173+ } ,
174+ filter : {
175+ status_phases : [ "ENVIRONMENT_PHASE_STOPPED" , "ENVIRONMENT_PHASE_UNSPECIFIED" ]
176+ }
145177 } ,
146- }
147- ) ;
178+ {
179+ headers : {
180+ "Content-Type" : "application/json" ,
181+ Authorization : `Bearer ${ gitpodToken } ` ,
182+ } ,
183+ }
184+ ) ;
185+
186+ core . debug ( `ListEnvironments API Response: ${ JSON . stringify ( response . data ) } ` ) ;
187+ await sleep ( baseDelay ) ;
188+
189+ const environments = response . data . environments ;
190+ totalEnvironmentsChecked += environments . length ;
191+ core . debug ( `Fetched ${ environments . length } stopped environments` ) ;
192+
193+ for ( const env of environments ) {
194+ core . debug ( `Checking environment ${ env . id } :` ) ;
195+
196+ const isRemoteRunner = await getRunner ( env . metadata . runnerId , gitpodToken ) ;
197+ core . debug ( `- Is remote runner: ${ isRemoteRunner } ` ) ;
198+
199+ const hasNoChangedFiles = ! ( env . status . content ?. git ?. totalChangedFiles ) ;
200+ core . debug ( `- Has no changed files: ${ hasNoChangedFiles } ` ) ;
148201
149- core . debug ( `Fetched ${ response . data . environments . length } environments` ) ;
150-
151- const environments = response . data . environments ;
152-
153- environments . forEach ( ( env ) => {
154- const isStopped = env . status . phase === "ENVIRONMENT_PHASE_STOPPED" ;
155- const hasNoChangedFiles = ! ( env . status . content ?. git ?. totalChangedFiles ) ;
156- const hasNoUnpushedCommits = ! ( env . status . content ?. git ?. totalUnpushedCommits ) ;
157- const isInactive = isStale ( env . metadata . lastStartedAt , olderThanDays ) ;
158-
159- if ( isStopped && hasNoChangedFiles && hasNoUnpushedCommits && isInactive ) {
160- toDelete . push ( {
161- id : env . id ,
162- projectUrl : getProjectUrl ( env ) ,
163- lastStarted : env . metadata . lastStartedAt ,
164- createdAt : env . metadata . createdAt ,
165- creator : env . metadata . creator . id ,
166- inactiveDays : getDaysSince ( env . metadata . lastStartedAt )
167- } ) ;
168-
169- core . debug (
170- `Marked for deletion: Environment ${ env . id } \n` +
171- `Project: ${ getProjectUrl ( env ) } \n` +
172- `Last Started: ${ env . metadata . lastStartedAt } \n` +
173- `Days Inactive: ${ getDaysSince ( env . metadata . lastStartedAt ) } \n` +
174- `Creator: ${ env . metadata . creator . id } `
175- ) ;
202+ const hasNoUnpushedCommits = ! ( env . status . content ?. git ?. totalUnpushedCommits ) ;
203+ core . debug ( `- Has no unpushed commits: ${ hasNoUnpushedCommits } ` ) ;
204+
205+ const isInactive = isStale ( env . metadata . lastStartedAt , olderThanDays ) ;
206+ core . debug ( `- Is inactive: ${ isInactive } ` ) ;
207+
208+
209+
210+ if ( isRemoteRunner && hasNoChangedFiles && hasNoUnpushedCommits && isInactive ) {
211+ toDelete . push ( {
212+ id : env . id ,
213+ projectUrl : getProjectUrl ( env ) ,
214+ lastStarted : env . metadata . lastStartedAt ,
215+ createdAt : env . metadata . createdAt ,
216+ creator : env . metadata . creator . id ,
217+ inactiveDays : getDaysSince ( env . metadata . lastStartedAt )
218+ } ) ;
219+ }
176220 }
177- } ) ;
178221
179- pageToken = response . data . pagination . next_page_token ;
222+ pageToken = response . data . pagination . nextToken ;
223+ retryCount = 0 ;
224+ } catch ( error ) {
225+ if ( axios . isAxiosError ( error ) && error . response ?. status === 429 && retryCount < maxRetries ) {
226+ const delay = baseDelay * Math . pow ( 2 , retryCount ) ;
227+ core . debug ( `Rate limit hit in ListEnvironments, waiting ${ delay } ms before retry ${ retryCount + 1 } ...` ) ;
228+ await sleep ( delay ) ;
229+ retryCount ++ ;
230+ continue ;
231+ }
232+ throw error ;
233+ }
180234 } while ( pageToken ) ;
181235
236+ core . info ( `Total environments checked: ${ totalEnvironmentsChecked } ` ) ;
237+ core . info ( `Environments matching deletion criteria: ${ toDelete . length } ` ) ;
238+
182239 return toDelete ;
183240 } catch ( error ) {
184241 core . error ( `Error in listEnvironments: ${ error } ` ) ;
@@ -194,24 +251,41 @@ async function deleteEnvironment(
194251 gitpodToken : string ,
195252 organizationId : string
196253) {
197- try {
198- await axios . post (
199- "https://app.gitpod.io/api/gitpod.v1.EnvironmentService/DeleteEnvironment" ,
200- {
201- environment_id : environmentId ,
202- organization_id : organizationId
203- } ,
204- {
205- headers : {
206- "Content-Type" : "application/json" ,
207- Authorization : `Bearer ${ gitpodToken } ` ,
254+ let retryCount = 0 ;
255+ const maxRetries = 3 ;
256+ const baseDelay = 2000 ;
257+
258+ while ( retryCount <= maxRetries ) {
259+ try {
260+ const response = await axios . post (
261+ "https://app.gitpod.io/api/gitpod.v1.EnvironmentService/DeleteEnvironment" ,
262+ {
263+ environment_id : environmentId ,
264+ organization_id : organizationId
208265 } ,
266+ {
267+ headers : {
268+ "Content-Type" : "application/json" ,
269+ Authorization : `Bearer ${ gitpodToken } ` ,
270+ } ,
271+ }
272+ ) ;
273+
274+ core . debug ( `DeleteEnvironment API Response for ${ environmentId } : ${ JSON . stringify ( response . data ) } ` ) ;
275+ await sleep ( baseDelay ) ;
276+ core . debug ( `Successfully deleted environment: ${ environmentId } ` ) ;
277+ return ;
278+ } catch ( error ) {
279+ if ( axios . isAxiosError ( error ) && error . response ?. status === 429 && retryCount < maxRetries ) {
280+ const delay = baseDelay * Math . pow ( 2 , retryCount ) ;
281+ core . debug ( `Rate limit hit in DeleteEnvironment, waiting ${ delay } ms before retry ${ retryCount + 1 } ...` ) ;
282+ await sleep ( delay ) ;
283+ retryCount ++ ;
284+ } else {
285+ core . error ( `Error deleting environment ${ environmentId } : ${ error } ` ) ;
286+ throw error ;
209287 }
210- ) ;
211- core . debug ( `Deleted environment: ${ environmentId } ` ) ;
212- } catch ( error ) {
213- core . error ( `Error deleting environment ${ environmentId } : ${ error } ` ) ;
214- throw error ;
288+ }
215289 }
216290}
217291
@@ -248,15 +322,33 @@ async function run() {
248322
249323 // Process deletions
250324 for ( const envInfo of environmentsToDelete ) {
251- try {
252- await deleteEnvironment ( envInfo . id , gitpodToken , organizationId ) ;
253- deletedEnvironments . push ( envInfo ) ;
254- totalDaysInactive += envInfo . inactiveDays ;
255-
256- core . debug ( `Successfully deleted environment: ${ envInfo . id } ` ) ;
257- } catch ( error ) {
258- core . warning ( `Failed to delete environment ${ envInfo . id } : ${ error } ` ) ;
259- // Continue with other deletions even if one fails
325+ let retryCount = 0 ;
326+ const maxRetries = 5 ;
327+ const baseDelay = 2000 ;
328+ while ( retryCount <= maxRetries ) {
329+ try {
330+ await deleteEnvironment ( envInfo . id , gitpodToken , organizationId ) ;
331+ await sleep ( baseDelay ) ;
332+ deletedEnvironments . push ( envInfo ) ;
333+ totalDaysInactive += envInfo . inactiveDays ;
334+ core . debug ( `Successfully deleted environment: ${ envInfo . id } ` ) ;
335+ } catch ( error ) {
336+ if ( axios . isAxiosError ( error ) && error . response ?. status === 429 ) {
337+ // If we hit rate limit, wait 5 seconds before retrying
338+ core . debug ( 'Rate limit hit, waiting 5 seconds...' ) ;
339+ await sleep ( 5000 ) ;
340+ // Retry the deletion
341+ try {
342+ await deleteEnvironment ( envInfo . id , gitpodToken , organizationId ) ;
343+ deletedEnvironments . push ( envInfo ) ;
344+ totalDaysInactive += envInfo . inactiveDays ;
345+ } catch ( retryError ) {
346+ core . warning ( `Failed to delete environment ${ envInfo . id } after retry: ${ retryError } ` ) ;
347+ }
348+ } else {
349+ core . warning ( `Failed to delete environment ${ envInfo . id } : ${ error } ` ) ;
350+ }
351+ }
260352 }
261353 }
262354
0 commit comments