@@ -4,7 +4,7 @@ import axios from "axios";
4
4
import * as core from "@actions/core" ;
5
5
6
6
interface PaginationResponse {
7
- next_page_token ?: string ;
7
+ nextToken ?: string ;
8
8
}
9
9
10
10
interface GitStatus {
@@ -83,6 +83,11 @@ interface DeletedEnvironmentInfo {
83
83
inactiveDays : number ;
84
84
}
85
85
86
+ /**
87
+ * Sleep function to add delay between API calls
88
+ */
89
+ const sleep = ( ms : number ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
90
+
86
91
/**
87
92
* Formats a date difference in days
88
93
*/
@@ -116,6 +121,30 @@ function isStale(lastStartedAt: string, days: number): boolean {
116
121
return lastStarted < cutoffDate ;
117
122
}
118
123
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
+
119
148
/**
120
149
* Lists and filters environments that should be deleted
121
150
*/
@@ -126,59 +155,87 @@ async function listEnvironments(
126
155
) : Promise < DeletedEnvironmentInfo [ ] > {
127
156
const toDelete : DeletedEnvironmentInfo [ ] = [ ] ;
128
157
let pageToken : string | undefined = undefined ;
158
+ const baseDelay = 2000 ;
159
+ let retryCount = 0 ;
160
+ const maxRetries = 3 ;
161
+ let totalEnvironmentsChecked = 0 ;
129
162
130
163
try {
131
164
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
+ }
145
177
} ,
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 } ` ) ;
148
201
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
+ }
176
220
}
177
- } ) ;
178
221
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
+ }
180
234
} while ( pageToken ) ;
181
235
236
+ core . info ( `Total environments checked: ${ totalEnvironmentsChecked } ` ) ;
237
+ core . info ( `Environments matching deletion criteria: ${ toDelete . length } ` ) ;
238
+
182
239
return toDelete ;
183
240
} catch ( error ) {
184
241
core . error ( `Error in listEnvironments: ${ error } ` ) ;
@@ -194,24 +251,41 @@ async function deleteEnvironment(
194
251
gitpodToken : string ,
195
252
organizationId : string
196
253
) {
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
208
265
} ,
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 ;
209
287
}
210
- ) ;
211
- core . debug ( `Deleted environment: ${ environmentId } ` ) ;
212
- } catch ( error ) {
213
- core . error ( `Error deleting environment ${ environmentId } : ${ error } ` ) ;
214
- throw error ;
288
+ }
215
289
}
216
290
}
217
291
@@ -248,15 +322,33 @@ async function run() {
248
322
249
323
// Process deletions
250
324
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
+ }
260
352
}
261
353
}
262
354
0 commit comments