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