diff options
Diffstat (limited to 'packages/monorepo-scripts/src')
-rw-r--r-- | packages/monorepo-scripts/src/constants.ts | 5 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/convert_changelogs.ts | 99 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/globals.d.ts | 2 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/postpublish_utils.ts | 46 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/publish.ts | 211 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/types.ts | 24 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils.ts | 15 |
7 files changed, 396 insertions, 6 deletions
diff --git a/packages/monorepo-scripts/src/constants.ts b/packages/monorepo-scripts/src/constants.ts new file mode 100644 index 000000000..74387a159 --- /dev/null +++ b/packages/monorepo-scripts/src/constants.ts @@ -0,0 +1,5 @@ +import * as path from 'path'; + +export const constants = { + monorepoRootPath: path.join(__dirname, '../../..'), +}; diff --git a/packages/monorepo-scripts/src/convert_changelogs.ts b/packages/monorepo-scripts/src/convert_changelogs.ts new file mode 100644 index 000000000..b5be14ed8 --- /dev/null +++ b/packages/monorepo-scripts/src/convert_changelogs.ts @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * TEMPORARY SCRIPT + * This script exists to migrate the legacy CHANGELOG.md to the canonical CHANGELOG.md + * TODO: Remove after migration is successful and committed. + */ + +import * as fs from 'fs'; +import lernaGetPackages = require('lerna-get-packages'); +import * as _ from 'lodash'; +import * as moment from 'moment'; +import * as path from 'path'; +import { exec as execAsync } from 'promisify-child-process'; + +import { constants } from './constants'; +import { Changelog, Changes, UpdatedPackage } from './types'; +import { utils } from './utils'; + +const HEADER_PRAGMA = '##'; + +(async () => { + const allLernaPackages = lernaGetPackages(constants.monorepoRootPath); + const publicLernaPackages = _.filter(allLernaPackages, pkg => !pkg.package.private); + for (const lernaPackage of publicLernaPackages) { + const changelogMdIfExists = getChangelogMdIfExists(lernaPackage.package.name, lernaPackage.location); + if (_.isUndefined(changelogMdIfExists)) { + throw new Error(`${lernaPackage.package.name} should have CHANGELOG.md b/c it's public. Add one.`); + } + + const lines = changelogMdIfExists.split('\n'); + const changelogs: Changelog[] = []; + let changelog: Changelog = { + version: '', + changes: [], + }; + /** + * Example MD entry: + * ## v0.3.1 - _March 18, 2018_ + * + * * Add TS types for `yargs` (#400) + */ + for (const line of lines) { + if (_.startsWith(line, `${HEADER_PRAGMA} `)) { + let version = line.substr(4).split(' - ')[0]; + if (version === '0.x.x') { + version = utils.getNextPatchVersion(lernaPackage.package.version); + } + const dateStr = line.split('_')[1]; + let date; + if (!_.includes(dateStr, 'TBD')) { + date = moment(dateStr, 'MMMM D, YYYY'); + } + changelog = { + version, + changes: [], + }; + if (!_.isUndefined(date)) { + changelog.timestamp = date.unix(); + } + if (!_.includes(dateStr, 'TBD')) { + changelog.isPublished = true; + } + changelogs.push(changelog); + } else if (_.includes(line, '* ')) { + const note = line.split('* ')[1].split(' (#')[0]; + const prChunk = line.split(' (#')[1]; + let pr; + if (!_.isUndefined(prChunk)) { + pr = prChunk.split(')')[0]; + } + const changes: Changes = { + note, + }; + if (!_.isUndefined(pr)) { + changes.pr = _.parseInt(pr); + } + changelog.changes.push(changes); + } + } + const changelogJSON = JSON.stringify(changelogs, null, 4); + const changelogJSONPath = `${lernaPackage.location}/CHANGELOG.json`; + fs.writeFileSync(changelogJSONPath, changelogJSON); + await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); + } +})().catch(err => { + utils.log(err.stdout); + process.exit(1); +}); + +function getChangelogMdIfExists(packageName: string, location: string): string | undefined { + const changelogPath = path.join(location, 'CHANGELOG.md'); + let changelogMd: string; + try { + changelogMd = fs.readFileSync(changelogPath, 'utf-8'); + return changelogMd; + } catch (err) { + return undefined; + } +} diff --git a/packages/monorepo-scripts/src/globals.d.ts b/packages/monorepo-scripts/src/globals.d.ts index 1d49559f2..c5898d0f5 100644 --- a/packages/monorepo-scripts/src/globals.d.ts +++ b/packages/monorepo-scripts/src/globals.d.ts @@ -1,6 +1,7 @@ declare module 'async-child-process'; declare module 'publish-release'; declare module 'es6-promisify'; +declare module 'semver-diff'; // semver-sort declarations declare module 'semver-sort' { @@ -11,6 +12,7 @@ declare interface LernaPackage { location: string; package: { private?: boolean; + version: string; name: string; main?: string; config?: { diff --git a/packages/monorepo-scripts/src/postpublish_utils.ts b/packages/monorepo-scripts/src/postpublish_utils.ts index 898b00c47..fb1680afd 100644 --- a/packages/monorepo-scripts/src/postpublish_utils.ts +++ b/packages/monorepo-scripts/src/postpublish_utils.ts @@ -1,9 +1,12 @@ import { execAsync } from 'async-child-process'; import * as promisify from 'es6-promisify'; +import * as fs from 'fs'; import * as _ from 'lodash'; +import * as path from 'path'; import * as publishRelease from 'publish-release'; import semverSort = require('semver-sort'); +import { constants } from './constants'; import { utils } from './utils'; const publishReleaseAsync = promisify(publishRelease); @@ -88,23 +91,52 @@ export const postpublishUtils = { ); }, async publishReleaseNotesAsync(cwd: string, packageName: string, version: string, assets: string[]): Promise<void> { + const notes = this.getReleaseNotes(packageName); const releaseName = this.getReleaseName(packageName, version); const tag = this.getTag(packageName, version); - utils.log('POSTPUBLISH: Releasing ', releaseName, '...'); const finalAssets = this.adjustAssetPaths(cwd, assets); + utils.log('POSTPUBLISH: Releasing ', releaseName, '...'); const result = await publishReleaseAsync({ token: githubPersonalAccessToken, owner: '0xProject', repo: '0x-monorepo', tag, name: releaseName, - notes: 'N/A', + notes, draft: false, prerelease: false, reuseRelease: true, reuseDraftOnly: false, assets, }); + this.updateChangelogIsPublished(packageName); + }, + getReleaseNotes(packageName: string) { + const changelogJSONPath = path.join(constants.monorepoRootPath, 'packages', packageName, 'CHANGELOG.json'); + const changelogJSON = fs.readFileSync(changelogJSONPath, 'utf-8'); + const changelogs = JSON.parse(changelogJSON); + const latestLog = changelogs[0]; + if (_.isUndefined(latestLog.isPublished)) { + let notes = ''; + _.each(latestLog.changes, change => { + notes = `* ${change.note}`; + if (change.pr) { + notes += ` (${change.pr})`; + } + notes += `\n`; + }); + return notes; + } + return 'N/A'; + }, + updateChangelogIsPublished(packageName: string) { + const changelogJSONPath = path.join(constants.monorepoRootPath, 'packages', packageName, 'CHANGELOG.json'); + const changelogJSON = fs.readFileSync(changelogJSONPath, 'utf-8'); + const changelogs = JSON.parse(changelogJSON); + const latestLog = changelogs[0]; + latestLog.isPublished = true; + changelogs[0] = latestLog; + fs.writeFileSync(changelogJSONPath, JSON.stringify(changelogs, null, '\t')); }, getTag(packageName: string, version: string) { return `${packageName}@${version}`; @@ -122,14 +154,16 @@ export const postpublishUtils = { }, adjustFileIncludePaths(fileIncludes: string[], cwd: string): string[] { const fileIncludesAdjusted = _.map(fileIncludes, fileInclude => { - let path = _.startsWith(fileInclude, './') ? `${cwd}/${fileInclude.substr(2)}` : `${cwd}/${fileInclude}`; + let includePath = _.startsWith(fileInclude, './') + ? `${cwd}/${fileInclude.substr(2)}` + : `${cwd}/${fileInclude}`; // HACK: tsconfig.json needs wildcard directory endings as `/**/*` // but TypeDoc needs it as `/**` in order to pick up files at the root - if (_.endsWith(path, '/**/*')) { - path = path.slice(0, -2); + if (_.endsWith(includePath, '/**/*')) { + includePath = includePath.slice(0, -2); } - return path; + return includePath; }); return fileIncludesAdjusted; }, diff --git a/packages/monorepo-scripts/src/publish.ts b/packages/monorepo-scripts/src/publish.ts new file mode 100644 index 000000000..240158c77 --- /dev/null +++ b/packages/monorepo-scripts/src/publish.ts @@ -0,0 +1,211 @@ +#!/usr/bin/env node + +import * as fs from 'fs'; +import lernaGetPackages = require('lerna-get-packages'); +import * as _ from 'lodash'; +import * as moment from 'moment'; +import * as path from 'path'; +import { exec as execAsync, spawn } from 'promisify-child-process'; +import semverDiff = require('semver-diff'); +import semverSort = require('semver-sort'); + +import { constants } from './constants'; +import { Changelog, Changes, SemVerIndex, UpdatedPackage } from './types'; +import { utils } from './utils'; + +const IS_DRY_RUN = process.env.IS_DRY_RUN === 'true'; +const TODAYS_TIMESTAMP = moment().unix(); +const LERNA_EXECUTABLE = './node_modules/lerna/bin/lerna.js'; +const semverNameToIndex: { [semver: string]: number } = { + patch: SemVerIndex.Patch, + minor: SemVerIndex.Minor, + major: SemVerIndex.Major, +}; + +(async () => { + const updatedPublicPackages = await getPublicLernaUpdatedPackagesAsync(); + const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name); + + const allLernaPackages = lernaGetPackages(constants.monorepoRootPath); + const updatedPublicLernaPackages = _.filter(allLernaPackages, pkg => { + return _.includes(updatedPackageNames, pkg.package.name); + }); + const updatedPublicLernaPackageNames = _.map(updatedPublicLernaPackages, pkg => pkg.package.name); + utils.log(`Will update CHANGELOGs and publish: \n${updatedPublicLernaPackageNames.join('\n')}\n`); + + const packageToVersionChange: { [name: string]: string } = {}; + for (const lernaPackage of updatedPublicLernaPackages) { + const packageName = lernaPackage.package.name; + const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json'); + const changelogJSON = getChangelogJSONOrCreateIfMissing(lernaPackage.package.name, changelogJSONPath); + let changelogs: Changelog[]; + try { + changelogs = JSON.parse(changelogJSON); + } catch (err) { + throw new Error( + `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, + ); + } + + const currentVersion = lernaPackage.package.version; + const shouldAddNewEntry = shouldAddNewChangelogEntry(changelogs); + if (shouldAddNewEntry) { + // Create a new entry for a patch version with generic changelog entry. + const nextPatchVersion = utils.getNextPatchVersion(currentVersion); + const newChangelogEntry: Changelog = { + timestamp: TODAYS_TIMESTAMP, + version: nextPatchVersion, + changes: [ + { + note: 'Dependencies updated', + }, + ], + }; + changelogs = [newChangelogEntry, ...changelogs]; + packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersion); + } else { + // Update existing entry with timestamp + const lastEntry = changelogs[0]; + if (_.isUndefined(lastEntry.timestamp)) { + lastEntry.timestamp = TODAYS_TIMESTAMP; + } + // Check version number is correct. + const proposedNextVersion = lastEntry.version; + lastEntry.version = updateVersionNumberIfNeeded(currentVersion, proposedNextVersion); + changelogs[0] = lastEntry; + packageToVersionChange[packageName] = semverDiff(currentVersion, lastEntry.version); + } + + // Save updated CHANGELOG.json + fs.writeFileSync(changelogJSONPath, JSON.stringify(changelogs, null, 4)); + await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); + utils.log(`${packageName}: Updated CHANGELOG.json`); + // Generate updated CHANGELOG.md + const changelogMd = generateChangelogMd(changelogs); + const changelogMdPath = path.join(lernaPackage.location, 'CHANGELOG.md'); + fs.writeFileSync(changelogMdPath, changelogMd); + await utils.prettifyAsync(changelogMdPath, constants.monorepoRootPath); + utils.log(`${packageName}: Updated CHANGELOG.md`); + } + + if (!IS_DRY_RUN) { + await execAsync(`git add . --all`, { cwd: constants.monorepoRootPath }); + await execAsync(`git commit -m "Updated CHANGELOGS"`, { cwd: constants.monorepoRootPath }); + await execAsync(`git push`, { cwd: constants.monorepoRootPath }); + utils.log(`Pushed CHANGELOG updates to Github`); + } + + utils.log('Version updates to apply:'); + _.each(packageToVersionChange, (versionChange: string, packageName: string) => { + utils.log(`${packageName} -> ${versionChange}`); + }); + utils.log(`Calling 'lerna publish'...`); + await lernaPublishAsync(packageToVersionChange); +})().catch(err => { + utils.log(err); + process.exit(1); +}); + +async function lernaPublishAsync(packageToVersionChange: { [name: string]: string }) { + // HACK: Lerna publish does not provide a way to specify multiple package versions via + // flags so instead we need to interact with their interactive prompt interface. + const child = spawn('lerna', ['publish', '--registry=https://registry.npmjs.org/'], { + cwd: constants.monorepoRootPath, + }); + child.stdout.on('data', (data: Buffer) => { + const output = data.toString('utf8'); + const isVersionPrompt = _.includes(output, 'Select a new version'); + if (isVersionPrompt) { + const outputStripLeft = output.split('new version for ')[1]; + const packageName = outputStripLeft.split(' ')[0]; + let versionChange = packageToVersionChange[packageName]; + const isPrivatePackage = _.isUndefined(versionChange); + if (isPrivatePackage) { + versionChange = 'patch'; // Always patch updates to private packages. + } + const semVerIndex = semverNameToIndex[versionChange]; + child.stdin.write(`${semVerIndex}\n`); + } + const isFinalPrompt = _.includes(output, 'Are you sure you want to publish the above changes?'); + if (isFinalPrompt && !IS_DRY_RUN) { + child.stdin.write(`y\n`); + } else if (isFinalPrompt && IS_DRY_RUN) { + utils.log( + `Submitted all versions to Lerna but since this is a dry run, did not confirm. You need to CTRL-C to exit.`, + ); + } + }); +} + +async function getPublicLernaUpdatedPackagesAsync(): Promise<UpdatedPackage[]> { + const result = await execAsync(`${LERNA_EXECUTABLE} updated --json`, { cwd: constants.monorepoRootPath }); + const updatedPackages = JSON.parse(result.stdout); + const updatedPublicPackages = _.filter(updatedPackages, updatedPackage => !updatedPackage.private); + return updatedPublicPackages; +} + +function updateVersionNumberIfNeeded(currentVersion: string, proposedNextVersion: string) { + if (proposedNextVersion === currentVersion) { + return utils.getNextPatchVersion(currentVersion); + } + const sortedVersions = semverSort.desc([proposedNextVersion, currentVersion]); + if (sortedVersions[0] !== proposedNextVersion) { + return utils.getNextPatchVersion(currentVersion); + } + return proposedNextVersion; +} + +function getChangelogJSONOrCreateIfMissing(packageName: string, changelogPath: string): string { + let changelogJSON: string; + try { + changelogJSON = fs.readFileSync(changelogPath, 'utf-8'); + return changelogJSON; + } catch (err) { + // If none exists, create new, empty one. + const emptyChangelogJSON = JSON.stringify([], null, 4); + fs.writeFileSync(changelogPath, emptyChangelogJSON); + return emptyChangelogJSON; + } +} + +function shouldAddNewChangelogEntry(changelogs: Changelog[]): boolean { + if (_.isEmpty(changelogs)) { + return true; + } + const lastEntry = changelogs[0]; + return !!lastEntry.isPublished; +} + +function generateChangelogMd(changelogs: Changelog[]): string { + let changelogMd = `<!-- +This file is auto-generated using the monorepo-scripts package. Don't edit directly. +Edit the package's CHANGELOG.json file only. +--> + +CHANGELOG + `; + + _.each(changelogs, changelog => { + if (_.isUndefined(changelog.timestamp)) { + throw new Error( + 'All CHANGELOG.json entries must be updated to include a timestamp before generating their MD version', + ); + } + const date = moment(`${changelog.timestamp}`, 'X').format('MMMM D, YYYY'); + const title = `\n## v${changelog.version} - _${date}_\n\n`; + changelogMd += title; + + let changes = ''; + _.each(changelog.changes, change => { + let line = ` * ${change.note}`; + if (!_.isUndefined(change.pr)) { + line += ` (#${change.pr})`; + } + line += '\n'; + changes += line; + }); + changelogMd += `${changes}`; + }); + + return changelogMd; +} diff --git a/packages/monorepo-scripts/src/types.ts b/packages/monorepo-scripts/src/types.ts new file mode 100644 index 000000000..7adec202f --- /dev/null +++ b/packages/monorepo-scripts/src/types.ts @@ -0,0 +1,24 @@ +export interface UpdatedPackage { + name: string; + version: string; + private: boolean; +} + +export interface Changes { + note: string; + pr?: number; +} + +export interface Changelog { + timestamp?: number; + version: string; + changes: Changes[]; + isPublished?: boolean; +} + +export enum SemVerIndex { + Invalid, + Patch, + Minor, + Major, +} diff --git a/packages/monorepo-scripts/src/utils.ts b/packages/monorepo-scripts/src/utils.ts index 5423cabd9..9aa37e272 100644 --- a/packages/monorepo-scripts/src/utils.ts +++ b/packages/monorepo-scripts/src/utils.ts @@ -1,5 +1,20 @@ +import * as _ from 'lodash'; +import { exec as execAsync, spawn } from 'promisify-child-process'; + export const utils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console }, + getNextPatchVersion(currentVersion: string): string { + const versionSegments = currentVersion.split('.'); + const patch = _.parseInt(_.last(versionSegments) as string); + const newPatch = patch + 1; + const newPatchVersion = `${versionSegments[0]}.${versionSegments[1]}.${newPatch}`; + return newPatchVersion; + }, + async prettifyAsync(filePath: string, cwd: string) { + await execAsync(`prettier --write ${filePath} --config .prettierrc`, { + cwd, + }); + }, }; |