diff options
| author | Fabio Berger <me@fabioberger.com> | 2018-06-18 22:55:59 +0800 | 
|---|---|---|
| committer | Fabio Berger <me@fabioberger.com> | 2018-06-19 05:06:32 +0800 | 
| commit | 8633fa702436cceeafa52ec39a7fabb5b2650c38 (patch) | |
| tree | 0702ff5b590396e86f71508a16a87b7440ffe0c0 /packages/monorepo-scripts/src | |
| parent | 19668b9b48eb08645f500dd8453b8cb2f7abc400 (diff) | |
| download | dexon-sol-tools-8633fa702436cceeafa52ec39a7fabb5b2650c38.tar.gz dexon-sol-tools-8633fa702436cceeafa52ec39a7fabb5b2650c38.tar.zst dexon-sol-tools-8633fa702436cceeafa52ec39a7fabb5b2650c38.zip | |
Add more prepublish checks
Diffstat (limited to 'packages/monorepo-scripts/src')
| -rw-r--r-- | packages/monorepo-scripts/src/prepublish_checks.ts | 96 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/publish.ts | 24 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/types.ts | 13 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/utils/changelog_utils.ts | 55 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/utils/npm_utils.ts | 28 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/utils/semver_utils.ts | 56 | ||||
| -rw-r--r-- | packages/monorepo-scripts/src/utils/utils.ts | 98 | 
7 files changed, 331 insertions, 39 deletions
| 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<void> { +    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<void> { +    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<void> { +    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<void> { +    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<void> {      // check to see if logged into npm before publishing      try { @@ -65,7 +159,7 @@ async function checkPublishRequiredSetupAsync(): Promise<void> {      }  } -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 = `  <!--  This file is auto-generated using the monorepo-scripts package. Don't edit directly. @@ -44,12 +51,58 @@ export const changelogUtils = {          return changelogMd;      }, -    shouldAddNewChangelogEntry(currentVersion: string, changelog: Changelog): boolean { +    shouldAddNewChangelogEntry(packageName: string, currentVersion: string, changelog: Changelog): boolean {          if (_.isEmpty(changelog)) {              return true;          }          const lastEntry = changelog[0]; +        if (semverUtils.lessThan(lastEntry.version, currentVersion)) { +            throw new Error( +                `Found CHANGELOG version lower then current package version. ${packageName} current: ${currentVersion}, Changelog: ${ +                    lastEntry.version +                }`, +            ); +        }          const isLastEntryCurrentVersion = lastEntry.version === currentVersion;          return isLastEntryCurrentVersion;      }, +    getChangelogJSONIfExists(changelogPath: string): string | undefined { +        try { +            const changelogJSON = fs.readFileSync(changelogPath, 'utf-8'); +            return changelogJSON; +        } catch (err) { +            return undefined; +        } +    }, +    getChangelogOrCreateIfMissing(packageName: string, packageLocation: string): Changelog { +        const changelogJSONPath = path.join(packageLocation, 'CHANGELOG.json'); +        let changelogJsonIfExists = this.getChangelogJSONIfExists(changelogJSONPath); +        if (_.isUndefined(changelogJsonIfExists)) { +            // If none exists, create new, empty one. +            changelogJsonIfExists = '[]'; +            fs.writeFileSync(changelogJSONPath, changelogJsonIfExists); +        } +        let changelog: Changelog; +        try { +            changelog = JSON.parse(changelogJsonIfExists); +        } catch (err) { +            throw new Error(`${packageName}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`); +        } +        return changelog; +    }, +    async writeChangelogJsonFileAsync(packageLocation: string, changelog: Changelog): Promise<void> { +        const changelogJSONPath = path.join(packageLocation, 'CHANGELOG.json'); +        fs.writeFileSync(changelogJSONPath, JSON.stringify(changelog, null, '\t')); +        await this.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); +    }, +    async writeChangelogMdFileAsync(packageLocation: string, changelog: Changelog): Promise<void> { +        const changelogMarkdownPath = path.join(packageLocation, 'CHANGELOG.md'); +        fs.writeFileSync(changelogMarkdownPath, JSON.stringify(changelog, null, '\t')); +        await this.prettifyAsync(changelogMarkdownPath, constants.monorepoRootPath); +    }, +    async prettifyAsync(filePath: string, cwd: string): Promise<void> { +        await execAsync(`prettier --write ${filePath} --config .prettierrc`, { +            cwd, +        }); +    },  }; diff --git a/packages/monorepo-scripts/src/utils/npm_utils.ts b/packages/monorepo-scripts/src/utils/npm_utils.ts new file mode 100644 index 000000000..cc1e046e7 --- /dev/null +++ b/packages/monorepo-scripts/src/utils/npm_utils.ts @@ -0,0 +1,28 @@ +import 'isomorphic-fetch'; +import * as _ from 'lodash'; + +import { PackageRegistryJson } from '../types'; + +const NPM_REGISTRY_BASE_URL = 'https://registry.npmjs.org'; +const SUCCESS_STATUS = 200; +const NOT_FOUND_STATUS = 404; + +export const npmUtils = { +    async getPackageRegistryJsonIfExistsAsync(packageName: string): Promise<PackageRegistryJson | undefined> { +        const url = `${NPM_REGISTRY_BASE_URL}/${packageName}`; +        const response = await fetch(url); + +        if (response.status === NOT_FOUND_STATUS) { +            return undefined; +        } else if (response.status !== SUCCESS_STATUS) { +            throw new Error(`Request to ${url} failed. Check your internet connection and that npmjs.org is up.`); +        } +        const packageRegistryJson = await response.json(); +        return packageRegistryJson; +    }, +    getPreviouslyPublishedVersions(packageRegistryJson: PackageRegistryJson): string[] { +        const timeWithOnlyVersions = _.omit(packageRegistryJson.time, ['modified', 'created']); +        const versions = _.keys(timeWithOnlyVersions); +        return versions; +    }, +}; diff --git a/packages/monorepo-scripts/src/utils/semver_utils.ts b/packages/monorepo-scripts/src/utils/semver_utils.ts new file mode 100644 index 000000000..d5c6b2d17 --- /dev/null +++ b/packages/monorepo-scripts/src/utils/semver_utils.ts @@ -0,0 +1,56 @@ +import * as _ from 'lodash'; +import semverSort = require('semver-sort'); + +// Regex that matches semantic versions only including digits and dots. +const SEM_VER_REGEX = /^(\d+\.){1}(\d+\.){1}(\d+){1}$/gm; + +export const semverUtils = { +    /** +     * Checks whether version a is lessThan version b. Supplied versions must be +     * Semantic Versions containing only numbers and dots (e.g 1.4.0). +     * @param a version of interest +     * @param b version to compare a against +     * @return Whether version a is lessThan version b +     */ +    lessThan(a: string, b: string): boolean { +        this.assertValidSemVer('a', a); +        this.assertValidSemVer('b', b); +        if (a === b) { +            return false; +        } +        const sortedVersions = semverSort.desc([a, b]); +        const isALessThanB = sortedVersions[0] === b; +        return isALessThanB; +    }, +    /** +     * Checks whether version a is greaterThan version b. Supplied versions must be +     * Semantic Versions containing only numbers and dots (e.g 1.4.0). +     * @param a version of interest +     * @param b version to compare a against +     * @return Whether version a is greaterThan version b +     */ +    greaterThan(a: string, b: string): boolean { +        this.assertValidSemVer('a', a); +        this.assertValidSemVer('b', b); +        if (a === b) { +            return false; +        } +        const sortedVersions = semverSort.desc([a, b]); +        const isAGreaterThanB = sortedVersions[0] === a; +        return isAGreaterThanB; +    }, +    assertValidSemVer(variableName: string, version: string): void { +        if (!version.match(SEM_VER_REGEX)) { +            throw new Error( +                `SemVer versions should only contain numbers and dots. Encountered: ${variableName} = ${version}`, +            ); +        } +    }, +    getLatestVersion(versions: string[]): string { +        _.each(versions, version => { +            this.assertValidSemVer('version', version); +        }); +        const sortedVersions = semverSort.desc(versions); +        return sortedVersions[0]; +    }, +}; diff --git a/packages/monorepo-scripts/src/utils/utils.ts b/packages/monorepo-scripts/src/utils/utils.ts index 0b8ac4c0b..93de0d940 100644 --- a/packages/monorepo-scripts/src/utils/utils.ts +++ b/packages/monorepo-scripts/src/utils/utils.ts @@ -1,10 +1,11 @@ -import * as fs from 'fs';  import lernaGetPackages = require('lerna-get-packages');  import * as _ from 'lodash';  import { exec as execAsync } from 'promisify-child-process';  import { constants } from '../constants'; -import { UpdatedPackage } from '../types'; +import { GitTagsByPackageName, UpdatedPackage } from '../types'; + +import { changelogUtils } from './changelog_utils';  export const utils = {      log(...args: any[]): void { @@ -17,11 +18,6 @@ export const utils = {          const newPatchVersion = `${versionSegments[0]}.${versionSegments[1]}.${newPatch}`;          return newPatchVersion;      }, -    async prettifyAsync(filePath: string, cwd: string): Promise<void> { -        await execAsync(`prettier --write ${filePath} --config .prettierrc`, { -            cwd, -        }); -    },      async getUpdatedLernaPackagesAsync(shouldIncludePrivate: boolean): Promise<LernaPackage[]> {          const updatedPublicPackages = await this.getLernaUpdatedPackagesAsync(shouldIncludePrivate);          const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name); @@ -43,22 +39,82 @@ export const utils = {          }          return updatedPackages;      }, -    getChangelogJSONIfExists(changelogPath: string): string | undefined { -        try { -            const changelogJSON = fs.readFileSync(changelogPath, 'utf-8'); -            return changelogJSON; -        } catch (err) { -            return undefined; +    async getNextPackageVersionAsync( +        currentVersion: string, +        packageName: string, +        packageLocation: string, +    ): Promise<string> { +        let nextVersion; +        const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, packageLocation); +        if (_.isEmpty(changelog)) { +            nextVersion = this.getNextPatchVersion(currentVersion);          } +        const lastEntry = changelog[0]; +        nextVersion = +            lastEntry.version === currentVersion ? this.getNextPatchVersion(currentVersion) : lastEntry.version; +        return nextVersion; +    }, +    async getRemoteGitTagsAsync(): Promise<string[]> { +        const result = await execAsync(`git ls-remote --tags`, { +            cwd: constants.monorepoRootPath, +        }); +        const tagsString = result.stdout; +        const tagOutputs: string[] = tagsString.split('\n'); +        const tags = _.compact( +            _.map(tagOutputs, tagOutput => { +                const tag = tagOutput.split('refs/tags/')[1]; +                // Tags with `^{}` are duplicateous so we ignore them +                // Source: https://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name +                if (_.endsWith(tag, '^{}')) { +                    return undefined; +                } +                return tag; +            }), +        ); +        return tags; +    }, +    async getLocalGitTagsAsync(): Promise<string[]> { +        const result = await execAsync(`git tags`, { +            cwd: constants.monorepoRootPath, +        }); +        const tagsString = result.stdout; +        const tags = tagsString.split('\n'); +        return tags;      }, -    getChangelogJSONOrCreateIfMissing(changelogPath: string): string { -        const changelogIfExists = this.getChangelogJSONIfExists(changelogPath); -        if (_.isUndefined(changelogIfExists)) { -            // If none exists, create new, empty one. -            const emptyChangelogJSON = JSON.stringify([]); -            fs.writeFileSync(changelogPath, emptyChangelogJSON); -            return emptyChangelogJSON; +    async getGitTagsByPackageNameAsync(packageNames: string[], gitTags: string[]): Promise<GitTagsByPackageName> { +        const tagVersionByPackageName: GitTagsByPackageName = {}; +        _.each(gitTags, tag => { +            const packageNameIfExists = _.find(packageNames, name => { +                return _.includes(tag, `${name}@`); +            }); +            if (_.isUndefined(packageNameIfExists)) { +                return; // ignore tags not related to a package we care about. +            } +            const splitTag = tag.split(`${packageNameIfExists}@`); +            if (splitTag.length !== 2) { +                throw new Error(`Unexpected tag name found: ${tag}`); +            } +            const version = splitTag[1]; +            (tagVersionByPackageName[packageNameIfExists] || (tagVersionByPackageName[packageNameIfExists] = [])).push( +                version, +            ); +        }); +        return tagVersionByPackageName; +    }, +    async removeLocalTagAsync(tagName: string): Promise<void> { +        const result = await execAsync(`git tag -d ${tagName}`, { +            cwd: constants.monorepoRootPath, +        }); +        if (!_.isEmpty(result.stderr)) { +            throw new Error(`Failed to delete local git tag. Got err: ${result.stderr}`); +        } +    }, +    async removeRemoteTagAsync(tagName: string): Promise<void> { +        const result = await execAsync(`git push origin ${tagName}`, { +            cwd: constants.monorepoRootPath, +        }); +        if (!_.isEmpty(result.stderr)) { +            throw new Error(`Failed to delete remote git tag. Got err: ${result.stderr}`);          } -        return changelogIfExists;      },  }; | 
