Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9aa72be

Browse files
committedNov 25, 2021
Add support for multiple runners
1 parent 502fc5c commit 9aa72be

File tree

5 files changed

+55
-42
lines changed

5 files changed

+55
-42
lines changed
 

‎action.yml

+11-5
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ inputs:
4343
The label is used to remove the runner from GitHub when the runner is not needed anymore.
4444
This input is required if you use the 'stop' mode.
4545
required: false
46-
ec2-instance-id:
46+
ec2-instance-ids:
4747
description: >-
48-
EC2 Instance Id of the created runner.
49-
The id is used to terminate the EC2 instance when the runner is not needed anymore.
48+
EC2 Instance Id(s) of the created runner(s).
49+
The Id(s) are used to terminate the EC2 instance(s) when the runner is not needed anymore.
5050
This input is required if you use the 'stop' mode.
5151
required: false
52+
default: 'null'
5253
iam-role-name:
5354
description: >-
5455
IAM Role Name to attach to the created EC2 instance.
@@ -65,16 +66,21 @@ inputs:
6566
description: >-
6667
Directory that contains actions-runner software and scripts. E.g. /home/runner/actions-runner.
6768
required: false
69+
runner-count:
70+
description: >-
71+
Number of instances to create.
72+
required: false
73+
default: '1'
6874
outputs:
6975
label:
7076
description: >-
7177
Name of the unique label assigned to the runner.
7278
The label is used in two cases:
7379
- to use as the input of 'runs-on' property for the following jobs;
7480
- to remove the runner from GitHub when it is not needed anymore.
75-
ec2-instance-id:
81+
ec2-instance-ids:
7682
description: >-
77-
EC2 Instance Id of the created runner.
83+
EC2 Instance Id(s) of the created runner(s).
7884
The id is used to terminate the EC2 instance when the runner is not needed anymore.
7985
runs:
8086
using: node12

‎src/aws.js

