aboutsummaryrefslogtreecommitdiffstats
path: root/packages/monorepo-scripts/src/prepublish_checks.ts
blob: 3b4ff9082e149ea81a11d5472dfded1a064d68f4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import * as _ from 'lodash';
import { exec as execAsync } from 'promisify-child-process';
import semver = require('semver');
import semverSort = require('semver-sort');

import { constants } from './constants';
import { changelogUtils } from './utils/changelog_utils';
import { npmUtils } from './utils/npm_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 remoteTagVersions = remoteTagVersionsByPackageName[packageName];
        if (_.includes(remoteTagVersions, nextVersion)) {
            const tagName = `:refs/tags/${packageName}@${nextVersion}`;
            await utils.removeRemoteTagAsync(tagName);
        }

        const localTagVersions = localTagVersionsByPackageName[packageName];
        if (_.includes(localTagVersions, nextVersion)) {
            const tagName = `${packageName}@${nextVersion}`;
            await utils.removeLocalTagAsync(tagName);
        }
    }
}

async function checkCurrentVersionMatchesLatestPublishedNPMPackageAsync(
    updatedPublicLernaPackages: LernaPackage[],
): Promise<void> {
    utils.log('Check package versions against npmjs.org...');
    const versionMismatches = [];
    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 sortedVersions = semverSort.desc(allVersionsIncludingUnpublished);
        const latestNPMVersion = sortedVersions[0];
        if (packageVersion !== latestNPMVersion) {
            versionMismatches.push({
                packageJsonVersion: packageVersion,
                npmVersion: latestNPMVersion,
                packageName,
            });
        }
    }
    if (!_.isEmpty(versionMismatches)) {
        utils.log(`Found version mismatches between package.json and NPM published versions (might be unpublished).`);
        _.each(versionMismatches, versionMismatch => {
            utils.log(
                `${versionMismatch.packageName}: ${versionMismatch.packageJsonVersion} package.json, ${
                    versionMismatch.npmVersion
                } on NPM`,
            );
        });
        throw new Error(`Please fix the above package.json/NPM inconsistencies.`);
    }
}

async function checkChangelogFormatAsync(updatedPublicLernaPackages: LernaPackage[]): Promise<void> {
    utils.log('Check CHANGELOGs for inconsistencies...');
    const changeLogInconsistencies = [];
    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 (semver.lt(lastEntry.version, currentVersion)) {
                changeLogInconsistencies.push({
                    packageJsonVersion: currentVersion,
                    changelogVersion: lastEntry.version,
                    packageName,
                });
            } else if (semver.gt(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.`);
            }
        }
    }
    if (!_.isEmpty(changeLogInconsistencies)) {
        utils.log(`CHANGELOG versions cannot below package.json versions:`);
        _.each(changeLogInconsistencies, inconsistency => {
            utils.log(
                `${inconsistency.packageName}: ${inconsistency.packageJsonVersion} package.json, ${
                    inconsistency.changelogVersion
                } CHANGELOG.json`,
            );
        });
        throw new Error('Fix the above inconsistencies to continue.');
    }
}

async function checkPublishRequiredSetupAsync(): Promise<void> {
    // 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).
        utils.log('Checking that the user is logged in on npm...');
        await execAsync(`sudo npm whoami`);
    } catch (err) {
        throw new Error('You must be logged into npm in the commandline to publish. Run `npm login` and try again.');
    }

    // Check to see if Git personal token setup
    if (_.isUndefined(constants.githubPersonalAccessToken)) {
        throw new Error(
            'You must have a Github personal access token set to an envVar named `GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS`. Add it then try again.',
        );
    }

    // Check Yarn version is 1.X
    utils.log('Checking the yarn version...');
    const result = await execAsync(`yarn --version`);
    const version = result.stdout;
    const versionSegments = version.split('.');
    const majorVersion = _.parseInt(versionSegments[0]);
    if (majorVersion < 1) {
        throw new Error('Your yarn version must be v1.x or higher. Upgrade yarn and try again.');
    }

    // Check that `aws` commandline tool is installed
    try {
        utils.log('Checking that aws CLI tool is installed...');
        await execAsync(`aws help`);
    } catch (err) {
        throw new Error('You must have `awscli` commandline tool installed. Install it and try again.');
    }

    // Check that `aws` credentials are setup
    try {
        utils.log('Checking that aws credentials are configured...');
        await execAsync(`aws sts get-caller-identity`);
    } catch (err) {
        throw new Error('You must setup your AWS credentials by running `aws configure`. Do this and try again.');
    }

    utils.log('Checking that git branch is up to date with upstream...');
    await execAsync('git fetch');
    const res = await execAsync('git status -bs'); // s - short format, b - branch info
    /**
     * Possible outcomes
     * ## branch_name...origin/branch_name [behind n]
     * ## branch_name...origin/branch_name [ahead n]
     * ## branch_name...origin/branch_name
     */
    const gitShortStatusHeader = res.stdout.split('\n')[0];
    if (gitShortStatusHeader.includes('behind')) {
        throw new Error('Your branch is behind upstream. Please pull before publishing.');
    } else if (gitShortStatusHeader.includes('ahead')) {
        throw new Error('Your branch is ahead of upstream. Please push before publishing.');
    }
}

prepublishChecksAsync().catch(err => {
    utils.log(err.message);
    process.exit(1);
});