@@ -47,6 +47,27 @@ async function getOrCreateOctokit(runner: RunnerInfo): Promise<Octokit> {
47
47
return octokit ;
48
48
}
49
49
50
+ async function getGitHubRunnerBusyState ( client : Octokit , ec2runner : RunnerInfo , runnerId : number ) : Promise < boolean > {
51
+ const state =
52
+ ec2runner . type === 'Org'
53
+ ? await client . actions . getSelfHostedRunnerForOrg ( {
54
+ runner_id : runnerId ,
55
+ org : ec2runner . owner ,
56
+ } )
57
+ : await client . actions . getSelfHostedRunnerForRepo ( {
58
+ runner_id : runnerId ,
59
+ owner : ec2runner . owner . split ( '/' ) [ 0 ] ,
60
+ repo : ec2runner . owner . split ( '/' ) [ 1 ] ,
61
+ } ) ;
62
+
63
+ logger . info (
64
+ `Runner '${ ec2runner . instanceId } ' - GitHub Runner ID '${ runnerId } ' - Busy: ${ state . data . busy } ` ,
65
+ LogFields . print ( ) ,
66
+ ) ;
67
+
68
+ return state . data . busy ;
69
+ }
70
+
50
71
async function listGitHubRunners ( runner : RunnerInfo ) : Promise < GhRunners > {
51
72
const key = runner . owner as string ;
52
73
const cachedRunners = githubCache . runners . get ( key ) ;
@@ -86,29 +107,48 @@ function bootTimeExceeded(ec2Runner: RunnerInfo): boolean {
86
107
return launchTimePlusBootTime < moment ( new Date ( ) ) . utc ( ) ;
87
108
}
88
109
89
- async function removeRunner ( ec2runner : RunnerInfo , ghRunnerId : number ) : Promise < void > {
110
+ async function removeRunner ( ec2runner : RunnerInfo , ghRunnerIds : number [ ] ) : Promise < void > {
90
111
const githubAppClient = await getOrCreateOctokit ( ec2runner ) ;
91
112
try {
92
- const result =
93
- ec2runner . type === 'Org'
94
- ? await githubAppClient . actions . deleteSelfHostedRunnerFromOrg ( {
95
- runner_id : ghRunnerId ,
96
- org : ec2runner . owner ,
97
- } )
98
- : await githubAppClient . actions . deleteSelfHostedRunnerFromRepo ( {
99
- runner_id : ghRunnerId ,
100
- owner : ec2runner . owner . split ( '/' ) [ 0 ] ,
101
- repo : ec2runner . owner . split ( '/' ) [ 1 ] ,
102
- } ) ;
103
-
104
- if ( result . status == 204 ) {
105
- await terminateRunner ( ec2runner . instanceId ) ;
113
+ const states = await Promise . all (
114
+ ghRunnerIds . map ( async ( ghRunnerId ) => {
115
+ // Get busy state instead of using the output of listGitHubRunners(...) to minimize to race condition.
116
+ return await getGitHubRunnerBusyState ( githubAppClient , ec2runner , ghRunnerId ) ;
117
+ } ) ,
118
+ ) ;
119
+
120
+ if ( states . every ( ( busy ) => busy === false ) ) {
121
+ const statuses = await Promise . all (
122
+ ghRunnerIds . map ( async ( ghRunnerId ) => {
123
+ return (
124
+ ec2runner . type === 'Org'
125
+ ? await githubAppClient . actions . deleteSelfHostedRunnerFromOrg ( {
126
+ runner_id : ghRunnerId ,
127
+ org : ec2runner . owner ,
128
+ } )
129
+ : await githubAppClient . actions . deleteSelfHostedRunnerFromRepo ( {
130
+ runner_id : ghRunnerId ,
131
+ owner : ec2runner . owner . split ( '/' ) [ 0 ] ,
132
+ repo : ec2runner . owner . split ( '/' ) [ 1 ] ,
133
+ } )
134
+ ) . status ;
135
+ } ) ,
136
+ ) ;
137
+
138
+ if ( statuses . every ( ( status ) => status == 204 ) ) {
139
+ await terminateRunner ( ec2runner . instanceId ) ;
140
+ logger . info (
141
+ `AWS runner instance '${ ec2runner . instanceId } ' is terminated and GitHub runner is de-registered.` ,
142
+ LogFields . print ( ) ,
143
+ ) ;
144
+ } else {
145
+ logger . error ( `Failed to de-register GitHub runner: ${ statuses } ` , LogFields . print ( ) ) ;
146
+ }
147
+ } else {
106
148
logger . info (
107
- `AWS runner instance '${ ec2runner . instanceId } ' is terminated and GitHub runner is de-registered .` ,
149
+ `Runner '${ ec2runner . instanceId } ' cannot be de-registered, because it is still busy .` ,
108
150
LogFields . print ( ) ,
109
151
) ;
110
- } else {
111
- logger . error ( `Failed to de-register GitHub runner: ${ result . status } ` , LogFields . print ( ) ) ;
112
152
}
113
153
} catch ( e ) {
114
154
logger . error ( `Runner '${ ec2runner . instanceId } ' cannot be de-registered. Error: ${ e } ` , LogFields . print ( ) ) ;
@@ -130,15 +170,20 @@ async function evaluateAndRemoveRunners(
130
170
) ;
131
171
for ( const ec2Runner of ec2RunnersFiltered ) {
132
172
const ghRunners = await listGitHubRunners ( ec2Runner ) ;
133
- const ghRunner = ghRunners . find ( ( runner ) => runner . name === ec2Runner . instanceId ) ;
134
- if ( ghRunner ) {
135
- if ( ! ghRunner . busy && runnerMinimumTimeExceeded ( ec2Runner ) ) {
173
+ const ghRunnersFiltered = ghRunners . filter ( ( runner : { name : string } ) =>
174
+ runner . name . startsWith ( ec2Runner . instanceId ) ,
175
+ ) ;
176
+ if ( ghRunnersFiltered . length ) {
177
+ if ( runnerMinimumTimeExceeded ( ec2Runner ) ) {
136
178
if ( idleCounter > 0 ) {
137
179
idleCounter -- ;
138
180
logger . info ( `Runner '${ ec2Runner . instanceId } ' will be kept idle.` , LogFields . print ( ) ) ;
139
181
} else {
140
182
logger . info ( `Runner '${ ec2Runner . instanceId } ' will be terminated.` , LogFields . print ( ) ) ;
141
- await removeRunner ( ec2Runner , ghRunner . id ) ;
183
+ await removeRunner (
184
+ ec2Runner ,
185
+ ghRunnersFiltered . map ( ( runner : { id : number } ) => runner . id ) ,
186
+ ) ;
142
187
}
143
188
}
144
189
} else {
0 commit comments