Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ See [below](#example) the YAML code of the depicted workflow. <br><br>

**Table of Contents**

- [Use cases](#use-cases)
- [Access private resources in your VPC](#access-private-resources-in-your-vpc)
- [Customize hardware configuration](#customize-hardware-configuration)
- [Save costs](#save-costs)
- [Usage](#usage)
- [How to start](#how-to-start)
- [Inputs](#inputs)
- [Environment variables](#environment-variables)
- [Outputs](#outputs)
- [Example](#example)
- [Real user examples](#real-user-examples)
- [Self-hosted runner security with public repositories](#self-hosted-runner-security-with-public-repositories)
- [License Summary](#license-summary)
- [On-demand self-hosted AWS EC2 runner for GitHub Actions](#on-demand-self-hosted-aws-ec2-runner-for-github-actions)
- [Use cases](#use-cases)
- [Access private resources in your VPC](#access-private-resources-in-your-vpc)
- [Customize hardware configuration](#customize-hardware-configuration)
- [Save costs](#save-costs)
- [Usage](#usage)
- [How to start](#how-to-start)
- [Inputs](#inputs)
- [Environment variables](#environment-variables)
- [Outputs](#outputs)
- [Example](#example)
- [Real user examples](#real-user-examples)
- [Self-hosted runner security with public repositories](#self-hosted-runner-security-with-public-repositories)
- [License Summary](#license-summary)

## Use cases

Expand Down Expand Up @@ -203,6 +204,7 @@ Now you're ready to go!
| `iam-role-name` | Optional. Used only with the `start` mode. | IAM role name to attach to the created EC2 runner. <br><br> This allows the runner to have permissions to run additional actions within the AWS account, without having to manage additional GitHub secrets and AWS users. <br><br> Setting this requires additional AWS permissions for the role launching the instance (see above). |
| `aws-resource-tags` | Optional. Used only with the `start` mode. | Specifies tags to add to the EC2 instance and any attached storage. <br><br> This field is a stringified JSON array of tag objects, each containing a `Key` and `Value` field (see example below). <br><br> Setting this requires additional AWS permissions for the role launching the instance (see above). |
| `runner-home-dir` | Optional. Used only with the `start` mode. | Specifies a directory where pre-installed actions-runner software and scripts are located.<br><br> |
|``

### Environment variables

Expand All @@ -219,8 +221,11 @@ We recommend using [aws-actions/configure-aws-credentials](https://github.com/aw

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Description |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Name of the unique label assigned to the runner. <br><br> The label is used in two cases: <br> - to use as the input of `runs-on` property for the following jobs; <br> - to remove the runner from GitHub when it is not needed anymore. |
| `label` | Name of the unique label assigned to the runner. If it is not informed, the plugin will generate a random name. <br><br> The label is used in two cases: <br> - to use as the input of `runs-on` property for the following jobs; <br> - to remove the runner from GitHub when it is not needed anymore. |
| `ec2-instance-id` | EC2 Instance Id of the created runner. <br><br> The id is used to terminate the EC2 instance when the runner is not needed anymore. |
|`scope`| The scope of yours runner it can be `repository`or `organization`. You need to have a Github Token with the correct permissions to create the org runner |
| `host-id` | Created to support macOS Dedicated Hosts. You need to allocate the Dedicated Host and to inform its hostId to use this feature. |
| `timeout` | The timeout to wait for the runner to be ready. The default value is 5 minutes|.

### Example

Expand Down
16 changes: 15 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ inputs:
required: false
label:
description: >-
Name of the unique label assigned to the runner.
Defines the runner's label name when set. If this value is not defined, the runner will generate a random label.
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
scope:
description: >-
The runner's scope. The runner scope must be "repository", "organization" or "enterprise".
required: true
options: ["repository", "organization", "enterprise"]
ec2-instance-id:
description: >-
EC2 Instance Id of the created runner.
Expand All @@ -65,6 +70,15 @@ inputs:
description: >-
Directory that contains actions-runner software and scripts. E.g. /home/runner/actions-runner.
required: false
host-id:
description: >-
The host id to provide a dedicated host in your account. It is necessary for macos instances.
required: false
timeout:
description:
The time (in minutes) that this actions will wait the the runner be created. The default value is 5 minutes.
required: false
default: 5
outputs:
label:
description: >-
Expand Down
70 changes: 50 additions & 20 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62811,18 +62811,22 @@ function buildUserDataScript(githubRegistrationToken, label) {
'#!/bin/bash',
`cd "${config.input.runnerHomeDir}"`,
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
`./config.sh --url ${config.github.url} --token ${githubRegistrationToken} --labels ${label} --name ${label} --runnergroup default --work "${config.input.runnerHomeDir}" --replace`,
'./run.sh',
];
} else {
return [
'#!/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.299.1/actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'cd /opt && mkdir actions-runner && cd actions-runner',
'case $(uname -m) in aarch64|arm64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
'case $(uname -a) in Darwin*) OS="osx" ;; Linux*) OS="linux" ;; esac && export RUNNER_OS=${OS}',
'export VERSION="2.303.0"',
'curl -O -L https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-${RUNNER_OS}-${RUNNER_ARCH}-${VERSION}.tar.gz',
'export LC_ALL=en_US.UTF-8',
'export LANG=en_EN.UTF-8',
'tar xzf ./actions-runner-${RUNNER_OS}-${RUNNER_ARCH}-${VERSION}.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 ${config.github.url} --token ${githubRegistrationToken} --labels ${label} --name ${label} --runnergroup default --work $(pwd) --replace`,
'./run.sh',
];
}
Expand All @@ -62845,6 +62849,13 @@ async function startEc2Instance(label, githubRegistrationToken) {
TagSpecifications: config.tagSpecifications,
};

if (config.input.hostId) {
params.Placement = {
Tenancy: 'host',
HostId: config.input.hostId
}
}

try {
const result = await ec2.runInstances(params).promise();
const ec2InstanceId = result.Instances[0].InstanceId;
Expand Down Expand Up @@ -62918,6 +62929,24 @@ class Config {
ec2InstanceId: core.getInput('ec2-instance-id'),
iamRoleName: core.getInput('iam-role-name'),
runnerHomeDir: core.getInput('runner-home-dir'),
scope: core.getInput('scope'),
hostId: core.getInput('host-id'),
};

this.GITHUB_SCOPES = {
organization: {
url: `https://github.com/${github.context.repo.owner}`,
context: { owner: github.context.repo.owner },
apiPath: `/orgs/${github.context.repo.owner}`
},
repository: {
url: `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}`,
apiPath: `/repos/${github.context.repo.owner}/${github.context.repo.repo}`,
context: {
owner: github.context.repo.owner,
repo: github.context.repo.repo
}
}
};

const tags = JSON.parse(core.getInput('aws-resource-tags'));
Expand All @@ -62926,13 +62955,10 @@ class Config {
this.tagSpecifications = [{ResourceType: 'instance', Tags: tags}, {ResourceType: 'volume', Tags: tags}];
}

// the values of github.context.repo.owner and github.context.repo.repo are taken from
// the environment variable GITHUB_REPOSITORY specified in "owner/repo" format and
// provided by the GitHub Action on the runtime
this.githubContext = {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
};
this.github = this.GITHUB_SCOPES[this.input.scope];
if (!this.github) {
throw new Error(`The 'scope' input is not valid`);
}

//
// validate input
Expand All @@ -62959,8 +62985,12 @@ class Config {
}
}

generateUniqueLabel() {
return Math.random().toString(36).substr(2, 5);
generateLabel() {
if (!this.input.label) {
return Math.random().toString(36).substr(2, 5);
}

return this.input.label
}
}

Expand Down Expand Up @@ -62988,7 +63018,7 @@ async function getRunner(label) {
const octokit = github.getOctokit(config.input.githubToken);

try {
const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
const runners = await octokit.paginate(`GET ${config.github.apiPath}/actions/runners`);
const foundRunners = _.filter(runners, { labels: [{ name: label }] });
return foundRunners.length > 0 ? foundRunners[0] : null;
} catch (error) {
Expand All @@ -63001,7 +63031,7 @@ async function getRegistrationToken() {
const octokit = github.getOctokit(config.input.githubToken);

try {
const response = await octokit.request('POST /repos/{owner}/{repo}/actions/runners/registration-token', config.githubContext);
const response = await octokit.request(`POST ${config.github.apiPath}/actions/runners/registration-token`);
core.info('GitHub Registration Token is received');
return response.data.token;
} catch (error) {
Expand All @@ -63021,7 +63051,7 @@ async function removeRunner() {
}

try {
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id }));
await octokit.request(`DELETE ${config.github.apiPath}/actions/runners/{runner_id}`, { runner_id: runner.id });
core.info(`GitHub self-hosted runner ${runner.name} is removed`);
return;
} catch (error) {
Expand All @@ -63031,7 +63061,7 @@ async function removeRunner() {
}

async function waitForRunnerRegistered(label) {
const timeoutMinutes = 5;
const timeoutMinutes = core.getInput('timeout')
const retryIntervalSeconds = 10;
const quietPeriodSeconds = 30;
let waitSeconds = 0;
Expand Down Expand Up @@ -63085,7 +63115,7 @@ function setOutput(label, ec2InstanceId) {
}

async function start() {
const label = config.generateUniqueLabel();
const label = config.generateLabel();
const githubRegistrationToken = await gh.getRegistrationToken();
const ec2InstanceId = await aws.startEc2Instance(label, githubRegistrationToken);
setOutput(label, ec2InstanceId);
Expand Down
23 changes: 17 additions & 6 deletions src/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ function buildUserDataScript(githubRegistrationToken, label) {
'#!/bin/bash',
`cd "${config.input.runnerHomeDir}"`,
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
`./config.sh --url ${config.github.url} --token ${githubRegistrationToken} --labels ${label} --name ${label} --runnergroup default --work "${config.input.runnerHomeDir}" --replace`,
'./run.sh',
];
} else {
return [
'#!/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.299.1/actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
'cd /opt && mkdir actions-runner && cd actions-runner',
'case $(uname -m) in aarch64|arm64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
'case $(uname -a) in Darwin*) OS="osx" ;; Linux*) OS="linux" ;; esac && export RUNNER_OS=${OS}',
'export VERSION="2.303.0"',
'curl -O -L https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-${RUNNER_OS}-${RUNNER_ARCH}-${VERSION}.tar.gz',
'export LC_ALL=en_US.UTF-8',
'export LANG=en_EN.UTF-8',
'tar xzf ./actions-runner-${RUNNER_OS}-${RUNNER_ARCH}-${VERSION}.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 ${config.github.url} --token ${githubRegistrationToken} --labels ${label} --name ${label} --runnergroup default --work $(pwd) --replace`,
'./run.sh',
];
}
Expand All @@ -45,6 +49,13 @@ async function startEc2Instance(label, githubRegistrationToken) {
TagSpecifications: config.tagSpecifications,
};

if (config.input.hostId) {
params.Placement = {
Tenancy: 'host',
HostId: config.input.hostId
}
}

try {
const result = await ec2.runInstances(params).promise();
const ec2InstanceId = result.Instances[0].InstanceId;
Expand Down
37 changes: 28 additions & 9 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ class Config {
ec2InstanceId: core.getInput('ec2-instance-id'),
iamRoleName: core.getInput('iam-role-name'),
runnerHomeDir: core.getInput('runner-home-dir'),
scope: core.getInput('scope'),
hostId: core.getInput('host-id'),
};

this.GITHUB_SCOPES = {
organization: {
url: `https://github.com/${github.context.repo.owner}`,
context: { owner: github.context.repo.owner },
apiPath: `/orgs/${github.context.repo.owner}`
},
repository: {
url: `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}`,
apiPath: `/repos/${github.context.repo.owner}/${github.context.repo.repo}`,
context: {
owner: github.context.repo.owner,
repo: github.context.repo.repo
}
}
};

const tags = JSON.parse(core.getInput('aws-resource-tags'));
Expand All @@ -22,13 +40,10 @@ class Config {
this.tagSpecifications = [{ResourceType: 'instance', Tags: tags}, {ResourceType: 'volume', Tags: tags}];
}

// the values of github.context.repo.owner and github.context.repo.repo are taken from
// the environment variable GITHUB_REPOSITORY specified in "owner/repo" format and
// provided by the GitHub Action on the runtime
this.githubContext = {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
};
this.github = this.GITHUB_SCOPES[this.input.scope];
if (!this.github) {
throw new Error(`The 'scope' input is not valid`);
}

//
// validate input
Expand All @@ -55,8 +70,12 @@ class Config {
}
}

generateUniqueLabel() {
return Math.random().toString(36).substr(2, 5);
generateLabel() {
if (!this.input.label) {
return Math.random().toString(36).substr(2, 5);
}

return this.input.label
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/gh.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async function getRunner(label) {
const octokit = github.getOctokit(config.input.githubToken);

try {
const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
const runners = await octokit.paginate(`GET ${config.github.apiPath}/actions/runners`);
const foundRunners = _.filter(runners, { labels: [{ name: label }] });
return foundRunners.length > 0 ? foundRunners[0] : null;
} catch (error) {
Expand All @@ -22,7 +22,7 @@ async function getRegistrationToken() {
const octokit = github.getOctokit(config.input.githubToken);

try {
const response = await octokit.request('POST /repos/{owner}/{repo}/actions/runners/registration-token', config.githubContext);
const response = await octokit.request(`POST ${config.github.apiPath}/actions/runners/registration-token`);
core.info('GitHub Registration Token is received');
return response.data.token;
} catch (error) {
Expand All @@ -42,7 +42,7 @@ async function removeRunner() {
}

try {
await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner.id }));
await octokit.request(`DELETE ${config.github.apiPath}/actions/runners/{runner_id}`, { runner_id: runner.id });
core.info(`GitHub self-hosted runner ${runner.name} is removed`);
return;
} catch (error) {
Expand All @@ -52,7 +52,7 @@ async function removeRunner() {
}

async function waitForRunnerRegistered(label) {
const timeoutMinutes = 5;
const timeoutMinutes = core.getInput('timeout')
const retryIntervalSeconds = 10;
const quietPeriodSeconds = 30;
let waitSeconds = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function setOutput(label, ec2InstanceId) {
}

async function start() {
const label = config.generateUniqueLabel();
const label = config.generateLabel();
const githubRegistrationToken = await gh.getRegistrationToken();
const ec2InstanceId = await aws.startEc2Instance(label, githubRegistrationToken);
setOutput(label, ec2InstanceId);
Expand Down