tools/@aws-cdk/cdk-release/lib/lifecycles/changelog.ts (136 lines of code) (raw):

import * as stream from 'stream'; import * as fs from 'fs-extra'; import { ConventionalCommit, filterCommits } from '../conventional-commits'; import { writeFile } from '../private/files'; import { notify, debug } from '../private/print'; import { ExperimentalChangesTreatment, LifecyclesSkip, PackageInfo, Versions } 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]+|<a name=)/m; export interface WriteChangelogOptions extends ChangelogOptions { skip?: LifecyclesSkip; alphaChangelogFile?: string; experimentalChangesTreatment?: ExperimentalChangesTreatment; currentVersion: Versions; newVersion: Versions; commits: ConventionalCommit[]; packages: PackageInfo[]; } export interface ChangelogOptions { changelogFile: string; dryRun?: boolean; verbose?: boolean; silent?: boolean; changeLogHeader?: string; includeDateInChangelog?: boolean; } export interface ChangelogResult { readonly filePath: string; readonly fileContents: string; } export async function writeChangelogs(opts: WriteChangelogOptions): Promise<ChangelogResult[]> { if (opts.skip?.changelog) { return []; } const experimentalChangesTreatment = opts.experimentalChangesTreatment ?? ExperimentalChangesTreatment.INCLUDE; const alphaPackages = opts.packages.filter(p => p.alpha); const stableCommits = filterCommits(opts.commits, { excludePackages: alphaPackages.map(p => p.name) }); switch (experimentalChangesTreatment) { case ExperimentalChangesTreatment.INCLUDE: const allContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, opts.commits); return [{ filePath: opts.changelogFile, fileContents: allContents }]; case ExperimentalChangesTreatment.STRIP: const strippedContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); return [{ filePath: opts.changelogFile, fileContents: strippedContents }]; case ExperimentalChangesTreatment.SEPARATE: if (!opts.currentVersion.alphaVersion || !opts.newVersion.alphaVersion) { throw new Error('unable to create separate alpha Changelog without alpha package versions'); } if (!opts.alphaChangelogFile) { throw new Error('alphaChangelogFile must be specified if experimentalChangesTreatment is SEPARATE'); } const changelogResults: ChangelogResult[] = []; const contents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); changelogResults.push({ filePath: opts.changelogFile, fileContents: contents }); const alphaCommits = filterCommits(opts.commits, { includePackages: alphaPackages.map(p => p.name) }); const alphaContents = await changelog( { ...opts, changelogFile: opts.alphaChangelogFile }, opts.currentVersion.alphaVersion, opts.newVersion.alphaVersion, alphaCommits); changelogResults.push({ filePath: opts.alphaChangelogFile, fileContents: alphaContents }); return changelogResults; default: throw new Error(`unsupported experimentalChanges type: ${opts.experimentalChangesTreatment}`); } } export async function changelog( args: ChangelogOptions, currentVersion: string, newVersion: string, commits: ConventionalCommit[], ): Promise<string> { createChangelogIfMissing(args); // find the position of the last release and remove header let oldContent = args.dryRun ? '' : fs.readFileSync(args.changelogFile, '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', }); 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.changelogFile]); if (args.dryRun) { debug(args, `\n---\n${content.trim()}\n---\n`); } else { writeFile(args, args.changelogFile, args.changeLogHeader + '\n' + (content + oldContent).replace(/\n+$/, '\n')); } return resolve(content); }); }); } function createChangelogIfMissing(args: ChangelogOptions) { if (!fs.existsSync(args.changelogFile)) { notify(args, 'created %s', [args.changelogFile]); writeFile(args, args.changelogFile, '\n'); } }