Skip to content

Commit

Permalink
feat: Download runner release via latest release API (#2455)
Browse files Browse the repository at this point in the history
* test

* feat: Download latest release via latest release API

- Use release API instead of listing releases
- Drop support draft release

* revert examples
  • Loading branch information
npalm authored Sep 24, 2022
1 parent 87bad10 commit e75e092
Show file tree
Hide file tree
Showing 18 changed files with 1,878 additions and 10,171 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ In case the setup does not work as intended follow the trace of events:
| <a name="input_role_path"></a> [role\_path](#input\_role\_path) | The path that will be added to role path for created roles, if not set the environment name will be used. | `string` | `null` | no |
| <a name="input_role_permissions_boundary"></a> [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created roles. | `string` | `null` | no |
| <a name="input_runner_additional_security_group_ids"></a> [runner\_additional\_security\_group\_ids](#input\_runner\_additional\_security\_group\_ids) | (optional) List of additional security groups IDs to apply to the runner | `list(string)` | `[]` | no |
| <a name="input_runner_allow_prerelease_binaries"></a> [runner\_allow\_prerelease\_binaries](#input\_runner\_allow\_prerelease\_binaries) | Allow the runners to update to prerelease binaries. | `bool` | `false` | no |
| <a name="input_runner_allow_prerelease_binaries"></a> [runner\_allow\_prerelease\_binaries](#input\_runner\_allow\_prerelease\_binaries) | (Deprecated, no longer used), allow the runners to update to prerelease binaries. | `bool` | `null` | no |
| <a name="input_runner_architecture"></a> [runner\_architecture](#input\_runner\_architecture) | The platform architecture of the runner instance\_type. | `string` | `"x64"` | no |
| <a name="input_runner_as_root"></a> [runner\_as\_root](#input\_runner\_as\_root) | Run the action runner under the root user. Variable `runner_run_as` will be ignored. | `bool` | `false` | no |
| <a name="input_runner_binaries_s3_sse_configuration"></a> [runner\_binaries\_s3\_sse\_configuration](#input\_runner\_binaries\_s3\_sse\_configuration) | Map containing server-side encryption configuration for runner-binaries S3 bucket. | `any` | `{}` | no |
Expand Down
2 changes: 1 addition & 1 deletion examples/ubuntu/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ module "runners" {
# Uncomment to enable ephemeral runners
# delay_webhook_event = 0
# enable_ephemeral_runners = true
# enabled_userdata = false
# enabled_userdata = true

# Uncommet idle config to have idle runners from 9 to 5 in time zone Amsterdam
# idle_config = [{
Expand Down
2 changes: 1 addition & 1 deletion modules/runner-binaries-syncer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ No modules.
| <a name="input_prefix"></a> [prefix](#input\_prefix) | The prefix used for naming resources | `string` | `"github-actions"` | no |
| <a name="input_role_path"></a> [role\_path](#input\_role\_path) | The path that will be added to the role, if not set the environment name will be used. | `string` | `null` | no |
| <a name="input_role_permissions_boundary"></a> [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no |
| <a name="input_runner_allow_prerelease_binaries"></a> [runner\_allow\_prerelease\_binaries](#input\_runner\_allow\_prerelease\_binaries) | Allow the runners to update to prerelease binaries. | `bool` | `false` | no |
| <a name="input_runner_allow_prerelease_binaries"></a> [runner\_allow\_prerelease\_binaries](#input\_runner\_allow\_prerelease\_binaries) | (Deprecated, no longer used), allow the runners to update to prerelease binaries. | `bool` | `null` | no |
| <a name="input_runner_architecture"></a> [runner\_architecture](#input\_runner\_architecture) | The platform architecture of the runner instance\_type. | `string` | `"x64"` | no |
| <a name="input_runner_os"></a> [runner\_os](#input\_runner\_os) | The EC2 Operating System type to use for action runner instances (linux,windows). | `string` | `"linux"` | no |
| <a name="input_server_side_encryption_configuration"></a> [server\_side\_encryption\_configuration](#input\_server\_side\_encryption\_configuration) | Map containing server-side encryption configuration. | `any` | `{}` | no |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts'],
collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts', '!src/**/*.d.ts'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
branches: 85,
functions: 78,
lines: 93,
statements: 93
}
}
};
Empty file.
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { S3 } from 'aws-sdk';
import axios from 'axios';
import { request } from 'http';
import { EventEmitter, PassThrough, Readable } from 'stream';
import { PassThrough } from 'stream';

import listReleasesEmpty from '../../test/resources/github-list-releases-empty-assets.json';
import listReleasesNoArm64 from '../../test/resources/github-list-releases-no-arm64.json';
import listReleasesNoLinux from '../../test/resources/github-list-releases-no-linux.json';
import listReleases from '../../test/resources/github-list-releases.json';
import mockDataLatestRelease from '../../test/resources/github-latest-release.json';
import noX64Assets from '../../test/resources/github-releases-no-x64.json';
import { sync } from './syncer';

const mockOctokit = {
repos: {
listReleases: jest.fn(),
getLatestRelease: jest.fn(),
},
};
jest.mock('@octokit/rest', () => ({
Expand Down Expand Up @@ -54,191 +50,75 @@ const bucketObjectKey = (os: string) => bucketObjectNames[os];

const runnerOs = [['linux'], ['win']];

const latestRelease = '2.287.0';
const latestPreRelease = '2.287.1';
const latestRelease = '2.296.2';

beforeEach(() => {
jest.clearAllMocks();
});

jest.setTimeout(60 * 1000);

describe('Synchronize action distribution.', () => {
describe('Synchronize action distribution (no S3 tags).', () => {
beforeEach(() => {
process.env.S3_BUCKET_NAME = bucketName;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'false';

mockOctokit.repos.listReleases.mockImplementation(() => ({
data: listReleases,
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
data: mockDataLatestRelease,
}));
});

test.each(runnerOs)('%p Distribution is up-to-date with latest release.', async (os) => {
test.each(runnerOs)('%p Distribution is S3 has no tags.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}` }],
TagSet: undefined,
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(0);
});

test.each(runnerOs)(
'%p Distribution is up-to-date with latest release when there are no prereleases.',
async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
const releases = listReleases.slice(1);

mockOctokit.repos.listReleases.mockImplementation(() => ({
data: releases,
}));
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}` }],
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(0);
},
);

test.each(runnerOs)('%p Distribution is up-to-date with latest prerelease.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-${latestPreRelease}${objectExtension[os]}` }],
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(0);
});

test.each(runnerOs)('%p Distribution should update to release.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-0${objectExtension[os]}` }],
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(1);
const s3JsonBody = mockS3.upload.mock.calls[0][0];
expect(s3JsonBody['Tagging']).toEqual(`name=actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}`);
});
});

test.each(runnerOs)('%p Distribution should update to release if there are no pre-releases.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
const releases = listReleases.slice(1);
describe('Synchronize action distribution (up-to-date).', () => {
beforeEach(() => {
process.env.S3_BUCKET_NAME = bucketName;

mockOctokit.repos.listReleases.mockImplementation(() => ({
data: releases,
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
data: mockDataLatestRelease,
}));
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-0${objectExtension[os]}` }],
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(1);
const s3JsonBody = mockS3.upload.mock.calls[0][0];
expect(s3JsonBody['Tagging']).toEqual(`name=actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}`);
});

test.each(runnerOs)('%p Distribution should update to prerelease.', async (os) => {
test.each(runnerOs)('%p Distribution is up-to-date with latest release.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-0${objectExtension[os]}` }],
TagSet: [{ Key: 'name', Value: `actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}` }],
});
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(1);
const s3JsonBody = mockS3.upload.mock.calls[0][0];
expect(s3JsonBody['Tagging']).toEqual(`name=actions-runner-${os}-x64-${latestPreRelease}${objectExtension[os]}`);
expect(mockS3.upload).toBeCalledTimes(0);
});

test.each(runnerOs)('%p Distribution should not update to prerelease if there is a newer release.', async (os) => {
test.each(runnerOs)('%p Distribution should update to release.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
const releases = listReleases;
releases[0].prerelease = false;
releases[1].prerelease = true;

mockOctokit.repos.listReleases.mockImplementation(() => ({
data: releases,
}));
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
Expand All @@ -250,34 +130,14 @@ describe('Synchronize action distribution.', () => {
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(1);
const s3JsonBody = mockS3.upload.mock.calls[0][0];
expect(s3JsonBody['Tagging']).toEqual(`name=actions-runner-${os}-x64-${latestPreRelease}${objectExtension[os]}`);
});

test.each(runnerOs)('%p No tag in S3, distribution should update.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
mockS3.getObjectTagging.mockImplementation(() => {
return {
promise() {
throw new Error();
},
};
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
});
expect(mockS3.upload).toBeCalledTimes(1);
expect(s3JsonBody['Tagging']).toEqual(`name=actions-runner-${os}-x64-${latestRelease}${objectExtension[os]}`);
});

test.each(runnerOs)('%p Tags, but no version, distribution should update.', async (os) => {
Expand All @@ -292,7 +152,7 @@ describe('Synchronize action distribution.', () => {
});

await sync();
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
expect(mockS3.getObjectTagging).toBeCalledWith({
Bucket: bucketName,
Key: bucketObjectKey(os),
Expand All @@ -308,9 +168,9 @@ describe('No release assets found.', () => {
process.env.S3_OBJECT_KEY = bucketObjectKey('linux');
});

it('Empty list of assets.', async () => {
mockOctokit.repos.listReleases.mockImplementation(() => ({
data: listReleasesEmpty,
test('Empty result.', async () => {
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
data: undefined,
}));

await expect(sync()).rejects.toThrow(errorMessage);
Expand All @@ -319,55 +179,30 @@ describe('No release assets found.', () => {
test.each(runnerOs)('No %p x64 asset.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
mockOctokit.repos.listReleases.mockImplementation(() => ({
data: [listReleasesNoLinux],
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
data: noX64Assets,
}));

await expect(sync()).rejects.toThrow(errorMessage);
});

it('Empty asset list.', async () => {
mockOctokit.repos.listReleases.mockImplementation(() => ({
data: [],
}));

await expect(sync()).rejects.toThrow(errorMessage);
});
});

describe('Invalid config', () => {
const errorMessage = 'Please check all mandatory variables are set.';
it('No bucket and object key.', async () => {
test('No bucket and object key.', async () => {
delete process.env.S3_OBJECT_KEY;
delete process.env.S3_BUCKET_NAME;
await expect(sync()).rejects.toThrow(errorMessage);
});
it('No bucket.', async () => {

test('No bucket.', async () => {
delete process.env.S3_BUCKET_NAME;
process.env.S3_OBJECT_KEY = bucketObjectKey('linux');
await expect(sync()).rejects.toThrow(errorMessage);
});
it('No object key.', async () => {
delete process.env.S3_OBJECT_KEY;
process.env.S3_BUCKET_NAME = bucketName;
await expect(sync()).rejects.toThrow(errorMessage);
});
});

describe('Synchronize action distribution for arm64.', () => {
const errorMessage = 'Cannot find GitHub release asset.';
beforeEach(() => {
test('No object key.', async () => {
delete process.env.S3_OBJECT_KEY;
process.env.S3_BUCKET_NAME = bucketName;
process.env.GITHUB_RUNNER_ARCHITECTURE = 'arm64';
});

test.each(runnerOs)('No %p arm64 asset.', async (os) => {
process.env.S3_OBJECT_KEY = bucketObjectKey(os);
process.env.GITHUB_RUNNER_OS = os;
mockOctokit.repos.listReleases.mockImplementation(() => ({
data: [listReleasesNoArm64],
}));

await expect(sync()).rejects.toThrow(errorMessage);
});
});
Loading

0 comments on commit e75e092

Please sign in to comment.