diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index b937b383..acc500fc 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -162,9 +162,6 @@ jobs: [.nvmrc, .tool-versions, .tool-versions-node, package.json] steps: - uses: actions/checkout@v4 - - name: Remove volta from package.json - shell: bash - run: cat <<< "$(jq 'del(.volta)' ./__tests__/data/package.json)" > ./__tests__/data/package.json - name: Setup node from node version file uses: ./ with: @@ -183,7 +180,22 @@ jobs: - name: Setup node from node version file uses: ./ with: - node-version-file: '__tests__/data/package.json' + node-version-file: '__tests__/data/package-volta.json' + - name: Verify node + run: __tests__/verify-node.sh 16 + + version-file-volta-extends: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - name: Setup node from node version file + uses: ./ + with: + node-version-file: '__tests__/data/package-volta-extends.json' - name: Verify node run: __tests__/verify-node.sh 16 diff --git a/__tests__/data/package-volta-extends.json b/__tests__/data/package-volta-extends.json new file mode 100644 index 00000000..ee6d7451 --- /dev/null +++ b/__tests__/data/package-volta-extends.json @@ -0,0 +1,5 @@ +{ + "volta": { + "extends": "./package-volta.json" + } +} diff --git a/__tests__/data/package-volta.json b/__tests__/data/package-volta.json new file mode 100644 index 00000000..ebee7dfc --- /dev/null +++ b/__tests__/data/package-volta.json @@ -0,0 +1,8 @@ +{ + "engines": { + "node": "^14.0.0" + }, + "volta": { + "node": "16.0.0" + } +} diff --git a/__tests__/data/package.json b/__tests__/data/package.json index ebee7dfc..b201009d 100644 --- a/__tests__/data/package.json +++ b/__tests__/data/package.json @@ -1,8 +1,5 @@ { "engines": { "node": "^14.0.0" - }, - "volta": { - "node": "16.0.0" } } diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 36024e65..e54e5ea9 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -24,11 +24,9 @@ describe('main tests', () => { let startGroupSpy: jest.SpyInstance; let endGroupSpy: jest.SpyInstance; - let existsSpy: jest.SpyInstance; - let getExecOutputSpy: jest.SpyInstance; - let parseNodeVersionSpy: jest.SpyInstance; + let getNodeVersionFromFileSpy: jest.SpyInstance; let cnSpy: jest.SpyInstance; let findSpy: jest.SpyInstance; let isCacheActionAvailable: jest.SpyInstance; @@ -41,6 +39,7 @@ describe('main tests', () => { // node os = {}; console.log('::stop-commands::stoptoken'); + process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data'); process.env['GITHUB_PATH'] = ''; // Stub out ENV file functionality so we can verify it writes to standard out process.env['GITHUB_OUTPUT'] = ''; // Stub out ENV file functionality so we can verify it writes to standard out infoSpy = jest.spyOn(core, 'info'); @@ -62,12 +61,10 @@ describe('main tests', () => { isCacheActionAvailable = jest.spyOn(cache, 'isFeatureAvailable'); - existsSpy = jest.spyOn(fs, 'existsSync'); - cnSpy = jest.spyOn(process.stdout, 'write'); cnSpy.mockImplementation(line => { // uncomment to debug - // process.stderr.write('write:' + line + '\n'); + process.stderr.write('write:' + line + '\n'); }); setupNodeJsSpy = jest.spyOn(OfficialBuilds.prototype, 'setupNodeJs'); @@ -85,7 +82,7 @@ describe('main tests', () => { jest.restoreAllMocks(); }, 100000); - describe('parseNodeVersionFile', () => { + describe('getNodeVersionFromFile', () => { each` contents | expected ${'12'} | ${'12'} @@ -100,10 +97,27 @@ describe('main tests', () => { ${'unknown format'} | ${'unknown format'} ${' 14.1.0 '} | ${'14.1.0'} ${'{"volta": {"node": ">=14.0.0 <=17.0.0"}}'}| ${'>=14.0.0 <=17.0.0'} + ${'{"volta": {"extends": "./package.json"}}'}| ${'18.0.0'} ${'{"engines": {"node": "17.0.0"}}'} | ${'17.0.0'} ${'{}'} | ${null} `.it('parses "$contents"', ({contents, expected}) => { - expect(util.parseNodeVersionFile(contents)).toBe(expected); + const existsSpy = jest.spyOn(fs, 'existsSync'); + existsSpy.mockImplementation(() => true); + + const readFileSpy = jest.spyOn(fs, 'readFileSync'); + readFileSpy.mockImplementation(filePath => { + if ( + typeof filePath === 'string' && + path.basename(filePath) === 'package.json' + ) { + // Special case for volta.extends + return '{"volta": {"node": "18.0.0"}}'; + } + + return contents; + }); + + expect(util.getNodeVersionFromFile('file')).toBe(expected); }); }); @@ -142,10 +156,17 @@ describe('main tests', () => { describe('node-version-file flag', () => { beforeEach(() => { - parseNodeVersionSpy = jest.spyOn(util, 'parseNodeVersionFile'); + delete inputs['node-version']; + inputs['node-version-file'] = '.nvmrc'; + + getNodeVersionFromFileSpy = jest.spyOn(util, 'getNodeVersionFromFile'); }); - it('not used if node-version is provided', async () => { + afterEach(() => { + getNodeVersionFromFileSpy.mockRestore(); + }); + + it('does not read node-version-file if node-version is provided', async () => { // Arrange inputs['node-version'] = '12'; @@ -153,107 +174,54 @@ describe('main tests', () => { await main.run(); // Assert - expect(parseNodeVersionSpy).toHaveBeenCalledTimes(0); - }, 10000); - - it('not used if node-version-file not provided', async () => { - // Act - await main.run(); - - // Assert - expect(parseNodeVersionSpy).toHaveBeenCalledTimes(0); - }); - - it('reads node-version-file if provided', async () => { - // Arrange - const versionSpec = 'v14'; - const versionFile = '.nvmrc'; - const expectedVersionSpec = '14'; - process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data'); - inputs['node-version-file'] = versionFile; - - parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); - existsSpy.mockImplementationOnce( - input => input === path.join(__dirname, 'data', versionFile) - ); - - // Act - await main.run(); - - // Assert - expect(existsSpy).toHaveBeenCalledTimes(1); - expect(existsSpy).toHaveReturnedWith(true); - expect(parseNodeVersionSpy).toHaveBeenCalledWith(versionSpec); - expect(infoSpy).toHaveBeenCalledWith( - `Resolved ${versionFile} as ${expectedVersionSpec}` - ); - }, 10000); - - it('reads package.json as node-version-file if provided', async () => { - // Arrange - const versionSpec = fs.readFileSync( - path.join(__dirname, 'data/package.json'), - 'utf-8' - ); - const versionFile = 'package.json'; - const expectedVersionSpec = '14'; - process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data'); - inputs['node-version-file'] = versionFile; - - parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); - existsSpy.mockImplementationOnce( - input => input === path.join(__dirname, 'data', versionFile) - ); - // Act - await main.run(); - - // Assert - expect(existsSpy).toHaveBeenCalledTimes(1); - expect(existsSpy).toHaveReturnedWith(true); - expect(parseNodeVersionSpy).toHaveBeenCalledWith(versionSpec); - expect(infoSpy).toHaveBeenCalledWith( - `Resolved ${versionFile} as ${expectedVersionSpec}` - ); - }, 10000); - - it('both node-version-file and node-version are provided', async () => { - inputs['node-version'] = '12'; - const versionSpec = 'v14'; - const versionFile = '.nvmrc'; - const expectedVersionSpec = '14'; - process.env['GITHUB_WORKSPACE'] = path.join(__dirname, '..'); - inputs['node-version-file'] = versionFile; - - parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); - - // Act - await main.run(); - - // Assert - expect(existsSpy).toHaveBeenCalledTimes(0); - expect(parseNodeVersionSpy).not.toHaveBeenCalled(); + expect(inputs['node-version']).toBeDefined(); + expect(inputs['node-version-file']).toBeDefined(); + expect(getNodeVersionFromFileSpy).not.toHaveBeenCalled(); expect(warningSpy).toHaveBeenCalledWith( 'Both node-version and node-version-file inputs are specified, only node-version will be used' ); }); - it('should throw an error if node-version-file is not found', async () => { - const versionFile = '.nvmrc'; - const versionFilePath = path.join(__dirname, '..', versionFile); - inputs['node-version-file'] = versionFile; + it('does not read node-version-file if node-version-file is not provided', async () => { + // Arrange + delete inputs['node-version-file']; - inSpy.mockImplementation(name => inputs[name]); - existsSpy.mockImplementationOnce( - input => input === path.join(__dirname, 'data', versionFile) + // Act + await main.run(); + + // Assert + expect(getNodeVersionFromFileSpy).not.toHaveBeenCalled(); + }); + + it('reads node-version-file', async () => { + // Arrange + const expectedVersionSpec = '14'; + getNodeVersionFromFileSpy.mockImplementation(() => expectedVersionSpec); + + // Act + await main.run(); + + // Assert + expect(getNodeVersionFromFileSpy).toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledWith( + `Resolved ${inputs['node-version-file']} as ${expectedVersionSpec}` + ); + }, 10000); + + it('should throw an error if node-version-file is not accessible', async () => { + // Arrange + inputs['node-version-file'] = 'non-existing-file'; + const versionFilePath = path.join( + __dirname, + 'data', + inputs['node-version-file'] ); // Act await main.run(); // Assert - expect(existsSpy).toHaveBeenCalled(); - expect(existsSpy).toHaveReturnedWith(false); - expect(parseNodeVersionSpy).not.toHaveBeenCalled(); + expect(getNodeVersionFromFileSpy).toHaveBeenCalled(); expect(cnSpy).toHaveBeenCalledWith( `::error::The specified node version file at: ${versionFilePath} does not exist${osm.EOL}` ); diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js index ece5ae39..35d54949 100644 --- a/dist/cache-save/index.js +++ b/dist/cache-save/index.js @@ -83329,49 +83329,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.unique = exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0; +exports.unique = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; const core = __importStar(__nccwpck_require__(2186)); const exec = __importStar(__nccwpck_require__(1514)); -function parseNodeVersionFile(contents) { - var _a, _b, _c; - let nodeVersion; +const fs_1 = __importDefault(__nccwpck_require__(7147)); +const path_1 = __importDefault(__nccwpck_require__(1017)); +function getNodeVersionFromFile(versionFilePath) { + var _a, _b, _c, _d, _e; + if (!fs_1.default.existsSync(versionFilePath)) { + throw new Error(`The specified node version file at: ${versionFilePath} does not exist`); + } + const contents = fs_1.default.readFileSync(versionFilePath, 'utf8'); // Try parsing the file as an NPM `package.json` file. try { const manifest = JSON.parse(contents); - // JSON can parse numbers, but that's handled later - if (typeof manifest === 'object') { - nodeVersion = (_a = manifest.volta) === null || _a === void 0 ? void 0 : _a.node; - if (!nodeVersion) - nodeVersion = (_b = manifest.engines) === null || _b === void 0 ? void 0 : _b.node; - // if contents are an object, we parsed JSON + // Presume package.json file. + if (typeof manifest === 'object' && !!manifest) { + // Support Volta. + // See https://docs.volta.sh/guide/understanding#managing-your-project + if ((_a = manifest.volta) === null || _a === void 0 ? void 0 : _a.node) { + return manifest.volta.node; + } + if ((_b = manifest.engines) === null || _b === void 0 ? void 0 : _b.node) { + return manifest.engines.node; + } + // Support Volta workspaces. + // See https://docs.volta.sh/advanced/workspaces + if ((_c = manifest.volta) === null || _c === void 0 ? void 0 : _c.extends) { + const extendedFilePath = path_1.default.resolve(path_1.default.dirname(versionFilePath), manifest.volta.extends); + core.info('Resolving node version from ' + extendedFilePath); + return getNodeVersionFromFile(extendedFilePath); + } + // If contents are an object, we parsed JSON // this can happen if node-version-file is a package.json // yet contains no volta.node or engines.node // - // if node-version file is _not_ json, control flow + // If node-version file is _not_ JSON, control flow // will not have reached these lines. // // And because we've reached here, we know the contents // *are* JSON, so no further string parsing makes sense. - if (!nodeVersion) { - return null; - } + return null; } } - catch (_d) { + catch (_f) { core.info('Node version file is not JSON file'); } - if (!nodeVersion) { - const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); - nodeVersion = (_c = found === null || found === void 0 ? void 0 : found.groups) === null || _c === void 0 ? void 0 : _c.version; - } - // In the case of an unknown format, - // return as is and evaluate the version separately. - if (!nodeVersion) - nodeVersion = contents.trim(); - return nodeVersion; + const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); + return (_e = (_d = found === null || found === void 0 ? void 0 : found.groups) === null || _d === void 0 ? void 0 : _d.version) !== null && _e !== void 0 ? _e : contents.trim(); } -exports.parseNodeVersionFile = parseNodeVersionFile; +exports.getNodeVersionFromFile = getNodeVersionFromFile; function printEnvDetailsAndSetOutput() { return __awaiter(this, void 0, void 0, function* () { core.startGroup('Environment details'); diff --git a/dist/setup/index.js b/dist/setup/index.js index 4fb187cf..d9e70011 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -93652,7 +93652,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.run = void 0; const core = __importStar(__nccwpck_require__(2186)); -const fs_1 = __importDefault(__nccwpck_require__(7147)); const os_1 = __importDefault(__nccwpck_require__(2037)); const auth = __importStar(__nccwpck_require__(7573)); const path = __importStar(__nccwpck_require__(1017)); @@ -93727,10 +93726,7 @@ function resolveVersionInput() { } if (versionFileInput) { const versionFilePath = path.join(process.env.GITHUB_WORKSPACE, versionFileInput); - if (!fs_1.default.existsSync(versionFilePath)) { - throw new Error(`The specified node version file at: ${versionFilePath} does not exist`); - } - const parsedVersion = (0, util_1.parseNodeVersionFile)(fs_1.default.readFileSync(versionFilePath, 'utf8')); + const parsedVersion = (0, util_1.getNodeVersionFromFile)(versionFilePath); if (parsedVersion) { version = parsedVersion; } @@ -93782,49 +93778,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.unique = exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0; +exports.unique = exports.printEnvDetailsAndSetOutput = exports.getNodeVersionFromFile = void 0; const core = __importStar(__nccwpck_require__(2186)); const exec = __importStar(__nccwpck_require__(1514)); -function parseNodeVersionFile(contents) { - var _a, _b, _c; - let nodeVersion; +const fs_1 = __importDefault(__nccwpck_require__(7147)); +const path_1 = __importDefault(__nccwpck_require__(1017)); +function getNodeVersionFromFile(versionFilePath) { + var _a, _b, _c, _d, _e; + if (!fs_1.default.existsSync(versionFilePath)) { + throw new Error(`The specified node version file at: ${versionFilePath} does not exist`); + } + const contents = fs_1.default.readFileSync(versionFilePath, 'utf8'); // Try parsing the file as an NPM `package.json` file. try { const manifest = JSON.parse(contents); - // JSON can parse numbers, but that's handled later - if (typeof manifest === 'object') { - nodeVersion = (_a = manifest.volta) === null || _a === void 0 ? void 0 : _a.node; - if (!nodeVersion) - nodeVersion = (_b = manifest.engines) === null || _b === void 0 ? void 0 : _b.node; - // if contents are an object, we parsed JSON + // Presume package.json file. + if (typeof manifest === 'object' && !!manifest) { + // Support Volta. + // See https://docs.volta.sh/guide/understanding#managing-your-project + if ((_a = manifest.volta) === null || _a === void 0 ? void 0 : _a.node) { + return manifest.volta.node; + } + if ((_b = manifest.engines) === null || _b === void 0 ? void 0 : _b.node) { + return manifest.engines.node; + } + // Support Volta workspaces. + // See https://docs.volta.sh/advanced/workspaces + if ((_c = manifest.volta) === null || _c === void 0 ? void 0 : _c.extends) { + const extendedFilePath = path_1.default.resolve(path_1.default.dirname(versionFilePath), manifest.volta.extends); + core.info('Resolving node version from ' + extendedFilePath); + return getNodeVersionFromFile(extendedFilePath); + } + // If contents are an object, we parsed JSON // this can happen if node-version-file is a package.json // yet contains no volta.node or engines.node // - // if node-version file is _not_ json, control flow + // If node-version file is _not_ JSON, control flow // will not have reached these lines. // // And because we've reached here, we know the contents // *are* JSON, so no further string parsing makes sense. - if (!nodeVersion) { - return null; - } + return null; } } - catch (_d) { + catch (_f) { core.info('Node version file is not JSON file'); } - if (!nodeVersion) { - const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); - nodeVersion = (_c = found === null || found === void 0 ? void 0 : found.groups) === null || _c === void 0 ? void 0 : _c.version; - } - // In the case of an unknown format, - // return as is and evaluate the version separately. - if (!nodeVersion) - nodeVersion = contents.trim(); - return nodeVersion; + const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); + return (_e = (_d = found === null || found === void 0 ? void 0 : found.groups) === null || _d === void 0 ? void 0 : _d.version) !== null && _e !== void 0 ? _e : contents.trim(); } -exports.parseNodeVersionFile = parseNodeVersionFile; +exports.getNodeVersionFromFile = getNodeVersionFromFile; function printEnvDetailsAndSetOutput() { return __awaiter(this, void 0, void 0, function* () { core.startGroup('Environment details'); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 079b8bfa..bf62e071 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -84,6 +84,8 @@ When using the `package.json` input, the action will look for `volta.node` first } ``` +Otherwise, when [`volta.extends`](https://docs.volta.sh/advanced/workspaces) is defined, then it will resolve the corresponding file and look for `volta.node` or `engines.node` recursively. + ## Architecture You can use any of the [supported operating systems](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners), and the compatible `architecture` can be selected using `architecture`. Values are `x86`, `x64`, `arm64`, `armv6l`, `armv7l`, `ppc64le`, `s390x` (not all of the architectures are available on all platforms). diff --git a/src/main.ts b/src/main.ts index 34f94310..c55c3b00 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,5 @@ import * as core from '@actions/core'; -import fs from 'fs'; import os from 'os'; import * as auth from './authutil'; @@ -8,7 +7,7 @@ import * as path from 'path'; import {restoreCache} from './cache-restore'; import {isCacheFeatureAvailable} from './cache-utils'; import {getNodejsDistribution} from './distributions/installer-factory'; -import {parseNodeVersionFile, printEnvDetailsAndSetOutput} from './util'; +import {getNodeVersionFromFile, printEnvDetailsAndSetOutput} from './util'; import {State} from './constants'; export async function run() { @@ -99,15 +98,7 @@ function resolveVersionInput(): string { versionFileInput ); - if (!fs.existsSync(versionFilePath)) { - throw new Error( - `The specified node version file at: ${versionFilePath} does not exist` - ); - } - - const parsedVersion = parseNodeVersionFile( - fs.readFileSync(versionFilePath, 'utf8') - ); + const parsedVersion = getNodeVersionFromFile(versionFilePath); if (parsedVersion) { version = parsedVersion; diff --git a/src/util.ts b/src/util.ts index 0b2b1490..cc6ac310 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,45 +1,62 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; -export function parseNodeVersionFile(contents: string): string | null { - let nodeVersion: string | undefined; +import fs from 'fs'; +import path from 'path'; + +export function getNodeVersionFromFile(versionFilePath: string): string | null { + if (!fs.existsSync(versionFilePath)) { + throw new Error( + `The specified node version file at: ${versionFilePath} does not exist` + ); + } + + const contents = fs.readFileSync(versionFilePath, 'utf8'); // Try parsing the file as an NPM `package.json` file. try { const manifest = JSON.parse(contents); - // JSON can parse numbers, but that's handled later - if (typeof manifest === 'object') { - nodeVersion = manifest.volta?.node; - if (!nodeVersion) nodeVersion = manifest.engines?.node; + // Presume package.json file. + if (typeof manifest === 'object' && !!manifest) { + // Support Volta. + // See https://docs.volta.sh/guide/understanding#managing-your-project + if (manifest.volta?.node) { + return manifest.volta.node; + } - // if contents are an object, we parsed JSON + if (manifest.engines?.node) { + return manifest.engines.node; + } + + // Support Volta workspaces. + // See https://docs.volta.sh/advanced/workspaces + if (manifest.volta?.extends) { + const extendedFilePath = path.resolve( + path.dirname(versionFilePath), + manifest.volta.extends + ); + core.info('Resolving node version from ' + extendedFilePath); + return getNodeVersionFromFile(extendedFilePath); + } + + // If contents are an object, we parsed JSON // this can happen if node-version-file is a package.json // yet contains no volta.node or engines.node // - // if node-version file is _not_ json, control flow + // If node-version file is _not_ JSON, control flow // will not have reached these lines. // // And because we've reached here, we know the contents // *are* JSON, so no further string parsing makes sense. - if (!nodeVersion) { - return null; - } + return null; } } catch { core.info('Node version file is not JSON file'); } - if (!nodeVersion) { - const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); - nodeVersion = found?.groups?.version; - } - - // In the case of an unknown format, - // return as is and evaluate the version separately. - if (!nodeVersion) nodeVersion = contents.trim(); - - return nodeVersion as string; + const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m); + return found?.groups?.version ?? contents.trim(); } export async function printEnvDetailsAndSetOutput() {