diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index 2058b0223..4dc5c312f 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -29,6 +29,20 @@ jobs: run: __tests__/verify-node.sh "${{ matrix.node-version }}" 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: runs-on: ${{ matrix.os }} strategy: diff --git a/__tests__/data/versions-manifest.json b/__tests__/data/versions-manifest.json index d313b16bb..4cf2ccee6 100644 --- a/__tests__/data/versions-manifest.json +++ b/__tests__/data/versions-manifest.json @@ -2,6 +2,7 @@ { "version": "14.0.0", "stable": true, + "lts": "Fermium", "release_url": "https://github.com/actions/node-versions/releases/tag/14.0.0-20200423.30", "files": [ { @@ -52,6 +53,7 @@ { "version": "12.16.2", "stable": true, + "lts": "Erbium", "release_url": "https://github.com/actions/node-versions/releases/tag/12.16.2-20200423.28", "files": [ { @@ -77,6 +79,7 @@ { "version": "10.20.1", "stable": true, + "lts": "Dubnium", "release_url": "https://github.com/actions/node-versions/releases/tag/10.20.1-20200423.27", "files": [ { @@ -102,6 +105,7 @@ { "version": "8.17.0", "stable": true, + "lts": "Carbon", "release_url": "https://github.com/actions/node-versions/releases/tag/8.17.0-20200423.26", "files": [ { @@ -127,6 +131,7 @@ { "version": "6.17.1", "stable": true, + "lts": "Boron", "release_url": "https://github.com/actions/node-versions/releases/tag/6.17.1-20200423.25", "files": [ { diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 4fe757792..994892365 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -134,6 +134,7 @@ describe('setup-node', () => { let match = await tc.findFromManifest('12.16.2', true, versions); expect(match).toBeDefined(); expect(match?.version).toBe('12.16.2'); + expect((match as any).lts).toBe('Erbium'); }); 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); expect(match).toBeDefined(); expect(match?.version).toBe('12.16.2'); + expect((match as any).lts).toBe('Erbium'); }); it('can find 10 from manifest on windows', async () => { @@ -162,6 +164,7 @@ describe('setup-node', () => { let match = await tc.findFromManifest('10', true, versions); expect(match).toBeDefined(); 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); }); + //-------------------------------------------------- + // Manifest tests + //-------------------------------------------------- + it('downloads a version from a manifest match', async () => { os.platform = 'linux'; os.arch = 'x64'; @@ -392,6 +399,10 @@ describe('setup-node', () => { expect(logSpy).not.toHaveBeenCalledWith( '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 () => { @@ -412,6 +423,10 @@ describe('setup-node', () => { expect(logSpy).toHaveBeenCalledWith( '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(`Found in cache @ ${toolPath}`); }); @@ -436,6 +451,10 @@ describe('setup-node', () => { expect(logSpy).toHaveBeenCalledWith( '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( `Acquiring 12.16.2 - ${os.arch} from ${expectedUrl}` @@ -472,6 +491,10 @@ describe('setup-node', () => { expect(logSpy).toHaveBeenCalledWith( '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( `Failed to resolve version ${versionSpec} from manifest` ); @@ -525,4 +548,222 @@ describe('setup-node', () => { 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}` + ); + }); + }); }); diff --git a/dist/index.js b/dist/index.js index 800d35a9c..1c614cd83 100644 --- a/dist/index.js +++ b/dist/index.js @@ -13105,11 +13105,19 @@ const semver = __importStar(__webpack_require__(280)); const fs = __webpack_require__(747); function getNode(versionSpec, stable, checkLatest, auth, arch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { + // Store manifest data to avoid multiple calls + let manifest; let osPlat = os.platform(); let osArch = 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 = yield getManifest(auth); + versionSpec = resolveLtsAliasFromManifest(versionSpec, stable, manifest); + } if (checkLatest) { core.info('Attempt to resolve the latest version from manifest...'); - const resolvedVersion = yield resolveVersionFromManifest(versionSpec, stable, auth, osArch); + const resolvedVersion = yield resolveVersionFromManifest(versionSpec, stable, auth, osArch, manifest); if (resolvedVersion) { versionSpec = resolvedVersion; core.info(`Resolved as '${versionSpec}'`); @@ -13133,7 +13141,7 @@ function getNode(versionSpec, stable, checkLatest, auth, arch = os.arch()) { // Try download from internal distribution (popular versions only) // try { - info = yield getInfoFromManifest(versionSpec, stable, auth, osArch); + info = yield getInfoFromManifest(versionSpec, stable, auth, osArch, manifest); if (info) { core.info(`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`); downloadPath = yield tc.downloadTool(info.downloadUrl, undefined, auth); @@ -13216,11 +13224,38 @@ function getNode(versionSpec, stable, checkLatest, auth, arch = os.arch()) { }); } exports.getNode = getNode; -function getInfoFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch())) { +function isLtsAlias(versionSpec) { + return versionSpec.startsWith('lts/'); +} +function getManifest(auth) { + core.debug('Getting manifest from actions/node-versions@main'); + return tc.getManifestFromRepo('actions', 'node-versions', auth, 'main'); +} +function resolveLtsAliasFromManifest(versionSpec, stable, manifest) { + var _a; + const alias = (_a = versionSpec.split('lts/')[1]) === null || _a === void 0 ? void 0 : _a.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/` and `lts/*`. Where asterisk means highest possible LTS. + const release = alias === '*' + ? manifest.find(x => !!x.lts && x.stable === stable) + : manifest.find(x => { var _a; return ((_a = x.lts) === null || _a === void 0 ? void 0 : _a.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]; +} +function getInfoFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch()), manifest) { return __awaiter(this, void 0, void 0, function* () { let info = null; - const releases = yield tc.getManifestFromRepo('actions', 'node-versions', auth, 'main'); - const rel = yield tc.findFromManifest(versionSpec, stable, releases, osArch); + if (!manifest) { + core.debug('No manifest cached'); + manifest = yield getManifest(auth); + } + const rel = yield tc.findFromManifest(versionSpec, stable, manifest, osArch); if (rel && rel.files.length > 0) { info = {}; info.resolvedVersion = rel.version; @@ -13257,10 +13292,10 @@ function getInfoFromDist(versionSpec, arch = os.arch()) { }; }); } -function resolveVersionFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch())) { +function resolveVersionFromManifest(versionSpec, stable, auth, osArch = translateArchToDistUrl(os.arch()), manifest) { return __awaiter(this, void 0, void 0, function* () { try { - const info = yield getInfoFromManifest(versionSpec, stable, auth, osArch); + const info = yield getInfoFromManifest(versionSpec, stable, auth, osArch, manifest); return info === null || info === void 0 ? void 0 : info.resolvedVersion; } catch (err) { diff --git a/src/installer.ts b/src/installer.ts index cc45e2248..b43e5448c 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -24,6 +24,10 @@ interface INodeVersionInfo { fileName: string; } +interface INodeRelease extends tc.IToolRelease { + lts?: string; +} + export async function getNode( versionSpec: string, stable: boolean, @@ -31,16 +35,28 @@ export async function getNode( auth: string | undefined, arch: string = os.arch() ) { + // Store manifest data to avoid multiple calls + let manifest: INodeRelease[] | undefined; let osPlat: string = os.platform(); 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) { core.info('Attempt to resolve the latest version from manifest...'); const resolvedVersion = await resolveVersionFromManifest( versionSpec, stable, auth, - osArch + osArch, + manifest ); if (resolvedVersion) { versionSpec = resolvedVersion; @@ -66,7 +82,13 @@ export async function getNode( // Try download from internal distribution (popular versions only) // try { - info = await getInfoFromManifest(versionSpec, stable, auth, osArch); + info = await getInfoFromManifest( + versionSpec, + stable, + auth, + osArch, + manifest + ); if (info) { core.info( `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` @@ -170,20 +192,65 @@ export async function getNode( core.addPath(toolPath); } +function isLtsAlias(versionSpec: string): boolean { + return versionSpec.startsWith('lts/'); +} + +function getManifest(auth: string | undefined): Promise { + 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/` 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( versionSpec: string, stable: boolean, auth: string | undefined, - osArch: string = translateArchToDistUrl(os.arch()) + osArch: string = translateArchToDistUrl(os.arch()), + manifest: tc.IToolRelease[] | undefined ): Promise { let info: INodeVersionInfo | null = null; - const releases = await tc.getManifestFromRepo( - 'actions', - 'node-versions', - auth, - 'main' - ); - const rel = await tc.findFromManifest(versionSpec, stable, releases, osArch); + if (!manifest) { + core.debug('No manifest cached'); + manifest = await getManifest(auth); + } + + const rel = await tc.findFromManifest(versionSpec, stable, manifest, osArch); if (rel && rel.files.length > 0) { info = {}; @@ -234,10 +301,17 @@ async function resolveVersionFromManifest( versionSpec: string, stable: boolean, auth: string | undefined, - osArch: string = translateArchToDistUrl(os.arch()) + osArch: string = translateArchToDistUrl(os.arch()), + manifest: tc.IToolRelease[] | undefined ): Promise { try { - const info = await getInfoFromManifest(versionSpec, stable, auth, osArch); + const info = await getInfoFromManifest( + versionSpec, + stable, + auth, + osArch, + manifest + ); return info?.resolvedVersion; } catch (err) { core.info('Unable to resolve version from manifest...');