mirror of
https://github.com/actions/setup-node.git
synced 2026-07-28 00:05:48 +00:00
Add Node version validation and manifest fetch retry
This commit is contained in:
parent
04be95cf35
commit
349d704a2a
@ -129,7 +129,11 @@ describe('setup-node', () => {
|
|||||||
|
|
||||||
// @actions/exec
|
// @actions/exec
|
||||||
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
|
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
|
||||||
getExecOutputSpy.mockImplementation(() => 'v16.15.0');
|
getExecOutputSpy.mockImplementation(async () => ({
|
||||||
|
stdout: 'v16.15.0',
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -250,6 +254,11 @@ describe('setup-node', () => {
|
|||||||
whichSpy.mockImplementation(cmd => {
|
whichSpy.mockImplementation(cmd => {
|
||||||
return `some/${cmd}/path`;
|
return `some/${cmd}/path`;
|
||||||
});
|
});
|
||||||
|
getExecOutputSpy.mockImplementation(async (cmd: string) => ({
|
||||||
|
stdout: cmd === 'node' ? `v${resolvedVersion}` : '1.0.0',
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0
|
||||||
|
}));
|
||||||
|
|
||||||
await main.run();
|
await main.run();
|
||||||
|
|
||||||
@ -627,7 +636,7 @@ describe('setup-node', () => {
|
|||||||
`Attempting to download ${versionSpec}...`
|
`Attempting to download ${versionSpec}...`
|
||||||
);
|
);
|
||||||
expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
|
expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
|
||||||
});
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LTS version', () => {
|
describe('LTS version', () => {
|
||||||
@ -800,9 +809,9 @@ describe('setup-node', () => {
|
|||||||
'Getting manifest from actions/node-versions@main'
|
'Getting manifest from actions/node-versions@main'
|
||||||
);
|
);
|
||||||
expect(cnSpy).toHaveBeenCalledWith(
|
expect(cnSpy).toHaveBeenCalledWith(
|
||||||
`::error::Unable to download manifest${osm.EOL}`
|
`::error::Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest${osm.EOL}`
|
||||||
);
|
);
|
||||||
});
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('latest alias syntax', () => {
|
describe('latest alias syntax', () => {
|
||||||
@ -824,10 +833,13 @@ describe('setup-node', () => {
|
|||||||
await main.run();
|
await main.run();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(logSpy).toHaveBeenCalledWith('Unable to download manifest');
|
expect(logSpy).toHaveBeenCalledWith(
|
||||||
|
'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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -898,4 +910,91 @@ describe('setup-node', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 100000);
|
}, 100000);
|
||||||
|
|
||||||
|
describe('manifest retry and validation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
os.platform = 'linux';
|
||||||
|
os.arch = 'x64';
|
||||||
|
inputs['node-version'] = 'lts/erbium';
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => ({
|
||||||
|
stdout: `v${path.basename(path.dirname(toolPath))}\n`,
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
await main.run();
|
||||||
|
|
||||||
|
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'`
|
||||||
|
);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
it('rejects an empty manifest as invalid and retries', async () => {
|
||||||
|
getManifestSpy.mockImplementation(() => []);
|
||||||
|
|
||||||
|
await main.run();
|
||||||
|
|
||||||
|
expect(getManifestSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(cnSpy).toHaveBeenCalledWith(
|
||||||
|
`::error::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.${osm.EOL}`
|
||||||
|
);
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('node version verification', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
os.platform = 'linux';
|
||||||
|
os.arch = 'x64';
|
||||||
|
inputs['node-version'] = '12';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when the installed node version does not match the expected version', async () => {
|
||||||
|
const toolPath = path.normalize('/cache/node/12.16.1/x64');
|
||||||
|
findSpy.mockReturnValue(toolPath);
|
||||||
|
getExecOutputSpy.mockImplementation(async () => ({
|
||||||
|
stdout: 'v10.0.0\n',
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
await main.run();
|
||||||
|
|
||||||
|
expect(cnSpy).toHaveBeenCalledWith(
|
||||||
|
`::error::Node v12.16.1 installation failed, likely due to an incomplete or corrupted download.${osm.EOL}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when the node executable cannot be invoked', async () => {
|
||||||
|
const toolPath = path.normalize('/cache/node/12.16.1/x64');
|
||||||
|
findSpy.mockReturnValue(toolPath);
|
||||||
|
getExecOutputSpy.mockImplementation(async () => {
|
||||||
|
throw new Error('node not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
await main.run();
|
||||||
|
|
||||||
|
expect(cnSpy).toHaveBeenCalledWith(
|
||||||
|
`::error::Node installation failed. Node may not be installed or not on PATH: node not found${osm.EOL}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
474
dist/cache-save/index.js
vendored
474
dist/cache-save/index.js
vendored
File diff suppressed because one or more lines are too long
24402
dist/setup/index.js
vendored
24402
dist/setup/index.js
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
|||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as tc from '@actions/tool-cache';
|
import * as tc from '@actions/tool-cache';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import * as exec from '@actions/exec';
|
||||||
|
|
||||||
import BaseDistribution from '../base-distribution';
|
import BaseDistribution from '../base-distribution';
|
||||||
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models';
|
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models';
|
||||||
@ -62,7 +63,9 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
|
|
||||||
if (toolPath) {
|
if (toolPath) {
|
||||||
core.info(`Found in cache @ ${toolPath}`);
|
core.info(`Found in cache @ ${toolPath}`);
|
||||||
|
const installedDir = toolPath;
|
||||||
this.addToolPath(toolPath);
|
this.addToolPath(toolPath);
|
||||||
|
await this.verifyNodeVersion(installedDir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +126,13 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
toolPath = await this.downloadDirectlyFromNode();
|
toolPath = await this.downloadDirectlyFromNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const installedDir = toolPath;
|
||||||
if (this.osPlat != 'win32') {
|
if (this.osPlat != 'win32') {
|
||||||
toolPath = path.join(toolPath, 'bin');
|
toolPath = path.join(toolPath, 'bin');
|
||||||
}
|
}
|
||||||
|
|
||||||
core.addPath(toolPath);
|
core.addPath(toolPath);
|
||||||
|
await this.verifyNodeVersion(installedDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addToolPath(toolPath: string) {
|
protected addToolPath(toolPath: string) {
|
||||||
@ -185,15 +190,39 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
return `${url}/dist`;
|
return `${url}/dist`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getManifest(): Promise<tc.IToolRelease[]> {
|
private async getManifest(): Promise<tc.IToolRelease[]> {
|
||||||
core.debug('Getting manifest from actions/node-versions@main');
|
let lastError: Error | undefined;
|
||||||
return tc.getManifestFromRepo(
|
const maxAttempts = 3;
|
||||||
'actions',
|
core.debug(`Getting manifest from actions/node-versions@main`);
|
||||||
'node-versions',
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
try {
|
||||||
? this.nodeInfo.mirrorToken
|
const manifest = await tc.getManifestFromRepo(
|
||||||
: this.nodeInfo.auth,
|
'actions',
|
||||||
'main'
|
'node-versions',
|
||||||
|
this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
||||||
|
? this.nodeInfo.mirrorToken
|
||||||
|
: this.nodeInfo.auth,
|
||||||
|
'main'
|
||||||
|
);
|
||||||
|
if (Array.isArray(manifest) && manifest.length > 0) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
core.debug(
|
||||||
|
`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`
|
||||||
|
);
|
||||||
|
if (attempt < maxAttempts) {
|
||||||
|
core.info(`Retrying to fetch the manifest...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Retry after a delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,4 +327,25 @@ export default class OfficialBuilds extends BaseDistribution {
|
|||||||
private isLatestSyntax(versionSpec): boolean {
|
private isLatestSyntax(versionSpec): boolean {
|
||||||
return ['current', 'latest', 'node'].includes(versionSpec);
|
return ['current', 'latest', 'node'].includes(versionSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async verifyNodeVersion(installedDir: string) {
|
||||||
|
// tool-cache layout: <root>/node/<version>/<arch>
|
||||||
|
const expectedVersion = 'v' + path.basename(path.dirname(installedDir));
|
||||||
|
let actualVersion = '';
|
||||||
|
try {
|
||||||
|
const {stdout} = await exec.getExecOutput('node', ['--version'], {
|
||||||
|
silent: true
|
||||||
|
});
|
||||||
|
actualVersion = stdout.trim();
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Node installation failed. Node may not be installed or not on PATH: ${(err as Error).message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (actualVersion !== expectedVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`Node ${expectedVersion} installation failed, likely due to an incomplete or corrupted download.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user