@@ -47,6 +47,27 @@ async function getOrCreateOctokit(runner: RunnerInfo): Promise<Octokit> {
4747 return octokit ;
4848}
4949
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+
5071async function listGitHubRunners ( runner : RunnerInfo ) : Promise < GhRunners > {
5172 const key = runner . owner as string ;
5273 const cachedRunners = githubCache . runners . get ( key ) ;
@@ -86,29 +107,48 @@ function bootTimeExceeded(ec2Runner: RunnerInfo): boolean {
86107 return launchTimePlusBootTime < moment ( new Date ( ) ) . utc ( ) ;
87108}
88109
89- async function removeRunner ( ec2runner : RunnerInfo , ghRunnerId : number ) : Promise < void > {
110+ async function removeRunner ( ec2runner : RunnerInfo , ghRunnerIds : number [ ] ) : Promise < void > {
90111 const githubAppClient = await getOrCreateOctokit ( ec2runner ) ;
91112 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 {
106148 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 .` ,
108150 LogFields . print ( ) ,
109151 ) ;
110- } else {
111- logger . error ( `Failed to de-register GitHub runner: ${ result . status } ` , LogFields . print ( ) ) ;
112152 }
113153 } catch ( e ) {
114154 logger . error ( `Runner '${ ec2runner . instanceId } ' cannot be de-registered. Error: ${ e } ` , LogFields . print ( ) ) ;
@@ -130,15 +170,20 @@ async function evaluateAndRemoveRunners(
130170 ) ;
131171 for ( const ec2Runner of ec2RunnersFiltered ) {
132172 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 ) ) {
136178 if ( idleCounter > 0 ) {
137179 idleCounter -- ;
138180 logger . info ( `Runner '${ ec2Runner . instanceId } ' will be kept idle.` , LogFields . print ( ) ) ;
139181 } else {
140182 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+ ) ;
142187 }
143188 }
144189 } else {
0 commit comments