+14-14
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ function buildUserDataScript(githubRegistrationToken, label) {
2020
'#!/bin/bash',
2121
'mkdir actions-runner && cd actions-runner',
2222
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
23-
'curl -O -L https://github.com/actions/runner/releases/download/v2.280.3/actions-runner-linux-${RUNNER_ARCH}-2.280.3.tar.gz',
24-
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.280.3.tar.gz',
23+
'curl -O -L https://github.com/actions/runner/releases/download/v2.284.0/actions-runner-linux-${RUNNER_ARCH}-2.284.0.tar.gz',
24+
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.284.0.tar.gz',
2525
'export RUNNER_ALLOW_RUNASROOT=1',
2626
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
2727
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
@@ -38,8 +38,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
3838
const params = {
3939
ImageId: config.input.ec2ImageId,
4040
InstanceType: config.input.ec2InstanceType,
41-
MinCount: 1,
42-
MaxCount: 1,
41+
MinCount: config.input.runnerCount,
42+
MaxCount: config.input.runnerCount,
4343
UserData: Buffer.from(userData.join('\n')).toString('base64'),
4444
SubnetId: config.input.subnetId,
4545
SecurityGroupIds: [config.input.securityGroupId],
@@ -49,9 +49,9 @@ async function startEc2Instance(label, githubRegistrationToken) {
4949

5050
try {
5151
const result = await ec2.runInstances(params).promise();
52-
const ec2InstanceId = result.Instances[0].InstanceId;
53-
core.info(`AWS EC2 instance ${ec2InstanceId} is started`);
54-
return ec2InstanceId;
52+
const ec2InstanceIds = result.Instances.map(inst => inst.InstanceId);
53+
core.info(`AWS EC2 instances ${ec2InstanceIds} are started`);
54+
return ec2InstanceIds;
5555
} catch (error) {
5656
core.error('AWS EC2 instance starting error');
5757
throw error;
@@ -62,32 +62,32 @@ async function terminateEc2Instance() {
6262
const ec2 = new AWS.EC2();
6363

6464
const params = {
65-
InstanceIds: [config.input.ec2InstanceId],
65+
InstanceIds: config.input.ec2InstanceIds,
6666
};
6767

6868
try {
6969
await ec2.terminateInstances(params).promise();
70-
core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
70+
core.info(`AWS EC2 instances ${config.input.ec2InstanceIds} are terminated`);
7171
return;
7272
} catch (error) {
73-
core.error(`AWS EC2 instance ${config.input.ec2InstanceId} termination error`);
73+
core.error(`AWS EC2 instances ${config.input.ec2InstanceIds} termination error`);
7474
throw error;
7575
}
7676
}
7777

78-
async function waitForInstanceRunning(ec2InstanceId) {
78+
async function waitForInstanceRunning(ec2InstanceIds) {
7979
const ec2 = new AWS.EC2();
8080

8181
const params = {
82-
InstanceIds: [ec2InstanceId],
82+
InstanceIds: ec2InstanceIds,
8383
};
8484

8585
try {
8686
await ec2.waitFor('instanceRunning', params).promise();
87-
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
87+
core.info(`AWS EC2 instances ${ec2InstanceIds} are up and running`);
8888
return;
8989
} catch (error) {
90-
core.error(`AWS EC2 instance ${ec2InstanceId} initialization error`);
90+
core.error(`AWS EC2 instances ${ec2InstanceIds} initialization error`);
9191
throw error;
9292
}
9393
}

‎src/config.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ class Config {
1111
subnetId: core.getInput('subnet-id'),
1212
securityGroupId: core.getInput('security-group-id'),
1313
label: core.getInput('label'),
14-
ec2InstanceId: core.getInput('ec2-instance-id'),
14+
ec2InstanceIds: JSON.parse(core.getInput('ec2-instance-ids')),
1515
iamRoleName: core.getInput('iam-role-name'),
1616
runnerHomeDir: core.getInput('runner-home-dir'),
17+
runnerCount: core.getInput('runner-count'),
1718
};
1819

1920
const tags = JSON.parse(core.getInput('aws-resource-tags'));
@@ -47,7 +48,7 @@ class Config {
4748
throw new Error(`Not all the required inputs are provided for the 'start' mode`);
4849
}
4950
} else if (this.input.mode === 'stop') {
50-
if (!this.input.label || !this.input.ec2InstanceId) {
51+
if (!this.input.label || !this.input.ec2InstanceIds) {
5152
throw new Error(`Not all the required inputs are provided for the 'stop' mode`);
5253
}
5354
} else {

‎src/gh.js

+22-16
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function getRunner(label) {
1111
try {
1212
const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
1313
const foundRunners = _.filter(runners, { labels: [{ name: label }] });
14-
return foundRunners.length > 0 ? foundRunners[0] : null;
14+
return foundRunners.length > 0 ? foundRunners : null;
1515
} catch (error) {
1616
return null;
1717
}
@@ -32,29 +32,34 @@ async function getRegistrationToken() {
3232
}
3333

3434
async function removeRunner() {
35-
const runner = await getRunner(config.input.label);
35+
const runners = await getRunner(config.input.label);
3636
const octokit = github.getOctokit(config.input.githubToken);
3737

3838
// skip the runner removal process if the runner is not found
39-
if (!runner) {
39+
if (!runners) {
4040
core.info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`);
4141
return;
4242
}
4343

44-
try {
45-
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id }));
46-
core.info(`GitHub self-hosted runner ${runner.name} is removed`);
47-
return;
48-
} catch (error) {
49-
core.error('GitHub self-hosted runner removal error');
50-
throw error;
44+
const errors = [];
45+
for (const runner of runners) {
46+
try {
47+
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id }));
48+
core.info(`GitHub self-hosted runner ${runner.name} is removed`);
49+
} catch (error) {
50+
core.error(`GitHub self-hosted runner ${runner} removal error: ${error}`);
51+
errors.push(error);
52+
}
53+
}
54+
if (errors.length > 0) {
55+
core.setFailed('Failures occurred when removing runners.');
5156
}
5257
}
5358

5459
async function waitForRunnerRegistered(label) {
55-
const timeoutMinutes = 5;
56-
const retryIntervalSeconds = 10;
57-
const quietPeriodSeconds = 30;
60+
const timeoutMinutes = 8;
61+
const retryIntervalSeconds = 20;
62+
const quietPeriodSeconds = 40;
5863
let waitSeconds = 0;
5964

6065
core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instance to be registered in GitHub as a new self-hosted runner`);
@@ -63,22 +68,23 @@ async function waitForRunnerRegistered(label) {
6368

6469
return new Promise((resolve, reject) => {
6570
const interval = setInterval(async () => {
66-
const runner = await getRunner(label);
71+
const runners = await getRunner(label);
6772

6873
if (waitSeconds > timeoutMinutes * 60) {
6974
core.error('GitHub self-hosted runner registration error');
7075
clearInterval(interval);
7176
reject(`A timeout of ${timeoutMinutes} minutes is exceeded. Your AWS EC2 instance was not able to register itself in GitHub as a new self-hosted runner.`);
7277
}
7378

74-
if (runner && runner.status === 'online') {
75-
core.info(`GitHub self-hosted runner ${runner.name} is registered and ready to use`);
79+
if (runners && runners.every((runner => runner.status === 'online'))) {
80+
core.info(`GitHub self-hosted runners ${runners} are registered and ready to use`);
7681
clearInterval(interval);
7782
resolve();
7883
} else {
7984
waitSeconds += retryIntervalSeconds;
8085
core.info('Checking...');
8186
}
87+
8288
}, retryIntervalSeconds * 1000);
8389
});
8490
}

‎src/index.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ const gh = require('./gh');
33
const config = require('./config');
44
const core = require('@actions/core');
55

6-
function setOutput(label, ec2InstanceId) {
6+
function setOutput(label, ec2InstanceIds) {
77
core.setOutput('label', label);
8-
core.setOutput('ec2-instance-id', ec2InstanceId);
8+
core.setOutput('ec2-instance-ids', ec2InstanceIds);
99
}
1010

1111
async function start() {
1212
const label = config.generateUniqueLabel();
1313
const githubRegistrationToken = await gh.getRegistrationToken();
14-
const ec2InstanceId = await aws.startEc2Instance(label, githubRegistrationToken);
15-
setOutput(label, ec2InstanceId);
16-
await aws.waitForInstanceRunning(ec2InstanceId);
14+
const ec2InstanceIds = await aws.startEc2Instance(label, githubRegistrationToken);
15+
setOutput(label, ec2InstanceIds);
16+
await aws.waitForInstanceRunning(ec2InstanceIds);
1717
await gh.waitForRunnerRegistered(label);
1818
}
1919

0 commit comments

Comments
 (0)
Please sign in to comment.