@@ -9,13 +9,18 @@ import { createRunners } from '../scale-runners/scale-up';
99import { adjust } from './pool' ;
1010
1111const mockOctokit = {
12- paginate : jest . fn ( ) ,
12+ paginate : ( f : any , o : any ) => f ( o ) ,
1313 checks : { get : jest . fn ( ) } ,
1414 actions : {
1515 createRegistrationTokenForOrg : jest . fn ( ) ,
16+ listJobsForWorkflowRunAttempt : jest . fn ( ) ,
17+ listSelfHostedRunnersForOrg : jest . fn ( ) ,
18+ listSelfHostedRunnersForRepo : jest . fn ( ) ,
19+ listWorkflowRunsForRepo : jest . fn ( ) ,
1620 } ,
1721 apps : {
1822 getOrgInstallation : jest . fn ( ) ,
23+ listReposAccessibleToInstallation : jest . fn ( ) ,
1924 } ,
2025} ;
2126
@@ -42,6 +47,7 @@ const cleanEnv = process.env;
4247
4348const ORG = 'my-org' ;
4449const MINIMUM_TIME_RUNNING = 15 ;
50+ const LABELS = [ 'label1' , 'label2' ] ;
4551
4652const ec2InstancesRegistered = [
4753 {
@@ -79,31 +85,46 @@ const githubRunnersRegistered = [
7985 os : 'linux' ,
8086 status : 'online' ,
8187 busy : false ,
82- labels : [ ] ,
88+ labels : LABELS ,
8389 } ,
8490 {
8591 id : 2 ,
8692 name : 'i-2-busy' ,
8793 os : 'linux' ,
8894 status : 'online' ,
8995 busy : true ,
90- labels : [ ] ,
96+ labels : LABELS ,
9197 } ,
9298 {
9399 id : 3 ,
94100 name : 'i-3-offline' ,
95101 os : 'linux' ,
96102 status : 'offline' ,
97103 busy : false ,
98- labels : [ ] ,
104+ labels : LABELS ,
99105 } ,
100106 {
101107 id : 3 ,
102108 name : 'i-4-idle-older-than-minimum-time-running' ,
103109 os : 'linux' ,
104110 status : 'online' ,
105111 busy : false ,
106- labels : [ ] ,
112+ labels : LABELS ,
113+ } ,
114+ ] ;
115+
116+ const githubReposAccessibleToInstallation = [
117+ {
118+ owner : {
119+ login : ORG ,
120+ } ,
121+ name : 'my-repo-1' ,
122+ } ,
123+ {
124+ owner : {
125+ login : ORG ,
126+ } ,
127+ name : 'my-repo-2' ,
107128 } ,
108129] ;
109130
@@ -126,6 +147,7 @@ beforeEach(() => {
126147 process . env . INSTANCE_TARGET_CAPACITY_TYPE = 'spot' ;
127148 process . env . RUNNER_OWNER = ORG ;
128149 process . env . RUNNER_BOOT_TIME_IN_MINUTES = MINIMUM_TIME_RUNNING . toString ( ) ;
150+ process . env . RUNNER_LABELS = LABELS . join ( ',' ) ;
129151
130152 const mockTokenReturnValue = {
131153 data : {
@@ -134,7 +156,15 @@ beforeEach(() => {
134156 } ;
135157 mockOctokit . actions . createRegistrationTokenForOrg . mockImplementation ( ( ) => mockTokenReturnValue ) ;
136158
137- mockOctokit . paginate . mockImplementation ( ( ) => githubRunnersRegistered ) ;
159+ mockOctokit . actions . listSelfHostedRunnersForOrg . mockImplementation ( ( ) => githubRunnersRegistered ) ;
160+
161+ mockOctokit . actions . listSelfHostedRunnersForRepo . mockImplementation ( ( ) => githubRunnersRegistered ) ;
162+
163+ mockOctokit . apps . listReposAccessibleToInstallation . mockImplementation ( ( ) => githubReposAccessibleToInstallation ) ;
164+
165+ mockOctokit . actions . listWorkflowRunsForRepo . mockImplementation ( async ( ) => [ ] ) ;
166+
167+ mockOctokit . actions . listJobsForWorkflowRunAttempt . mockImplementation ( async ( ) => [ ] ) ;
138168
139169 mockListRunners . mockImplementation ( async ( ) => ec2InstancesRegistered ) ;
140170
@@ -171,7 +201,7 @@ describe('Test simple pool.', () => {
171201 await expect ( await adjust ( { poolSize : 3 } ) ) . resolves ;
172202 expect ( createRunners ) . toHaveBeenCalledTimes ( 1 ) ;
173203 expect ( createRunners ) . toHaveBeenCalledWith (
174- expect . anything ( ) ,
204+ expect . objectContaining ( { runnerOwner : ORG , runnerType : 'Org' } ) ,
175205 expect . objectContaining ( { numberOfRunners : 1 } ) ,
176206 expect . anything ( ) ,
177207 ) ;
@@ -206,7 +236,7 @@ describe('Test simple pool.', () => {
206236 // 2 idle + 1 booting = 3, top up with 2 to match a pool of 5
207237 await expect ( await adjust ( { poolSize : 5 } ) ) . resolves ;
208238 expect ( createRunners ) . toHaveBeenCalledWith (
209- expect . anything ( ) ,
239+ expect . objectContaining ( { runnerOwner : ORG , runnerType : 'Org' } ) ,
210240 expect . objectContaining ( { numberOfRunners : 2 } ) ,
211241 expect . anything ( ) ,
212242 ) ;
@@ -247,7 +277,7 @@ describe('Test simple pool.', () => {
247277 await expect ( await adjust ( { poolSize : 5 } ) ) . resolves ;
248278 // 2 idle, top up with 3 to match a pool of 5
249279 expect ( createRunners ) . toHaveBeenCalledWith (
250- expect . anything ( ) ,
280+ expect . objectContaining ( { runnerOwner : ORG , runnerType : 'Org' } ) ,
251281 expect . objectContaining ( { numberOfRunners : 3 } ) ,
252282 expect . anything ( ) ,
253283 ) ;
@@ -261,7 +291,7 @@ describe('Test simple pool.', () => {
261291
262292 it ( 'Should top up with fewer runners when there are idle prefixed runners' , async ( ) => {
263293 // Add prefixed runners to github
264- mockOctokit . paginate . mockImplementation ( async ( ) => [
294+ mockOctokit . actions . listSelfHostedRunnersForOrg . mockImplementation ( async ( ) => [
265295 ...githubRunnersRegistered ,
266296 {
267297 id : 5 ,
@@ -301,10 +331,110 @@ describe('Test simple pool.', () => {
301331 await expect ( await adjust ( { poolSize : 5 } ) ) . resolves ;
302332 // 2 idle, 2 prefixed idle top up with 1 to match a pool of 5
303333 expect ( createRunners ) . toHaveBeenCalledWith (
334+ expect . objectContaining ( { runnerOwner : ORG , runnerType : 'Org' } ) ,
335+ expect . objectContaining ( { numberOfRunners : 1 } ) ,
336+ expect . anything ( ) ,
337+ ) ;
338+ } ) ;
339+ } ) ;
340+
341+ describe ( 'With Negative Pool Size' , ( ) => {
342+ // effective pool size is 2 (1 queued job with matching labels x 1 workflows x 2 accessible repositories)
343+ it ( 'Should not top up if there are fewer queued jobs than idle runners.' , async ( ) => {
344+ mockOctokit . actions . listWorkflowRunsForRepo . mockImplementation ( async ( { owner, repo } ) => [
345+ {
346+ repository : {
347+ owner : { login : owner } ,
348+ name : repo ,
349+ } ,
350+ id : 1 ,
351+ attempt_number : 1 ,
352+ } ,
353+ ] ) ;
354+ mockOctokit . actions . listJobsForWorkflowRunAttempt . mockImplementation ( async ( ) => [
355+ {
356+ status : 'completed' ,
357+ labels : LABELS ,
358+ } ,
359+ {
360+ status : 'queued' ,
361+ labels : LABELS ,
362+ } ,
363+ {
364+ status : 'queued' ,
365+ labels : [ ...LABELS , 'label3' ] ,
366+ } ,
367+ ] ) ;
368+ await expect ( await adjust ( { poolSize : - 1 } ) ) . resolves ;
369+ expect ( createRunners ) . not . toHaveBeenCalled ( ) ;
370+ } ) ;
371+ // effective pool size is 8 (2 queued job with matching labels x 2 workflows x 2 accessible repositories)
372+ it ( 'Should top up if there are more queued jobs with matching labels than idle runners.' , async ( ) => {
373+ mockOctokit . actions . listWorkflowRunsForRepo . mockImplementation ( async ( { owner, repo } ) => [
374+ {
375+ repository : {
376+ owner : { login : owner } ,
377+ name : repo ,
378+ } ,
379+ id : 1 ,
380+ attempt_number : 1 ,
381+ } ,
382+ {
383+ repository : {
384+ owner : { login : owner } ,
385+ name : repo ,
386+ } ,
387+ id : 2 ,
388+ attempt_number : 1 ,
389+ } ,
390+ ] ) ;
391+ mockOctokit . actions . listJobsForWorkflowRunAttempt . mockImplementation ( async ( ) => [
392+ {
393+ status : 'queued' ,
394+ labels : LABELS ,
395+ } ,
396+ {
397+ status : 'queued' ,
398+ labels : LABELS ,
399+ } ,
400+ ] ) ;
401+ await expect ( await adjust ( { poolSize : - 1 } ) ) . resolves ;
402+ expect ( createRunners ) . toHaveBeenCalledTimes ( 1 ) ;
403+ expect ( createRunners ) . toHaveBeenCalledWith (
404+ expect . objectContaining ( { runnerOwner : ORG , runnerType : 'Org' } ) ,
405+ expect . objectContaining ( { numberOfRunners : 6 } ) ,
304406 expect . anything ( ) ,
407+ ) ;
408+ } ) ;
409+ } ) ;
410+
411+ describe ( 'With Runner Type Repo' , ( ) => {
412+ it ( 'Should top up the repository runners pool' , async ( ) => {
413+ const runnerOwner = `${ ORG } /my-repo-1` ;
414+ process . env . RUNNER_OWNER = runnerOwner ;
415+ await expect ( await adjust ( { poolSize : 3 } ) ) . resolves ;
416+ expect ( createRunners ) . toHaveBeenCalledTimes ( 1 ) ;
417+ expect ( createRunners ) . toHaveBeenCalledWith (
418+ expect . objectContaining ( { runnerOwner, runnerType : 'Repo' } ) ,
305419 expect . objectContaining ( { numberOfRunners : 1 } ) ,
306420 expect . anything ( ) ,
307421 ) ;
308422 } ) ;
309423 } ) ;
424+
425+ describe ( 'With Multiple Runner Owners' , ( ) => {
426+ it ( 'Should top up pools for all runner owners' , async ( ) => {
427+ const runnerOwners = [ `${ ORG } /my-repo-1` , `${ ORG } /my-repo-2` ] ;
428+ process . env . RUNNER_OWNER = runnerOwners . join ( ',' ) ;
429+ await expect ( await adjust ( { poolSize : 3 } ) ) . resolves ;
430+ expect ( createRunners ) . toHaveBeenCalledTimes ( 2 ) ;
431+ for ( const runnerOwner of runnerOwners ) {
432+ expect ( createRunners ) . toHaveBeenCalledWith (
433+ expect . objectContaining ( { runnerOwner, runnerType : 'Repo' } ) ,
434+ expect . objectContaining ( { numberOfRunners : 1 } ) ,
435+ expect . anything ( ) ,
436+ ) ;
437+ }
438+ } ) ;
439+ } ) ;
310440} ) ;
0 commit comments