Skip to content

Commit 28fbe1c

Browse files
authored
feat: support for using spot instances (#210)
* Update ecmaVersion to 2020 * Run prettier on src * Add marketType input * Specify InstanceMarketOptions * Fix typo * Generate dist * Allow specifying market type
1 parent 6a96fd4 commit 28fbe1c

File tree

6 files changed

+96
-43
lines changed

6 files changed

+96
-43
lines changed

.eslintrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ env:
55
extends:
66
- eslint:recommended
77
parserOptions:
8-
ecmaVersion: 2018
8+
ecmaVersion: 2020
99
sourceType: module
1010
rules:
1111
no-use-before-define: error

action.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ inputs:
2222
required: false
2323
ec2-instance-type:
2424
description: >-
25-
EC2 Instance Type.
25+
EC2 Instance Type.
2626
This input is required if you use the 'start' mode.
2727
required: false
2828
subnet-id:
@@ -32,7 +32,7 @@ inputs:
3232
required: false
3333
security-group-id:
3434
description: >-
35-
EC2 Security Group Id.
35+
EC2 Security Group Id.
3636
The security group should belong to the same VPC as the specified subnet.
3737
The runner doesn't require any inbound traffic. However, outbound traffic should be allowed.
3838
This input is required if you use the 'start' mode.
@@ -69,6 +69,11 @@ inputs:
6969
description: >-
7070
Specifies bash commands to run before the runner starts. It's useful for installing dependencies with apt-get, yum, dnf, etc.
7171
required: false
72+
market-type:
73+
description: >-
74+
Specifies the market (purchasing) option for the instance:
75+
- 'spot' - Use a spot instance
76+
required: false
7277

7378
outputs:
7479
label:

dist/index.js

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -148362,7 +148362,7 @@ function wrappy (fn, cb) {
148362148362
/***/ 1150:
148363148363
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
148364148364

148365-
const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning } = __nccwpck_require__(3802);
148365+
const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning } = __nccwpck_require__(3802);
148366148366

148367148367
const core = __nccwpck_require__(2186);
148368148368
const config = __nccwpck_require__(4570);
@@ -148379,7 +148379,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
148379148379
'source pre-runner-script.sh',
148380148380
'export RUNNER_ALLOW_RUNASROOT=1',
148381148381
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
148382-
'./run.sh'
148382+
'./run.sh',
148383148383
];
148384148384
} else {
148385148385
return [
@@ -148392,11 +148392,24 @@ function buildUserDataScript(githubRegistrationToken, label) {
148392148392
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.313.0.tar.gz',
148393148393
'export RUNNER_ALLOW_RUNASROOT=1',
148394148394
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
148395-
'./run.sh'
148395+
'./run.sh',
148396148396
];
148397148397
}
148398148398
}
148399148399

148400+
function buildMarketOptions() {
148401+
if (config.input.marketType !== 'spot') {
148402+
return undefined;
148403+
}
148404+
148405+
return {
148406+
MarketType: config.input.marketType,
148407+
SpotOptions: {
148408+
SpotInstanceType: 'one-time',
148409+
},
148410+
};
148411+
}
148412+
148400148413
async function startEc2Instance(label, githubRegistrationToken) {
148401148414
const ec2 = new EC2Client();
148402148415

@@ -148411,7 +148424,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
148411148424
SubnetId: config.input.subnetId,
148412148425
UserData: Buffer.from(userData.join('\n')).toString('base64'),
148413148426
IamInstanceProfile: { Name: config.input.iamRoleName },
148414-
TagSpecifications: config.tagSpecifications
148427+
TagSpecifications: config.tagSpecifications,
148428+
InstanceMarketOptions: buildMarketOptions(),
148415148429
};
148416148430

