-
Notifications
You must be signed in to change notification settings - Fork 342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for multiple runners #82
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,8 +20,8 @@ function buildUserDataScript(githubRegistrationToken, label) { | |
'#!/bin/bash', | ||
'mkdir actions-runner && cd actions-runner', | ||
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}', | ||
'curl -O -L https://github.com/actions/runner/releases/download/v2.280.3/actions-runner-linux-${RUNNER_ARCH}-2.280.3.tar.gz', | ||
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.280.3.tar.gz', | ||
'curl -O -L https://github.com/actions/runner/releases/download/v2.284.0/actions-runner-linux-${RUNNER_ARCH}-2.284.0.tar.gz', | ||
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.284.0.tar.gz', | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the version update here is not required, could be pulled out - would still be nice for this to just be automatically the most recent release (#16) |
||
'export RUNNER_ALLOW_RUNASROOT=1', | ||
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1', | ||
`./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) { | |
const params = { | ||
ImageId: config.input.ec2ImageId, | ||
InstanceType: config.input.ec2InstanceType, | ||
MinCount: 1, | ||
MaxCount: 1, | ||
MinCount: config.input.runnerCount, | ||
MaxCount: config.input.runnerCount, | ||
UserData: Buffer.from(userData.join('\n')).toString('base64'), | ||
SubnetId: config.input.subnetId, | ||
SecurityGroupIds: [config.input.securityGroupId], | ||
|
@@ -49,9 +49,9 @@ async function startEc2Instance(label, githubRegistrationToken) { | |
|
||
try { | ||
const result = await ec2.runInstances(params).promise(); | ||
const ec2InstanceId = result.Instances[0].InstanceId; | ||
core.info(`AWS EC2 instance ${ec2InstanceId} is started`); | ||
return ec2InstanceId; | ||
const ec2InstanceIds = result.Instances.map(inst => inst.InstanceId); | ||
core.info(`AWS EC2 instances ${ec2InstanceIds} are started`); | ||
return ec2InstanceIds; | ||
} catch (error) { | ||
core.error('AWS EC2 instance starting error'); | ||
throw error; | ||
|
@@ -62,32 +62,32 @@ async function terminateEc2Instance() { | |
const ec2 = new AWS.EC2(); | ||
|
||
const params = { | ||
InstanceIds: [config.input.ec2InstanceId], | ||
InstanceIds: config.input.ec2InstanceIds, | ||
}; | ||
|
||
try { | ||
await ec2.terminateInstances(params).promise(); | ||
core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`); | ||
core.info(`AWS EC2 instances ${config.input.ec2InstanceIds} are terminated`); | ||
return; | ||
} catch (error) { | ||
core.error(`AWS EC2 instance ${config.input.ec2InstanceId} termination error`); | ||
core.error(`AWS EC2 instances ${config.input.ec2InstanceIds} termination error`); | ||
throw error; | ||
} | ||
} | ||
|
||
async function waitForInstanceRunning(ec2InstanceId) { | ||
async function waitForInstanceRunning(ec2InstanceIds) { | ||
const ec2 = new AWS.EC2(); | ||
|
||
const params = { | ||
InstanceIds: [ec2InstanceId], | ||
InstanceIds: ec2InstanceIds, | ||
}; | ||
|
||
try { | ||
await ec2.waitFor('instanceRunning', params).promise(); | ||
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`); | ||
core.info(`AWS EC2 instances ${ec2InstanceIds} are up and running`); | ||
return; | ||
} catch (error) { | ||
core.error(`AWS EC2 instance ${ec2InstanceId} initialization error`); | ||
core.error(`AWS EC2 instances ${ec2InstanceIds} initialization error`); | ||
throw error; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ async function getRunner(label) { | |
try { | ||
const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext); | ||
const foundRunners = _.filter(runners, { labels: [{ name: label }] }); | ||
return foundRunners.length > 0 ? foundRunners[0] : null; | ||
return foundRunners.length > 0 ? foundRunners : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is worth a bit of discussion. Technically only 1 of N runners may be ready at this point. Do we want to wait for all the expected runners, or can we let the workflow proceed even if some runners may not yet be ready? I'm more inclined to let it proceed even if not all runners come online. It will probably take care of itself, and if not, it will just run slower than expected. |
||
} catch (error) { | ||
return null; | ||
} | ||
|
@@ -32,29 +32,34 @@ async function getRegistrationToken() { | |
} | ||
|
||
async function removeRunner() { | ||
const runner = await getRunner(config.input.label); | ||
const runners = await getRunner(config.input.label); | ||
const octokit = github.getOctokit(config.input.githubToken); | ||
|
||
// skip the runner removal process if the runner is not found | ||
if (!runner) { | ||
if (!runners) { | ||
core.info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`); | ||
return; | ||
} | ||
|
||
try { | ||
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id })); | ||
core.info(`GitHub self-hosted runner ${runner.name} is removed`); | ||
return; | ||
} catch (error) { | ||
core.error('GitHub self-hosted runner removal error'); | ||
throw error; | ||
const errors = []; | ||
for (const runner of runners) { | ||
try { | ||
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id })); | ||
core.info(`GitHub self-hosted runner ${runner.name} is removed`); | ||
} catch (error) { | ||
core.error(`GitHub self-hosted runner ${runner} removal error: ${error}`); | ||
errors.push(error); | ||
} | ||
} | ||
if (errors.length > 0) { | ||
core.setFailed('Failures occurred when removing runners.'); | ||
} | ||
} | ||
|
||
async function waitForRunnerRegistered(label) { | ||
const timeoutMinutes = 5; | ||
const retryIntervalSeconds = 10; | ||
const quietPeriodSeconds = 30; | ||
const timeoutMinutes = 8; | ||
const retryIntervalSeconds = 20; | ||
const quietPeriodSeconds = 40; | ||
Comment on lines
+60
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these changes are not intended to be merged - they were just for testing |
||
let waitSeconds = 0; | ||
|
||
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) { | |
|
||
return new Promise((resolve, reject) => { | ||
const interval = setInterval(async () => { | ||
const runner = await getRunner(label); | ||
const runners = await getRunner(label); | ||
|
||
if (waitSeconds > timeoutMinutes * 60) { | ||
core.error('GitHub self-hosted runner registration error'); | ||
clearInterval(interval); | ||
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.`); | ||
} | ||
|
||
if (runner && runner.status === 'online') { | ||
core.info(`GitHub self-hosted runner ${runner.name} is registered and ready to use`); | ||
if (runners && runners.every((runner => runner.status === 'online'))) { | ||
core.info(`GitHub self-hosted runners ${runners} are registered and ready to use`); | ||
clearInterval(interval); | ||
resolve(); | ||
} else { | ||
waitSeconds += retryIntervalSeconds; | ||
core.info('Checking...'); | ||
} | ||
|
||
}, retryIntervalSeconds * 1000); | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the name change here is considered breaking api-wise, we could always revert this and still treat it as one or more. I just thought it was more clear to make it plural.