Compare commits

..
Author SHA1 Message Date
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 7407 additions and 11417 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:
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/cache"
version: 6.2.0
version: 6.3.0
type: npm
summary: Actions cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/cache
-20
View File
@@ -1,20 +0,0 @@
---
name: "@actions/glob"
version: 0.6.1
type: npm
summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []
File renamed without changes.
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: brace-expansion
version: 5.0.9
version: 5.0.12
type: npm
summary: Brace expansion as known from sh/bash
homepage:
-26
View File
@@ -1,26 +0,0 @@
---
name: minimatch
version: 3.1.5
type: npm
summary: a glob matcher in javascript
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []
@@ -1,10 +1,10 @@
---
name: minimatch
version: 10.2.5
version: 10.2.6
type: npm
summary: a glob matcher in javascript
homepage:
license: blueoak-1.0.0
license: BlueOak-1.0.0
licenses:
- sources: LICENSE.md
text: |
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 6.28.0
version: 6.29.0
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
+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:
+578 -869
View File
File diff suppressed because it is too large. Load diff
+10 -10
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,25 +41,25 @@
"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": "^7.0.2"
"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
);