Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 105 additions & 6 deletions __tests__/official-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ describe('setup-node', () => {

// @actions/exec
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation(() => 'v16.15.0');
getExecOutputSpy.mockImplementation(async () => ({
stdout: 'v16.15.0',
stderr: '',
exitCode: 0
}));
});

afterEach(() => {
Expand Down Expand Up @@ -250,6 +254,11 @@ describe('setup-node', () => {
whichSpy.mockImplementation(cmd => {
return `some/${cmd}/path`;
});
getExecOutputSpy.mockImplementation(async (cmd: string) => ({
stdout: cmd === 'node' ? `v${resolvedVersion}` : '1.0.0',
stderr: '',
exitCode: 0
}));

await main.run();

Expand Down Expand Up @@ -627,7 +636,7 @@ describe('setup-node', () => {
`Attempting to download ${versionSpec}...`
);
expect(cnSpy).toHaveBeenCalledWith(`::add-path::${expPath}${osm.EOL}`);
});
}, 10000);
});

describe('LTS version', () => {
Expand Down Expand Up @@ -800,9 +809,9 @@ describe('setup-node', () => {
'Getting manifest from actions/node-versions@main'
);
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', () => {
Expand All @@ -824,10 +833,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
);
});

Expand Down Expand Up @@ -898,4 +910,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(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}`
);
});
});
});
53 changes: 48 additions & 5 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57847,6 +57847,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(37484));
const tc = __importStar(__nccwpck_require__(33472));
const path_1 = __importDefault(__nccwpck_require__(16928));
const exec = __importStar(__nccwpck_require__(95236));
const base_distribution_1 = __importDefault(__nccwpck_require__(60709));
class OfficialBuilds extends base_distribution_1.default {
constructor(nodeInfo) {
Expand Down Expand Up @@ -57882,7 +57883,9 @@ class OfficialBuilds extends base_distribution_1.default {
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 = '';
Expand Down Expand Up @@ -57917,10 +57920,12 @@ class OfficialBuilds extends base_distribution_1.default {
if (!toolPath) {
toolPath = await this.downloadDirectlyFromNode();
}
const installedDir = toolPath;
if (this.osPlat != 'win32') {
toolPath = path_1.default.join(toolPath, 'bin');
}
core.addPath(toolPath);
await this.verifyNodeVersion(installedDir);
}
addToolPath(toolPath) {
if (this.osPlat != 'win32') {
Expand Down Expand Up @@ -57962,11 +57967,30 @@ class OfficialBuilds extends base_distribution_1.default {
const url = mirror || 'https://nodejs.org';
return `${url}/dist`;
}
getManifest() {
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');
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 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 * attempt)); // 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();
Expand Down Expand Up @@ -58024,6 +58048,25 @@ class OfficialBuilds extends base_distribution_1.default {
isLatestSyntax(versionSpec) {
return ['current', 'latest', 'node'].includes(versionSpec);
}
async verifyNodeVersion(installedDir) {
// tool-cache layout: <root>/node/<version>/<arch>
const expectedVersion = 'v' + path_1.default.basename(path_1.default.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.message}`);
}
core.debug(`Expected Node version: ${expectedVersion}`);
core.debug(`Actual Node version: ${actualVersion}`);
if (actualVersion !== expectedVersion) {
throw new Error(`Node ${expectedVersion} installation failed, likely due to an incomplete or corrupted download.`);
}
}
}
exports["default"] = OfficialBuilds;

Expand Down
70 changes: 61 additions & 9 deletions src/distributions/official_builds/official_builds.ts
Original file line number Diff line number Diff line change
@@ -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';
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -185,15 +190,39 @@ 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 * attempt)); // Retry after a delay
}
}
throw new Error(
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
);
}

Expand Down Expand Up @@ -298,4 +327,27 @@ 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}`
);
}
core.debug(`Expected Node version: ${expectedVersion}`);
core.debug(`Actual Node version: ${actualVersion}`);
if (actualVersion !== expectedVersion) {
throw new Error(
`Node ${expectedVersion} installation failed, likely due to an incomplete or corrupted download.`
);
}
}
}
Loading