Skip to content

Commit

Permalink
test 1 of new action
Browse files Browse the repository at this point in the history
  • Loading branch information
BrusR committed Oct 5, 2023
1 parent c110282 commit ba546d2
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 70 deletions.
12 changes: 6 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: On-demand self-hosted AWS EC2 runner for GitHub Actions
description: GitHub Action for automatic creation and registration AWS EC2 instance as a GitHub Actions self-hosted runner.
author: Volodymyr Machula
author: Based on Volodymyr Machulav original action
branding:
icon: 'box'
color: 'orange'
Expand Down Expand Up @@ -37,13 +37,13 @@ inputs:
The runner doesn't require any inbound traffic. However, outbound traffic should be allowed.
This input is required if you use the 'start' mode.
required: false
label:
labels:
description: >-
Name of the unique label assigned to the runner.
The label is used to remove the runner from GitHub when the runner is not needed anymore.
This input is required if you use the 'stop' mode.
required: false
ec2-instance-id:
required: true
ec2-instances-ids:
description: >-
EC2 Instance Id of the created runner.
The id is used to terminate the EC2 instance when the runner is not needed anymore.
Expand Down Expand Up @@ -71,9 +71,9 @@ inputs:
required: false

outputs:
label:
labels:
description: >-
Name of the unique label assigned to the runner.
Name of the labels assigned to the runners.
The label is used in two cases:
- to use as the input of 'runs-on' property for the following jobs;
- to remove the runner from GitHub when it is not needed anymore.
Expand Down
175 changes: 119 additions & 56 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62802,8 +62802,29 @@ const AWS = __webpack_require__(71786);
const core = __webpack_require__(42186);
const config = __webpack_require__(34570);

function separateArrayWithCommas(arr) {
if (!Array.isArray(arr)) {
return "Input is not an array";
}
// Use the join() method to concatenate array elements into a string
return arr.join(",");
}
function retrieveInstanceIDsFromArrayofMaps(arrayOfMaps) {
// Check if the input is an array
if (!Array.isArray(arrayOfMaps)) {
return "Input is not an array";
}

// Use map() to extract the "instanceID" from each map
const instanceIDs = arrayOfMaps.map((map) => map["InstanceId"]);

// Filter out undefined values in case some maps don't have "instanceID"
const filteredInstanceIDs = instanceIDs.filter((id) => id !== undefined);

return filteredInstanceIDs;
}
// User data scripts are run as the root user
function buildUserDataScript(githubRegistrationToken, label) {
function buildUserDataScript(githubRegistrationToken, labels) {
if (config.input.runnerHomeDir) {
// If runner home directory is specified, we expect the actions-runner software (and dependencies)
// to be pre-installed in the AMI, so we simply cd into that directory and then start the runner
Expand All @@ -62813,7 +62834,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
`echo "${config.input.preRunnerScript}" > pre-runner-script.sh`,
'source pre-runner-script.sh',
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${separateArrayWithCommas(labels)}`,
'./run.sh',
];
} else {
Expand All @@ -62826,22 +62847,22 @@ function buildUserDataScript(githubRegistrationToken, label) {
'curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${separateArrayWithCommas(labels)}`,
'./run.sh',
];
}
}

