mirror of
https://github.com/actions/setup-node.git
synced 2026-11-05 10:50:51 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
101b347414 | ||
|
|
949feb2413 | ||
|
|
7207c4f118 | ||
|
|
54337e0d02 | ||
|
|
fb54fa8c14 | ||
|
|
7f84f1d52d | ||
|
|
0c54c7b074 | ||
|
|
a829419004 | ||
|
|
680d1e489b | ||
|
|
db0fb3a98e | ||
|
|
94196ee1d1 | ||
|
|
d1fcc24e82 |
No files matched your search
@@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
version: 10
|
||||
- name: Generate pnpm file
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ allowed:
|
||||
- mit
|
||||
- cc0-1.0
|
||||
- unlicense
|
||||
- blueoak-1.0.0
|
||||
- BlueOak-1.0.0
|
||||
|
||||
reviewed:
|
||||
npm:
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/cache"
|
||||
version: 6.2.0
|
||||
version: 6.3.0
|
||||
type: npm
|
||||
summary: Actions cache lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/cache
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: "@actions/glob"
|
||||
version: 0.6.1
|
||||
type: npm
|
||||
summary: Actions glob lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/glob
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
File renamed without changes.
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: brace-expansion
|
||||
version: 5.0.9
|
||||
version: 5.0.12
|
||||
type: npm
|
||||
summary: Brace expansion as known from sh/bash
|
||||
homepage:
|
||||
|
||||
Generated
-26
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: minimatch
|
||||
version: 3.1.5
|
||||
type: npm
|
||||
summary: a glob matcher in javascript
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: minimatch
|
||||
version: 10.2.5
|
||||
version: 10.2.6
|
||||
type: npm
|
||||
summary: a glob matcher in javascript
|
||||
homepage:
|
||||
license: blueoak-1.0.0
|
||||
license: BlueOak-1.0.0
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 6.28.0
|
||||
version: 6.29.0
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
@@ -233,7 +233,11 @@ jobs:
|
||||
|
||||
## Using `setup-node` on GHES
|
||||
|
||||
`setup-node` comes pre-installed on the appliance with GHES if Actions is enabled. When dynamically downloading Nodejs distributions, `setup-node` downloads distributions from [`actions/node-versions`](https://github.com/actions/node-versions) on github.com (outside of the appliance). These calls to `actions/node-versions` are made via unauthenticated requests, which are limited to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). If more requests are made within the time frame, then you will start to see rate-limit errors during downloading that looks like: `##[error]API rate limit exceeded for...`. After that error the action will try to download versions directly from the official site, but it also can have rate limit so it's better to put token.
|
||||
`setup-node` comes pre-installed on the appliance with GHES if Actions is enabled. It downloads Node.js distributions from [`actions/node-versions`](https://github.com/actions/node-versions) on github.com, outside the appliance.
|
||||
|
||||
Manifest requests use the GitHub API first, with the `token` input for authentication when supplied. If API retrieval fails, the action tries the manifest on `raw.githubusercontent.com`. This fallback also applies when `check-latest` is enabled. If both manifest sources are unavailable, numeric versions can fall back to nodejs.org. LTS aliases require a manifest to resolve the requested version.
|
||||
|
||||
Unauthenticated API requests are limited to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). Supplying a token increases the API rate limit.
|
||||
|
||||
To get a higher rate limit, you can [generate a personal access token on github.com](https://github.com/settings/tokens/new) and pass it as the `token` input for the action:
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ describe('main tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
each`
|
||||
contents | expected
|
||||
${'12'} | ${'12'}
|
||||
@@ -315,11 +319,39 @@ describe('main tests', () => {
|
||||
|
||||
// Assert
|
||||
expect(getNodeVersionFromFileSpy).toHaveBeenCalled();
|
||||
// A relative input is still resolved against the workspace.
|
||||
expect(getNodeVersionFromFileSpy).toHaveBeenCalledWith(
|
||||
path.join(process.env['GITHUB_WORKSPACE']!, '.nvmrc')
|
||||
);
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
`Resolved ${inputs['node-version-file']} as ${expectedVersionSpec}`
|
||||
);
|
||||
}, 10000);
|
||||
|
||||
it('reads node-version-file given as an absolute path outside the workspace', async () => {
|
||||
// Arrange: a composite action passes `${{ github.action_path }}/.nvmrc`,
|
||||
// which is absolute and may sit outside GITHUB_WORKSPACE.
|
||||
// The workspace deliberately points at a different existing directory,
|
||||
// so the absolute version file lies outside it.
|
||||
const workspace = path.join(__dirname, 'mock');
|
||||
process.env['GITHUB_WORKSPACE'] = workspace;
|
||||
const versionFilePath = path.join(__dirname, 'data', '.nvmrc');
|
||||
// Guard the premise: the version file lives outside the workspace.
|
||||
expect(path.relative(workspace, versionFilePath).startsWith('..')).toBe(
|
||||
true
|
||||
);
|
||||
inputs['node-version-file'] = versionFilePath;
|
||||
|
||||
// Act
|
||||
await main.run();
|
||||
|
||||
// Assert: the path is used as provided and the real file is read.
|
||||
// The expected `24` comes from `__tests__/data/.nvmrc` (`v24`).
|
||||
expect(getNodeVersionFromFileSpy).toHaveBeenCalledWith(versionFilePath);
|
||||
expect(infoSpy).toHaveBeenCalledWith(`Resolved ${versionFilePath} as 24`);
|
||||
expect(core.setFailed as jest.Mock).not.toHaveBeenCalled();
|
||||
}, 10000);
|
||||
|
||||
it('should throw an error if node-version-file is not accessible', async () => {
|
||||
// Arrange
|
||||
inputs['node-version-file'] = 'non-existing-file';
|
||||
|
||||
@@ -14,6 +14,8 @@ import osm from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const nodeVersionsManifestUrl =
|
||||
'https://raw.githubusercontent.com/actions/node-versions/main/versions-manifest.json';
|
||||
|
||||
// Mock @actions modules before importing anything that depends on them
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
@@ -129,6 +131,21 @@ const {default: nodeV8CanaryTestDist} = await import(
|
||||
import type {INodeVersion} from '../src/distributions/base-models.js';
|
||||
import type {IToolRelease} from '@actions/tool-cache';
|
||||
|
||||
function useRetryTimers() {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
}
|
||||
|
||||
async function runWithRetries() {
|
||||
const setup = main.run();
|
||||
await jest.runAllTimersAsync();
|
||||
await setup;
|
||||
}
|
||||
|
||||
describe('setup-node', () => {
|
||||
let build: InstanceType<typeof OfficialBuilds>;
|
||||
let inputs = {} as any;
|
||||
@@ -233,7 +250,9 @@ describe('setup-node', () => {
|
||||
|
||||
getJsonSpy.mockImplementation((url: any) => {
|
||||
let res: any;
|
||||
if (url.includes('/rc')) {
|
||||
if (url === nodeVersionsManifestUrl) {
|
||||
throw new Error('Unable to download raw manifest');
|
||||
} else if (url.includes('/rc')) {
|
||||
res = <INodeVersion[]>nodeTestDistRc;
|
||||
} else if (url.includes('/nightly')) {
|
||||
res = <INodeVersion[]>nodeTestDistNightly;
|
||||
@@ -649,7 +668,7 @@ describe('setup-node', () => {
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Attempt to resolve the latest version from manifest...'
|
||||
);
|
||||
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).toHaveBeenCalledWith(
|
||||
'Getting manifest from actions/node-versions@main'
|
||||
);
|
||||
@@ -677,7 +696,7 @@ describe('setup-node', () => {
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Attempt to resolve the latest version from manifest...'
|
||||
);
|
||||
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).toHaveBeenCalledWith(
|
||||
'Getting manifest from actions/node-versions@main'
|
||||
);
|
||||
@@ -716,7 +735,7 @@ describe('setup-node', () => {
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Attempt to resolve the latest version from manifest...'
|
||||
);
|
||||
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
|
||||
expect(dbgSpy).toHaveBeenCalledWith(
|
||||
'Getting manifest from actions/node-versions@main'
|
||||
);
|
||||
@@ -774,6 +793,8 @@ describe('setup-node', () => {
|
||||
});
|
||||
|
||||
describe('LTS version', () => {
|
||||
useRetryTimers();
|
||||
|
||||
beforeEach(() => {
|
||||
os.platform = 'linux';
|
||||
os.arch = 'x64';
|
||||
@@ -929,7 +950,7 @@ describe('setup-node', () => {
|
||||
});
|
||||
|
||||
// act
|
||||
await main.run();
|
||||
await runWithRetries();
|
||||
|
||||
// assert
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
@@ -939,12 +960,14 @@ describe('setup-node', () => {
|
||||
'Getting manifest from actions/node-versions@main'
|
||||
);
|
||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||
`Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest`
|
||||
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('latest alias syntax', () => {
|
||||
useRetryTimers();
|
||||
|
||||
it.each(['latest', 'current', 'node'])(
|
||||
'download the %s version if alias is provided',
|
||||
async inputVersion => {
|
||||
@@ -960,16 +983,15 @@ describe('setup-node', () => {
|
||||
});
|
||||
|
||||
// Act
|
||||
await main.run();
|
||||
await runWithRetries();
|
||||
|
||||
// assert
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest'
|
||||
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
|
||||
);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith('getting latest node version...');
|
||||
},
|
||||
10000
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1040,53 +1062,272 @@ describe('setup-node', () => {
|
||||
}
|
||||
}, 100000);
|
||||
|
||||
describe('manifest retry and validation', () => {
|
||||
describe('manifest retrieval', () => {
|
||||
useRetryTimers();
|
||||
let installedVersion: string;
|
||||
|
||||
beforeEach(() => {
|
||||
installedVersion = '12.16.2';
|
||||
os.platform = 'linux';
|
||||
os.arch = 'x64';
|
||||
inputs['node-version'] = 'lts/erbium';
|
||||
inputs['token'] = 'manifest-token';
|
||||
whichSpy.mockImplementation(async (tool: string) => tool);
|
||||
findSpy.mockImplementation(() => '');
|
||||
});
|
||||
|
||||
it('retries fetching the manifest and succeeds on a later attempt', async () => {
|
||||
let calls = 0;
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
throw new Error('transient network failure');
|
||||
}
|
||||
return <tc.IToolRelease[]>nodeTestManifest;
|
||||
});
|
||||
|
||||
getJsonSpy.mockImplementation((url: string) => ({
|
||||
result:
|
||||
url === nodeVersionsManifestUrl ? nodeTestManifest : nodeTestDist
|
||||
}));
|
||||
dlSpy.mockImplementation(async () => '/some/temp/path');
|
||||
const toolPath = path.normalize('/cache/node/12.16.2/x64');
|
||||
exSpy.mockImplementation(async () => '/some/other/temp/path');
|
||||
cacheSpy.mockImplementation(async () => toolPath);
|
||||
cacheSpy.mockImplementation(
|
||||
async (
|
||||
_sourceDirectory: string,
|
||||
_tool: string,
|
||||
version: string,
|
||||
arch: string
|
||||
) => {
|
||||
installedVersion = version;
|
||||
return path.normalize(`/cache/node/${version}/${arch}`);
|
||||
}
|
||||
);
|
||||
getExecOutputSpy.mockImplementation(async () => ({
|
||||
stdout: `v${path.basename(path.dirname(toolPath))}\n`,
|
||||
stdout: `v${installedVersion}\n`,
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}));
|
||||
});
|
||||
|
||||
await main.run();
|
||||
describe.each([
|
||||
{versionSpec: 'lts/erbium', checkLatest: 'false'},
|
||||
{versionSpec: 'lts/erbium', checkLatest: 'true'},
|
||||
{versionSpec: '12', checkLatest: 'false'},
|
||||
{versionSpec: '12', checkLatest: 'true'}
|
||||
])('$versionSpec, check-latest: $checkLatest', mode => {
|
||||
beforeEach(() => {
|
||||
inputs['node-version'] = mode.versionSpec;
|
||||
inputs['check-latest'] = mode.checkLatest;
|
||||
});
|
||||
|
||||
expect(calls).toBe(2);
|
||||
expect(logSpy).toHaveBeenCalledWith('Retrying to fetch the manifest...');
|
||||
expect(dbgSpy).toHaveBeenCalledWith(
|
||||
`Found LTS release '12.16.2' for Node version 'lts/erbium'`
|
||||
it('uses the authenticated API without requesting raw', async () => {
|
||||
await runWithRetries();
|
||||
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getManifestSpy).toHaveBeenCalledWith(
|
||||
'actions',
|
||||
'node-versions',
|
||||
'token manifest-token',
|
||||
'main'
|
||||
);
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.16.2');
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([403, 429])(
|
||||
'falls back to raw immediately after API HTTP %i',
|
||||
async statusCode => {
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
throw Object.assign(new Error('API unavailable'), {statusCode});
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getJsonSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getJsonSpy).toHaveBeenCalledWith(nodeVersionsManifestUrl);
|
||||
expect(getManifestSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
getJsonSpy.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(Date.now()).toBe(startedAt);
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
'node-version',
|
||||
'v12.16.2'
|
||||
);
|
||||
}
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
it('rejects an empty manifest as invalid and retries', async () => {
|
||||
getManifestSpy.mockImplementation(() => []);
|
||||
it('refreshes the manifest for a later action invocation', async () => {
|
||||
inputs['node-version'] = '12';
|
||||
await runWithRetries();
|
||||
getManifestSpy.mockClear();
|
||||
|
||||
await main.run();
|
||||
const latestVersion = '12.17.0';
|
||||
getManifestSpy.mockImplementation(() => [
|
||||
{
|
||||
version: latestVersion,
|
||||
stable: true,
|
||||
release_url: `https://github.com/actions/node-versions/releases/tag/${latestVersion}`,
|
||||
files: [
|
||||
{
|
||||
filename: `node-${latestVersion}-linux-x64.tar.gz`,
|
||||
arch: 'x64',
|
||||
platform: 'linux',
|
||||
download_url: `https://github.com/actions/node-versions/releases/download/${latestVersion}/node-${latestVersion}-linux-x64.tar.gz`
|
||||
}
|
||||
]
|
||||
},
|
||||
...nodeTestManifest
|
||||
]);
|
||||
inputs['check-latest'] = 'true';
|
||||
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(3);
|
||||
await runWithRetries();
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(`Resolved as '${latestVersion}'`);
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.17.0');
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fetch a manifest for an installed numeric version', async () => {
|
||||
inputs['node-version'] = '12';
|
||||
findSpy.mockReturnValue(path.normalize('/cache/node/12.16.2/x64'));
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getManifestSpy).not.toHaveBeenCalled();
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.16.2');
|
||||
});
|
||||
|
||||
it('checks the manifest before using an installed version with check-latest', async () => {
|
||||
inputs['node-version'] = '12';
|
||||
inputs['check-latest'] = 'true';
|
||||
findSpy.mockReturnValue(path.normalize('/cache/node/12.16.2/x64'));
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getManifestSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
findSpy.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses an installed version when check-latest finds no matching release', async () => {
|
||||
os.arch = 'arm64';
|
||||
inputs['node-version'] = '12';
|
||||
inputs['check-latest'] = 'true';
|
||||
findSpy.mockReturnValue(path.normalize('/cache/node/12.16.2/arm64'));
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Failed to resolve version 12 from manifest'
|
||||
);
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.16.2');
|
||||
});
|
||||
|
||||
it('retries a transient API failure before requesting raw', async () => {
|
||||
getManifestSpy.mockImplementationOnce(() => {
|
||||
throw new Error('transient network failure');
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(2);
|
||||
expect(Date.now() - startedAt).toBe(1000);
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['empty', 'unavailable'])(
|
||||
'uses raw after retrying an %s API manifest',
|
||||
async failure => {
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
if (failure === 'unavailable') {
|
||||
throw new Error('API unavailable');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(3);
|
||||
expect(getJsonSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getJsonSpy).toHaveBeenCalledWith(nodeVersionsManifestUrl);
|
||||
expect(Date.now() - startedAt).toBe(3000);
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.16.2');
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['empty', []],
|
||||
['malformed', null]
|
||||
])(
|
||||
'fails LTS setup when the API fails and raw is %s',
|
||||
async (_, result) => {
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
throw Object.assign(new Error('API unavailable'), {statusCode: 429});
|
||||
});
|
||||
getJsonSpy.mockImplementationOnce(() => ({result}));
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(getJsonSpy).toHaveBeenCalledWith(nodeVersionsManifestUrl);
|
||||
expect(getManifestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(dlSpy).not.toHaveBeenCalled();
|
||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('API: API unavailable. Raw: The manifest')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('retains both source errors when LTS setup cannot fetch a manifest', async () => {
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
throw Object.assign(new Error('API unavailable'), {statusCode: 403});
|
||||
});
|
||||
getJsonSpy.mockImplementationOnce(() => {
|
||||
throw new Error('Raw unavailable');
|
||||
});
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(dlSpy).not.toHaveBeenCalled();
|
||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||
`Failed to fetch a valid manifest after 3 attempts. Last error: The manifest fetched is empty, truncated, or does not contain any valid tool release entries.`
|
||||
expect.stringContaining('API: API unavailable. Raw: Raw unavailable')
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
it.each(['false', 'true'])(
|
||||
'uses Node dist when both sources fail with check-latest: %s',
|
||||
async checkLatest => {
|
||||
inputs['node-version'] = '12';
|
||||
inputs['check-latest'] = checkLatest;
|
||||
getManifestSpy.mockImplementation(() => {
|
||||
throw Object.assign(new Error('API unavailable'), {statusCode: 403});
|
||||
});
|
||||
getJsonSpy.mockImplementation((url: string) => {
|
||||
if (url === nodeVersionsManifestUrl) {
|
||||
throw new Error('Raw unavailable');
|
||||
}
|
||||
return {result: nodeTestDist};
|
||||
});
|
||||
|
||||
await runWithRetries();
|
||||
|
||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||
expect(getJsonSpy).toHaveBeenCalledWith(nodeVersionsManifestUrl);
|
||||
expect(dlSpy).toHaveBeenCalledWith(
|
||||
'https://nodejs.org/dist/v12.16.3/node-v12.16.3-linux-x64.tar.gz',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(core.setOutput).toHaveBeenCalledWith('node-version', 'v12.16.3');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('node version verification', () => {
|
||||
|
||||
Vendored
+3150
-5148
File diff suppressed because it is too large.
Load diff
Vendored
+3283
-5255
File diff suppressed because it is too large.
Load diff
@@ -79,7 +79,7 @@ steps:
|
||||
The `node-version-file` input accepts a path to a file containing the version of Node.js to be used by a project, for example `.nvmrc`, `.node-version`, `.tool-versions`, `mise.toml`, or `package.json`. If both the `node-version` and the `node-version-file` inputs are provided then the `node-version` input is used.
|
||||
See [supported version syntax](https://github.com/actions/setup-node#supported-version-syntax).
|
||||
|
||||
> The action will search for the node version file relative to the repository root.
|
||||
> The action resolves a relative `node-version-file` path against `GITHUB_WORKSPACE`, which is the repository root by default. If the input is a full absolute path, the action uses that path directly instead of appending it to the workspace.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
|
||||
Generated
+578
-869
File diff suppressed because it is too large.
Load diff
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "setup-node",
|
||||
"version": "7.0.0",
|
||||
"version": "7.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "setup node action",
|
||||
@@ -29,7 +29,7 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/cache": "^6.2.0",
|
||||
"@actions/cache": "^6.3.0",
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/exec": "^3.0.0",
|
||||
"@actions/github": "^9.1.1",
|
||||
@@ -41,25 +41,25 @@
|
||||
"smol-toml": "^1.7.1"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "5.0.9"
|
||||
"brace-expansion": "5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.62.0",
|
||||
"@vercel/ncc": "^0.44.0",
|
||||
"eslint": "^10.5.0",
|
||||
"@typescript-eslint/parser": "^8.70.0",
|
||||
"@vercel/ncc": "^0.45.0",
|
||||
"eslint": "^10.11.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jest": "^29.15.2",
|
||||
"eslint-plugin-n": "^18.1.0",
|
||||
"eslint-plugin-n": "^18.3.0",
|
||||
"globals": "^17.7.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest-each": "^30.4.1",
|
||||
"jest-each": "^30.5.2",
|
||||
"prettier": "^3.9.6",
|
||||
"ts-jest": "^29.4.11",
|
||||
"typescript": "^7.0.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
import path from 'path';
|
||||
import * as exec from '@actions/exec';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
|
||||
import BaseDistribution from '../base-distribution.js';
|
||||
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models.js';
|
||||
@@ -10,6 +11,15 @@ interface INodeRelease extends tc.IToolRelease {
|
||||
lts?: string;
|
||||
}
|
||||
|
||||
const nodeVersionsManifestUrl =
|
||||
'https://raw.githubusercontent.com/actions/node-versions/main/versions-manifest.json';
|
||||
const invalidManifestMessage =
|
||||
'The manifest fetched is empty, truncated, or does not contain any valid tool release entries.';
|
||||
|
||||
function isValidManifest(manifest: unknown): manifest is tc.IToolRelease[] {
|
||||
return Array.isArray(manifest) && manifest.length > 0;
|
||||
}
|
||||
|
||||
export default class OfficialBuilds extends BaseDistribution {
|
||||
constructor(nodeInfo: NodeInputs) {
|
||||
super(nodeInfo);
|
||||
@@ -43,12 +53,20 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
|
||||
if (this.nodeInfo.checkLatest) {
|
||||
core.info('Attempt to resolve the latest version from manifest...');
|
||||
const resolvedVersion = await this.resolveVersionFromManifest(
|
||||
this.nodeInfo.versionSpec,
|
||||
this.nodeInfo.stable,
|
||||
osArch,
|
||||
manifest
|
||||
);
|
||||
let resolvedVersion: string | undefined;
|
||||
try {
|
||||
manifest ??= await this.getManifest();
|
||||
const info = await this.getInfoFromManifest(
|
||||
this.nodeInfo.versionSpec,
|
||||
this.nodeInfo.stable,
|
||||
osArch,
|
||||
manifest
|
||||
);
|
||||
resolvedVersion = info?.resolvedVersion;
|
||||
} catch (error) {
|
||||
core.info('Unable to resolve version from manifest...');
|
||||
core.debug((error as Error).message);
|
||||
}
|
||||
if (resolvedVersion) {
|
||||
this.nodeInfo.versionSpec = resolvedVersion;
|
||||
core.info(`Resolved as '${resolvedVersion}'`);
|
||||
@@ -193,7 +211,7 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
private async getManifest(): Promise<tc.IToolRelease[]> {
|
||||
let lastError: Error | undefined;
|
||||
const maxAttempts = 3;
|
||||
core.debug(`Getting manifest from actions/node-versions@main`);
|
||||
core.debug('Getting manifest from actions/node-versions@main');
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const manifest = await tc.getManifestFromRepo(
|
||||
@@ -204,28 +222,48 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
: this.nodeInfo.auth,
|
||||
'main'
|
||||
);
|
||||
if (Array.isArray(manifest) && manifest.length > 0) {
|
||||
if (isValidManifest(manifest)) {
|
||||
return manifest;
|
||||
}
|
||||
lastError = new Error(
|
||||
`The manifest fetched is empty, truncated, or does not contain any valid tool release entries.`
|
||||
);
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
lastError = new Error(invalidManifestMessage);
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
const status = (error as {statusCode?: number})?.statusCode;
|
||||
if (status === 403 || status === 429) {
|
||||
core.debug(
|
||||
`GitHub API returned HTTP ${status}; trying raw manifest.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
core.debug(
|
||||
`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`
|
||||
);
|
||||
if (attempt < maxAttempts) {
|
||||
core.info(`Retrying to fetch the manifest...`);
|
||||
core.info('Retrying to fetch the manifest...');
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, 1000 * 2 ** (attempt - 1))
|
||||
); // Retry after a delay
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
|
||||
);
|
||||
|
||||
core.debug(`GitHub API manifest fetch failed: ${lastError?.message}`);
|
||||
core.debug(`Getting manifest from ${nodeVersionsManifestUrl}`);
|
||||
try {
|
||||
const {result} = await this.httpClient.getJson<tc.IToolRelease[]>(
|
||||
nodeVersionsManifestUrl
|
||||
);
|
||||
if (!isValidManifest(result)) {
|
||||
throw new Error(invalidManifestMessage);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = (error as Error).message;
|
||||
throw new Error(
|
||||
`Failed to fetch a valid manifest from the GitHub API and raw URL. API: ${lastError?.message}. Raw: ${message}`,
|
||||
{cause: error}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveLtsAliasFromManifest(
|
||||
@@ -272,26 +310,6 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
return release.version.split('.')[0];
|
||||
}
|
||||
|
||||
private async resolveVersionFromManifest(
|
||||
versionSpec: string,
|
||||
stable: boolean,
|
||||
osArch: string,
|
||||
manifest: tc.IToolRelease[] | undefined
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const info = await this.getInfoFromManifest(
|
||||
versionSpec,
|
||||
stable,
|
||||
osArch,
|
||||
manifest
|
||||
);
|
||||
return info?.resolvedVersion;
|
||||
} catch (err) {
|
||||
core.info('Unable to resolve version from manifest...');
|
||||
core.debug((err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
private async getInfoFromManifest(
|
||||
versionSpec: string,
|
||||
stable: boolean,
|
||||
|
||||
+3
-1
@@ -120,7 +120,9 @@ function resolveVersionInput(): string {
|
||||
}
|
||||
|
||||
if (versionFileInput) {
|
||||
const versionFilePath = path.join(
|
||||
// `path.resolve` (unlike `path.join`) keeps an already-absolute input as-is,
|
||||
// so a composite action can pass `${{ github.action_path }}/.nvmrc`.
|
||||
const versionFilePath = path.resolve(
|
||||
process.env.GITHUB_WORKSPACE!,
|
||||
versionFileInput
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user