148417148431
try {
@@ -148429,7 +148443,7 @@ async function terminateEc2Instance() {
148429148443
const ec2 = new EC2Client();
148430148444

148431148445
const params = {
148432-
InstanceIds: [config.input.ec2InstanceId]
148446+
InstanceIds: [config.input.ec2InstanceId],
148433148447
};
148434148448

148435148449
try {
@@ -148445,21 +148459,21 @@ async function terminateEc2Instance() {
148445148459
async function waitForInstanceRunning(ec2InstanceId) {
148446148460
const ec2 = new EC2Client();
148447148461
try {
148448-
core.info(`Cheking for instance ${ec2InstanceId} to be up and running`)
148462+
core.info(`Checking for instance ${ec2InstanceId} to be up and running`);
148449148463
await waitUntilInstanceRunning(
148450148464
{
148451148465
client: ec2,
148452148466
maxWaitTime: 300,
148453-
}, {
148454-
Filters: [
148455-
{
148456-
Name: 'instance-id',
148457-
Values: [
148458-
ec2InstanceId,
148459-
],
148460-
},
148461-
],
148462-
});
148467+
},
148468+
{
148469+
Filters: [
148470+
{
148471+
Name: 'instance-id',
148472+
Values: [ec2InstanceId],
148473+
},
148474+
],
148475+
},
148476+
);
148463148477

148464148478
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
148465148479
return;
@@ -148472,7 +148486,7 @@ async function waitForInstanceRunning(ec2InstanceId) {
148472148486
module.exports = {
148473148487
startEc2Instance,
148474148488
terminateEc2Instance,
148475-
waitForInstanceRunning
148489+
waitForInstanceRunning,
148476148490
};
148477148491

148478148492

@@ -148498,12 +148512,16 @@ class Config {
148498148512
iamRoleName: core.getInput('iam-role-name'),
148499148513
runnerHomeDir: core.getInput('runner-home-dir'),
148500148514
preRunnerScript: core.getInput('pre-runner-script'),
148515+
marketType: core.getInput('market-type'),
148501148516
};
148502148517

148503148518
const tags = JSON.parse(core.getInput('aws-resource-tags'));
148504148519
this.tagSpecifications = null;
148505148520
if (tags.length > 0) {
148506-
this.tagSpecifications = [{ResourceType: 'instance', Tags: tags}, {ResourceType: 'volume', Tags: tags}];
148521+
this.tagSpecifications = [
148522+
{ ResourceType: 'instance', Tags: tags },
148523+
{ ResourceType: 'volume', Tags: tags },
148524+
];
148507148525
}
148508148526

148509148527
// the values of github.context.repo.owner and github.context.repo.repo are taken from
@@ -148530,6 +148548,10 @@ class Config {
148530148548
if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId) {
148531148549
throw new Error(`Not all the required inputs are provided for the 'start' mode`);
148532148550
}
148551+
148552+
if (this.marketType?.length > 0 && this.input.marketType !== 'spot') {
148553+
throw new Error('Invalid `market-type` input. Allowed values: spot.');
148554+
}
148533148555
} else if (this.input.mode === 'stop') {
148534148556
if (!this.input.label || !this.input.ec2InstanceId) {
148535148557
throw new Error(`Not all the required inputs are provided for the 'stop' mode`);
@@ -148617,7 +148639,7 @@ async function waitForRunnerRegistered(label) {
148617148639
let waitSeconds = 0;
148618148640

148619148641
core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instance to be registered in GitHub as a new self-hosted runner`);
148620-
await new Promise(r => setTimeout(r, quietPeriodSeconds * 1000));
148642+
await new Promise((r) => setTimeout(r, quietPeriodSeconds * 1000));
148621148643
core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runner is registered`);
148622148644

148623148645
return new Promise((resolve, reject) => {
@@ -148627,7 +148649,9 @@ async function waitForRunnerRegistered(label) {
148627148649
if (waitSeconds > timeoutMinutes * 60) {
148628148650
core.error('GitHub self-hosted runner registration error');
148629148651
clearInterval(interval);
148630-
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.`);
148652+
reject(
148653+
`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.`,
148654+
);
148631148655
}
148632148656

148633148657
if (runner && runner.status === 'online') {

src/aws.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning } = require('@aws-sdk/client-ec2');
1+
const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning } = require('@aws-sdk/client-ec2');
22

33
const core = require('@actions/core');
44
const config = require('./config');
@@ -15,7 +15,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
1515
'source pre-runner-script.sh',
1616
'export RUNNER_ALLOW_RUNASROOT=1',
1717
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
18-
'./run.sh'
18+
'./run.sh',
1919
];
2020
} else {
2121
return [
@@ -28,11 +28,24 @@ function buildUserDataScript(githubRegistrationToken, label) {
2828
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.313.0.tar.gz',
2929
'export RUNNER_ALLOW_RUNASROOT=1',
3030
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
31-
'./run.sh'
31+
'./run.sh',
3232
];
3333
}
3434
}
3535

36+
function buildMarketOptions() {
37+
if (config.input.marketType !== 'spot') {
38+
return undefined;
39+
}
40+
41+
return {
42+
MarketType: config.input.marketType,
43+
SpotOptions: {
44+
SpotInstanceType: 'one-time',
45+
},
46+
};
47+
}
48+
3649
async function startEc2Instance(label, githubRegistrationToken) {
3750
const ec2 = new EC2Client();
3851

@@ -47,7 +60,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
4760
SubnetId: config.input.subnetId,
4861
UserData: Buffer.from(userData.join('\n')).toString('base64'),
4962
IamInstanceProfile: { Name: config.input.iamRoleName },
50-
TagSpecifications: config.tagSpecifications
63+
TagSpecifications: config.tagSpecifications,
64+
InstanceMarketOptions: buildMarketOptions(),
5165
};
5266

5367
try {
@@ -65,7 +79,7 @@ async function terminateEc2Instance() {
6579
const ec2 = new EC2Client();
6680

6781
const params = {
68-
InstanceIds: [config.input.ec2InstanceId]
82+
InstanceIds: [config.input.ec2InstanceId],
6983
};
7084

7185
try {
@@ -81,21 +95,21 @@ async function terminateEc2Instance() {
8195
async function waitForInstanceRunning(ec2InstanceId) {
8296
const ec2 = new EC2Client();
8397
try {
84-
core.info(`Cheking for instance ${ec2InstanceId} to be up and running`)
98+
core.info(`Checking for instance ${ec2InstanceId} to be up and running`);
8599
await waitUntilInstanceRunning(
86100
{
87101
client: ec2,
88102
maxWaitTime: 300,
89-
}, {
90-
Filters: [
91-
{
92-
Name: 'instance-id',
93-
Values: [
94-
ec2InstanceId,
95-
],
96-
},
97-
],
98-
});
103+
},
104+
{
105+
Filters: [
106+
{
107+
Name: 'instance-id',
108+
Values: [ec2InstanceId],
109+
},
110+
],
111+
},
112+
);
99113

100114
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
101115
return;
@@ -108,5 +122,5 @@ async function waitForInstanceRunning(ec2InstanceId) {
108122
module.exports = {
109123
startEc2Instance,
110124
terminateEc2Instance,
111-
waitForInstanceRunning
125+
waitForInstanceRunning,
112126
};

src/config.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ class Config {
1515
iamRoleName: core.getInput('iam-role-name'),
1616
runnerHomeDir: core.getInput('runner-home-dir'),
1717
preRunnerScript: core.getInput('pre-runner-script'),
18+
marketType: core.getInput('market-type'),
1819
};
1920

2021
const tags = JSON.parse(core.getInput('aws-resource-tags'));
2122
this.tagSpecifications = null;
2223
if (tags.length > 0) {
23-
this.tagSpecifications = [{ResourceType: 'instance', Tags: tags}, {ResourceType: 'volume', Tags: tags}];
24+
this.tagSpecifications = [
25+
{ ResourceType: 'instance', Tags: tags },
26+
{ ResourceType: 'volume', Tags: tags },
27+
];
2428
}
2529

2630
// the values of github.context.repo.owner and github.context.repo.repo are taken from
@@ -47,6 +51,10 @@ class Config {
4751
if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId) {
4852
throw new Error(`Not all the required inputs are provided for the 'start' mode`);
4953
}
54+
55+
if (this.marketType?.length > 0 && this.input.marketType !== 'spot') {
56+
throw new Error('Invalid `market-type` input. Allowed values: spot.');
57+
}
5058
} else if (this.input.mode === 'stop') {
5159
if (!this.input.label || !this.input.ec2InstanceId) {
5260
throw new Error(`Not all the required inputs are provided for the 'stop' mode`);

src/gh.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async function waitForRunnerRegistered(label) {
5858
let waitSeconds = 0;
5959

6060
core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instance to be registered in GitHub as a new self-hosted runner`);
61-
await new Promise(r => setTimeout(r, quietPeriodSeconds * 1000));
61+
await new Promise((r) => setTimeout(r, quietPeriodSeconds * 1000));
6262
core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runner is registered`);
6363

6464
return new Promise((resolve, reject) => {
@@ -68,7 +68,9 @@ async function waitForRunnerRegistered(label) {
6868
if (waitSeconds > timeoutMinutes * 60) {
6969
core.error('GitHub self-hosted runner registration error');
7070
clearInterval(interval);
71-
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.`);
71+
reject(
72+
`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.`,
73+
);
7274
}
7375

7476
if (runner && runner.status === 'online') {

0 commit comments

Comments
 (0)