async function startEc2Instance(label, githubRegistrationToken) {
async function startEc2Instances(labels, githubRegistrationToken) {
const ec2 = new AWS.EC2();

const userData = buildUserDataScript(githubRegistrationToken, label);
const userData = buildUserDataScript(githubRegistrationToken, labels);

const params = {
ImageId: config.input.ec2ImageId,
InstanceType: config.input.ec2InstanceType,
MinCount: 1,
MaxCount: 1,
MinCount: config.input.instanceQuantity,
MaxCount: config.input.instanceQuantity,
UserData: Buffer.from(userData.join('\n')).toString('base64'),
SubnetId: config.input.subnetId,
SecurityGroupIds: [config.input.securityGroupId],
Expand All @@ -62851,53 +62872,53 @@ 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 ec2InstancesIds = retrieveInstanceIDsFromArrayofMaps(result.Instances)
core.info(`AWS EC2 instances ${separateArrayWithCommas(ec2InstancesIds)} are started`);
return ec2InstancesIds;
} catch (error) {
core.error('AWS EC2 instance starting error');
core.error('AWS EC2 instances starting error');
throw error;
}
}

async function terminateEc2Instance() {
async function terminateEc2Instances() {
const ec2 = new AWS.EC2();

const params = {
InstanceIds: [config.input.ec2InstanceId],
InstanceIds: config.getec2InstanceIds(),
};

try {
await ec2.terminateInstances(params).promise();
core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
core.info(`AWS EC2 instances ${separateArrayWithCommas(config.getec2InstanceIds())} are terminated`);
return;
} catch (error) {
core.error(`AWS EC2 instance ${config.input.ec2InstanceId} termination error`);
core.error(`AWS EC2 instances ${separateArrayWithCommas(config.getec2InstanceIds())} termination error`);
throw error;
}
}

async function waitForInstanceRunning(ec2InstanceId) {
async function waitForInstancesRunning(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 instance ${separateArrayWithCommas(ec2InstanceIds)} are up and running`);
return;
} catch (error) {
core.error(`AWS EC2 instance ${ec2InstanceId} initialization error`);
core.error(`AWS EC2 instance ${separateArrayWithCommas(ec2InstanceIds)} initialization error`);
throw error;
}
}

module.exports = {
startEc2Instance,
terminateEc2Instance,
waitForInstanceRunning,
startEc2Instances,
terminateEc2Instances,
waitForInstancesRunning,
};


Expand All @@ -62918,11 +62939,12 @@ class Config {
ec2InstanceType: core.getInput('ec2-instance-type'),
subnetId: core.getInput('subnet-id'),
securityGroupId: core.getInput('security-group-id'),
label: core.getInput('label'),
ec2InstanceId: core.getInput('ec2-instance-id'),
labels: core.getInput('labels'),
ec2InstanceIds: core.getInput('ec2-instances-ids'),
iamRoleName: core.getInput('iam-role-name'),
runnerHomeDir: core.getInput('runner-home-dir'),
preRunnerScript: core.getInput('pre-runner-script'),
instanceQuantity: core.getInput('instance-quantity')
};

const tags = JSON.parse(core.getInput('aws-resource-tags'));
Expand Down Expand Up @@ -62967,6 +62989,12 @@ class Config {
generateUniqueLabel() {
return Math.random().toString(36).substr(2, 5);
}
getLabels() {
return JSON.parse(this.labels)
}
getec2InstanceIds(){
return JSON.parse(this.ec2InstanceIds)
}
}

try {
Expand All @@ -62987,15 +63015,40 @@ const github = __webpack_require__(95438);
const _ = __webpack_require__(90250);
const config = __webpack_require__(34570);


function separateArrayWithCommas(arr) {
if (!Array.isArray(arr)) {
return "Input is not an array";
}
// Use the join() method to concatenate array elements into a string
return arr.join(",");
}

function areRunnersOnline(runners){
if (runners){
var result = true
runners.forEach((runner) => {
if (runner.status !== 'online'){
result = false
}
});
return result
} else {
return false
}
}
// use the unique label to find the runner
// as we don't have the runner's id, it's not possible to get it in any other way
async function getRunner(label) {
async function getRunners(multiple_labels) {
const octokit = github.getOctokit(config.input.githubToken);

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;
var foundRunners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
multiple_labels.forEach((label) => {
foundRunners = _.filter(foundRunners, { labels: [{ name: label }] });
});

return foundRunners.length > 0 ? foundRunners : null;
} catch (error) {
return null;
}
Expand All @@ -63014,49 +63067,60 @@ async function getRegistrationToken() {
throw error;
}
}

async function removeRunner() {
const runner = await getRunner(config.input.label);
async function removeRunners() {
const runners = await getRunners(config.getLabels());
const octokit = github.getOctokit(config.input.githubToken);

// skip the runner removal process if the runner is not found
if (!runner) {
core.info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`);
// skip the runner removal process if no runners are found
if (!runners || runners.length === 0) {
core.info(`No GitHub self-hosted runners with labels ${separateArrayWithCommas(config.getLabels())} found, so the removal is skipped`);
return;
}

// Use Promise.all to remove runners asynchronously
const removalPromises = runners.map(async (runner) => {
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(`Error removing GitHub self-hosted runner ${runner.name}`);
throw error;
}
});

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;
await Promise.all(removalPromises);
core.info('All GitHub self-hosted runners are removed');
} catch (error) {
core.error('GitHub self-hosted runner removal error');
core.error('Error removing some GitHub self-hosted runners');
throw error;
}
}

