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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions workflow-steps/install-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,38 @@ For example:

- Install a specific node version: `node_version: '20.11.1'`
- Install latest of major node version: `node_version: '20'`

#### package_manager

The package manager to install as the default on the agent machine. Use `"skip"` to not install any package managers or `"all"` to install all. If using `"all"` then the defaults are yarn v1, pnpm v10, and bundled npm version with node.

This input is optional and defaults to `"all"`.

It's recommended to set this value to `"skip"` if the environment already using corepack to manage your versions since corepack will download the defined version from the `package.json#packageManager` field. If you are not using corepack, then it's recommended to only install the package manager you're using for your project.

For example:

- Install all package managers: `package_manager: 'all'` (default)
- Skip package manager installation: `package_manager: 'skip'`
- Install a specific package manager: `package_manager: 'pnpm'`

#### package_manager_version

The version to use with the defined `package_manager` input. This value is ignored if `package_manager` is set to `"all"` and the defaults versions are used.

This input is optional.

For example:

- Install a specific pnpm version: `package_manager: 'pnpm'` and `package_manager_version: '10`

#### corepack_version

The version of corepack to use.

This input is optional and defaults to `"latest"`. You can pin the version of corepack, but make sure to stay updated as older versions can run into certificate expiration issues preventing downloading the package manager.

For example:

- Use latest corepack: `corepack_version: 'latest'` (default)
- Use a specific corepack version: `corepack_version: '0.34'`
273 changes: 200 additions & 73 deletions workflow-steps/install-node/main.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
// @ts-check
const { platform } = require('os');
const { execSync } = require('child_process');
const { existsSync, readFileSync } = require('fs');

const PM_DEFAULTS = {
pnpm: '10',
yarn: '1',
// use version that comes with node
npm: '',
};

