mirror of
https://github.com/actions/setup-node.git
synced 2026-07-15 20:31:40 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 363be39a57 |
@@ -258,7 +258,11 @@ describe('setup-node', () => {
|
||||
|
||||
// @actions/exec
|
||||
getExecOutputSpy = exec.getExecOutput as jest.Mock;
|
||||
getExecOutputSpy.mockImplementation(() => 'v16.15.0');
|
||||
getExecOutputSpy.mockImplementation(async () => ({
|
||||
stdout: 'v16.15.0',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -379,6 +383,16 @@ describe('setup-node', () => {
|
||||
whichSpy.mockImplementation((cmd: any) => {
|
||||
return `some/${cmd}/path`;
|
||||
});
|
||||
getExecOutputSpy.mockImplementation(async (cmd: string) => ({
|
||||
stdout:
|
||||
cmd === 'node'
|
||||
? `v${resolvedVersion}`
|
||||
: cmd === 'npm'
|
||||
? '11.12.1'
|
||||
: '4.17.1',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}));
|
||||
|
||||
await main.run();
|
||||
|
||||
@@ -756,7 +770,7 @@ describe('setup-node', () => {
|
||||
`Attempting to download ${versionSpec}...`
|
||||
);
|
||||
expect(addPathSpy).toHaveBeenCalledWith(expPath);
|
||||
});
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('LTS version', () => {
|
||||
@@ -924,8 +938,10 @@ describe('setup-node', () => {
|
||||
expect(dbgSpy).toHaveBeenCalledWith(
|
||||
'Getting manifest from actions/node-versions@main'
|
||||
);
|
||||
expect(setFailedSpy).toHaveBeenCalledWith('Unable to download manifest');
|
||||
});
|
||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||
`Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest`
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('latest alias syntax', () => {
|
||||
@@ -947,10 +963,13 @@ describe('setup-node', () => {
|
||||
await main.run();
|
||||
|
||||
// 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...');
|
||||
}
|
||||
},
|
||||
10000
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1020,4 +1039,91 @@ describe('setup-node', () => {
|
||||
);
|
||||
}
|
||||
}, 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(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.`
|
||||
);
|
||||
}, 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: 'v22.22.3\n',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}));
|
||||
|
||||
await main.run();
|
||||
|
||||
expect(setFailedSpy).toHaveBeenCalledWith(
|
||||
`Node v12.16.1 installation failed, likely due to an incomplete or corrupted download.`
|
||||
);
|
||||
});
|
||||
|
||||
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(setFailedSpy).toHaveBeenCalledWith(
|
||||
`Node installation failed. Node may not be installed or not on PATH: node not found`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+47
-5
@@ -99439,6 +99439,7 @@ class NightlyNodejs extends BasePrereleaseNodejs {
|
||||
|
||||
|
||||
|
||||
|
||||
class OfficialBuilds extends BaseDistribution {
|
||||
constructor(nodeInfo) {
|
||||
super(nodeInfo);
|
||||
@@ -99473,7 +99474,9 @@ class OfficialBuilds extends BaseDistribution {
|
||||
let toolPath = this.findVersionInHostedToolCacheDirectory();
|
||||
if (toolPath) {
|
||||
core_info(`Found in cache @ ${toolPath}`);
|
||||
const installedDir = toolPath;
|
||||
this.addToolPath(toolPath);
|
||||
await this.verifyNodeVersion(installedDir);
|
||||
return;
|
||||
}
|
||||
let downloadPath = '';
|
||||
@@ -99508,10 +99511,12 @@ class OfficialBuilds extends BaseDistribution {
|
||||
if (!toolPath) {
|
||||
toolPath = await this.downloadDirectlyFromNode();
|
||||
}
|
||||
const installedDir = toolPath;
|
||||
if (this.osPlat != 'win32') {
|
||||
toolPath = external_path_default().join(toolPath, 'bin');
|
||||
}
|
||||
addPath(toolPath);
|
||||
await this.verifyNodeVersion(installedDir);
|
||||
}
|
||||
addToolPath(toolPath) {
|
||||
if (this.osPlat != 'win32') {
|
||||
@@ -99553,11 +99558,30 @@ class OfficialBuilds extends BaseDistribution {
|
||||
const url = mirror || 'https://nodejs.org';
|
||||
return `${url}/dist`;
|
||||
}
|
||||
getManifest() {
|
||||
core_debug('Getting manifest from actions/node-versions@main');
|
||||
return getManifestFromRepo('actions', 'node-versions', this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
||||
? this.nodeInfo.mirrorToken
|
||||
: this.nodeInfo.auth, 'main');
|
||||
async getManifest() {
|
||||
let lastError;
|
||||
const maxAttempts = 3;
|
||||
core_debug(`Getting manifest from actions/node-versions@main`);
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const manifest = await getManifestFromRepo('actions', '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 * 2 ** (attempt - 1))); // Retry after a delay
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`);
|
||||
}
|
||||
resolveLtsAliasFromManifest(versionSpec, stable, manifest) {
|
||||
const alias = versionSpec.split('lts/')[1]?.toLowerCase();
|
||||
@@ -99615,6 +99639,24 @@ class OfficialBuilds extends BaseDistribution {
|
||||
isLatestSyntax(versionSpec) {
|
||||
return ['current', 'latest', 'node'].includes(versionSpec);
|
||||
}
|
||||
async verifyNodeVersion(installedDir) {
|
||||
// tool-cache layout: <root>/node/<version>/<arch>
|
||||
const expectedVersion = 'v' + external_path_default().basename(external_path_default().dirname(installedDir));
|
||||
let actualVersion = '';
|
||||
try {
|
||||
const { stdout } = await 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.message}`, { cause: err });
|
||||
}
|
||||
if (actualVersion !== expectedVersion) {
|
||||
core_debug(`Node installation failed: expected ${expectedVersion} but "node --version" reported ${actualVersion || '(empty)'} (installedDir: ${installedDir}).`);
|
||||
throw new Error(`Node ${expectedVersion} installation failed, likely due to an incomplete or corrupted download.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
;// CONCATENATED MODULE: ./src/distributions/rc/rc_builds.ts
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
import path from 'path';
|
||||
import * as exec from '@actions/exec';
|
||||
|
||||
import BaseDistribution from '../base-distribution.js';
|
||||
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models.js';
|
||||
@@ -62,7 +63,9 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
|
||||
if (toolPath) {
|
||||
core.info(`Found in cache @ ${toolPath}`);
|
||||
const installedDir = toolPath;
|
||||
this.addToolPath(toolPath);
|
||||
await this.verifyNodeVersion(installedDir);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,11 +126,13 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
toolPath = await this.downloadDirectlyFromNode();
|
||||
}
|
||||
|
||||
const installedDir = toolPath;
|
||||
if (this.osPlat != 'win32') {
|
||||
toolPath = path.join(toolPath, 'bin');
|
||||
}
|
||||
|
||||
core.addPath(toolPath);
|
||||
await this.verifyNodeVersion(installedDir);
|
||||
}
|
||||
|
||||
protected addToolPath(toolPath: string) {
|
||||
@@ -185,15 +190,41 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
return `${url}/dist`;
|
||||
}
|
||||
|
||||
private getManifest(): Promise<tc.IToolRelease[]> {
|
||||
core.debug('Getting manifest from actions/node-versions@main');
|
||||
return tc.getManifestFromRepo(
|
||||
'actions',
|
||||
'node-versions',
|
||||
this.nodeInfo.mirror && this.nodeInfo.mirrorToken
|
||||
? this.nodeInfo.mirrorToken
|
||||
: this.nodeInfo.auth,
|
||||
'main'
|
||||
private async getManifest(): Promise<tc.IToolRelease[]> {
|
||||
let lastError: Error | undefined;
|
||||
const maxAttempts = 3;
|
||||
core.debug(`Getting manifest from actions/node-versions@main`);
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const manifest = await tc.getManifestFromRepo(
|
||||
'actions',
|
||||
'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 * 2 ** (attempt - 1))
|
||||
); // Retry after a delay
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,4 +329,30 @@ export default class OfficialBuilds extends BaseDistribution {
|
||||
private isLatestSyntax(versionSpec): boolean {
|
||||
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}`,
|
||||
{cause: err}
|
||||
);
|
||||
}
|
||||
if (actualVersion !== expectedVersion) {
|
||||
core.debug(
|
||||
`Node installation failed: expected ${expectedVersion} but "node --version" reported ${actualVersion || '(empty)'} (installedDir: ${installedDir}).`
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Node ${expectedVersion} installation failed, likely due to an incomplete or corrupted download.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user