async function waitForRunnerRegistered(label) {
async function waitForRunnersRegistered(labels) {
const timeoutMinutes = 5;
const retryIntervalSeconds = 10;
const quietPeriodSeconds = 30;
let waitSeconds = 0;

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

return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
const runner = await getRunner(label);
const runners = await getRunners(labels);

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 (areRunnersOnline(runners)) {
runners.forEach((runner, index ) => {
core.info(`GitHub self-hosted runner number ${index}, ${runner.name}, is registered and ready to use`);
});
clearInterval(interval);
resolve();
} else {
Expand All @@ -63069,8 +63133,8 @@ async function waitForRunnerRegistered(label) {

module.exports = {
getRegistrationToken,
removeRunner,
waitForRunnerRegistered,
removeRunners,
waitForRunnersRegistered,
};


Expand All @@ -63084,23 +63148,23 @@ const gh = __webpack_require__(56989);
const config = __webpack_require__(34570);
const core = __webpack_require__(42186);

function setOutput(label, ec2InstanceId) {
core.setOutput('label', label);
core.setOutput('ec2-instance-id', ec2InstanceId);
function setOutput(labels, ec2InstanceIds) {
core.setOutput('labels', JSON.stringify(labels));
core.setOutput('ec2-instances-ids', JSON.stringify(ec2InstanceIds));
}

async function start() {
const label = config.generateUniqueLabel();
const labels = config.getLabels()
const githubRegistrationToken = await gh.getRegistrationToken();
const ec2InstanceId = await aws.startEc2Instance(label, githubRegistrationToken);
setOutput(label, ec2InstanceId);
await aws.waitForInstanceRunning(ec2InstanceId);
await gh.waitForRunnerRegistered(label);
const ec2InstanceIds = await aws.startEc2Instances(labels, githubRegistrationToken);
setOutput(labels, ec2InstanceIds);
await aws.waitForInstancesRunning(ec2InstanceIds);
await gh.waitForRunnersRegistered(labels);
}

async function stop() {
await aws.terminateEc2Instance();
await gh.removeRunner();
await aws.terminateEc2Instances();
await gh.removeRunners();
}

(async function () {
Expand All @@ -63112,7 +63176,6 @@ async function stop() {
}
})();


/***/ }),

/***/ 22877:
Expand Down
2 changes: 1 addition & 1 deletion src/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function buildUserDataScript(githubRegistrationToken, labels) {
'curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${labels_str}`,
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${separateArrayWithCommas(labels)}`,
'./run.sh',
];
}
Expand Down
3 changes: 1 addition & 2 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ class Config {
ec2InstanceType: core.getInput('ec2-instance-type'),
subnetId: core.getInput('subnet-id'),
securityGroupId: core.getInput('security-group-id'),
label: core.getInput('label'),
labels: core.getInput('labels'),
ec2InstanceIds: core.getInput('ec2-instance-id'),
ec2InstanceIds: core.getInput('ec2-instances-ids'),
iamRoleName: core.getInput('iam-role-name'),
runnerHomeDir: core.getInput('runner-home-dir'),
preRunnerScript: core.getInput('pre-runner-script'),
Expand Down
Loading

0 comments on commit ba546d2

Please sign in to comment.