From 1f8aaead28e8c9b57505ed7377bf5a0e47045dc4 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 21 Jun 2021 14:03:40 -0700 Subject: [PATCH] chore: new private 'cdk-release' tool for performing releases --- bump.sh | 6 +- scripts/bump.js | 51 +++-- tools/cdk-release/.eslintrc.js | 3 + tools/cdk-release/.gitignore | 15 ++ tools/cdk-release/.npmignore | 16 ++ tools/cdk-release/LICENSE | 201 ++++++++++++++++++ tools/cdk-release/NOTICE | 2 + tools/cdk-release/README.md | 21 ++ tools/cdk-release/jest.config.js | 10 + tools/cdk-release/lib/conventional-commits.ts | 138 ++++++++++++ tools/cdk-release/lib/defaults.ts | 32 +++ tools/cdk-release/lib/index.ts | 57 +++++ tools/cdk-release/lib/lifecycles/bump.ts | 122 +++++++++++ tools/cdk-release/lib/lifecycles/changelog.ts | 123 +++++++++++ tools/cdk-release/lib/lifecycles/commit.ts | 45 ++++ tools/cdk-release/lib/private/files.ts | 9 + tools/cdk-release/lib/private/print.ts | 23 ++ .../cdk-release/lib/private/run-exec-file.ts | 19 ++ tools/cdk-release/lib/types.ts | 64 ++++++ tools/cdk-release/lib/updaters/index.ts | 59 +++++ tools/cdk-release/lib/updaters/types/json.ts | 28 +++ .../lib/updaters/types/plain-text.ts | 12 ++ tools/cdk-release/package.json | 70 ++++++ tools/cdk-release/test/changelog.test.ts | 110 ++++++++++ tools/cdk-release/tsconfig.json | 21 ++ 25 files changed, 1239 insertions(+), 18 deletions(-) create mode 100644 tools/cdk-release/.eslintrc.js create mode 100644 tools/cdk-release/.gitignore create mode 100644 tools/cdk-release/.npmignore create mode 100644 tools/cdk-release/LICENSE create mode 100644 tools/cdk-release/NOTICE create mode 100644 tools/cdk-release/README.md create mode 100644 tools/cdk-release/jest.config.js create mode 100644 tools/cdk-release/lib/conventional-commits.ts create mode 100644 tools/cdk-release/lib/defaults.ts create mode 100644 tools/cdk-release/lib/index.ts create mode 100644 tools/cdk-release/lib/lifecycles/bump.ts create mode 100644 tools/cdk-release/lib/lifecycles/changelog.ts create mode 100644 tools/cdk-release/lib/lifecycles/commit.ts create mode 100644 tools/cdk-release/lib/private/files.ts create mode 100644 tools/cdk-release/lib/private/print.ts create mode 100644 tools/cdk-release/lib/private/run-exec-file.ts create mode 100644 tools/cdk-release/lib/types.ts create mode 100644 tools/cdk-release/lib/updaters/index.ts create mode 100644 tools/cdk-release/lib/updaters/types/json.ts create mode 100644 tools/cdk-release/lib/updaters/types/plain-text.ts create mode 100644 tools/cdk-release/package.json create mode 100644 tools/cdk-release/test/changelog.test.ts create mode 100644 tools/cdk-release/tsconfig.json diff --git a/bump.sh b/bump.sh index 73954148de7c6..b3c04198b50d3 100755 --- a/bump.sh +++ b/bump.sh @@ -15,5 +15,9 @@ set -euo pipefail scriptdir=$(cd $(dirname $0) && pwd) cd ${scriptdir} -yarn --frozen-lockfile +yarn install --frozen-lockfile +if [[ "${LEGACY_BUMP:-}" == "" ]]; then + # if we're using 'cdk-release' for the bump, build that package, including all of its dependencies + npx lerna run build --include-dependencies --scope cdk-release +fi ${scriptdir}/scripts/bump.js ${1:-minor} diff --git a/scripts/bump.js b/scripts/bump.js index dee713472c944..a0a05f398669f 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -1,15 +1,13 @@ #!/usr/bin/env node + const fs = require('fs'); const path = require('path'); const semver = require('semver'); const ver = require('./resolve-version'); const { exec } = require('child_process'); -const repoRoot = path.join(__dirname, '..'); - -const releaseAs = process.argv[2] || 'minor'; -const forTesting = process.env.BUMP_CANDIDATE || false; async function main() { + const releaseAs = process.argv[2] || 'minor'; if (releaseAs !== 'minor' && releaseAs !== 'patch') { throw new Error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); } @@ -17,6 +15,7 @@ async function main() { console.error(`Starting ${releaseAs} version bump`); console.error('Current version information:', JSON.stringify(ver, undefined, 2)); + const repoRoot = path.join(__dirname, '..'); const changelogPath = path.join(repoRoot, ver.changelogFile); const opts = { releaseAs: releaseAs, @@ -30,6 +29,12 @@ async function main() { } }; + const majorVersion = semver.major(ver.version); + if (majorVersion > 1) { + opts.stripExperimentalChanges = true; + } + + const forTesting = process.env.BUMP_CANDIDATE || false; if (forTesting) { opts.skip.commit = true; opts.skip.changelog = true; @@ -37,24 +42,36 @@ async function main() { // if we are on a "stable" branch, add a pre-release tag ("rc") to the // version number as a safety in case this version will accidentally be // published. - opts.prerelease = ver.prerelease || 'rc' + opts.prerelease = ver.prerelease || 'rc'; console.error(`BUMP_CANDIDATE is set, so bumping version for testing (with the "${opts.prerelease}" prerelease tag)`); } - // `standard-release` will -- among other things -- create the changelog. - // However, on the v2 branch, `conventional-changelog` (which `standard-release` uses) gets confused - // and creates really muddled changelogs with both v1 and v2 releases intermingled, and lots of missing data. - // A super HACK here is to locally remove all version tags that don't match this major version prior - // to doing the bump, and then later fetching to restore those tags. - const majorVersion = semver.major(ver.version); - await exec(`git tag -d $(git tag -l | grep -v '^v${majorVersion}.')`); + const useLegacyBump = process.env.LEGACY_BUMP || false; + if (useLegacyBump) { + console.error("ℹī¸ Using the third-party 'standard-version' package to perform the bump"); - // Delay loading standard-version until the git tags have been pruned. - const standardVersion = require('standard-version'); - await standardVersion(opts); + // `standard-release` will -- among other things -- create the changelog. + // However, on the v2 branch, `conventional-changelog` (which `standard-release` uses) gets confused + // and creates really muddled changelogs with both v1 and v2 releases intermingled, and lots of missing data. + // A super HACK here is to locally remove all version tags that don't match this major version prior + // to doing the bump, and then later fetching to restore those tags. + await exec(`git tag -d $(git tag -l | grep -v '^v${majorVersion}.')`); - // fetch back the tags, and only the tags, removed locally above - await exec('git fetch origin "refs/tags/*:refs/tags/*"'); + // Delay loading standard-version until the git tags have been pruned. + const standardVersion = require('standard-version'); + await standardVersion(opts); + + // fetch back the tags, and only the tags, removed locally above + await exec('git fetch origin "refs/tags/*:refs/tags/*"'); + } else { + // this is incredible, but passing this option to standard-version actually makes it crash! + // good thing we're getting rid of it... + opts.verbose = !!process.env.VERBOSE; + console.error("🎉 Calling our 'cdk-release' package to make the bump"); + console.error("ℹī¸ Set the LEGACY_BUMP env variable to use the old 'standard-version' bump instead"); + const cdkRelease = require('cdk-release'); + cdkRelease(opts); + } } main().catch(err => { diff --git a/tools/cdk-release/.eslintrc.js b/tools/cdk-release/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/tools/cdk-release/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/tools/cdk-release/.gitignore b/tools/cdk-release/.gitignore new file mode 100644 index 0000000000000..acdfee7f84c04 --- /dev/null +++ b/tools/cdk-release/.gitignore @@ -0,0 +1,15 @@ +*.js +node_modules +*.js.map +*.d.ts + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +*.snk +!.eslintrc.js + +junit.xml + +!jest.config.js \ No newline at end of file diff --git a/tools/cdk-release/.npmignore b/tools/cdk-release/.npmignore new file mode 100644 index 0000000000000..c480a1570dbe3 --- /dev/null +++ b/tools/cdk-release/.npmignore @@ -0,0 +1,16 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +.LAST_BUILD +*.snk +.eslintrc.js + +# exclude cdk artifacts +**/cdk.out +junit.xml + +jest.config.js \ No newline at end of file diff --git a/tools/cdk-release/LICENSE b/tools/cdk-release/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/tools/cdk-release/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/cdk-release/NOTICE b/tools/cdk-release/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/tools/cdk-release/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/tools/cdk-release/README.md b/tools/cdk-release/README.md new file mode 100644 index 0000000000000..16997e2fe72dd --- /dev/null +++ b/tools/cdk-release/README.md @@ -0,0 +1,21 @@ +# cdk-release + +This is a repo-private tool that we use for performing a release: +bumping the version of the package(s), +generating the Changelog file(s), +creating a commit, etc. + +We used to rely on [standard-version](https://www.npmjs.com/package/standard-version) +for this purpose, but our case is so (haha) non-standard, +with `aws-cdk-lib` excluding experimental modules, +and the need for separate Changelog files for V2 experimental modules, +that we decided we need a tool that we have full control over +(plus, `standard-version` has some problems too, +like messing up the headings, +and having problems with both V1 and V2 tags in the same repo). + +This library is called from the +[`bump.js` file](../../scripts/bump.js), +which is called from the [`bump.sh` script](../../bump.sh), +which is called by a CodeBuild job that creates the 'bump' +PR every time we perform a CDK release. diff --git a/tools/cdk-release/jest.config.js b/tools/cdk-release/jest.config.js new file mode 100644 index 0000000000000..07f5f6c432bb6 --- /dev/null +++ b/tools/cdk-release/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/tools/cdk-release/lib/conventional-commits.ts b/tools/cdk-release/lib/conventional-commits.ts new file mode 100644 index 0000000000000..ddf8b82f22050 --- /dev/null +++ b/tools/cdk-release/lib/conventional-commits.ts @@ -0,0 +1,138 @@ +import * as fs from 'fs-extra'; +import { ReleaseOptions } from './types'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const lerna_project = require('@lerna/project'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const conventionalCommitsParser = require('conventional-commits-parser'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const gitRawCommits = require('git-raw-commits'); + +/** + * The optional notes in the commit message. + * Today, the only notes are 'BREAKING CHANGES'. + */ +export interface ConventionalCommitNote { + /** Today, always 'BREAKING CHANGE'. */ + readonly title: string; + + /** The body of the note. */ + readonly text: string; +} + +/** For now, only needed for unit tests. */ +export interface ConventionalCommitReference { +} + +export interface ConventionalCommit { + /** The type of the commit ('feat', 'fix', etc.). */ + readonly type: string; + + /** The optional scope of the change ('core', 'aws-s3', 's3', etc.). */ + readonly scope?: string; + + /** The subject is the remaining part of the first line without 'type' and 'scope'. */ + readonly subject: string; + + /** + * The header is the entire first line of the commit + * ((): ). + */ + readonly header: string; + + /** + * The optional notes in the commit message. + * Today, the only notes are 'BREAKING CHANGES'. + */ + readonly notes: ConventionalCommitNote[]; + + /** + * References inside the commit body + * (for example, to issues or Pull Requests that this commit is linked to). + */ + readonly references: ConventionalCommitReference[]; +} + +/** + * Returns a list of all Conventional Commits in the Git repository since the tag `gitTag`. + * The commits will be sorted in chronologically descending order + * (that is, later/newer commits will be earlier in the array). + * + * @param gitTag the string representing the Git tag, + * will be used to limit the returned commits to only those added after that tag + */ +export async function getConventionalCommitsFromGitHistory(gitTag: string): Promise { + const ret = new Array(); + return new Promise((resolve, reject) => { + const conventionalCommitsStream = gitRawCommits({ + format: '%B%n-hash-%n%H', + // our tags have the 'v' prefix + from: gitTag, + // path: options.path, + }).pipe(conventionalCommitsParser()); + + conventionalCommitsStream.on('data', function (data: any) { + // filter out all commits that don't conform to the Conventional Commits standard + // (they will have an empty 'type' property) + if (data.type) { + ret.push(data); + } + }); + conventionalCommitsStream.on('end', function () { + resolve(ret); + }); + conventionalCommitsStream.on('error', function (err: any) { + reject(err); + }); + }); +} + +/** + * Filters commits based on the criteria in `args` + * (right now, the only criteria is whether to remove commits that relate to experimental packages). + * + * @param args configuration + * @param commits the array of Conventional Commits to filter + * @returns an array of ConventionalCommit objects which is a subset of `commits` + * (possibly exactly equal to `commits`) + */ +export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[]): ConventionalCommit[] { + if (!args.stripExperimentalChanges) { + return commits; + } + + // a get a list of packages from our monorepo + const project = new lerna_project.Project(); + const packages = project.getPackagesSync(); + const experimentalPackageNames: string[] = packages + .filter((pkg: any) => { + const pkgJson = fs.readJsonSync(pkg.manifestLocation); + return pkgJson.name.startsWith('@aws-cdk/') + && (pkgJson.maturity === 'experimental' || pkgJson.maturity === 'developer-preview'); + }) + .map((pkg: any) => pkg.name.substr('@aws-cdk/'.length)); + + const experimentalScopes = flatMap(experimentalPackageNames, (pkgName) => [ + pkgName, + ...(pkgName.startsWith('aws-') + ? [ + // if the package name starts with 'aws', like 'aws-s3', + // also include in the scopes variants without the prefix, + // and without the '-' in the prefix + // (so, 's3' and 'awss3') + pkgName.substr('aws-'.length), + pkgName.replace(/^aws-/, 'aws'), + ] + : [] + ), + ]); + + return commits.filter(commit => !commit.scope || !experimentalScopes.includes(commit.scope)); +} + +function flatMap(xs: T[], fn: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { + ret.push(...fn(x)); + } + return ret; +} diff --git a/tools/cdk-release/lib/defaults.ts b/tools/cdk-release/lib/defaults.ts new file mode 100644 index 0000000000000..22f53ebe806aa --- /dev/null +++ b/tools/cdk-release/lib/defaults.ts @@ -0,0 +1,32 @@ +import { ReleaseOptions } from './types'; + +const defaultPackageFiles = [ + 'package.json', + 'bower.json', + 'manifest.json', +]; + +export const defaultBumpFiles = defaultPackageFiles.concat([ + 'package-lock.json', + 'npm-shrinkwrap.json', +]); + +export const defaults: Partial = { + infile: 'CHANGELOG.md', + // firstRelease: false, + sign: false, + // noVerify: false, + // commitAll: false, + silent: false, + scripts: {}, + skip: { + tag: true, + }, + packageFiles: defaultPackageFiles, + bumpFiles: defaultBumpFiles, + dryRun: false, + // gitTagFallback: true, + releaseCommitMessageFormat: 'chore(release): {{currentTag}}', + changeLogHeader: '# Changelog\n\nAll notable changes to this project will be documented in this file. ' + + 'See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n', +}; diff --git a/tools/cdk-release/lib/index.ts b/tools/cdk-release/lib/index.ts new file mode 100644 index 0000000000000..a770db47868e6 --- /dev/null +++ b/tools/cdk-release/lib/index.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { filterCommits, getConventionalCommitsFromGitHistory } from './conventional-commits'; +import { defaults } from './defaults'; +import { bump } from './lifecycles/bump'; +import { changelog } from './lifecycles/changelog'; +import { commit } from './lifecycles/commit'; +import { debug, debugObject } from './private/print'; +import { ReleaseOptions } from './types'; +import { resolveUpdaterObjectFromArgument } from './updaters'; + +module.exports = async function main(opts: ReleaseOptions): Promise { + // handle the default options + const args: ReleaseOptions = { + ...defaults, + ...opts, + }; + debugObject(args, 'options are (including defaults)', args); + + const packageInfo = determinePackageInfo(args); + debugObject(args, 'packageInfo is', packageInfo); + + const currentVersion = packageInfo.version; + debug(args, 'Current version is: ' + currentVersion); + + const commits = await getConventionalCommitsFromGitHistory(`v${currentVersion}`); + const filteredCommits = filterCommits(args, commits); + debugObject(args, 'Found and filtered commits', filteredCommits); + + const bumpResult = await bump(args, currentVersion); + const newVersion = bumpResult.newVersion; + debug(args, 'New version is: ' + newVersion); + + const changelogResult = await changelog(args, currentVersion, newVersion, filteredCommits); + + await commit(args, newVersion, [...bumpResult.changedFiles, ...changelogResult.changedFiles]); +}; + +interface PackageInfo { + version: string; + private: string | boolean | null | undefined; +} + +function determinePackageInfo(args: ReleaseOptions): PackageInfo { + for (const packageFile of args.packageFiles ?? []) { + const updater = resolveUpdaterObjectFromArgument(packageFile); + const pkgPath = path.resolve(process.cwd(), updater.filename); + const contents = fs.readFileSync(pkgPath, 'utf8'); + // we stop on the first (successful) option + return { + version: updater.updater.readVersion(contents), + private: typeof updater.updater.isPrivate === 'function' ? updater.updater.isPrivate(contents) : false, + }; + } + + throw new Error('Could not establish the version to bump!'); +} diff --git a/tools/cdk-release/lib/lifecycles/bump.ts b/tools/cdk-release/lib/lifecycles/bump.ts new file mode 100644 index 0000000000000..bb4465461f2d5 --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/bump.ts @@ -0,0 +1,122 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import { writeFile } from '../private/files'; +import { notify } from '../private/print'; +import { ReleaseOptions, ReleaseType } from '../types'; +import { resolveUpdaterObjectFromArgument } from '../updaters/index'; + +export interface BumpResult { + readonly newVersion: string; + readonly changedFiles: string[]; +} + +export async function bump(args: ReleaseOptions, currentVersion: string): Promise { + if (args.skip?.bump) { + return { + newVersion: currentVersion, + changedFiles: [], + }; + } + + const releaseType = getReleaseType(args.prerelease, args.releaseAs, currentVersion); + const newVersion = semver.inc(currentVersion, releaseType, args.prerelease); + if (!newVersion) { + throw new Error('Could not increment version: ' + currentVersion); + } + const changedFiles = updateBumpFiles(args, newVersion); + return { newVersion, changedFiles }; +} + +function getReleaseType(prerelease: string | undefined, expectedReleaseType: ReleaseType, currentVersion: string): semver.ReleaseType { + if (typeof prerelease === 'string') { + if (isInPrerelease(currentVersion)) { + if (shouldContinuePrerelease(currentVersion, expectedReleaseType) || + getTypePriority(getCurrentActiveType(currentVersion)) > getTypePriority(expectedReleaseType) + ) { + return 'prerelease'; + } + } + + return 'pre' + expectedReleaseType as semver.ReleaseType; + } else { + return expectedReleaseType; + } +} + +function isInPrerelease(version: string): boolean { + return Array.isArray(semver.prerelease(version)); +} + +/** + * if a version is currently in pre-release state, + * and if it current in-pre-release type is same as expect type, + * it should continue the pre-release with the same type + * + * @param version + * @param expectType + * @return {boolean} + */ +function shouldContinuePrerelease(version: string, expectType: ReleaseType): boolean { + return getCurrentActiveType(version) === expectType; +} + +const TypeList = ['major', 'minor', 'patch'].reverse(); +/** + * extract the in-pre-release type in target version + * + * @param version + * @return {string} + */ +function getCurrentActiveType(version: string): string { + for (const item of TypeList) { + if ((semver as any)[item](version)) { + return item; + } + } + throw new Error('unreachable'); +} + +/** + * calculate the priority of release type, + * major - 2, minor - 1, patch - 0 + * + * @param type + * @return {number} + */ +function getTypePriority(type: string): number { + return TypeList.indexOf(type); +} + +/** + * attempt to update the version number in provided `bumpFiles` + * @param args config object + * @param newVersion version number to update to. + * @return the collection of file paths that were actually changed + */ +function updateBumpFiles(args: ReleaseOptions, newVersion: string): string[] { + const ret = new Array(); + + for (const bumpFile of (args.bumpFiles ?? [])) { + const updater = resolveUpdaterObjectFromArgument(bumpFile); + if (!updater) { + continue; + } + const configPath = path.resolve(process.cwd(), updater.filename); + const stat = fs.lstatSync(configPath); + if (!stat.isFile()) { + continue; + } + const contents = fs.readFileSync(configPath, 'utf8'); + notify(args, + 'bumping version in ' + updater.filename + ' from %s to %s', + [updater.updater.readVersion(contents), newVersion], + ); + writeFile(args, configPath, + updater.updater.writeVersion(contents, newVersion), + ); + ret.push(updater.filename); + } + + return ret; +} diff --git a/tools/cdk-release/lib/lifecycles/changelog.ts b/tools/cdk-release/lib/lifecycles/changelog.ts new file mode 100644 index 0000000000000..f961fe53b175c --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/changelog.ts @@ -0,0 +1,123 @@ +import * as stream from 'stream'; +import * as fs from 'fs-extra'; +import { ConventionalCommit } from '../conventional-commits'; +import { writeFile } from '../private/files'; +import { notify, debug, debugObject } from '../private/print'; +import { ReleaseOptions } from '../types'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const conventionalChangelogPresetLoader = require('conventional-changelog-preset-loader'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const conventionalChangelogWriter = require('conventional-changelog-writer'); + +const START_OF_LAST_RELEASE_PATTERN = /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+| { + if (args.skip?.changelog) { + return { + contents: '', + changedFiles: [], + }; + } + createChangelogIfMissing(args); + + // find the position of the last release and remove header + let oldContent = args.dryRun ? '' : fs.readFileSync(args.infile!, 'utf-8'); + const oldContentStart = oldContent.search(START_OF_LAST_RELEASE_PATTERN); + if (oldContentStart !== -1) { + oldContent = oldContent.substring(oldContentStart); + } + + // load the default configuration that we use for the Changelog generation + const presetConfig = await conventionalChangelogPresetLoader({ + name: 'conventional-changelog-conventionalcommits', + }); + debugObject(args, 'conventionalChangelogPresetLoader returned', presetConfig); + + return new Promise((resolve, reject) => { + // convert an array of commits into a Stream, + // which conventionalChangelogWriter expects + const commitsStream = new stream.Stream.Readable({ + objectMode: true, + }); + commits.forEach(commit => commitsStream.push(commit)); + // mark the end of the stream + commitsStream.push(null); + + const host = 'https://github.com', owner = 'aws', repository = 'aws-cdk'; + const context = { + issue: 'issues', + commit: 'commit', + version: newVersion, + host, + owner, + repository, + repoUrl: `${host}/${owner}/${repository}`, + linkCompare: true, + previousTag: `v${currentVersion}`, + currentTag: `v${newVersion}`, + // when isPatch is 'true', the default template used for the header renders an H3 instead of an H2 + // (see: https://github.com/conventional-changelog/conventional-changelog/blob/f1f50f56626099e92efe31d2f8c5477abd90f1b7/packages/conventional-changelog-conventionalcommits/templates/header.hbs#L1-L5) + isPatch: false, + }; + // invoke the conventionalChangelogWriter package that will perform the actual Changelog rendering + const changelogStream = commitsStream + .pipe(conventionalChangelogWriter(context, + { + // CDK uses the settings from 'conventional-changelog-conventionalcommits' + // (by way of 'standard-version'), + // which are different than the 'conventionalChangelogWriter' defaults + ...presetConfig.writerOpts, + finalizeContext: (ctx: { noteGroups?: { title: string }[], date?: string }) => { + // the heading of the "BREAKING CHANGES" section is governed by this Handlebars template: + // https://github.com/conventional-changelog/conventional-changelog/blob/f1f50f56626099e92efe31d2f8c5477abd90f1b7/packages/conventional-changelog-conventionalcommits/templates/template.hbs#L3-L12 + // to change the heading from 'BREAKING CHANGES' to 'BREAKING CHANGES TO EXPERIMENTAL FEATURES', + // we have to change the title of the 'BREAKING CHANGES' noteGroup + ctx.noteGroups?.forEach(noteGroup => { + if (noteGroup.title === 'BREAKING CHANGES') { + noteGroup.title = 'BREAKING CHANGES TO EXPERIMENTAL FEATURES'; + } + }); + // in unit tests, we don't want to have the date in the Changelog + if (args.includeDateInChangelog === false) { + ctx.date = undefined; + } + return ctx; + }, + })); + + changelogStream.on('error', function (err: any) { + reject(err); + }); + let content = ''; + changelogStream.on('data', function (buffer: any) { + content += buffer.toString(); + }); + changelogStream.on('end', function () { + notify(args, 'outputting changes to %s', [args.infile]); + if (args.dryRun) { + debug(args, `\n---\n${content.trim()}\n---\n`); + } else { + writeFile(args, args.infile!, args.changeLogHeader + '\n' + (content + oldContent).replace(/\n+$/, '\n')); + } + return resolve({ + contents: content, + changedFiles: [args.infile!], + }); + }); + }); +} + +function createChangelogIfMissing(args: ReleaseOptions) { + if (!fs.existsSync(args.infile!)) { + notify(args, 'created %s', [args.infile]); + // args.outputUnreleased = true + writeFile(args, args.infile!, '\n'); + } +} diff --git a/tools/cdk-release/lib/lifecycles/commit.ts b/tools/cdk-release/lib/lifecycles/commit.ts new file mode 100644 index 0000000000000..d908e0799954a --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/commit.ts @@ -0,0 +1,45 @@ +import * as path from 'path'; +import { notify } from '../private/print'; +import { runExecFile } from '../private/run-exec-file'; +import { ReleaseOptions } from '../types'; + +export async function commit(args: ReleaseOptions, newVersion: string, modifiedFiles: string[]): Promise { + if (args.skip?.commit) { + return; + } + + let msg = 'committing %s'; + const paths = new Array(); + const toAdd = new Array(); + // commit any of the config files that we've updated + // the version # for. + for (const modifiedFile of modifiedFiles) { + paths.unshift(modifiedFile); + toAdd.push(path.relative(process.cwd(), modifiedFile)); + + // account for multiple files in the output message + if (paths.length > 1) { + msg += ' and %s'; + } + } + // nothing to do, exit without commit anything + if (toAdd.length === 0) { + return; + } + + notify(args, msg, paths); + + await runExecFile(args, 'git', ['add'].concat(toAdd)); + const sign = args.sign ? ['-S'] : []; + await runExecFile(args, 'git', ['commit'].concat( + sign, + [ + '-m', + `${formatCommitMessage(args.releaseCommitMessageFormat!, newVersion)}`, + ]), + ); +} + +function formatCommitMessage(rawMsg: string, newVersion: string): string { + return rawMsg.replace(/{{currentTag}}/g, newVersion); +} diff --git a/tools/cdk-release/lib/private/files.ts b/tools/cdk-release/lib/private/files.ts new file mode 100644 index 0000000000000..1850e8a79ad4a --- /dev/null +++ b/tools/cdk-release/lib/private/files.ts @@ -0,0 +1,9 @@ +import * as fs from 'fs'; +import { ReleaseOptions } from '../types'; + +export function writeFile(args: ReleaseOptions, filePath: string, content: string): void { + if (args.dryRun) { + return; + } + fs.writeFileSync(filePath, content, 'utf8'); +} diff --git a/tools/cdk-release/lib/private/print.ts b/tools/cdk-release/lib/private/print.ts new file mode 100644 index 0000000000000..84b7758a5c4b3 --- /dev/null +++ b/tools/cdk-release/lib/private/print.ts @@ -0,0 +1,23 @@ +import * as util from 'util'; +import { ReleaseOptions } from '../types'; + +export function debug(opts: ReleaseOptions, message: string): void { + if (opts.verbose) { + // eslint-disable-next-line no-console + console.log(`[cdk-release] ${message}`); + } +} + +export function debugObject(opts: ReleaseOptions, message: string, object: any): void { + if (opts.verbose) { + // eslint-disable-next-line no-console + console.log(`[cdk-release] ${message}:\n`, object); + } +} + +export function notify(opts: ReleaseOptions, msg: string, args: any[]) { + if (!opts.silent) { + // eslint-disable-next-line no-console + console.info('✔ ' + util.format(msg, ...args)); + } +} diff --git a/tools/cdk-release/lib/private/run-exec-file.ts b/tools/cdk-release/lib/private/run-exec-file.ts new file mode 100644 index 0000000000000..c19f312a1b92e --- /dev/null +++ b/tools/cdk-release/lib/private/run-exec-file.ts @@ -0,0 +1,19 @@ +import { execFile as childProcessExecFile } from 'child_process'; +import { promisify } from 'util'; +import { ReleaseOptions } from '../types'; +import { notify } from './print'; + +const execFile = promisify(childProcessExecFile); + +export async function runExecFile(args: ReleaseOptions, cmd: string, cmdArgs: string[]): Promise { + if (args.dryRun) { + notify(args, "would execute command: '%s %s'", [cmd, cmdArgs + // quote arguments with spaces, for a more realistic printing experience + .map(cmdArg => cmdArg.match(/\s/) ? `"${cmdArg}"` : cmdArg) + .join(' ')], + ); + return; + } + const streams = await execFile(cmd, cmdArgs); + return streams.stdout; +} diff --git a/tools/cdk-release/lib/types.ts b/tools/cdk-release/lib/types.ts new file mode 100644 index 0000000000000..f6314261cd5f1 --- /dev/null +++ b/tools/cdk-release/lib/types.ts @@ -0,0 +1,64 @@ +export interface Lifecycles { + bump?: string; + changelog?: string; + postchangelog?: string; + commit?: string; + + // we don't actually do tagging at all, but still support passing it as an option, + // for conformance with standard-version (CDK doesn't use its tagging capabilities anyway) + tag?: string; +} + +type LifecyclesSkip = { + [key in keyof Lifecycles]: boolean; +} + +/* ****** Updaters ******** */ + +export interface UpdaterModule { + isPrivate?: (contents: string) => string | boolean | null | undefined; + readVersion(contents: string): string; + writeVersion(contents: string, version: string): string; +} + +export interface ArgUpdater { + filename: string; + type?: string; + updater?: UpdaterModule | string; +} + +export type ArgFile = string | ArgUpdater; + +export interface Updater { + filename: string; + updater: UpdaterModule; +} + +export type ReleaseType = 'major' | 'minor' | 'patch'; + +export interface ConventionalCommitType { + type: string; + section?: string; + hidden?: boolean; +} + +/* ****** main options ******** */ + +export interface ReleaseOptions { + releaseAs: ReleaseType; + skip?: LifecyclesSkip; + packageFiles?: ArgFile[]; + bumpFiles?: ArgFile[]; + infile?: string; + prerelease?: string; + scripts?: Lifecycles; + dryRun?: boolean; + verbose?: boolean; + silent?: boolean; + sign?: boolean; + stripExperimentalChanges?: boolean; + + changeLogHeader?: string; + includeDateInChangelog?: boolean; + releaseCommitMessageFormat?: string; +} diff --git a/tools/cdk-release/lib/updaters/index.ts b/tools/cdk-release/lib/updaters/index.ts new file mode 100644 index 0000000000000..fd61828925316 --- /dev/null +++ b/tools/cdk-release/lib/updaters/index.ts @@ -0,0 +1,59 @@ +import * as path from 'path'; +import { defaultBumpFiles } from '../defaults'; +import { UpdaterModule, ArgFile, Updater } from '../types'; +import jsonUpdaterModule from './types/json'; +import plainTextUpdaterModule from './types/plain-text'; + +export function resolveUpdaterObjectFromArgument(arg: ArgFile): Updater { + arg = typeof arg === 'string' ? { filename: arg } : arg; + let updaterModule: UpdaterModule; + + if (arg.updater) { + updaterModule = getCustomUpdater(arg.updater); + } else if (arg.type) { + updaterModule = getUpdaterByType(arg.type); + } else { + updaterModule = getUpdaterByFilename(arg.filename); + } + + return { + updater: updaterModule, + filename: arg.filename, + }; +} + +function getCustomUpdater(updater: string | UpdaterModule): UpdaterModule { + if (typeof updater === 'string') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require(path.resolve(process.cwd(), updater)); + } + if ( + typeof updater.readVersion === 'function' && + typeof updater.writeVersion === 'function' + ) { + return updater; + } + throw new Error('Updater must be a string path or an object with readVersion and writeVersion methods'); +} + +const JSON_BUMP_FILES = defaultBumpFiles; +function getUpdaterByFilename(filename: any): UpdaterModule { + if (JSON_BUMP_FILES.includes(path.basename(filename))) { + return getUpdaterByType('json'); + } + throw Error( + `Unsupported file (${filename}) provided for bumping.\n Please specify the updater \`type\` or use a custom \`updater\`.`, + ); +} + +const updatersByType: { [key: string]: UpdaterModule } = { + 'json': jsonUpdaterModule, + 'plain-text': plainTextUpdaterModule, +}; +function getUpdaterByType(type: string): UpdaterModule { + const updater = updatersByType[type]; + if (!updater) { + throw Error(`Unable to locate updater for provided type (${type}).`); + } + return updater; +} diff --git a/tools/cdk-release/lib/updaters/types/json.ts b/tools/cdk-release/lib/updaters/types/json.ts new file mode 100644 index 0000000000000..23e8490f56f5c --- /dev/null +++ b/tools/cdk-release/lib/updaters/types/json.ts @@ -0,0 +1,28 @@ +import { UpdaterModule } from '../../types'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const detectIndent = require('detect-indent'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const detectNewline = require('detect-newline'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const stringifyPackage = require('stringify-package'); + +class JsonUpdaterModule implements UpdaterModule { + public readVersion(contents: string): string { + return JSON.parse(contents).version; + }; + + public writeVersion(contents: string, version: string): string { + const json = JSON.parse(contents); + const indent = detectIndent(contents).indent; + const newline = detectNewline(contents); + json.version = version; + return stringifyPackage(json, indent, newline); + }; + + public isPrivate(contents: string): string | boolean | null | undefined { + return JSON.parse(contents).private; + }; +} +const jsonUpdaterModule = new JsonUpdaterModule(); +export default jsonUpdaterModule; diff --git a/tools/cdk-release/lib/updaters/types/plain-text.ts b/tools/cdk-release/lib/updaters/types/plain-text.ts new file mode 100644 index 0000000000000..84ed4de20d563 --- /dev/null +++ b/tools/cdk-release/lib/updaters/types/plain-text.ts @@ -0,0 +1,12 @@ +import { UpdaterModule } from '../../types'; + +const plainTextUpdaterModule: UpdaterModule = { + readVersion(contents: string): string { + return contents; + }, + + writeVersion(_contents: string, version: string): string { + return version; + }, +}; +export default plainTextUpdaterModule; diff --git a/tools/cdk-release/package.json b/tools/cdk-release/package.json new file mode 100644 index 0000000000000..3608c4f212378 --- /dev/null +++ b/tools/cdk-release/package.json @@ -0,0 +1,70 @@ +{ + "name": "cdk-release", + "private": true, + "version": "0.0.0", + "description": "A tool for performing release-related tasks like version bumps, Changelog generation, etc.", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "tools/cdk-release" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "build+test+package": "yarn build+test", + "build+test": "yarn build && yarn test", + "build+extract": "yarn build", + "build+test+extract": "yarn build+test" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/fs-extra": "^8.1.1", + "@types/jest": "^26.0.23", + "@types/yargs": "^15.0.13", + "cdk-build-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0" + }, + "dependencies": { + "@lerna/project": "^4.0.0", + "conventional-changelog": "^3.1.24", + "conventional-changelog-config-spec": "^2.1.0", + "conventional-changelog-preset-loader": "^2.3.4", + "conventional-commits-parser": "^3.2.1", + "conventional-changelog-writer": "^4.1.0", + "fs-extra": "^9.1.0", + "git-raw-commits": "^2.0.10", + "semver": "^7.3.5", + "stringify-package": "^1.0.1", + "detect-indent": "^6.1.0", + "detect-newline": "^3.1.0" + }, + "keywords": [ + "aws", + "cdk", + "changelog", + "bump", + "release", + "version" + ], + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "cdk-build": { + "jest": true + }, + "ubergen": { + "exclude": true + } +} diff --git a/tools/cdk-release/test/changelog.test.ts b/tools/cdk-release/test/changelog.test.ts new file mode 100644 index 0000000000000..21fba9b6f2501 --- /dev/null +++ b/tools/cdk-release/test/changelog.test.ts @@ -0,0 +1,110 @@ +import { ConventionalCommit, filterCommits } from '../lib/conventional-commits'; +import { changelog } from '../lib/lifecycles/changelog'; +import { ReleaseOptions } from '../lib/types'; + +describe('Changelog generation', () => { + const args: ReleaseOptions = { + releaseAs: 'minor', + dryRun: true, + silent: true, + includeDateInChangelog: false, + }; + + test("correctly handles 'BREAKING CHANGES'", async () => { + const commits: ConventionalCommit[] = [ + buildCommit({ + type: 'feat', + subject: 'super important feature', + notes: [ + { + title: 'BREAKING CHANGE', + text: 'this is a breaking change', + }, + ], + }), + buildCommit({ + type: 'fix', + scope: 'scope', + subject: 'hairy bugfix', + }), + buildCommit({ + type: 'chore', + subject: 'this commit should not be rendered in the Changelog', + }), + ]; + + const changelogContents = await invokeChangelogFrom1_23_0to1_24_0(args, commits); + + expect(changelogContents).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* this is a breaking change + +### Features + +* super important feature + + +### Bug Fixes + +* **scope:** hairy bugfix + +`); + }); + + test("correctly skips experimental modules, even with 'BREAKING CHANGES'", async () => { + const commits: ConventionalCommit[] = [ + buildCommit({ + type: 'feat', + scope: 'scope', + subject: 'super important feature', + }), + buildCommit({ + type: 'fix', + scope: 'example-construct-library', // really hope we don't stabilize this one + subject: 'hairy bugfix', + notes: [ + { + title: 'BREAKING CHANGE', + text: 'this is a breaking change', + }, + ], + }), + ]; + + const changelogContents = await invokeChangelogFrom1_23_0to1_24_0({ + ...args, + stripExperimentalChanges: true, + }, commits); + + expect(changelogContents).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **scope:** super important feature + +`); + }); +}); + +interface PartialCommit extends Partial { + readonly type: string; + readonly subject: string; +} + +function buildCommit(commit: PartialCommit): ConventionalCommit { + return { + notes: [], + references: [], + header: `${commit.type}${commit.scope ? '(' + commit.scope + ')' : ''}: ${commit.subject}`, + ...commit, + }; +} + +async function invokeChangelogFrom1_23_0to1_24_0(args: ReleaseOptions, commits: ConventionalCommit[]): Promise { + const changelogResult = await changelog(args, '1.23.0', '1.24.0', filterCommits(args, commits)); + return changelogResult.contents; +} diff --git a/tools/cdk-release/tsconfig.json b/tools/cdk-release/tsconfig.json new file mode 100644 index 0000000000000..89d1f04da5020 --- /dev/null +++ b/tools/cdk-release/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": ["es2018"], + "strict": true, + "alwaysStrict": true, + "declaration": true, + "inlineSourceMap": true, + "inlineSources": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "composite": true, + "incremental": true + }, + "exclude": ["test/enrichments/**"], + "include": ["**/*.ts"] +}