From 8633fa702436cceeafa52ec39a7fabb5b2650c38 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Mon, 18 Jun 2018 16:55:59 +0200 Subject: Add more prepublish checks --- packages/monorepo-scripts/src/prepublish_checks.ts | 96 ++++++++++++++++++++- packages/monorepo-scripts/src/publish.ts | 24 ++---- packages/monorepo-scripts/src/types.ts | 13 +++ .../monorepo-scripts/src/utils/changelog_utils.ts | 55 +++++++++++- packages/monorepo-scripts/src/utils/npm_utils.ts | 28 +++++++ .../monorepo-scripts/src/utils/semver_utils.ts | 56 +++++++++++++ packages/monorepo-scripts/src/utils/utils.ts | 98 +++++++++++++++++----- 7 files changed, 331 insertions(+), 39 deletions(-) create mode 100644 packages/monorepo-scripts/src/utils/npm_utils.ts create mode 100644 packages/monorepo-scripts/src/utils/semver_utils.ts (limited to 'packages/monorepo-scripts/src') diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts index 2c096d8f6..64de56ece 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 { @@ -65,7 +159,7 @@ async function checkPublishRequiredSetupAsync(): Promise { } } -checkPublishRequiredSetupAsync().catch(err => { +prepublishChecksAsync().catch(err => { utils.log(err.message); process.exit(1); }); diff --git a/packages/monorepo-scripts/src/publish.ts b/packages/monorepo-scripts/src/publish.ts index 2efbc8bf2..637512a5a 100644 --- a/packages/monorepo-scripts/src/publish.ts +++ b/packages/monorepo-scripts/src/publish.ts @@ -119,19 +119,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); @@ -160,14 +155,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 36fb923b3..61bd75732 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 edfe65a80..4e09fc842 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 = `