Merge branch 'main' into add-caching-for-node-dependencies

This commit is contained in:
Dmitry Shibanov 2021-06-30 11:39:23 +03:00
commit 55e10498cf
6 changed files with 17754 additions and 12 deletions

View file

@ -29,6 +29,20 @@ jobs:
run: __tests__/verify-node.sh "${{ matrix.node-version }}" run: __tests__/verify-node.sh "${{ matrix.node-version }}"
shell: bash shell: bash
lts-syntax:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [lts/dubnium, lts/erbium, lts/fermium, lts/*]
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: ./
with:
node-version: ${{ matrix.node-version }}
manifest: manifest:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:

View file

@ -2,6 +2,7 @@
{ {
"version": "14.0.0", "version": "14.0.0",
"stable": true, "stable": true,
"lts": "Fermium",
"release_url": "https://github.com/actions/node-versions/releases/tag/14.0.0-20200423.30", "release_url": "https://github.com/actions/node-versions/releases/tag/14.0.0-20200423.30",
"files": [ "files": [
{ {
@ -52,6 +53,7 @@
{ {
"version": "12.16.2", "version": "12.16.2",
"stable": true, "stable": true,
"lts": "Erbium",
"release_url": "https://github.com/actions/node-versions/releases/tag/12.16.2-20200423.28", "release_url": "https://github.com/actions/node-versions/releases/tag/12.16.2-20200423.28",
"files": [ "files": [
{ {
@ -77,6 +79,7 @@
{ {
"version": "10.20.1", "version": "10.20.1",
"stable": true, "stable": true,
"lts": "Dubnium",
"release_url": "https://github.com/actions/node-versions/releases/tag/10.20.1-20200423.27", "release_url": "https://github.com/actions/node-versions/releases/tag/10.20.1-20200423.27",
"files": [ "files": [
{ {
@ -102,6 +105,7 @@
{ {
"version": "8.17.0", "version": "8.17.0",
"stable": true, "stable": true,
"lts": "Carbon",
"release_url": "https://github.com/actions/node-versions/releases/tag/8.17.0-20200423.26", "release_url": "https://github.com/actions/node-versions/releases/tag/8.17.0-20200423.26",
"files": [ "files": [
{ {
@ -127,6 +131,7 @@
{ {
"version": "6.17.1", "version": "6.17.1",
"stable": true, "stable": true,
"lts": "Boron",
"release_url": "https://github.com/actions/node-versions/releases/tag/6.17.1-20200423.25", "release_url": "https://github.com/actions/node-versions/releases/tag/6.17.1-20200423.25",
"files": [ "files": [
{ {

View file

@ -134,6 +134,7 @@ describe('setup-node', () => {
let match = await tc.findFromManifest('12.16.2', true, versions); let match = await tc.findFromManifest('12.16.2', true, versions);
expect(match).toBeDefined(); expect(match).toBeDefined();
expect(match?.version).toBe('12.16.2'); expect(match?.version).toBe('12.16.2');
expect((match as any).lts).toBe('Erbium');
}); });
it('can find 12 from manifest on linux', async () => { it('can find 12 from manifest on linux', async () => {
@ -148,6 +149,7 @@ describe('setup-node', () => {
let match = await tc.findFromManifest('12.16.2', true, versions); let match = await tc.findFromManifest('12.16.2', true, versions);
expect(match).toBeDefined(); expect(match).toBeDefined();
expect(match?.version).toBe('12.16.2'); expect(match?.version).toBe('12.16.2');
expect((match as any).lts).toBe('Erbium');
}); });
it('can find 10 from manifest on windows', async () => { it('can find 10 from manifest on windows', async () => {
@ -162,6 +164,7 @@ describe('setup-node', () => {
let match = await tc.findFromManifest('10', true, versions); let match = await tc.findFromManifest('10', true, versions);
expect(match).toBeDefined(); expect(match).toBeDefined();
expect(match?.version).toBe('10.20.1'); expect(match?.version).toBe('10.20.1');
expect((match as any).lts).toBe('Dubnium');
}); });
//-------------------------------------------------- //--------------------------------------------------
@ -217,6 +220,10 @@ describe('setup-node', () => {
expect(cnSpy).toHaveBeenCalledWith('::error::' + errMsg + osm.EOL); expect(cnSpy).toHaveBeenCalledWith('::error::' + errMsg + osm.EOL);
}); });
//--------------------------------------------------
// Manifest tests
//--------------------------------------------------
it('downloads a version from a manifest match', async () => { it('downloads a version from a manifest match', async () => {
os.platform = 'linux'; os.platform = 'linux';
os.arch = 'x64'; os.arch = 'x64';
@ -392,6 +399,10 @@ describe('setup-node', () => {
expect(logSpy).not.toHaveBeenCalledWith( expect(logSpy).not.toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...' 'Attempt to resolve the latest version from manifest...'
); );
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).not.toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
}); });
it('check latest version and resolve it from local cache', async () => { it('check latest version and resolve it from local cache', async () => {
@ -412,6 +423,10 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith( expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...' 'Attempt to resolve the latest version from manifest...'
); );
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(logSpy).toHaveBeenCalledWith("Resolved as '12.16.2'"); expect(logSpy).toHaveBeenCalledWith("Resolved as '12.16.2'");
expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`); expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`);
}); });
@ -436,6 +451,10 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith( expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...' 'Attempt to resolve the latest version from manifest...'
); );
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(logSpy).toHaveBeenCalledWith("Resolved as '12.16.2'"); expect(logSpy).toHaveBeenCalledWith("Resolved as '12.16.2'");
expect(logSpy).toHaveBeenCalledWith( expect(logSpy).toHaveBeenCalledWith(
`Acquiring 12.16.2 - ${os.arch} from ${expectedUrl}` `Acquiring 12.16.2 - ${os.arch} from ${expectedUrl}`
@ -472,6 +491,10 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith( expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...' 'Attempt to resolve the latest version from manifest...'
); );
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(logSpy).toHaveBeenCalledWith( expect(logSpy).toHaveBeenCalledWith(
`Failed to resolve version ${versionSpec} from manifest` `Failed to resolve version ${versionSpec} from manifest`
); );
@ -525,4 +548,222 @@ describe('setup-node', () => {
expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`); expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
}); });
}); });
describe('LTS version', () => {
beforeEach(() => {
os.platform = 'linux';
os.arch = 'x64';
inputs.stable = 'true';
});
it('find latest LTS version and resolve it from local cache (lts/erbium)', async () => {
// arrange
inputs['node-version'] = 'lts/erbium';
const toolPath = path.normalize('/cache/node/12.16.2/x64');
findSpy.mockReturnValue(toolPath);
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
`LTS alias 'erbium' for Node version 'lts/erbium'`
);
expect(dbgSpy).toHaveBeenCalledWith(
`Found LTS release '12.16.2' for Node version 'lts/erbium'`
);
expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`);
expect(cnSpy).toHaveBeenCalledWith(
`::add-path::${path.join(toolPath, 'bin')}${osm.EOL}`
);
});
it('find latest LTS version and install it from manifest (lts/erbium)', async () => {
// arrange
inputs['node-version'] = 'lts/erbium';
const toolPath = path.normalize('/cache/node/12.16.2/x64');
findSpy.mockImplementation(() => '');
dlSpy.mockImplementation(async () => '/some/temp/path');
exSpy.mockImplementation(async () => '/some/other/temp/path');
cacheSpy.mockImplementation(async () => toolPath);
const expectedUrl =
'https://github.com/actions/node-versions/releases/download/12.16.2-20200423.28/node-12.16.2-linux-x64.tar.gz';
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
`LTS alias 'erbium' for Node version 'lts/erbium'`
);
expect(dbgSpy).toHaveBeenCalledWith(
`Found LTS release '12.16.2' for Node version 'lts/erbium'`
);
expect(logSpy).toHaveBeenCalledWith('Attempting to download 12...');
expect(logSpy).toHaveBeenCalledWith(
`Acquiring 12.16.2 - ${os.arch} from ${expectedUrl}`
);
expect(logSpy).toHaveBeenCalledWith('Extracting ...');
expect(logSpy).toHaveBeenCalledWith('Adding to the cache ...');
expect(cnSpy).toHaveBeenCalledWith(
`::add-path::${path.join(toolPath, 'bin')}${osm.EOL}`
);
});
it('find latest LTS version and resolve it from local cache (lts/*)', async () => {
// arrange
inputs['node-version'] = 'lts/*';
const toolPath = path.normalize('/cache/node/14.0.0/x64');
findSpy.mockReturnValue(toolPath);
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
`LTS alias '*' for Node version 'lts/*'`
);
expect(dbgSpy).toHaveBeenCalledWith(
`Found LTS release '14.0.0' for Node version 'lts/*'`
);
expect(logSpy).toHaveBeenCalledWith(`Found in cache @ ${toolPath}`);
expect(cnSpy).toHaveBeenCalledWith(
`::add-path::${path.join(toolPath, 'bin')}${osm.EOL}`
);
});
it('find latest LTS version and install it from manifest (lts/*)', async () => {
// arrange
inputs['node-version'] = 'lts/*';
const toolPath = path.normalize('/cache/node/14.0.0/x64');
findSpy.mockImplementation(() => '');
dlSpy.mockImplementation(async () => '/some/temp/path');
exSpy.mockImplementation(async () => '/some/other/temp/path');
cacheSpy.mockImplementation(async () => toolPath);
const expectedUrl =
'https://github.com/actions/node-versions/releases/download/14.0.0-20200423.30/node-14.0.0-linux-x64.tar.gz';
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
`LTS alias '*' for Node version 'lts/*'`
);
expect(dbgSpy).toHaveBeenCalledWith(
`Found LTS release '14.0.0' for Node version 'lts/*'`
);
expect(logSpy).toHaveBeenCalledWith('Attempting to download 14...');
expect(logSpy).toHaveBeenCalledWith(
`Acquiring 14.0.0 - ${os.arch} from ${expectedUrl}`
);
expect(logSpy).toHaveBeenCalledWith('Extracting ...');
expect(logSpy).toHaveBeenCalledWith('Adding to the cache ...');
expect(cnSpy).toHaveBeenCalledWith(
`::add-path::${path.join(toolPath, 'bin')}${osm.EOL}`
);
});
it('fail with unable to parse LTS alias (lts/)', async () => {
// arrange
inputs['node-version'] = 'lts/';
findSpy.mockImplementation(() => '');
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(cnSpy).toHaveBeenCalledWith(
`::error::Unable to parse LTS alias for Node version 'lts/'${osm.EOL}`
);
});
it('fail to find LTS version (lts/unknown)', async () => {
// arrange
inputs['node-version'] = 'lts/unknown';
findSpy.mockImplementation(() => '');
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(dbgSpy).toHaveBeenCalledWith(
`LTS alias 'unknown' for Node version 'lts/unknown'`
);
expect(cnSpy).toHaveBeenCalledWith(
`::error::Unable to find LTS release 'unknown' for Node version 'lts/unknown'.${osm.EOL}`
);
});
it('fail if manifest is not available', async () => {
// arrange
inputs['node-version'] = 'lts/erbium';
// ... but not in the local cache
findSpy.mockImplementation(() => '');
getManifestSpy.mockImplementation(() => {
throw new Error('Unable to download manifest');
});
// act
await main.run();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve LTS alias from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
expect(cnSpy).toHaveBeenCalledWith(
`::error::Unable to download manifest${osm.EOL}`
);
});
});
}); });

17349
dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,59 @@
# 0. Caching dependencies
Date: 2021-05-21
Status: Proposed
# Context
`actions/setup-node` is the 2nd most popular action in GitHub Actions. A lot of customers use it in conjunction with [actions/cache](https://github.com/actions/cache) to speed up dependencies installation.
See more examples on proper usage in [actions/cache documentation](https://github.com/actions/cache/blob/main/examples.md#node---npm).
# Goals & Anti-Goals
Integration of caching functionality into `actions/setup-node` action will bring the following benefits for action users:
- Decrease the entry threshold for using the cache for Node.js dependencies and simplify initial configuration
- Simplify YAML pipelines because no need additional steps to enable caching
- More users will use cache for Node.js so more customers will have fast builds!
We will add support for NPM and Yarn dependencies caching.
As the first stage, we won't support custom locations for `package-lock.json`, `yarn.lock` files and action will work only when files are located in repository root.
We don't pursue the goal to provide wide customization of caching in scope of `actions/setup-node` action. The purpose of this integration is covering ~90% of basic use-cases. If user needs flexible customization, we should advice them to use `actions/cache` directly.
# Decision
- Add `cache` input parameter to `actions/setup-node`. For now, input will accept the following values:
- `npm` - enable caching for npm dependencies
- `yarn` - enable caching for yarn dependencies
- `''` - disable caching (default value)
- Cache feature will be disabled by default to make sure that we don't break existing customers. We will consider enabling cache by default in next major release (`v3`)
- Action will try to search `package-lock.json` or `yarn.lock` (npm 7.x supports `yarn.lock` files) files in the repository root and throw error if no one is found
- The hash of found file will be used as cache key (the same approach like [actions/cache](https://github.com/actions/cache/blob/main/examples.md#node---npm) recommends)
- The following key cache will be used `${{ runner.os }}-npm-${{ hashFiles('<package-lock-path>') }}`
- Action will cache global cache:
- Npm (retrieved via `npm config get cache`)
- Yarn 1 (retrieved via `yarn cache dir`)
- Yarn 2 (retrieved via `yarn config get cacheFolder`)
# Example of real use-cases
Npm package manager:
```yml
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: npm
```
Yarn package manager:
```yml
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: yarn
```
# Release process
As soon as functionality is implemented, we will release minor update of action. No need to bump major version since there are no breaking changes for existing users.
After that, we will update [starter-workflows](https://github.com/actions/starter-workflows/blob/main/ci/node.js.yml) and [GitHub Action documentation](https://docs.github.com/en/actions/guides/building-and-testing-nodejs#example-caching-dependencies).

View file

@ -24,6 +24,10 @@ interface INodeVersionInfo {
fileName: string; fileName: string;
} }
interface INodeRelease extends tc.IToolRelease {
lts?: string;
}
export async function getNode( export async function getNode(
versionSpec: string, versionSpec: string,
stable: boolean, stable: boolean,
@ -31,16 +35,28 @@ export async function getNode(
auth: string | undefined, auth: string | undefined,
arch: string = os.arch() arch: string = os.arch()
) { ) {
// Store manifest data to avoid multiple calls
let manifest: INodeRelease[] | undefined;
let osPlat: string = os.platform(); let osPlat: string = os.platform();
let osArch: string = translateArchToDistUrl(arch); let osArch: string = translateArchToDistUrl(arch);
if (isLtsAlias(versionSpec)) {
core.info('Attempt to resolve LTS alias from manifest...');
// No try-catch since it's not possible to resolve LTS alias without manifest
manifest = await getManifest(auth);
versionSpec = resolveLtsAliasFromManifest(versionSpec, stable, manifest);
}
if (checkLatest) { if (checkLatest) {
core.info('Attempt to resolve the latest version from manifest...'); core.info('Attempt to resolve the latest version from manifest...');
const resolvedVersion = await resolveVersionFromManifest( const resolvedVersion = await resolveVersionFromManifest(
versionSpec, versionSpec,
stable, stable,
auth, auth,
osArch osArch,
manifest
); );
if (resolvedVersion) { if (resolvedVersion) {
versionSpec = resolvedVersion; versionSpec = resolvedVersion;
@ -66,7 +82,13 @@ export async function getNode(
// Try download from internal distribution (popular versions only) // Try download from internal distribution (popular versions only)
// //
try { try {
info = await getInfoFromManifest(versionSpec, stable, auth, osArch); info = await getInfoFromManifest(
versionSpec,
stable,
auth,
osArch,
manifest
);
if (info) { if (info) {
core.info( core.info(
`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`
@ -170,20 +192,65 @@ export async function getNode(
core.addPath(toolPath); core.addPath(toolPath);
} }
function isLtsAlias(versionSpec: string): boolean {
return versionSpec.startsWith('lts/');
}
function getManifest(auth: string | undefined): Promise<tc.IToolRelease[]> {
core.debug('Getting manifest from actions/node-versions@main');
return tc.getManifestFromRepo('actions', 'node-versions', auth, 'main');
}
function resolveLtsAliasFromManifest(
versionSpec: string,
stable: boolean,
manifest: INodeRelease[]
): string {
const alias = versionSpec.split('lts/')[1]?.toLowerCase();
if (!alias) {
throw new Error(
`Unable to parse LTS alias for Node version '${versionSpec}'`
);
}
core.debug(`LTS alias '${alias}' for Node version '${versionSpec}'`);
// Supported formats are `lts/<alias>` and `lts/*`. Where asterisk means highest possible LTS.
const release =
alias === '*'
? manifest.find(x => !!x.lts && x.stable === stable)
: manifest.find(
x => x.lts?.toLowerCase() === alias && x.stable === stable
);
if (!release) {
throw new Error(
`Unable to find LTS release '${alias}' for Node version '${versionSpec}'.`
);
}
core.debug(
`Found LTS release '${release.version}' for Node version '${versionSpec}'`
);
return release.version.split('.')[0];
}
async function getInfoFromManifest( async function getInfoFromManifest(
versionSpec: string, versionSpec: string,
stable: boolean, stable: boolean,
auth: string | undefined, auth: string | undefined,
osArch: string = translateArchToDistUrl(os.arch()) osArch: string = translateArchToDistUrl(os.arch()),
manifest: tc.IToolRelease[] | undefined
): Promise<INodeVersionInfo | null> { ): Promise<INodeVersionInfo | null> {
let info: INodeVersionInfo | null = null; let info: INodeVersionInfo | null = null;
const releases = await tc.getManifestFromRepo( if (!manifest) {
'actions', core.debug('No manifest cached');
'node-versions', manifest = await getManifest(auth);
auth, }
'main'
); const rel = await tc.findFromManifest(versionSpec, stable, manifest, osArch);
const rel = await tc.findFromManifest(versionSpec, stable, releases, osArch);
if (rel && rel.files.length > 0) { if (rel && rel.files.length > 0) {
info = <INodeVersionInfo>{}; info = <INodeVersionInfo>{};
@ -234,10 +301,17 @@ async function resolveVersionFromManifest(
versionSpec: string, versionSpec: string,
stable: boolean, stable: boolean,
auth: string | undefined, auth: string | undefined,
osArch: string = translateArchToDistUrl(os.arch()) osArch: string = translateArchToDistUrl(os.arch()),
manifest: tc.IToolRelease[] | undefined
): Promise<string | undefined> { ): Promise<string | undefined> {
try { try {
const info = await getInfoFromManifest(versionSpec, stable, auth, osArch); const info = await getInfoFromManifest(
versionSpec,
stable,
auth,
osArch,
manifest
);
return info?.resolvedVersion; return info?.resolvedVersion;
} catch (err) { } catch (err) {
core.info('Unable to resolve version from manifest...'); core.info('Unable to resolve version from manifest...');