async function main() {
if (platform === 'win32') {
if (platform() === 'win32') {
throw new Error('Windows is not supported with this reuseable step yet.');
} else {
// Allow using inputs or env until we fully switch to inputs
const nodeVersionInput =
process.env.NX_CLOUD_INPUT_node_version || process.env.NODE_VERSION;
const maxRetries = process.env.NX_CLOUD_INPUT_max_retries || 3;
// NaN is falsy
const maxRetries = Number(process.env.NX_CLOUD_INPUT_max_retries) || 3;
const packageManager = process.env.NX_CLOUD_INPUT_package_manager || 'all';
const packageManagerVersion =
process.env.NX_CLOUD_INPUT_package_manager_version;
const corepackVersion =
process.env.NX_CLOUD_INPUT_corepack_version || 'latest';

// set defaults incase they are not set yet
process.env.NVM_DIR ??= '/home/workflows/.nvm';
process.env.COREPACK_ENABLE_AUTO_PIN ??= 0;
process.env.COREPACK_ENABLE_AUTO_PIN ??= '0';

const maybeVoltaNodeVersion = getVoltaNodeVersion();

if (nodeVersionInput) {
await runNvmInstall(nodeVersionInput, maxRetries);
await runNvmInstall(
nodeVersionInput,
maxRetries,
packageManager,
packageManagerVersion,
corepackVersion,
);
} else if (isUsingNvm()) {
// nvm will auto detect version in .nvmrc, no need to pass version
await runNvmInstall(null, maxRetries);
await runNvmInstall(
null,
maxRetries,
packageManager,
packageManagerVersion,
corepackVersion,
);
} else if (maybeVoltaNodeVersion) {
await runNvmInstall(maybeVoltaNodeVersion, maxRetries);
await runNvmInstall(
maybeVoltaNodeVersion,
maxRetries,
packageManager,
packageManagerVersion,
corepackVersion,
);
} else {
console.warn(
`No node version specified. You can use the step inputs to define a node version.`,
Expand All @@ -35,87 +67,182 @@ async function main() {
);
}
}
function getVoltaNodeVersion() {
try {
if (existsSync('package.json')) {
const packageJsonContents =
JSON.parse(readFileSync('package.json')) ?? {};
}

return packageJsonContents['volta']?.['node'];
}
} catch (e) {
return null;
function getVoltaNodeVersion() {
try {
if (existsSync('package.json')) {
const packageJsonContents =
JSON.parse(readFileSync('package.json', 'utf8')) ?? {};

return packageJsonContents['volta']?.['node'];
}
} catch (e) {
return null;
}
}

function isUsingNvm() {
try {
return existsSync('.nvmrc');
} catch (e) {
return false;
}
function isUsingNvm() {
try {
return existsSync('.nvmrc');
} catch (e) {
return false;
}
}

/**
* @param {string} corepackVersion
* @param {'yarn' | 'npm' | 'pnpm' | 'all' | 'skip' | string} packageManager
* @param {string | null} packageManagerVersion
**/
function getPMCommands(corepackVersion, packageManager, packageManagerVersion) {
const commands = [];
if (corepackVersion === 'skip') {
commands.push('echo "skipping corepack re-enable"');
} else {
commands.push(
`npm install -g corepack@${corepackVersion} && corepack enable`,
);
}

async function runNvmInstall(version, maxRetries = 3) {
// enable nvm and then run the install command with -b to only install pre-build binaries
// nvm command isn't available since nx agents don't run the bash profile
const installNodeWithNvm = `. $NVM_DIR/nvm.sh && nvm install -b ${
version || ''
} --default`;
const reenableCorePack = `npm install -g corepack@latest && corepack enable`;
// install outside of the current directory,
// otherwise corepack errors if a different package mangager is used than is defined in the workspace
const reinstallPackageManagers = `cd .. && corepack prepare yarn@1 && corepack prepare pnpm@9`;
const printVersions = ['node', 'npm', 'yarn', 'pnpm']
.map((cmd) => `echo "${cmd}: $(${cmd} -v)"`)
.join(' && ');

// path will be updated via nvm to include the new node versions,
const saveEnvVars = `echo "PATH=$PATH\nNVM_DIR=${process.env.NVM_DIR}\nCOREPACK_ENABLE_AUTO_PIN=0" >> $NX_CLOUD_ENV`;
const run = () =>
execSync(
[
installNodeWithNvm,
reenableCorePack,
reinstallPackageManagers,
printVersions,
saveEnvVars,
].join(' && '),
{
stdio: 'inherit',
},
switch (packageManager) {
case 'all':
console.warn(
"It is recommended to only install the package manager you use. To do this set the package_manager input to 'npm', 'yarn', or 'pnpm'.",
);

if (corepackVersion === 'skip') {
console.error(
'Unable to install all package managers when corepack re-enable is skipped.',
);
console.error(
'Re-enable corepack by setting the corepack_version or set package_manager input to npm, yarn, pnpm.',
);
process.exit(1);
}

if (packageManagerVersion) {
console.warn(
'A package manager version was specified but will not be used since all package managers are to be installed. ',
);
console.warn('Defaults will be used instead:');
const pmVersionDisplay = [
`- pnpm: ${PM_DEFAULTS['pnpm']}`,
`- yarn: ${PM_DEFAULTS['yarn']}`,
`- npm: bundled with node`,
];
console.warn(pmVersionDisplay.join('\n'));
}
commands.push(
`cd .. && corepack prepare yarn@${PM_DEFAULTS['yarn']} && corepack prepare pnpm@${PM_DEFAULTS['pnpm']}`,
);
break;
case 'npm':
if (packageManagerVersion) {
commands.push(`npm i -g npm@${packageManagerVersion}`);
} else {
commands.push(`echo "using bundled npm version from node"`);
}
break;
case 'pnpm':
// install outside of the current directory,
// otherwise corepack errors if a different package manager is used than is defined in the workspace
commands.push(
`cd .. && corepack prepare pnpm@${packageManagerVersion || PM_DEFAULTS['pnpm']}`,
);
break;
case 'yarn':
commands.push(
`cd .. && corepack prepare yarn@${packageManagerVersion || PM_DEFAULTS['yarn']}`,
);
break;
case 'skip':
commands.push('echo "skipping package manager reinstall"');
Comment thread
barbados-clemens marked this conversation as resolved.
break;
default:
console.error(
`Unknown package manager option: ${packageManager} - unable to proceed with install.`,
);
process.exit(1);
}

return commands;
}

let retryCount = 0;
/**
* @param {string | null} version
* @param {number} maxRetries
* @param {'yarn' | 'npm' | 'pnpm' | 'all' | 'skip' | string} packageManager
* @param {string | null} packageManagerVersion
Comment thread
barbados-clemens marked this conversation as resolved.
* @param {string} corepackVersion - The version of corepack to install and enable.
**/
async function runNvmInstall(
version,
maxRetries = 3,
packageManager = 'all',
packageManagerVersion = null,
corepackVersion = 'latest',
) {
const commands = [];
// enable nvm and then run the install command with -b to only install pre-build binaries
// nvm command isn't available since nx agents don't run the bash profile
commands.push(
`. $NVM_DIR/nvm.sh && nvm install -b ${version || ''} --default`,
);

while (retryCount < maxRetries) {
try {
run();
break;
} catch (e) {
retryCount++;
commands.push(
...getPMCommands(corepackVersion, packageManager, packageManagerVersion),
);

if (retryCount >= maxRetries) {
throw new Error(
`Failed to install node version using nvm ${version || ''}`,
);
}
// print node and selected pm version
const versionsToPrint = ['node'];

const delay = Math.max(
3_000,
Math.pow(2, retryCount) * Math.random() * 1_250,
);
console.log(
`Installing node failed. Retrying install in ${(delay / 1000).toFixed(
0,
)} seconds...`,
if (!['all', 'skip'].includes(packageManager)) {
versionsToPrint.push(packageManager);
}

const printVersions = versionsToPrint
.map((cmd) => `echo "${cmd}: $(${cmd} -v)"`)
.join(' && ');
commands.push(printVersions);
// path will be updated via nvm to include the new node versions,
commands.push(
`echo "PATH=$PATH\nNVM_DIR=${process.env.NVM_DIR}\nCOREPACK_ENABLE_AUTO_PIN=0" >> $NX_CLOUD_ENV`,
);
const run = () =>
execSync(commands.join(' && '), {
stdio: 'inherit',
});

let retryCount = 0;

while (retryCount < maxRetries) {
try {
run();
break;
} catch (e) {
retryCount++;

if (retryCount >= maxRetries) {
throw new Error(
`Failed to install node version using nvm ${version || ''}`,
);
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.warn(e);
}
}

await new Promise((resolve) => setTimeout(resolve, delay));
const delay = Math.max(
3_000,
Math.pow(2, retryCount) * Math.random() * 1_250,
);
console.log(
`Installing node failed. Retrying install in ${(delay / 1000).toFixed(
0,
)} seconds...`,
);
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.warn(e);
}

await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions workflow-steps/install-node/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ description: Install a specific version of Node.js via nvm
inputs:
- name: node_version
description: 'The node version to be installed'
- name: package_manager
description: 'The package manager to install. use "skip" to not install any package managers or "all" to install all". If using "all" then the defaults are yarn v1, pnpm v10, and bundled npm version with node.'
default: 'all'
- name: package_manager_version
description: 'The version to used with the defined package_manager input. This value is ignored if package_manager is set to "all"'
- name: corepack_version
description: 'The version of corepack to use.'
default: 'latest'

definition:
using: 'node'
Expand Down