Skip to content

Commit

Permalink
feat: run in parallel unless specified
Browse files Browse the repository at this point in the history
  • Loading branch information
JPBM135 committed Jan 18, 2024
1 parent 5370f56 commit 047cdf4
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 30 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
<img style="border-radius: 6px" align="right" width="95" src="https://i.imgur.com/1tUj131.jpg"></img>
<h1 align="left">Self-Hosted-Actions-Polyfill</h1>
</span>

Loads modules and binaries that are not available in the GitHub Actions self-hosted runner environment but are available in the GitHub Actions cloud environment.
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: 'Self Hosted Action Polyfill'
description: 'Loads modules and binaries that are not available in the GitHub Actions self-hosted runner environment but are available in the GitHub Actions cloud environment.'
description: 'Loads packages and libs that should be available in all hosted actions but aren`t in self-hosted'

input:
ignore:
Expand All @@ -24,6 +24,12 @@ input:
default: false
type: boolean

run-in-band:
description: 'Run the installs in band, useful for debugging but will be slower'
required: false
default: false
type: boolean

runs:
using: 'node20'
main: 'dist/index.mjs'
111 changes: 109 additions & 2 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as core from '@actions/core';
import * as exec from '@actions/exec';
import { afterEach, beforeEach, describe, expect, it, vitest } from 'vitest';
import { POLYFILLS } from './constants.js';
import type { PolyfillKey, PolyfillLib } from './types/polyfills.js';
import { main } from './index.js';

vitest.mock('node:os');
Expand All @@ -20,7 +21,7 @@ describe('Self-Hosted Actions Polyfill', () => {
vitest.restoreAllMocks();
});

