1import chalk from 'chalk'; 2import inquirer from 'inquirer'; 3 4import Git from '../../Git'; 5import logger from '../../Logger'; 6import { Task } from '../../TasksRunner'; 7import { CommandOptions, Parcel, TaskArgs } from '../types'; 8import { loadRequestedParcels } from './loadRequestedParcels'; 9 10const { green, cyan, blue, yellow } = chalk; 11 12/** 13 * Checks packages integrity and warns about violations. 14 * Integrity is violated if the current version of a package: 15 * - has no `gitHead` property in its package view. 16 * - commit to which `gitHead` refers is not an ancestor of the current head commit. 17 * - mismatches last version found in changelog. 18 */ 19export const checkPackagesIntegrity = new Task<TaskArgs>( 20 { 21 name: 'checkPackagesIntegrity', 22 dependsOn: [loadRequestedParcels], 23 }, 24 async (parcels: Parcel[], options: CommandOptions): Promise<void | symbol> => { 25 logger.info('\n Checking packages integrity...'); 26 27 const resolver = async ({ pkg, pkgView, changelog }: Parcel): Promise<boolean> => { 28 if (!pkgView) { 29 // If package view is not there, then the package hasn't been released yet - no need to check integrity. 30 return true; 31 } 32 33 const isAncestor = !!pkgView.gitHead && (await Git.isAncestorAsync(pkgView.gitHead)); 34 const lastChangelogVersion = await changelog.getLastPublishedVersionAsync(); 35 const isVersionMatching = !lastChangelogVersion || pkgView.version === lastChangelogVersion; 36 const integral = isAncestor && isVersionMatching; 37 38 if (!integral) { 39 logger.warn(`\n⚠️ Integrity checks failed for ${green(pkg.packageName)}.`); 40 } 41 if (!pkgView.gitHead) { 42 logger.warn(` Cannot find ${blue('gitHead')} in package view.`); 43 } else if (!isAncestor) { 44 logger.warn( 45 ` Local version ${cyan(pkgView.version)} has been published from different branch.` 46 ); 47 } 48 if (!isVersionMatching) { 49 logger.warn( 50 ` Last version in changelog ${cyan(lastChangelogVersion!)}`, 51 `doesn't match local version ${cyan(pkgView.version)}.` 52 ); 53 } 54 return integral; 55 }; 56 57 const results = await Promise.all(parcels.map(resolver)); 58 const somethingFailed = results.some((result) => !result); 59 60 if (options.checkIntegrity) { 61 if (somethingFailed) { 62 logger.error('\n Integrity checks failed.'); 63 } else { 64 logger.success('\n✅ All integrity checks passed.'); 65 } 66 return; 67 } 68 if (somethingFailed && (await shouldStopOnFailedIntegrityChecksAsync())) { 69 if (process.env.CI) { 70 throw new Error('Some integrity checks failed – it is prohibited on the CI.'); 71 } 72 return Task.STOP; 73 } 74 } 75); 76 77/** 78 * Resolves to a boolean value that means whether to stop the workflow if some integrity checks have failed. 79 * It immediately returns `true` if it's run on the CI. 80 */ 81async function shouldStopOnFailedIntegrityChecksAsync(): Promise<boolean> { 82 if (process.env.CI) { 83 return true; 84 } 85 const { proceed } = await inquirer.prompt<{ proceed: boolean }>([ 86 { 87 type: 'confirm', 88 name: 'proceed', 89 prefix: '❔', 90 message: yellow('Some integrity checks have failed. Do you want to proceed either way?'), 91 default: true, 92 }, 93 ]); 94 return !proceed; 95} 96