From b4a5e7258c12f7d483ae6e30ef7f358b515d31d0 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Mon, 18 Jun 2018 16:55:59 +0200 Subject: [PATCH] Add the following prePublish checks: - That package version matches latest NPM package version (including unpublished versions) - That changelogs are properly formatted (lastEntry version >= currentVersion, no accidental timestamps) - Checks for and removes any git tags locally and remotely that might be left-over from a previous failed publish Moved changelog related helpers to changelogUtils and other small cleanup refactors. --- packages/monorepo-scripts/package.json | 1 + .../monorepo-scripts/src/prepublish_checks.ts | 96 ++++++++++++++++- packages/monorepo-scripts/src/publish.ts | 72 ++----------- packages/monorepo-scripts/src/types.ts | 13 +++ .../src/utils/changelog_utils.ts | 55 +++++++++- .../monorepo-scripts/src/utils/npm_utils.ts | 28 +++++ .../src/utils/semver_utils.ts | 56 ++++++++++ packages/monorepo-scripts/src/utils/utils.ts | 100 ++++++++++++++---- 8 files changed, 333 insertions(+), 88 deletions(-) create mode 100644 packages/monorepo-scripts/src/utils/npm_utils.ts create mode 100644 packages/monorepo-scripts/src/utils/semver_utils.ts diff --git a/packages/monorepo-scripts/package.json b/packages/monorepo-scripts/package.json index 0ae318e497..3a5a208d1b 100644 --- a/packages/monorepo-scripts/package.json +++ b/packages/monorepo-scripts/package.json @@ -47,6 +47,7 @@ "chalk": "2.3.2", "es6-promisify": "5.0.0", "glob": "7.1.1", + "isomorphic-fetch": "2.2.1", "lodash": "4.17.10", "moment": "2.21.0", "opn": "5.3.0", diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts index ae58524f50..a3e44ecafe 100644 --- a/packages/monorepo-scripts/src/prepublish_checks.ts +++ b/packages/monorepo-scripts/src/prepublish_checks.ts @@ -1,9 +1,103 @@ +import * as fs from 'fs'; import * as _ from 'lodash'; +import * as path from 'path'; import { exec as execAsync } from 'promisify-child-process'; import { constants } from './constants'; +import { Changelog, PackageRegistryJson } from './types'; +import { changelogUtils } from './utils/changelog_utils'; +import { npmUtils } from './utils/npm_utils'; +import { semverUtils } from './utils/semver_utils'; import { utils } from './utils/utils'; +async function prepublishChecksAsync(): Promise { + const shouldIncludePrivate = false; + const updatedPublicLernaPackages = await utils.getUpdatedLernaPackagesAsync(shouldIncludePrivate); + + await checkCurrentVersionMatchesLatestPublishedNPMPackageAsync(updatedPublicLernaPackages); + await checkChangelogFormatAsync(updatedPublicLernaPackages); + await checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicLernaPackages); + await checkPublishRequiredSetupAsync(); +} + +async function checkGitTagsForNextVersionAndDeleteIfExistAsync( + updatedPublicLernaPackages: LernaPackage[], +): Promise { + const packageNames = _.map(updatedPublicLernaPackages, lernaPackage => lernaPackage.package.name); + const localGitTags = await utils.getLocalGitTagsAsync(); + const localTagVersionsByPackageName = await utils.getGitTagsByPackageNameAsync(packageNames, localGitTags); + + const remoteGitTags = await utils.getRemoteGitTagsAsync(); + const remoteTagVersionsByPackageName = await utils.getGitTagsByPackageNameAsync(packageNames, remoteGitTags); + + for (const lernaPackage of updatedPublicLernaPackages) { + const currentVersion = lernaPackage.package.version; + const packageName = lernaPackage.package.name; + const packageLocation = lernaPackage.location; + const nextVersion = await utils.getNextPackageVersionAsync(currentVersion, packageName, packageLocation); + + const localTagVersions = localTagVersionsByPackageName[packageName]; + if (_.includes(localTagVersions, nextVersion)) { + const tagName = `${packageName}@${nextVersion}`; + await utils.removeLocalTagAsync(tagName); + } + + const remoteTagVersions = remoteTagVersionsByPackageName[packageName]; + if (_.includes(remoteTagVersions, nextVersion)) { + const tagName = `:refs/tags/${packageName}@${nextVersion}`; + await utils.removeRemoteTagAsync(tagName); + } + } +} + +async function checkCurrentVersionMatchesLatestPublishedNPMPackageAsync( + updatedPublicLernaPackages: LernaPackage[], +): Promise { + for (const lernaPackage of updatedPublicLernaPackages) { + const packageName = lernaPackage.package.name; + const packageVersion = lernaPackage.package.version; + const packageRegistryJsonIfExists = await npmUtils.getPackageRegistryJsonIfExistsAsync(packageName); + if (_.isUndefined(packageRegistryJsonIfExists)) { + continue; // noop for packages not yet published to NPM + } + const allVersionsIncludingUnpublished = npmUtils.getPreviouslyPublishedVersions(packageRegistryJsonIfExists); + const latestNPMVersion = semverUtils.getLatestVersion(allVersionsIncludingUnpublished); + if (packageVersion !== latestNPMVersion) { + throw new Error( + `Found verson ${packageVersion} in package.json but version ${latestNPMVersion} + on NPM (could be unpublished version) for ${packageName}. These versions must match. If you update + the package.json version, make sure to also update the internal dependency versions too.`, + ); + } + } +} + +async function checkChangelogFormatAsync(updatedPublicLernaPackages: LernaPackage[]): Promise { + for (const lernaPackage of updatedPublicLernaPackages) { + const packageName = lernaPackage.package.name; + const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, lernaPackage.location); + + const currentVersion = lernaPackage.package.version; + if (!_.isEmpty(changelog)) { + const lastEntry = changelog[0]; + const doesLastEntryHaveTimestamp = !_.isUndefined(lastEntry.timestamp); + if (semverUtils.lessThan(lastEntry.version, currentVersion)) { + throw new Error( + `CHANGELOG version cannot be below current package version. + Update ${packageName}'s CHANGELOG. It's current version is ${currentVersion} + but the latest CHANGELOG entry is: ${lastEntry.version}`, + ); + } else if (semverUtils.greaterThan(lastEntry.version, currentVersion) && doesLastEntryHaveTimestamp) { + // Remove incorrectly added timestamp + delete changelog[0].timestamp; + // Save updated CHANGELOG.json + await changelogUtils.writeChangelogJsonFileAsync(lernaPackage.location, changelog); + utils.log(`${packageName}: Removed timestamp from latest CHANGELOG.json entry.`); + } + } + } +} + async function checkPublishRequiredSetupAsync(): Promise { // check to see if logged into npm before publishing try { @@ -49,7 +143,7 @@ async function checkPublishRequiredSetupAsync(): Promise { } } -checkPublishRequiredSetupAsync().catch(err => { +prepublishChecksAsync().catch(err => { utils.log(err); process.exit(1); }); diff --git a/packages/monorepo-scripts/src/publish.ts b/packages/monorepo-scripts/src/publish.ts index 455200b95d..2e11c5ba96 100644 --- a/packages/monorepo-scripts/src/publish.ts +++ b/packages/monorepo-scripts/src/publish.ts @@ -109,54 +109,6 @@ package.ts. Please add an entry for it and try again.`, } } -async function checkPublishRequiredSetupAsync(): Promise { - // check to see if logged into npm before publishing - try { - // HACK: for some reason on some setups, the `npm whoami` will not recognize a logged-in user - // unless run with `sudo` (i.e Fabio's NVM setup) but is fine for others (Jacob's NVM setup). - await execAsync(`sudo npm whoami`); - } catch (err) { - utils.log('You must be logged into npm in the commandline to publish. Run `npm login` and try again.'); - return false; - } - - // Check to see if Git personal token setup - if (_.isUndefined(constants.githubPersonalAccessToken)) { - utils.log( - 'You must have a Github personal access token set to an envVar named `GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS`. Add it then try again.', - ); - return false; - } - - // Check Yarn version is 1.X - const result = await execAsync(`yarn --version`); - const version = result.stdout; - const versionSegments = version.split('.'); - const majorVersion = _.parseInt(versionSegments[0]); - if (majorVersion < 1) { - utils.log('Your yarn version must be v1.x or higher. Upgrade yarn and try again.'); - return false; - } - - // Check that `aws` commandline tool is installed - try { - await execAsync(`aws help`); - } catch (err) { - utils.log('You must have `awscli` commandline tool installed. Install it and try again.'); - return false; - } - - // Check that `aws` credentials are setup - try { - await execAsync(`aws sts get-caller-identity`); - } catch (err) { - utils.log('You must setup your AWS credentials by running `aws configure`. Do this and try again.'); - return false; - } - - return true; -} - async function pushChangelogsToGithubAsync(): Promise { await execAsync(`git add . --all`, { cwd: constants.monorepoRootPath }); await execAsync(`git commit -m "Updated CHANGELOGS"`, { cwd: constants.monorepoRootPath }); @@ -168,19 +120,14 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) const packageToVersionChange: PackageToVersionChange = {}; for (const lernaPackage of updatedPublicLernaPackages) { const packageName = lernaPackage.package.name; - const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json'); - const changelogJSON = utils.getChangelogJSONOrCreateIfMissing(changelogJSONPath); - let changelog: Changelog; - try { - changelog = JSON.parse(changelogJSON); - } catch (err) { - throw new Error( - `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, - ); - } + let changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, lernaPackage.location); const currentVersion = lernaPackage.package.version; - const shouldAddNewEntry = changelogUtils.shouldAddNewChangelogEntry(currentVersion, changelog); + const shouldAddNewEntry = changelogUtils.shouldAddNewChangelogEntry( + lernaPackage.package.name, + currentVersion, + changelog, + ); if (shouldAddNewEntry) { // Create a new entry for a patch version with generic changelog entry. const nextPatchVersion = utils.getNextPatchVersion(currentVersion); @@ -209,14 +156,11 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) } // Save updated CHANGELOG.json - fs.writeFileSync(changelogJSONPath, JSON.stringify(changelog, null, '\t')); - await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); + await changelogUtils.writeChangelogJsonFileAsync(lernaPackage.location, changelog); utils.log(`${packageName}: Updated CHANGELOG.json`); // Generate updated CHANGELOG.md const changelogMd = changelogUtils.generateChangelogMd(changelog); - const changelogMdPath = path.join(lernaPackage.location, 'CHANGELOG.md'); - fs.writeFileSync(changelogMdPath, changelogMd); - await utils.prettifyAsync(changelogMdPath, constants.monorepoRootPath); + await changelogUtils.writeChangelogMdFileAsync(lernaPackage.location, changelog); utils.log(`${packageName}: Updated CHANGELOG.md`); } diff --git a/packages/monorepo-scripts/src/types.ts b/packages/monorepo-scripts/src/types.ts index 36fb923b37..61bd757327 100644 --- a/packages/monorepo-scripts/src/types.ts +++ b/packages/monorepo-scripts/src/types.ts @@ -27,3 +27,16 @@ export enum SemVerIndex { export interface PackageToVersionChange { [name: string]: string; } + +export interface PackageRegistryJson { + versions: { + [version: string]: any; + }; + time: { + [version: string]: string; + }; +} + +export interface GitTagsByPackageName { + [packageName: string]: string[]; +} diff --git a/packages/monorepo-scripts/src/utils/changelog_utils.ts b/packages/monorepo-scripts/src/utils/changelog_utils.ts index edfe65a80b..4e09fc8426 100644 --- a/packages/monorepo-scripts/src/utils/changelog_utils.ts +++ b/packages/monorepo-scripts/src/utils/changelog_utils.ts @@ -1,8 +1,15 @@ +import * as fs from 'fs'; import * as _ from 'lodash'; import * as moment from 'moment'; +import * as path from 'path'; +import { exec as execAsync } from 'promisify-child-process'; +import semverSort = require('semver-sort'); +import { constants } from '../constants'; import { Change, Changelog, VersionChangelog } from '../types'; +import { semverUtils } from './semver_utils'; + const CHANGELOG_MD_HEADER = `