it('should run without errors', async () => {
it('should run without errors with all parameters default', async () => {
vitest.mocked(platform).mockReturnValue('linux');

vitest.mocked(core).getInput.mockReturnValue('');
Expand All @@ -33,7 +34,75 @@ describe('Self-Hosted Actions Polyfill', () => {
await main();

expect(core.setFailed).not.toHaveBeenCalled();
expect(exec.exec).toHaveBeenCalledTimes(6);
expect(exec.exec).toHaveBeenCalledTimes(8);

expect(exec.exec).toHaveBeenCalledWith('sudo', ['apt-get', 'update', '-y'], expect.anything());

const defaultPolyfills = Object.values(POLYFILLS).filter((polyfill) => polyfill.default && polyfill.aptPackage);
const needs = Object.values(POLYFILLS).reduce<PolyfillLib[]>(
(acc, polyfill) => [...acc, ...(polyfill.needs || []).map((pkg) => POLYFILLS[pkg as PolyfillKey])],
[],
);

expect(exec.exec).toHaveBeenCalledWith(
'sudo',
['apt-get', 'install', '-y', '--no-install-recommends', ...needs.map((polyfill) => polyfill.aptPackage!)],
expect.anything(),
);

const polyfillsToInstall = defaultPolyfills.filter(
(polyfill) => !needs.some((pkg) => pkg.aptPackage === polyfill.aptPackage),
);

expect(exec.exec).toHaveBeenCalledWith(
'sudo',
[
'apt-get',
'install',
'-y',
'--no-install-recommends',
...polyfillsToInstall.map((polyfill) => polyfill.aptPackage!),
],
expect.anything(),
);

expect(exec.exec).toHaveBeenCalledWith('/bin/bash', ['-c', POLYFILLS.yarn?.command], expect.anything());

expect(exec.exec).toHaveBeenCalledWith(
'/bin/bash',
['-c', `echo "${POLYFILLS.yarn.path}" >> $GITHUB_PATH`],
expect.anything(),
);

expect(exec.exec).toHaveBeenCalledWith('/bin/bash', ['-c', POLYFILLS.docker?.command], expect.anything());

expect(exec.exec).toHaveBeenCalledWith('/bin/bash', ['-c', POLYFILLS['aws-cli']?.command], expect.anything());

expect(exec.exec).toHaveBeenLastCalledWith('sudo', ['apt-get', 'autoremove', '-y'], expect.anything());
});

it('should run without errors if the run-in-band is true', async () => {
vitest.mocked(platform).mockReturnValue('linux');

vitest.mocked(core).getInput.mockImplementation((name: string): string => {
if (name === 'run-in-band') {
return 'true';
}

return '';
});
vitest.mocked(core).getBooleanInput.mockImplementation((name: string): boolean => {
return name === 'run-in-band';
});
vitest.mocked(core).addPath.mockImplementation(() => {});
vitest.mocked(core).setFailed.mockImplementation(() => {});

vitest.mocked(exec).exec.mockResolvedValue(0);

await main();

expect(core.setFailed).not.toHaveBeenCalled();
expect(exec.exec).toHaveBeenCalledTimes(7);

expect(exec.exec).toHaveBeenCalledWith('sudo', ['apt-get', 'update', '-y'], expect.anything());

Expand Down Expand Up @@ -66,6 +135,44 @@ describe('Self-Hosted Actions Polyfill', () => {
expect(exec.exec).toHaveBeenLastCalledWith('sudo', ['apt-get', 'autoremove', '-y'], expect.anything());
});

it('should run without errors if the skip-defaults is true and includes as curl', async () => {
vitest.mocked(platform).mockReturnValue('linux');

vitest.mocked(core).getInput.mockImplementation((name: string): string => {
if (name === 'skip-defaults') {
return 'true';
}

if (name === 'includes') {
return 'curl';
}

return '';
});
vitest.mocked(core).getBooleanInput.mockImplementation((name: string): boolean => {
return name === 'skip-defaults';
});
vitest.mocked(core).addPath.mockImplementation(() => {});
vitest.mocked(core).setFailed.mockImplementation(() => {});

vitest.mocked(exec).exec.mockResolvedValue(0);

await main();

expect(core.setFailed).not.toHaveBeenCalled();
expect(exec.exec).toHaveBeenCalledTimes(3);

expect(exec.exec).toHaveBeenCalledWith('sudo', ['apt-get', 'update', '-y'], expect.anything());

expect(exec.exec).toHaveBeenCalledWith(
'sudo',
['apt-get', 'install', '-y', '--no-install-recommends', POLYFILLS.curl.aptPackage!],
expect.anything(),
);

expect(exec.exec).toHaveBeenLastCalledWith('sudo', ['apt-get', 'autoremove', '-y'], expect.anything());
});

it('should throw an error if the platform is not supported', async () => {
vitest.mocked(platform).mockReturnValue('darwin');

Expand Down
111 changes: 84 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import os from 'node:os';
import process from 'node:process';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import { POLYFILLS } from './constants.js';
import type { PolyfillKey } from './types/polyfills.js';
import { createStreams } from './utils/createStreams.js';
import { parseModulesToInstall } from './utils/parseModulesToInstall.js';
import { validateInputs, validatePolyfillNeeds } from './utils/validate.js';
Expand All @@ -16,9 +18,13 @@ export async function main() {
trimWhitespace: true,
})
: false;
const RUN_IN_BAND = core.getInput('run-in-band') ? core.getBooleanInput('run-in-band') : false;

try {
core.debug(`Inputs: ${JSON.stringify({ IGNORE, INCLUDES, SKIP_DEFAULTS, RUN_IN_BAND }, null, 2)}`);

const platform = os.platform();
const promises: Promise<void>[] = [];

if (platform !== 'linux') {
throw new Error(`Unsupported platform: ${platform}`);
Expand All @@ -40,20 +46,21 @@ export async function main() {

validatePolyfillNeeds(modulesToInstall);

const aptModulesToInstall = modulesToInstall.filter(([, polyfill]) => polyfill.aptPackage);
let aptModulesToInstall = modulesToInstall.filter(([, polyfill]) => polyfill.aptPackage);
const nonAptModulesToInstall = modulesToInstall.filter(([, polyfill]) => polyfill.command);

if (aptModulesToInstall.length > 0) {
core.info(`Installing ${aptModulesToInstall.length} apt packages...`);
const needs = nonAptModulesToInstall.reduce<PolyfillKey[]>((acc, [, cur]) => {
if (!cur.needs?.length) return acc;

return [...acc, ...(cur.needs as PolyfillKey[])];
}, []);

if (!RUN_IN_BAND && needs.length > 0) {
core.info(`Installing ${needs.length} polyfills needed by other polyfills beforehand...`);

const code = await exec.exec(
'sudo',
[
'apt-get',
'install',
'-y',
'--no-install-recommends',
...aptModulesToInstall.map(([, polyfill]) => polyfill.aptPackage!),
],
['apt-get', 'install', '-y', '--no-install-recommends', ...needs.map((need) => POLYFILLS[need].aptPackage!)],
{
...createStreams(),
},
Expand All @@ -63,40 +70,90 @@ export async function main() {
throw new Error(`Apt failed with exit code ${code}.`);
}

core.info('Successfully installed apt packages.');
core.debug(`Removing apt packages that are no longer needed: ${needs.join(', ')}`);
const needsSet = new Set(needs);
aptModulesToInstall = aptModulesToInstall.filter(([name]) => !needsSet.has(name));

core.info('Successfully installed polyfills needed by other polyfills.');
}

const nonAptModulesToInstall = modulesToInstall.filter(([, polyfill]) => polyfill.command);
if (aptModulesToInstall.length > 0) {
const installAptPackages = async () => {
core.info(`Installing ${aptModulesToInstall.length} apt packages...`);

const code = await exec.exec(
'sudo',
[
'apt-get',
'install',
'-y',
'--no-install-recommends',
...aptModulesToInstall.map(([, polyfill]) => polyfill.aptPackage!),
],
{
...createStreams(),
},
);

if (code !== 0) {
throw new Error(`Apt failed with exit code ${code}.`);
}

core.info('Successfully installed apt packages.');
};

if (RUN_IN_BAND) {
await installAptPackages();
} else {
promises.push(installAptPackages());
}
}

for (const [polyfill, polyfillOptions] of nonAptModulesToInstall) {
core.info(`Installing ${polyfill} polyfill...`);
const prefixedCommand: [string, string, string] = ['/bin/bash', '-c', polyfillOptions.command!];

const code = await exec.exec(prefixedCommand[0], [prefixedCommand[1], prefixedCommand[2]], {
...createStreams(),
});
const installCommandPolyfill = async () => {
core.info(`Installing ${polyfill} polyfill...`);
const prefixedCommand: [string, string, string] = ['/bin/bash', '-c', polyfillOptions.command!];

if (polyfillOptions.path) {
const escapedPath = polyfillOptions.path.replaceAll('"', '\\"');

await exec.exec('/bin/bash', ['-c', `echo "${escapedPath}" >> $GITHUB_PATH`], {
const code = await exec.exec(prefixedCommand[0], [prefixedCommand[1], prefixedCommand[2]], {
...createStreams(),
});

core.info(`Added ${polyfill} polyfill to PATH.`);
}
if (polyfillOptions.path) {
const escapedPath = polyfillOptions.path.replaceAll('"', '\\"');

if (code !== 0) {
throw new Error(`Polyfill ${polyfill} failed with exit code ${code}.`);
await exec.exec('/bin/bash', ['-c', `echo "${escapedPath}" >> $GITHUB_PATH`], {
...createStreams(),
});

core.info(`Added ${polyfill} polyfill to PATH.`);
}

if (code !== 0) {
throw new Error(`Polyfill ${polyfill} failed with exit code ${code}.`);
}

core.info(`Successfully installed ${polyfill} polyfill.`);
};

if (RUN_IN_BAND) {
await installCommandPolyfill();
} else {
promises.push(installCommandPolyfill());
}
}

core.info(`Successfully installed ${polyfill} polyfill.`);
if (!RUN_IN_BAND) {
await Promise.all(promises);
}

core.info('Cleaning up...');

await exec.exec('sudo', ['apt-get', 'autoremove', '-y'], {
...createStreams(),
});

core.info('Successfully cleaned up.');

core.info('Successfully installed all polyfills.');
} catch (error) {
core.debug(String(error));
Expand Down

0 comments on commit 047cdf4

Please sign in to comment.