diff --git a/package.json b/package.json index 09adf9b..1bd5138 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,15 @@ "js-yaml": "^4.1.0", "loglevel": "^1.7.1", "semver": "^7.3.5", - "yargs": "^17.0.1" + "yargs": "^17.5.1" }, "devDependencies": { "@octokit/types": "^6.18.1", + "@types/cli-color": "^2.0.2", "@types/jest": "^26.0.24", "@types/node": "^16.3.0", "@types/semver": "^7.3.8", + "@types/yargs": "^17.0.13", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", "eslint": "^7.31.0", diff --git a/src/changelog.ts b/src/changelog.ts index 34bcd99..81c5243 100644 --- a/src/changelog.ts +++ b/src/changelog.ts @@ -40,10 +40,10 @@ async function* readChangelog(project: Project): AsyncGenerator const fp = fs.createReadStream(path.join(project.dir, 'CHANGELOG.md')); const rl = readline.createInterface(fp); - let version; + let version: string; let fullText = ''; for await (const line of rl) { - const matches = /^Changes in \[([\d\w.-]+)\]/.exec(line); + const matches = /^Changes in \[([\w.-]+)]/.exec(line); if (matches) { if (version) { yield { @@ -107,7 +107,7 @@ function sanitiseMarkdown(text: string): string { return text; } -function engJoin(things): string { +function engJoin(things: string[]): string { if (things.length === 1) return things[0]; const firstLot = things.slice(0, things.length - 2); @@ -140,20 +140,22 @@ export function makeChangeEntry(change: IChange, forProject: IProject): string { return line; } -function makeChangelogEntry(changes: IChange[], version: string, forProject: Project): string { - const formattedVersion = semver.parse(version).format(); // easy way of removing the leading 'v' +function makeChangelogEntry(changes: IChange[], version: string | null, forProject: Project): string { + const formattedVersion = version ? semver.parse(version).format() : null; // easy way of removing the leading 'v' const now = new Date(); - const lines = []; + const lines: string[] = []; - const padTwo = n => String(n).padStart(2, '0'); - lines.push(`Changes in ` + - `[${formattedVersion}]` + - `(https://github.com/${forProject.owner}/${forProject.repo}/releases/tag/v${formattedVersion}) ` + - `(${now.getFullYear()}-${padTwo(now.getMonth()+1)}-${padTwo(now.getDate())})`, - ); - lines.push('='.repeat(lines[0].length)); - lines.push(''); + if (version !== null) { + const padTwo = (n: number) => String(n).padStart(2, '0'); + lines.push(`Changes in ` + + `[${formattedVersion}]` + + `(https://github.com/${forProject.owner}/${forProject.repo}/releases/tag/v${formattedVersion}) ` + + `(${now.getFullYear()}-${padTwo(now.getMonth()+1)}-${padTwo(now.getDate())})`, + ); + lines.push('='.repeat(lines[0].length)); + lines.push(''); + } const shouldInclude = changes.filter(c => c.shouldInclude); const breaking = shouldInclude.filter(c => c.breaking); @@ -218,6 +220,10 @@ function isPrereleaseFor(version: SemVer, forVersion: SemVer): boolean { ); } +export async function previewChangelog(project: Project, changes: IChange[]) { + console.log(makeChangelogEntry(changes, null, project)); +} + export async function updateChangelog(project: Project, changes: IChange[], forVersion: string) { const forReleaseSemVer = semver.parse(forVersion); @@ -233,6 +239,15 @@ export async function updateChangelog(project: Project, changes: IChange[], forV // This is the exact version we should be updating: replace it await outHandle.write(makeChangelogEntry(changes, forVersion, project)); changeWritten = true; + } else if (isPrereleaseFor(semver.parse(entry.version), forReleaseSemVer)) { + log.debug(`Found ${entry.version} which is a prerelease of the version we should be updating`); + // This is a prerelease of the version we're trying to write, so remove the + // prerelease entry from the changelog and replace it with the entry we're + // writing, if we haven't already written it + if (!changeWritten) { + await outHandle.write(makeChangelogEntry(changes, forVersion, project)); + changeWritten = true; + } } else if (forReleaseSemVer.compare(entry.version) === 1) { // This one comes before the one we're updating, so if we haven't yet written // our changeset, we need to do it now. @@ -243,14 +258,6 @@ export async function updateChangelog(project: Project, changes: IChange[], forV } // and then write the one we found too await outHandle.write(entry.text); - } else if (isPrereleaseFor(semver.parse(entry.version), forReleaseSemVer)) { - log.debug(`Found ${entry.version} which is a prerelease of the version we should be updating`); - // This is a prerelease of the version we're trying to write, so remove the - // prerelease entry from the changelog and replace it with the entry we're - // writing, if we haven't already written it - if (!changeWritten) { - await outHandle.write(makeChangelogEntry(changes, forVersion, project)); - } } else { log.debug(`Found ${entry.version} which is newer than the version we should be updating`); await outHandle.write(entry.text); diff --git a/src/index.ts b/src/index.ts index 99a2bc0..7915400 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ limitations under the License. */ import log from 'loglevel'; -import yargs from 'yargs/yargs'; +import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import clc from 'cli-color'; import semver from 'semver'; @@ -31,7 +31,7 @@ import { import { getLatestRelease, getReleaseBefore, getReleases, releasesContains } from "./releases"; import { ChangesByProject, getPackageJsonAtVersion, Project, branchExists, BranchMode } from './projects'; import { formatIssue } from './issue'; -import { updateChangelog } from './changelog'; +import { previewChangelog, updateChangelog } from './changelog'; import { Octokit } from '@octokit/rest'; function formatChangeType(changeType: ChangeType) { @@ -74,21 +74,35 @@ function printChangeStatus(change: IChange, projectName: string, owner: string, } async function main() { - const args = yargs(hideBin(process.argv)).option('debug', { - alias: 'd', - type: 'boolean', - description: "Enable debug mode", - }).option('check', { - type: 'boolean', - description: "Don't update changelog, just output information on what changes would be included", - }).help().usage("Usage: $0 [-d] [--check] ").argv; - - if (args._.length !== 1 && !args.check) { + const args = yargs(hideBin(process.argv)).version(false).options({ + "debug": { + alias: 'd', + type: 'boolean', + description: "Enable debug mode", + }, + "check": { + type: 'boolean', + description: "Don't update changelog, just output information on what changes would be included", + conflicts: ["preview"], + }, + "preview": { + type: "boolean", + description: "Generate changelog as normal, but without version header and output to STDOUT.", + conflicts: ["check"], + }, + }).command("* [version]", "Generate changelog for the given version", yargs => ( + yargs.positional("version", { + description: "The version to generate the changelog for, " + + "required if --check and/or --preview are not specified.", + type: "string", + }) + )).help().parseSync(); + + if (!args.version && !args.check && !args.preview) { // Surely yargs should be able to do this? It seems incredibly confusing and I already regret using it console.log("No version specified"); return; } - const targetRelease = args._[0] as string; if (args.debug) { log.setLevel(log.levels.DEBUG); @@ -109,26 +123,33 @@ async function main() { let fromVer: string; let toVer: string; - if (targetRelease) { - const targetReleaseSemVer = semver.parse(targetRelease); + if (args.version) { + const targetReleaseSemVer = semver.parse(args.version); const targetIsPrerelease = targetReleaseSemVer.prerelease.length > 0; const toVerReleaseBranch = `release-v${targetReleaseSemVer.major}.${targetReleaseSemVer.minor}.${targetReleaseSemVer.patch}`; - if (releasesContains(rels, targetRelease)) { - log.debug("Found existing release for " + targetRelease); + if (releasesContains(rels, args.version)) { + log.debug("Found existing release for " + args.version); // nb. getReleases only gets the most recent 100 so this won't work // for older releases - fromVer = getReleaseBefore(rels, targetRelease, targetIsPrerelease).name; - toVer = targetRelease; - } else if (targetRelease !== 'develop' && await branchExists(dir, toVerReleaseBranch)) { - log.debug("Found release branch for " + targetRelease); + fromVer = getReleaseBefore(rels, args.version, targetIsPrerelease).name; + toVer = args.version; + } else if (args.version !== 'develop' && await branchExists(dir, toVerReleaseBranch)) { + log.debug("Found release branch for " + args.version); // 'to' release has had a release branch cut but not yet a full release // compare to the tip of the release branch fromVer = getLatestRelease(rels, targetIsPrerelease).name; toVer = toVerReleaseBranch; branchMode = BranchMode.Release; + } else if (args.version !== 'develop' && await branchExists(dir, "staging")) { + log.debug("Found release branch for " + args.version); + // 'to' release has had a release branch cut but not yet a full release + // compare to the tip of the release branch + fromVer = getLatestRelease(rels, targetIsPrerelease).name; + toVer = "staging"; + branchMode = BranchMode.Release; } else { - log.debug("Found neither release nor branch for " + targetRelease); + log.debug("Found neither release nor branch for " + args.version); // the 'to' release is an doesn't-yet-exist future release - // compare to the tip of develop (a better piece of software // might make this configurable...) @@ -177,7 +198,7 @@ async function main() { const numBreaking = allChanges.filter(c => c.breaking).length; const numFeatures = allChanges.filter(c => c.changeType == ChangeType.FEATURE).length; - let suggestedBumpType; + let suggestedBumpType: "major" | "minor" | "patch"; if (numBreaking) { suggestedBumpType = 'major'; } else if (numFeatures) { @@ -193,8 +214,13 @@ async function main() { return; } - log.debug("Updating changelog entry for " + targetRelease); - await updateChangelog(project, allChanges, targetRelease); + if (args.preview) { + await previewChangelog(project, allChanges); + return; + } + + log.debug("Updating changelog entry for " + args.version); + await updateChangelog(project, allChanges, args.version); } main(); diff --git a/yarn.lock b/yarn.lock index 77954f7..0f79a82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -777,6 +777,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/cli-color@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/cli-color/-/cli-color-2.0.2.tgz#01bd593722a12c26ec84c170ab251fe2d35856c5" + integrity sha512-1ErQIcmNHtNViGKTtB/TIKqMkC2RkKI2nBneCr9hSCPo9H05g9VzjlaXPW3H0vaI8zFGjJZvSav+VKDKCtKgKA== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -855,6 +860,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.13": + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.28.5": version "4.28.5" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz#8197f1473e7da8218c6a37ff308d695707835684" @@ -1006,6 +1018,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -3171,6 +3188,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -3178,6 +3204,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -3556,6 +3589,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.0.0: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^16.0.3: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -3569,18 +3607,18 @@ yargs@^16.0.3: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" - integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== +yargs@^17.5.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== dependencies: cliui "^7.0.2" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" + yargs-parser "^21.0.0" yn@3.1.1: version "3.1.1"