diff options
-rw-r--r-- | packages/monorepo-scripts/README.md | 2 | ||||
-rw-r--r-- | packages/monorepo-scripts/package.json | 7 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/prepublish_checks.ts | 121 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/publish.ts | 46 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/remove_tags.ts | 57 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/types.ts | 13 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/changelog_utils.ts | 53 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/npm_utils.ts | 28 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/utils.ts | 110 |
9 files changed, 321 insertions, 116 deletions
diff --git a/packages/monorepo-scripts/README.md b/packages/monorepo-scripts/README.md index 22b449870..d979e27dc 100644 --- a/packages/monorepo-scripts/README.md +++ b/packages/monorepo-scripts/README.md @@ -8,8 +8,6 @@ This repository contains a few helpful scripts for working with this mono repo. **`yarn find_unused_deps`**: Sometimes we accidentally leave dependencies listed in `package.json` that are no longer being used. This script finds potential dependencies that might no longer be in use. Please verify that it is no longer in use before removing, the `depcheck` package we use under-the-hood doesn't handle some TS quirks perfectly. -**`yarn remove_tags`**: Our publishing script calls `lerna publish` under-the-hood. If this command fails, it might have created new versioned git tags for each package. Removing these manually is tedious, so you can also run this command instead. Before doing so, check to see if `lerna` already created the publish commit. If so, first revert that with `git reset --hard HEAD~1`, then run this command. - **`yarn test:publish`**: Execute a test-run of the publish script. This dry run won't actually publish, nor will it commit/push anything to Github. ## Usage diff --git a/packages/monorepo-scripts/package.json b/packages/monorepo-scripts/package.json index 5a6d7b25a..5fbf7dbdf 100644 --- a/packages/monorepo-scripts/package.json +++ b/packages/monorepo-scripts/package.json @@ -14,12 +14,10 @@ "clean": "shx rm -rf lib", "test:publish": "run-s build script:publish", "find_unused_deps": "run-s build script:find_unused_deps", - "remove_tags": "run-s build script:remove_tags", "script:deps_versions": "node ./lib/deps_versions.js", "script:prepublish_checks": "node ./lib/prepublish_checks.js", "script:publish": "IS_DRY_RUN=true node ./lib/publish.js", - "script:find_unused_deps": "node ./lib/find_unused_dependencies.js", - "script:remove_tags": "node ./lib/remove_tags.js" + "script:find_unused_deps": "node ./lib/find_unused_dependencies.js" }, "repository": { "type": "git", @@ -40,6 +38,7 @@ "make-promises-safe": "^1.1.0", "npm-run-all": "^4.1.2", "shx": "^0.2.2", + "@types/semver": "5.5.0", "tslint": "5.8.0", "typescript": "2.7.1" }, @@ -50,6 +49,7 @@ "es6-promisify": "^5.0.0", "glob": "^7.1.2", "lodash": "^4.17.4", + "isomorphic-fetch": "2.2.1", "moment": "2.21.0", "opn": "^5.3.0", "promisify-child-process": "^1.0.5", @@ -57,6 +57,7 @@ "publish-release": "0xproject/publish-release", "rimraf": "^2.6.2", "semver-diff": "^2.1.0", + "semver": "5.5.0", "semver-sort": "0.0.4" }, "publishConfig": { diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts index 2c096d8f6..1c4ee1fc6 100644 --- a/packages/monorepo-scripts/src/prepublish_checks.ts +++ b/packages/monorepo-scripts/src/prepublish_checks.ts @@ -1,9 +1,128 @@ 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 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> { + 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 { @@ -65,7 +184,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..72c6c0a71 100644 --- a/packages/monorepo-scripts/src/publish.ts +++ b/packages/monorepo-scripts/src/publish.ts @@ -1,18 +1,17 @@ #!/usr/bin/env node import * as promisify from 'es6-promisify'; -import * as fs from 'fs'; import * as _ from 'lodash'; import * as moment from 'moment'; import opn = require('opn'); -import * as path from 'path'; import { exec as execAsync, spawn } from 'promisify-child-process'; import * as prompt from 'prompt'; +import semver = require('semver'); import semverDiff = require('semver-diff'); import semverSort = require('semver-sort'); import { constants } from './constants'; -import { Changelog, PackageToVersionChange, SemVerIndex, VersionChangelog } from './types'; +import { PackageToVersionChange, SemVerIndex, VersionChangelog } from './types'; import { changelogUtils } from './utils/changelog_utils'; import { utils } from './utils/utils'; @@ -119,25 +118,23 @@ 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); + const nextPatchVersionIfValid = semver.inc(currentVersion, 'patch'); + if (_.isNull(nextPatchVersionIfValid)) { + throw new Error(`Encountered invalid semver version: ${currentVersion} for package: ${packageName}`); + } const newChangelogEntry: VersionChangelog = { timestamp: TODAYS_TIMESTAMP, - version: nextPatchVersion, + version: nextPatchVersionIfValid, changes: [ { note: 'Dependencies updated', @@ -145,7 +142,7 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) ], }; changelog = [newChangelogEntry, ...changelog]; - packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersion); + packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersionIfValid); } else { // Update existing entry with timestamp const lastEntry = changelog[0]; @@ -160,14 +157,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, changelogMd); utils.log(`${packageName}: Updated CHANGELOG.md`); } @@ -217,12 +211,16 @@ async function lernaPublishAsync(packageToVersionChange: { [name: string]: strin } function updateVersionNumberIfNeeded(currentVersion: string, proposedNextVersion: string): string { + const updatedVersionIfValid = semver.inc(currentVersion, 'patch'); + if (_.isNull(updatedVersionIfValid)) { + throw new Error(`Encountered invalid semver: ${currentVersion}`); + } if (proposedNextVersion === currentVersion) { - return utils.getNextPatchVersion(currentVersion); + return updatedVersionIfValid; } const sortedVersions = semverSort.desc([proposedNextVersion, currentVersion]); if (sortedVersions[0] !== proposedNextVersion) { - return utils.getNextPatchVersion(currentVersion); + return updatedVersionIfValid; } return proposedNextVersion; } diff --git a/packages/monorepo-scripts/src/remove_tags.ts b/packages/monorepo-scripts/src/remove_tags.ts deleted file mode 100644 index 50e413495..000000000 --- a/packages/monorepo-scripts/src/remove_tags.ts +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node - -import * as _ from 'lodash'; -import * as path from 'path'; -import { exec as execAsync } from 'promisify-child-process'; -import semverSort = require('semver-sort'); - -import { constants } from './constants'; -import { Changelog } from './types'; -import { utils } from './utils/utils'; - -(async () => { - const shouldIncludePrivate = true; - const updatedPublicLernaPackages = await utils.getUpdatedLernaPackagesAsync(shouldIncludePrivate); - - for (const lernaPackage of updatedPublicLernaPackages) { - const packageName = lernaPackage.package.name; - const currentVersion = lernaPackage.package.version; - const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json'); - // Private packages don't have changelogs, and their versions are always incremented - // by a patch version. - const changelogJSONIfExists = utils.getChangelogJSONIfExists(changelogJSONPath); - - let latestChangelogVersion: string; - if (!_.isUndefined(changelogJSONIfExists)) { - let changelogs: Changelog; - try { - changelogs = JSON.parse(changelogJSONIfExists); - } catch (err) { - throw new Error( - `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, - ); - } - latestChangelogVersion = changelogs[0].version; - } else { - latestChangelogVersion = utils.getNextPatchVersion(currentVersion); - } - - const sortedVersions = semverSort.desc([latestChangelogVersion, currentVersion]); - if (sortedVersions[0] === latestChangelogVersion && latestChangelogVersion !== currentVersion) { - const tagName = `${packageName}@${latestChangelogVersion}`; - try { - await execAsync(`git tag -d ${tagName}`, { cwd: constants.monorepoRootPath }); - utils.log(`removed tag: ${tagName}`); - } catch (err) { - if (_.includes(err.message, 'not found')) { - utils.log(`Could not find tag: ${tagName}`); - } else { - throw err; - } - } - } - } -})().catch(err => { - utils.log(err); - process.exit(1); -}); 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..dbafb6f16 100644 --- a/packages/monorepo-scripts/src/utils/changelog_utils.ts +++ b/packages/monorepo-scripts/src/utils/changelog_utils.ts @@ -1,6 +1,11 @@ +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 semver = require('semver'); +import { constants } from '../constants'; import { Change, Changelog, VersionChangelog } from '../types'; const CHANGELOG_MD_HEADER = ` @@ -44,12 +49,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 (semver.lt(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, changelogMdString: string): Promise<void> { + const changelogMarkdownPath = path.join(packageLocation, 'CHANGELOG.md'); + fs.writeFileSync(changelogMarkdownPath, changelogMdString); + 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/utils.ts b/packages/monorepo-scripts/src/utils/utils.ts index 0b8ac4c0b..20bc57bae 100644 --- a/packages/monorepo-scripts/src/utils/utils.ts +++ b/packages/monorepo-scripts/src/utils/utils.ts @@ -1,27 +1,17 @@ -import * as fs from 'fs'; import lernaGetPackages = require('lerna-get-packages'); import * as _ from 'lodash'; import { exec as execAsync } from 'promisify-child-process'; +import semver = require('semver'); 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 { 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): 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 +33,86 @@ 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 nextVersionIfValid; + const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, packageLocation); + if (_.isEmpty(changelog)) { + nextVersionIfValid = semver.inc(currentVersion, 'patch'); + } + const lastEntry = changelog[0]; + nextVersionIfValid = semver.eq(lastEntry.version, currentVersion) + ? semver.inc(currentVersion, 'patch') + : lastEntry.version; + if (_.isNull(nextVersionIfValid)) { + throw new Error(`Encountered invalid semver: ${currentVersion} associated with ${packageName}`); } + return nextVersionIfValid; }, - 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 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; + }, + 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; }, }; |