Compare commits

...
Author SHA1 Message Date
dependabot[bot] 436c65619b Bump handlebars from 4.7.9 to 4.7.10 (#1648)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.9 to 4.7.10.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.10/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.9...v4.7.10)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-13 12:16:28 -06:00
Ruben Bridgewater 101b347414 fix: avoid GitHub API rate limits for the Node manifest (#1627)
* fix: avoid GitHub API rate limits for the Node manifest

Large job matrices exhaust the shared GITHUB_TOKEN rate limit because each cold manifest lookup resolves a repository tree and blob through the REST API.

Use the fixed raw manifest and reuse it within a job. check-latest always refreshes it, and the authenticated API remains a fallback.

* fix: preserve check-latest manifest freshness

Raw manifest responses can remain cached for five minutes, so check-latest keeps the GitHub API path while normal setup steps reuse the job manifest.

* fix(manifest): use authenticated API with raw fallback

LTS resolution with check-latest cannot recover from API failures because it
bypasses raw retrieval. The disk cache cannot reduce requests across matrix
jobs and introduces unneeded state.
2026-11-03 20:27:46 -06:00
dependabot[bot]andmahabaleshwars 949feb2413 Bump eslint from 10.5.0 to 10.11.0 and package.json from 7.0.0 to 7.1.0 (#1635)
* Bump eslint from 10.5.0 to 10.11.0

Bumps [eslint](https://github.com/eslint/eslint) from 10.5.0 to 10.11.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.11.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump version from 7.0.0 to 7.1.0 in package.json and package-lock.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
2026-10-07 21:43:26 -05:00
HarithaandHarithaVattikuti 7207c4f118 chore: update dependencies and add license files (#1642)
* chore: update dependencies and add license files

- Bump @actions/cache from 6.2.0 to 6.3.0 in package.json and package-lock.json
- Update @actions/glob from 0.6.1 to 0.7.0 in package-lock.json
- Update brace-expansion from 5.0.9 to 5.0.12 in package.json and package-lock.json
- Bump undici from 6.28.0 to 6.29.0 in package-lock.json
- Add license files for @actions/glob and minimatch dependencies

* chore: update minimatch dependency file

---------

Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com>
2026-10-07 21:39:15 -05:00
dependabot[bot] 54337e0d02 Bump eslint-plugin-n from 18.1.0 to 18.3.0 (#1638)
Bumps [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n) from 18.1.0 to 18.3.0.
- [Release notes](https://github.com/eslint-community/eslint-plugin-n/releases)
- [Changelog](https://github.com/eslint-community/eslint-plugin-n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint-community/eslint-plugin-n/compare/v18.1.0...v18.3.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-n
  dependency-version: 18.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 14:45:08 -05:00
dependabot[bot] fb54fa8c14 Bump jest-each from 30.4.1 to 30.5.2 (#1636)
Bumps [jest-each](https://github.com/jestjs/jest/tree/HEAD/packages/jest-each) from 30.4.1 to 30.5.2.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest-each)

---
updated-dependencies:
- dependency-name: jest-each
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 14:44:20 -05:00
dependabot[bot] 7f84f1d52d Bump pnpm/action-setup from 6.0.10 to 6.1.0 (#1631)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 6.0.10 to 6.1.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0977fd99725f1db4007ccb2928dbb4e90d06cc86...ea17c68df8912ef543352723c149a84f56e3d413)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-25 16:05:28 -05:00
dependabot[bot]andHarithaVattikuti 0c54c7b074 Bump @typescript-eslint/parser from 8.62.0 to 8.70.0 (#1615)
* Bump @typescript-eslint/parser from 8.62.0 to 8.70.0

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.62.0 to 8.70.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update minimatch dependency to version 10.2.6 and adjust module paths

* Fix casing for BlueOak license in .licensed.yml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com>
2026-09-24 12:32:40 -05:00
dependabot[bot] a829419004 Bump @types/semver from 7.7.1 to 7.8.0 (#1616)
Bumps [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) from 7.7.1 to 7.8.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

---
updated-dependencies:
- dependency-name: "@types/semver"
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-24 12:31:28 -05:00
v-mahabaleshwarsandmahabaleshwars 680d1e489b Support absolute paths in node-version-file (#1633)
* path.resolve for composite actions

* log and documentation update

---------

Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
2026-09-21 15:25:01 -05:00
dependabot[bot]andpriyagupta108 db0fb3a98e Bump baseline-browser-mapping from 2.10.38 to 2.11.22 (#1629)
* Bump baseline-browser-mapping from 2.10.38 to 2.11.22

Bumps [baseline-browser-mapping](https://github.com/web-platform-dx/baseline-browser-mapping) from 2.10.38 to 2.11.22.
- [Release notes](https://github.com/web-platform-dx/baseline-browser-mapping/releases)
- [Commits](https://github.com/web-platform-dx/baseline-browser-mapping/compare/v2.10.38...v2.11.22)

---
updated-dependencies:
- dependency-name: baseline-browser-mapping
  dependency-version: 2.11.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump dependencies in package-lock.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: priyagupta108 <priyagupta108@github.com>
2026-09-16 12:34:52 -05:00
dependabot[bot] 94196ee1d1 Bump pnpm/action-setup from 6.0.9 to 6.0.10 (#1602)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 6.0.9 to 6.0.10.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0ebf47130e4866e96fce0953f49152a61190b271...0977fd99725f1db4007ccb2928dbb4e90d06cc86)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 12:13:52 -05:00
dependabot[bot]andgowridurgad d1fcc24e82 Bump @vercel/ncc from 0.44.0 to 0.45.0 (#1610)
* Bump @vercel/ncc from 0.44.0 to 0.45.0

Bumps [@vercel/ncc](https://github.com/vercel/ncc) from 0.44.0 to 0.45.0.
- [Release notes](https://github.com/vercel/ncc/releases)
- [Commits](https://github.com/vercel/ncc/compare/0.44.0...0.45.0)

---
updated-dependencies:
- dependency-name: "@vercel/ncc"
  dependency-version: 0.45.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* rebuild bundle after ncc bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gowridurgad <gowridurgad@gmail.com>
2026-08-25 11:58:12 -05:00
19 changed files with 7335 additions and 10698 deletions

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
- name: Generate pnpm file
+1 -1
View File
@@ -10,7 +10,7 @@ allowed:
- mit
- cc0-1.0
- unlicense
- blueoak-1.0.0
- BlueOak-1.0.0
reviewed:
npm:
Binary file not shown.
Binary file not shown.
File renamed without changes.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+5 -1
View File
@@ -233,7 +233,11 @@ jobs:
## Using `setup-node` on GHES
`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.
`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.
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:
+32
View File
@@ -177,6 +177,10 @@ describe('main tests', () => {
);
});
afterEach(() => {
jest.restoreAllMocks();
});
each`
contents | expected
${'12'} | ${'12'}
@@ -315,11 +319,39 @@ describe('main tests', () => {
// Assert
expect(getNodeVersionFromFileSpy).toHaveBeenCalled();
// A relative input is still resolved against the workspace.
expect(getNodeVersionFromFileSpy).toHaveBeenCalledWith(
path.join(process.env['GITHUB_WORKSPACE']!, '.nvmrc')
);
expect(infoSpy).toHaveBeenCalledWith(
`Resolved ${inputs['node-version-file']} as ${expectedVersionSpec}`
);
}, 10000);
it('reads node-version-file given as an absolute path outside the workspace', async () => {
// Arrange: a composite action passes `${{ github.action_path }}/.nvmrc`,
// which is absolute and may sit outside GITHUB_WORKSPACE.
// The workspace deliberately points at a different existing directory,
// so the absolute version file lies outside it.
const workspace = path.join(__dirname, 'mock');
process.env['GITHUB_WORKSPACE'] = workspace;
const versionFilePath = path.join(__dirname, 'data', '.nvmrc');
// Guard the premise: the version file lives outside the workspace.
expect(path.relative(workspace, versionFilePath).startsWith('..')).toBe(
true
);
inputs['node-version-file'] = versionFilePath;
// Act
await main.run();
// Assert: the path is used as provided and the real file is read.
// The expected `24` comes from `__tests__/data/.nvmrc` (`v24`).
expect(getNodeVersionFromFileSpy).toHaveBeenCalledWith(versionFilePath);
expect(infoSpy).toHaveBeenCalledWith(`Resolved ${versionFilePath} as 24`);
expect(core.setFailed as jest.Mock).not.toHaveBeenCalled();
}, 10000);
it('should throw an error if node-version-file is not accessible', async () => {
// Arrange
inputs['node-version-file'] = 'non-existing-file';
+280 -39
View File
@@ -14,6 +14,8 @@ import osm from 'os';
import path from 'path';
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
jest.unstable_mockModule('@actions/core', () => ({
@@ -129,6 +131,21 @@ const {default: nodeV8CanaryTestDist} = await import(
import type {INodeVersion} from '../src/distributions/base-models.js';
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', () => {
let build: InstanceType<typeof OfficialBuilds>;
let inputs = {} as any;
@@ -233,7 +250,9 @@ describe('setup-node', () => {
getJsonSpy.mockImplementation((url: any) => {
let res: any;
if (url.includes('/rc')) {
if (url === nodeVersionsManifestUrl) {
throw new Error('Unable to download raw manifest');
} else if (url.includes('/rc')) {
res = <INodeVersion[]>nodeTestDistRc;
} else if (url.includes('/nightly')) {
res = <INodeVersion[]>nodeTestDistNightly;
@@ -649,7 +668,7 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
@@ -677,7 +696,7 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
@@ -716,7 +735,7 @@ describe('setup-node', () => {
expect(logSpy).toHaveBeenCalledWith(
'Attempt to resolve the latest version from manifest...'
);
expect(dbgSpy).toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).not.toHaveBeenCalledWith('No manifest cached');
expect(dbgSpy).toHaveBeenCalledWith(
'Getting manifest from actions/node-versions@main'
);
@@ -774,6 +793,8 @@ describe('setup-node', () => {
});
describe('LTS version', () => {
useRetryTimers();
beforeEach(() => {
os.platform = 'linux';
os.arch = 'x64';
@@ -929,7 +950,7 @@ describe('setup-node', () => {
});
// act
await main.run();
await runWithRetries();
// assert
expect(logSpy).toHaveBeenCalledWith(
@@ -939,12 +960,14 @@ describe('setup-node', () => {
'Getting manifest from actions/node-versions@main'
);
expect(setFailedSpy).toHaveBeenCalledWith(
`Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest`
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
);
}, 10000);
});
});
describe('latest alias syntax', () => {
useRetryTimers();
it.each(['latest', 'current', 'node'])(
'download the %s version if alias is provided',
async inputVersion => {
@@ -960,16 +983,15 @@ describe('setup-node', () => {
});
// Act
await main.run();
await runWithRetries();
// assert
expect(logSpy).toHaveBeenCalledWith(
'Failed to fetch a valid manifest after 3 attempts. Last error: Unable to download manifest'
'Failed to fetch a valid manifest from the GitHub API and raw URL. API: Unable to download manifest. Raw: Unable to download raw manifest'
);
expect(logSpy).toHaveBeenCalledWith('getting latest node version...');
},
10000
}
);
});
@@ -1040,53 +1062,272 @@ describe('setup-node', () => {
}
}, 100000);
describe('manifest retry and validation', () => {
describe('manifest retrieval', () => {
useRetryTimers();
let installedVersion: string;
beforeEach(() => {
installedVersion = '12.16.2';
os.platform = 'linux';
os.arch = 'x64';
inputs['node-version'] = 'lts/erbium';
inputs['token'] = 'manifest-token';
whichSpy.mockImplementation(async (tool: string) => tool);
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;
});
getJsonSpy.mockImplementation((url: string) => ({
result:
url === nodeVersionsManifestUrl ? nodeTestManifest : nodeTestDist
}));
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);
cacheSpy.mockImplementation(
async (
_sourceDirectory: string,
_tool: string,
version: string,
arch: string
) => {
installedVersion = version;
return path.normalize(`/cache/node/${version}/${arch}`);
}
);
getExecOutputSpy.mockImplementation(async () => ({
stdout: `v${path.basename(path.dirname(toolPath))}\n`,
stdout: `v${installedVersion}\n`,
stderr: '',
exitCode: 0
}));
});
await main.run();
describe.each([
{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;
});
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'`
it('uses the authenticated API without requesting raw', async () => {
await runWithRetries();
expect(getJsonSpy).not.toHaveBeenCalled();
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('rejects an empty manifest as invalid and retries', async () => {
getManifestSpy.mockImplementation(() => []);
it('refreshes the manifest for a later action invocation', async () => {
inputs['node-version'] = '12';
await runWithRetries();
getManifestSpy.mockClear();
await main.run();
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';
expect(getManifestSpy).toHaveBeenCalledTimes(3);
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(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(
`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.`
expect.stringContaining('API: API unavailable. Raw: Raw unavailable')
);
}, 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', () => {
+3150 -5148
View File
File diff suppressed because it is too large. Load diff
+3283 -5255
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -79,7 +79,7 @@ steps:
The `node-version-file` input accepts a path to a file containing the version of Node.js to be used by a project, for example `.nvmrc`, `.node-version`, `.tool-versions`, `mise.toml`, or `package.json`. If both the `node-version` and the `node-version-file` inputs are provided then the `node-version` input is used.
See [supported version syntax](https://github.com/actions/setup-node#supported-version-syntax).
> The action will search for the node version file relative to the repository root.
> The action resolves a relative `node-version-file` path against `GITHUB_WORKSPACE`, which is the repository root by default. If the input is a full absolute path, the action uses that path directly instead of appending it to the workspace.
```yaml
steps:
+512 -202
View File
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "setup-node",
"version": "7.0.0",
"version": "7.1.0",
"type": "module",
"private": true,
"description": "setup node action",
@@ -29,7 +29,7 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^6.2.0",
"@actions/cache": "^6.3.0",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/github": "^9.1.1",
@@ -41,23 +41,23 @@
"smol-toml": "^1.7.1"
},
"overrides": {
"brace-expansion": "5.0.9"
"brace-expansion": "5.0.12"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@types/node": "^24.1.0",
"@types/semver": "^7.7.0",
"@types/semver": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@vercel/ncc": "^0.44.0",
"eslint": "^10.5.0",
"@typescript-eslint/parser": "^8.70.0",
"@vercel/ncc": "^0.45.0",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-n": "^18.1.0",
"eslint-plugin-n": "^18.3.0",
"globals": "^17.7.0",
"jest": "^30.4.2",
"jest-each": "^30.4.1",
"jest-each": "^30.5.2",
"prettier": "^3.9.6",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3"
@@ -1,7 +1,8 @@
import path from 'node:path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import path from 'path';
import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache';
import BaseDistribution from '../base-distribution.js';
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models.js';
@@ -10,6 +11,15 @@ interface INodeRelease extends tc.IToolRelease {
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 {
constructor(nodeInfo: NodeInputs) {
super(nodeInfo);
@@ -43,12 +53,20 @@ export default class OfficialBuilds extends BaseDistribution {
if (this.nodeInfo.checkLatest) {
core.info('Attempt to resolve the latest version from manifest...');
const resolvedVersion = await this.resolveVersionFromManifest(
this.nodeInfo.versionSpec,
this.nodeInfo.stable,
osArch,
manifest
);
let resolvedVersion: string | undefined;
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 as Error).message);
}
if (resolvedVersion) {
this.nodeInfo.versionSpec = resolvedVersion;
core.info(`Resolved as '${resolvedVersion}'`);
@@ -193,7 +211,7 @@ export default class OfficialBuilds extends BaseDistribution {
private async getManifest(): Promise<tc.IToolRelease[]> {
let lastError: Error | undefined;
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++) {
try {
const manifest = await tc.getManifestFromRepo(
@@ -204,28 +222,48 @@ export default class OfficialBuilds extends BaseDistribution {
: this.nodeInfo.auth,
'main'
);
if (Array.isArray(manifest) && manifest.length > 0) {
if (isValidManifest(manifest)) {
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));
lastError = new Error(invalidManifestMessage);
} catch (error) {
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;
}
}
core.debug(
`Attempt ${attempt}/${maxAttempts} to fetch the manifest failed: ${lastError.message}`
);
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))
); // Retry after a delay
);
}
}
throw new Error(
`Failed to fetch a valid manifest after ${maxAttempts} attempts. Last error: ${lastError?.message}`
);
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(
`Failed to fetch a valid manifest from the GitHub API and raw URL. API: ${lastError?.message}. Raw: ${message}`,
{cause: error}
);
}
}
private resolveLtsAliasFromManifest(
@@ -272,26 +310,6 @@ export default class OfficialBuilds extends BaseDistribution {
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(
versionSpec: string,
stable: boolean,
+3 -1
View File
@@ -120,7 +120,9 @@ function resolveVersionInput(): string {
}
if (versionFileInput) {
const versionFilePath = path.join(
// `path.resolve` (unlike `path.join`) keeps an already-absolute input as-is,
// so a composite action can pass `${{ github.action_path }}/.nvmrc`.
const versionFilePath = path.resolve(
process.env.GITHUB_WORKSPACE!,
versionFileInput
);