diff --git a/common/changes/@microsoft/rush/install-run-lockfile_2022-10-04-00-27.json b/common/changes/@microsoft/rush/install-run-lockfile_2022-10-04-00-27.json new file mode 100644 index 00000000000..084dc617f73 --- /dev/null +++ b/common/changes/@microsoft/rush/install-run-lockfile_2022-10-04-00-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Support passing a lockfile to \"install-run.js\" and \"install-run-rush.js\" to ensure stable installation on CI.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/scripts/install-run-rush.ts b/libraries/rush-lib/src/scripts/install-run-rush.ts index ecd9d4dc400..82cc0245bf6 100644 --- a/libraries/rush-lib/src/scripts/install-run-rush.ts +++ b/libraries/rush-lib/src/scripts/install-run-rush.ts @@ -25,6 +25,8 @@ import { const PACKAGE_NAME: string = '@microsoft/rush'; const RUSH_PREVIEW_VERSION: string = 'RUSH_PREVIEW_VERSION'; +const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE: 'INSTALL_RUN_RUSH_LOCKFILE_PATH' = + 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; function _getRushVersion(logger: ILogger): string { const rushPreviewVersion: string | undefined = process.env[RUSH_PREVIEW_VERSION]; @@ -103,7 +105,14 @@ function _run(): void { const version: string = _getRushVersion(logger); logger.info(`The rush.json configuration requests Rush version ${version}`); - return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs); + const lockFilePath: string | undefined = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; + if (lockFilePath) { + logger.info( + `Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.` + ); + } + + return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); }); } diff --git a/libraries/rush-lib/src/scripts/install-run.ts b/libraries/rush-lib/src/scripts/install-run.ts index 5def08052bf..87a4d5e8b75 100644 --- a/libraries/rush-lib/src/scripts/install-run.ts +++ b/libraries/rush-lib/src/scripts/install-run.ts @@ -20,6 +20,7 @@ import { IPackageJson } from '@rushstack/node-core-library'; export const RUSH_JSON_FILENAME: string = 'rush.json'; const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME: string = 'RUSH_TEMP_FOLDER'; +const INSTALL_RUN_LOCKFILE_PATH_VARIABLE: 'INSTALL_RUN_LOCKFILE_PATH' = 'INSTALL_RUN_LOCKFILE_PATH'; const INSTALLED_FLAG_FILENAME: string = 'installed.flag'; const NODE_MODULES_FOLDER_NAME: string = 'node_modules'; const PACKAGE_JSON_FILENAME: string = 'package.json'; @@ -334,29 +335,50 @@ function _isPackageAlreadyInstalled(packageInstallFolder: string): boolean { } } +/** + * Delete a file. Fail silently if it does not exist. + */ +function _deleteFile(file: string): void { + try { + fs.unlinkSync(file); + } catch (err) { + if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { + throw err; + } + } +} + /** * Removes the following files and directories under the specified folder path: * - installed.flag * - * - node_modules */ -function _cleanInstallFolder(rushTempFolder: string, packageInstallFolder: string): void { +function _cleanInstallFolder( + rushTempFolder: string, + packageInstallFolder: string, + lockFilePath: string | undefined +): void { try { const flagFile: string = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); - if (fs.existsSync(flagFile)) { - fs.unlinkSync(flagFile); - } + _deleteFile(flagFile); const packageLockFile: string = path.resolve(packageInstallFolder, 'package-lock.json'); - if (fs.existsSync(packageLockFile)) { - fs.unlinkSync(packageLockFile); - } + if (lockFilePath) { + fs.copyFileSync(lockFilePath, packageLockFile); + } else { + // Not running `npm ci`, so need to cleanup + _deleteFile(packageLockFile); - const nodeModulesFolder: string = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); - if (fs.existsSync(nodeModulesFolder)) { - const rushRecyclerFolder: string = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); + const nodeModulesFolder: string = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); + if (fs.existsSync(nodeModulesFolder)) { + const rushRecyclerFolder: string = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); - fs.renameSync(nodeModulesFolder, path.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); + fs.renameSync( + nodeModulesFolder, + path.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`) + ); + } } } catch (e) { throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); @@ -386,18 +408,24 @@ function _createPackageJson(packageInstallFolder: string, name: string, version: /** * Run "npm install" in the package install folder. */ -function _installPackage(logger: ILogger, packageInstallFolder: string, name: string, version: string): void { +function _installPackage( + logger: ILogger, + packageInstallFolder: string, + name: string, + version: string, + command: 'install' | 'ci' +): void { try { logger.info(`Installing ${name}...`); const npmPath: string = getNpmPath(); - const result: childProcess.SpawnSyncReturns = childProcess.spawnSync(npmPath, ['install'], { + const result: childProcess.SpawnSyncReturns = childProcess.spawnSync(npmPath, [command], { stdio: 'inherit', cwd: packageInstallFolder, env: process.env }); if (result.status !== 0) { - throw new Error('"npm install" encountered an error'); + throw new Error(`"npm ${command}" encountered an error`); } logger.info(`Successfully installed ${name}@${version}`); @@ -432,7 +460,8 @@ export function installAndRun( packageName: string, packageVersion: string, packageBinName: string, - packageBinArgs: string[] + packageBinArgs: string[], + lockFilePath: string | undefined = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE] ): number { const rushJsonFolder: string = findRushJsonFolder(); const rushCommonFolder: string = path.join(rushJsonFolder, 'common'); @@ -445,13 +474,14 @@ export function installAndRun( if (!_isPackageAlreadyInstalled(packageInstallFolder)) { // The package isn't already installed - _cleanInstallFolder(rushTempFolder, packageInstallFolder); + _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); const sourceNpmrcFolder: string = path.join(rushCommonFolder, 'config', 'rush'); _syncNpmrc(logger, sourceNpmrcFolder, packageInstallFolder); _createPackageJson(packageInstallFolder, packageName, packageVersion); - _installPackage(logger, packageInstallFolder, packageName, packageVersion); + const command: 'install' | 'ci' = lockFilePath ? 'ci' : 'install'; + _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); _writeFlagFile(packageInstallFolder); }