mirror of
https://github.com/actions/setup-node.git
synced 2026-11-04 14:06:30 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e27dda5b |
No files matched your search
@@ -233,11 +233,7 @@ jobs:
|
|||||||
|
|
||||||
## Using `setup-node` on GHES
|
## Using `setup-node` on GHES
|
||||||
|
|
||||||
`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.
|
`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.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import osm from 'os';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
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
|
// Mock @actions modules before importing anything that depends on them
|
||||||
jest.unstable_mockModule('@actions/core', () => ({
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
@@ -131,21 +129,6 @@ const {default: nodeV8CanaryTestDist} = await import(
|
|||||||
import type {INodeVersion} from '../src/distributions/base-models.js';
|
import type {INodeVersion} from '../src/distributions/base-models.js';
|
||||||
import type {IToolRelease} from '@actions/tool-cache';
|
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', () => {
|
describe('setup-node', () => {
|
||||||
let build: InstanceType<typeof OfficialBuilds>;
|
let build: InstanceType<typeof OfficialBuilds>;
|
||||||
let inputs = {} as any;
|
let inputs = {} as any;
|
||||||
@@ -250,9 +233,7 @@ describe('setup-node', () => {
|
|||||||
|
|
||||||
getJsonSpy.mockImplementation((url: any) => {
|
getJsonSpy.mockImplementation((url: any) => {
|
||||||
let res: any;
|
let res: any;
|
||||||
if (url === nodeVersionsManifestUrl) {
|
if (url.includes('/rc')) {
|
||||||
throw new Error('Unable to download raw manifest');
|
|
||||||
} else if (url.includes('/rc')) {
|
|
||||||
res = <INodeVersion[]>nodeTestDistRc;
|
res = <INodeVersion[]>nodeTestDistRc;
|
||||||
} else if (url.includes('/nightly')) {
|
} else if (url.includes('/nightly')) {
|
||||||
res = <INodeVersion[]>nodeTestDistNightly;
|
res = <INodeVersion[]>nodeTestDistNightly;
|
||||||
@@ -668,7 +649,7 @@ 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).not.toHaveBeenCalledWith('No manifest cached');
|
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||||
expect(dbgSpy).toHaveBeenCalledWith(
|
expect(dbgSpy).toHaveBeenCalledWith(
|
||||||
'Getting manifest from actions/node-versions@main'
|
'Getting manifest from actions/node-versions@main'
|
||||||
);
|
);
|
||||||
@@ -696,7 +677,7 @@ 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).not.toHaveBeenCalledWith('No manifest cached');
|
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||||
expect(dbgSpy).toHaveBeenCalledWith(
|
expect(dbgSpy).toHaveBeenCalledWith(
|
||||||
'Getting manifest from actions/node-versions@main'
|
'Getting manifest from actions/node-versions@main'
|
||||||
);
|
);
|
||||||
@@ -735,7 +716,7 @@ 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).not.toHaveBeenCalledWith('No manifest cached');
|
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
|
||||||
expect(dbgSpy).toHaveBeenCalledWith(
|
expect(dbgSpy).toHaveBeenCalledWith(
|
||||||
'Getting manifest from actions/node-versions@main'
|
'Getting manifest from actions/node-versions@main'
|
||||||
);
|
);
|
||||||
@@ -793,8 +774,6 @@ describe('setup-node', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('LTS version', () => {
|
describe('LTS version', () => {
|
||||||
useRetryTimers();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
os.platform = 'linux';
|
os.platform = 'linux';
|
||||||
os.arch = 'x64';
|
os.arch = 'x64';
|
||||||
@@ -950,7 +929,7 @@ describe('setup-node', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await runWithRetries();
|
await main.run();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(logSpy).toHaveBeenCalledWith(
|
expect(logSpy).toHaveBeenCalledWith(
|
||||||
@@ -960,14 +939,12 @@ describe('setup-node', () => {
|
|||||||
'Getting manifest from actions/node-versions@main'
|
'Getting manifest from actions/node-versions@main'
|
||||||
);
|
);
|
||||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||||
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
|
`Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest`
|
||||||
);
|
);
|
||||||
});
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('latest alias syntax', () => {
|
describe('latest alias syntax', () => {
|
||||||
useRetryTimers();
|
|
||||||
|
|
||||||
it.each(['latest', 'current', 'node'])(
|
it.each(['latest', 'current', 'node'])(
|
||||||
'download the %s version if alias is provided',
|
'download the %s version if alias is provided',
|
||||||
async inputVersion => {
|
async inputVersion => {
|
||||||
@@ -983,15 +960,16 @@ describe('setup-node', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await runWithRetries();
|
await main.run();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(logSpy).toHaveBeenCalledWith(
|
expect(logSpy).toHaveBeenCalledWith(
|
||||||
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
|
'Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(logSpy).toHaveBeenCalledWith('getting latest node version...');
|
expect(logSpy).toHaveBeenCalledWith('getting latest node version...');
|
||||||
}
|
},
|
||||||
|
10000
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1062,272 +1040,53 @@ describe('setup-node', () => {
|
|||||||
}
|
}
|
||||||
}, 100000);
|
}, 100000);
|
||||||
|
|
||||||
describe('manifest retrieval', () => {
|
describe('manifest retry and validation', () => {
|
||||||
useRetryTimers();
|
|
||||||
let installedVersion: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
installedVersion = '12.16.2';
|
|
||||||
os.platform = 'linux';
|
os.platform = 'linux';
|
||||||
os.arch = 'x64';
|
os.arch = 'x64';
|
||||||
inputs['node-version'] = 'lts/erbium';
|
inputs['node-version'] = 'lts/erbium';
|
||||||
inputs['token'] = 'manifest-token';
|
|
||||||
whichSpy.mockImplementation(async (tool: string) => tool);
|
|
||||||
findSpy.mockImplementation(() => '');
|
findSpy.mockImplementation(() => '');
|
||||||
getJsonSpy.mockImplementation((url: string) => ({
|
});
|
||||||
result:
|
|
||||||
url === nodeVersionsManifestUrl ? nodeTestManifest : nodeTestDist
|
it('retries fetching the manifest and succeeds on a later attempt', async () => {
|
||||||
}));
|
let calls = 0;
|
||||||
dlSpy.mockImplementation(async () => '/some/temp/path');
|
getManifestSpy.mockImplementation(() => {
|
||||||
exSpy.mockImplementation(async () => '/some/other/temp/path');
|
calls++;
|
||||||
cacheSpy.mockImplementation(
|
if (calls < 2) {
|
||||||
async (
|
throw new Error('transient network failure');
|
||||||
_sourceDirectory: string,
|
|
||||||
_tool: string,
|
|
||||||
version: string,
|
|
||||||
arch: string
|
|
||||||
) => {
|
|
||||||
installedVersion = version;
|
|
||||||
return path.normalize(`/cache/node/${version}/${arch}`);
|
|
||||||
}
|
}
|
||||||
);
|
return <tc.IToolRelease[]>nodeTestManifest;
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
getExecOutputSpy.mockImplementation(async () => ({
|
getExecOutputSpy.mockImplementation(async () => ({
|
||||||
stdout: `v${installedVersion}\n`,
|
stdout: `v${path.basename(path.dirname(toolPath))}\n`,
|
||||||
stderr: '',
|
stderr: '',
|
||||||
exitCode: 0
|
exitCode: 0
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
describe.each([
|
await main.run();
|
||||||
{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;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the authenticated API without requesting raw', async () => {
|
expect(calls).toBe(2);
|
||||||
await runWithRetries();
|
expect(logSpy).toHaveBeenCalledWith('Retrying to fetch the manifest...');
|
||||||
|
expect(dbgSpy).toHaveBeenCalledWith(
|
||||||
expect(getJsonSpy).not.toHaveBeenCalled();
|
`Found LTS release '12.16.2' for Node version 'lts/erbium'`
|
||||||
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('refreshes the manifest for a later action invocation', async () => {
|
it('rejects an empty manifest as invalid and retries', async () => {
|
||||||
inputs['node-version'] = '12';
|
getManifestSpy.mockImplementation(() => []);
|
||||||
await runWithRetries();
|
|
||||||
getManifestSpy.mockClear();
|
|
||||||
|
|
||||||
const latestVersion = '12.17.0';
|
await main.run();
|
||||||
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';
|
|
||||||
|
|
||||||
await runWithRetries();
|
expect(getManifestSpy).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
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(
|
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('API: API unavailable. Raw: Raw unavailable')
|
`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.`
|
||||||
);
|
);
|
||||||
});
|
}, 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', () => {
|
describe('node version verification', () => {
|
||||||
|
|||||||
Vendored
+22
-46
@@ -98982,20 +98982,12 @@ class NightlyNodejs extends BasePrereleaseNodejs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: external "node:path"
|
|
||||||
const external_node_path_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:path");
|
|
||||||
var external_node_path_default = /*#__PURE__*/__nccwpck_require__.n(external_node_path_namespaceObject);
|
|
||||||
;// CONCATENATED MODULE: ./src/distributions/official_builds/official_builds.ts
|
;// CONCATENATED MODULE: ./src/distributions/official_builds/official_builds.ts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return Array.isArray(manifest) && manifest.length > 0;
|
|
||||||
}
|
|
||||||
class OfficialBuilds extends BaseDistribution {
|
class OfficialBuilds extends BaseDistribution {
|
||||||
constructor(nodeInfo) {
|
constructor(nodeInfo) {
|
||||||
super(nodeInfo);
|
super(nodeInfo);
|
||||||
@@ -99018,16 +99010,7 @@ class OfficialBuilds extends BaseDistribution {
|
|||||||
}
|
}
|
||||||
if (this.nodeInfo.checkLatest) {
|
if (this.nodeInfo.checkLatest) {
|
||||||
core_info('Attempt to resolve the latest version from manifest...');
|
core_info('Attempt to resolve the latest version from manifest...');
|
||||||
let resolvedVersion;
|
const resolvedVersion = await this.resolveVersionFromManifest(this.nodeInfo.versionSpec, this.nodeInfo.stable, osArch, manifest);
|
||||||
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.message);
|
|
||||||
}
|
|
||||||
if (resolvedVersion) {
|
if (resolvedVersion) {
|
||||||
this.nodeInfo.versionSpec = resolvedVersion;
|
this.nodeInfo.versionSpec = resolvedVersion;
|
||||||
core_info(`Resolved as '${resolvedVersion}'`);
|
core_info(`Resolved as '${resolvedVersion}'`);
|
||||||
@@ -99078,14 +99061,14 @@ class OfficialBuilds extends BaseDistribution {
|
|||||||
}
|
}
|
||||||
const installedDir = toolPath;
|
const installedDir = toolPath;
|
||||||
if (this.osPlat != 'win32') {
|
if (this.osPlat != 'win32') {
|
||||||
toolPath = external_node_path_default().join(toolPath, 'bin');
|
toolPath = external_path_default().join(toolPath, 'bin');
|
||||||
}
|
}
|
||||||
addPath(toolPath);
|
addPath(toolPath);
|
||||||
await this.verifyNodeVersion(installedDir);
|
await this.verifyNodeVersion(installedDir);
|
||||||
}
|
}
|
||||||
addToolPath(toolPath) {
|
addToolPath(toolPath) {
|
||||||
if (this.osPlat != 'win32') {
|
if (this.osPlat != 'win32') {
|
||||||
toolPath = external_node_path_default().join(toolPath, 'bin');
|
toolPath = external_path_default().join(toolPath, 'bin');
|
||||||
}
|
}
|
||||||
addPath(toolPath);
|
addPath(toolPath);
|
||||||
}
|
}
|
||||||
@@ -99126,44 +99109,27 @@ class OfficialBuilds extends BaseDistribution {
|
|||||||
async getManifest() {
|
async getManifest() {
|
||||||
let lastError;
|
let lastError;
|
||||||
const maxAttempts = 3;
|
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++) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
const manifest = await getManifestFromRepo('actions', 'node-versions', this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
const manifest = await getManifestFromRepo('actions', 'node-versions', this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
||||||
? this.nodeInfo.mirrorToken
|
? this.nodeInfo.mirrorToken
|
||||||
: this.nodeInfo.auth, 'main');
|
: this.nodeInfo.auth, 'main');
|
||||||
if (isValidManifest(manifest)) {
|
if (Array.isArray(manifest) && manifest.length > 0) {
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
lastError = new Error(invalidManifestMessage);
|
lastError = new Error(`The manifest fetched is empty, truncated, or does not contain any valid tool release entries.`);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (err) {
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
lastError = err instanceof Error ? err : new Error(String(err));
|
||||||
const status = error?.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}`);
|
core_debug(`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`);
|
||||||
if (attempt < maxAttempts) {
|
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)));
|
await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** (attempt - 1))); // Retry after a delay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core_debug(`GitHub API manifest fetch failed: ${lastError?.message}`);
|
throw new Error(`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`);
|
||||||
core_debug(`Getting manifest from ${nodeVersionsManifestUrl}`);
|
|
||||||
try {
|
|
||||||
const { result } = await this.httpClient.getJson(nodeVersionsManifestUrl);
|
|
||||||
if (!isValidManifest(result)) {
|
|
||||||
throw new Error(invalidManifestMessage);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
const message = 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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resolveLtsAliasFromManifest(versionSpec, stable, manifest) {
|
resolveLtsAliasFromManifest(versionSpec, stable, manifest) {
|
||||||
const alias = versionSpec.split('lts/')[1]?.toLowerCase();
|
const alias = versionSpec.split('lts/')[1]?.toLowerCase();
|
||||||
@@ -99189,6 +99155,16 @@ class OfficialBuilds extends BaseDistribution {
|
|||||||
core_debug(`Found LTS release '${release.version}' for Node version '${versionSpec}'`);
|
core_debug(`Found LTS release '${release.version}' for Node version '${versionSpec}'`);
|
||||||
return release.version.split('.')[0];
|
return release.version.split('.')[0];
|
||||||
}
|
}
|
||||||
|
async resolveVersionFromManifest(versionSpec, stable, osArch, manifest) {
|
||||||
|
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.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
async getInfoFromManifest(versionSpec, stable, osArch, manifest) {
|
async getInfoFromManifest(versionSpec, stable, osArch, manifest) {
|
||||||
let info = null;
|
let info = null;
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
@@ -99213,7 +99189,7 @@ class OfficialBuilds extends BaseDistribution {
|
|||||||
}
|
}
|
||||||
async verifyNodeVersion(installedDir) {
|
async verifyNodeVersion(installedDir) {
|
||||||
// tool-cache layout: <root>/node/<version>/<arch>
|
// tool-cache layout: <root>/node/<version>/<arch>
|
||||||
const expectedVersion = 'v' + external_node_path_default().basename(external_node_path_default().dirname(installedDir));
|
const expectedVersion = 'v' + external_path_default().basename(external_path_default().dirname(installedDir));
|
||||||
let actualVersion = '';
|
let actualVersion = '';
|
||||||
try {
|
try {
|
||||||
const { stdout } = await getExecOutput('node', ['--version'], {
|
const { stdout } = await getExecOutput('node', ['--version'], {
|
||||||
|
|||||||
Generated
+4
-4
@@ -4237,13 +4237,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/handlebars": {
|
"node_modules/handlebars": {
|
||||||
"version": "4.7.9",
|
"version": "4.7.10",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz",
|
||||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
"integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.8",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"source-map": "^0.6.1",
|
"source-map": "^0.6.1",
|
||||||
"wordwrap": "^1.0.0"
|
"wordwrap": "^1.0.0"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as exec from '@actions/exec';
|
|
||||||
import * as tc from '@actions/tool-cache';
|
import * as tc from '@actions/tool-cache';
|
||||||
|
import path from 'path';
|
||||||
|
import * as exec from '@actions/exec';
|
||||||
|
|
||||||
import BaseDistribution from '../base-distribution.js';
|
import BaseDistribution from '../base-distribution.js';
|
||||||
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models.js';
|
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models.js';
|
||||||
@@ -11,15 +10,6 @@ interface INodeRelease extends tc.IToolRelease {
|
|||||||
lts?: string;
|
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 {
|
export default class OfficialBuilds extends BaseDistribution {
|
||||||
constructor(nodeInfo: NodeInputs) {
|
constructor(nodeInfo: NodeInputs) {
|
||||||
super(nodeInfo);
|
super(nodeInfo);
|
||||||
@@ -53,20 +43,12 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
|
|
||||||
if (this.nodeInfo.checkLatest) {
|
if (this.nodeInfo.checkLatest) {
|
||||||
core.info('Attempt to resolve the latest version from manifest...');
|
core.info('Attempt to resolve the latest version from manifest...');
|
||||||
let resolvedVersion: string | undefined;
|
const resolvedVersion = await this.resolveVersionFromManifest(
|
||||||
try {
|
this.nodeInfo.versionSpec,
|
||||||
manifest ??= await this.getManifest();
|
this.nodeInfo.stable,
|
||||||
const info = await this.getInfoFromManifest(
|
osArch,
|
||||||
this.nodeInfo.versionSpec,
|
manifest
|
||||||
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) {
|
if (resolvedVersion) {
|
||||||
this.nodeInfo.versionSpec = resolvedVersion;
|
this.nodeInfo.versionSpec = resolvedVersion;
|
||||||
core.info(`Resolved as '${resolvedVersion}'`);
|
core.info(`Resolved as '${resolvedVersion}'`);
|
||||||
@@ -211,7 +193,7 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
private async getManifest(): Promise<tc.IToolRelease[]> {
|
private async getManifest(): Promise<tc.IToolRelease[]> {
|
||||||
let lastError: Error | undefined;
|
let lastError: Error | undefined;
|
||||||
const maxAttempts = 3;
|
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++) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
const manifest = await tc.getManifestFromRepo(
|
const manifest = await tc.getManifestFromRepo(
|
||||||
@@ -222,48 +204,28 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
: this.nodeInfo.auth,
|
: this.nodeInfo.auth,
|
||||||
'main'
|
'main'
|
||||||
);
|
);
|
||||||
if (isValidManifest(manifest)) {
|
if (Array.isArray(manifest) && manifest.length > 0) {
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
lastError = new Error(invalidManifestMessage);
|
lastError = new Error(
|
||||||
} catch (error) {
|
`The manifest fetched is empty, truncated, or does not contain any valid tool release entries.`
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
);
|
||||||
const status = (error as {statusCode?: number})?.statusCode;
|
} catch (err) {
|
||||||
if (status === 403 || status === 429) {
|
lastError = err instanceof Error ? err : new Error(String(err));
|
||||||
core.debug(
|
|
||||||
`GitHub API returned HTTP ${status}; trying raw manifest.`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
core.debug(
|
core.debug(
|
||||||
`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`
|
`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`
|
||||||
);
|
);
|
||||||
if (attempt < maxAttempts) {
|
if (attempt < maxAttempts) {
|
||||||
core.info('Retrying to fetch the manifest...');
|
core.info(`Retrying to fetch the manifest...`);
|
||||||
await new Promise(resolve =>
|
await new Promise(resolve =>
|
||||||
setTimeout(resolve, 1000 * 2 ** (attempt - 1))
|
setTimeout(resolve, 1000 * 2 ** (attempt - 1))
|
||||||
);
|
); // Retry after a delay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
core.debug(`GitHub API manifest fetch failed: ${lastError?.message}`);
|
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${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(
|
private resolveLtsAliasFromManifest(
|
||||||
@@ -310,6 +272,26 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
return release.version.split('.')[0];
|
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(
|
private async getInfoFromManifest(
|
||||||
versionSpec: string,
|
versionSpec: string,
|
||||||
stable: boolean,
|
stable: boolean,
|
||||||
|
|||||||
Reference in new issue
Block a user