Introduce retry mechanism for npm install in CI/CD

This commit addresses occasional pipeline failures caused by transient
network errors during dependency installation with `npm ci`. It
centralizes the logic for installing npm dependencies and introduces a
retry mechanism.

The new approach will attempt `npm ci` up to 5 times with a 5-second
interval between each attempt, thereby increasing the resilience of
CI/CD pipelines.

This commit adds a new script `npm-install.js` with `npm run
install-deps` command to centralize npm dependency installation process
throughout the project. Separate testing of scripts to a separate
workflow.

It removes unused `install` dependency from `package.json`.
This commit is contained in:
undergroundwires
2023-09-05 13:39:15 +02:00
parent 0a2a1a026b
commit 4beb1bb574
18 changed files with 313 additions and 190 deletions

View File

@@ -1,13 +1,8 @@
import { join } from 'path';
import { rm, readFile } from 'fs/promises';
import { exists, isDirMissingOrEmpty } from './io';
import { CommandResult, runCommand } from './run-command';
import { runCommand } from './run-command';
import { LogLevel, die, log } from './log';
import { sleep } from './sleep';
import type { ExecOptions } from 'child_process';
const NPM_INSTALL_MAX_RETRIES = 3;
const NPM_INSTALL_RETRY_DELAY_MS = 5 /* seconds */ * 1000;
export async function ensureNpmProjectDir(projectDir: string): Promise<void> {
if (!projectDir) { throw new Error('missing project directory'); }
@@ -20,13 +15,16 @@ export async function npmInstall(projectDir: string): Promise<void> {
if (!projectDir) { throw new Error('missing project directory'); }
const npmModulesPath = join(projectDir, 'node_modules');
if (!await isDirMissingOrEmpty(npmModulesPath)) {
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
log(`Directory "${npmModulesPath}" exists and has content. Skipping installing dependencies.`);
return;
}
log('Starting dependency installation...');
const { error } = await executeWithRetry('npm install --loglevel=error', {
cwd: projectDir,
}, NPM_INSTALL_MAX_RETRIES, NPM_INSTALL_RETRY_DELAY_MS);
const { error } = await runCommand(
`npm run install-deps -- --no-errors --root-directory ${projectDir}`,
{
cwd: projectDir,
},
);
if (error) {
die(error);
}
@@ -103,29 +101,3 @@ async function readPackageJsonContents(projectDir: string): Promise<string> {
return die(`Error detail: ${error}`);
}
}
async function executeWithRetry(
command: string,
options: ExecOptions,
maxRetries: number,
retryDelayInMs: number,
currentAttempt = 1,
): Promise<CommandResult> {
const result = await runCommand(command, options);
if (!result.error || currentAttempt >= maxRetries) {
return result;
}
log(`Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`);
await sleep(retryDelayInMs);
const retryResult = await executeWithRetry(
command,
options,
maxRetries,
retryDelayInMs,
currentAttempt + 1,
);
return retryResult;
}