Compare commits

..
Author SHA1 Message Date
dependabot[bot] 0265558534 Bump prettier from 3.9.6 to 3.9.9
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.6 to 3.9.9.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.9)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-12 20:55:47 +00:00
6 changed files with 99 additions and 386 deletions

No files matched your search

+1 -5
View File
@@ -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:
+39 -280
View File
@@ -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'); }, 10000);
expect(setFailedSpy).not.toHaveBeenCalled();
});
it.each([403, 429])( it('rejects an empty manifest as invalid and retries', async () => {
'falls back to raw immediately after API HTTP %i', getManifestSpy.mockImplementation(() => []);
async statusCode => {
getManifestSpy.mockImplementation(() => {
throw Object.assign(new Error('API unavailable'), {statusCode});
});
const startedAt = Date.now();
await runWithRetries(); await main.run();
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'
);
}
);
});
it('refreshes the manifest for a later action invocation', async () => {
inputs['node-version'] = '12';
await runWithRetries();
getManifestSpy.mockClear();
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';
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(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(setFailedSpy).toHaveBeenCalledWith(
expect.stringContaining('API: API unavailable. Raw: The manifest') `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.`
);
}
);
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.stringContaining('API: API unavailable. Raw: Raw unavailable')
);
});
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');
}
); );
}, 10000);
}); });
describe('node version verification', () => { describe('node version verification', () => {
+22 -46
View File
@@ -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) {
lastError = error instanceof Error ? error : new Error(String(error));
const status = error?.statusCode;
if (status === 403 || status === 429) {
core_debug(`GitHub API returned HTTP ${status}; trying raw manifest.`);
break;
} }
catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
} }
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'], {
+4 -4
View File
@@ -35,7 +35,7 @@
"globals": "^17.7.0", "globals": "^17.7.0",
"jest": "^30.4.2", "jest": "^30.4.2",
"jest-each": "^30.5.2", "jest-each": "^30.5.2",
"prettier": "^3.9.6", "prettier": "^3.9.9",
"ts-jest": "^29.4.11", "ts-jest": "^29.4.11",
"typescript": "^6.0.3" "typescript": "^6.0.3"
}, },
@@ -5910,9 +5910,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.9.6", "version": "3.9.9",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
+1 -1
View File
@@ -58,7 +58,7 @@
"globals": "^17.7.0", "globals": "^17.7.0",
"jest": "^30.4.2", "jest": "^30.4.2",
"jest-each": "^30.5.2", "jest-each": "^30.5.2",
"prettier": "^3.9.6", "prettier": "^3.9.9",
"ts-jest": "^29.4.11", "ts-jest": "^29.4.11",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }
@@ -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 {
manifest ??= await this.getManifest();
const info = await this.getInfoFromManifest(
this.nodeInfo.versionSpec, this.nodeInfo.versionSpec,
this.nodeInfo.stable, this.nodeInfo.stable,
osArch, osArch,
manifest 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,49 +204,29 @@ 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;
if (status === 403 || status === 429) {
core.debug(
`GitHub API returned HTTP ${status}; trying raw manifest.`
); );
break; } catch (err) {
} lastError = err instanceof Error ? err : new Error(String(err));
} }
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
} }
} }
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( throw new Error(
`Failed to fetch a valid manifest from the GitHub API and raw URL. API: ${lastError?.message}. Raw: ${message}`, `Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
{cause: error}
); );
} }
}
private resolveLtsAliasFromManifest( private resolveLtsAliasFromManifest(
versionSpec: string, versionSpec: string,
@@ -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,