Skip to content

Commit

Permalink
chore(cdk-release): create our release notes as part of the build
Browse files Browse the repository at this point in the history
Currently, our bump process creates `CHANGELOG.md` for the `master` branch, and
`CHANGELOG.v2.md` and `CHANGELOG.v2.alpha.md` on the `v2-main` branch. For the
latter, we need to combine the two changelogs into one for the GitHub release,
where all of the artifacts of the release (both aws-cdk-lib and all the alpha
modules) are bundled together.

As proposed in the [RFC], this combined format shows the stable/aws-cdk-lib
changes first, and then the alpha module changes under their own heading.  This
generated -- but not saved -- RELEASE_NOTES.md will be used by our publishing
pipeline to generate the GitHub release notes.

[RFC]: https://github.com/aws/aws-cdk-rfcs/blob/master/text/0249-v2-experiments.md#changelog--release-notes

fixes #16802
  • Loading branch information
njlynch committed Oct 13, 2021
1 parent 8f91531 commit fb23802
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ yarn-error.log
.nzm-*

/.versionrc.json
RELEASE_NOTES.md
3 changes: 3 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@ if [ "$check_compat" == "true" ]; then
/bin/bash scripts/check-api-compatibility.sh
fi

# Create the release notes for the current version. These are ephemeral and not saved in source.
node ./scripts/create-release-notes.js

touch $BUILD_INDICATOR
2 changes: 1 addition & 1 deletion scripts/bump.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async function main() {
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('@aws-cdk/cdk-release');
cdkRelease(opts);
cdkRelease.createRelease(opts);
}
}

Expand Down
18 changes: 18 additions & 0 deletions scripts/create-release-notes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

const cdkRelease = require('@aws-cdk/cdk-release');
const ver = require('./resolve-version');

async function main() {
await cdkRelease.createReleaseNotes({
versionFile: ver.versionFile,
changelogFile: ver.changelogFile,
alphaChangelogFile: ver.alphaChangelogFile,
releaseNotesFile: 'RELEASE_NOTES.md',
});
}

main().catch(err => {
console.error(err.stack);
process.exit(1);
});
18 changes: 5 additions & 13 deletions tools/@aws-cdk/cdk-release/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { getConventionalCommitsFromGitHistory } from './conventional-commits';
import { defaults } from './defaults';
import { bump } from './lifecycles/bump';
import { writeChangelogs } from './lifecycles/changelog';
import { commit } from './lifecycles/commit';
import { debug, debugObject } from './private/print';
import { PackageInfo, ReleaseOptions, Versions } from './types';
import { PackageInfo, ReleaseOptions } from './types';
import { readVersion } from './versions';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const lerna_project = require('@lerna/project');

module.exports = async function main(opts: ReleaseOptions): Promise<void> {
export * from './release-notes';

export async function createRelease(opts: ReleaseOptions): Promise<void> {
// handle the default options
const args: ReleaseOptions = {
...defaults,
Expand All @@ -34,15 +35,6 @@ module.exports = async function main(opts: ReleaseOptions): Promise<void> {
await commit(args, newVersion.stableVersion, [args.versionFile, ...changelogResults.map(r => r.filePath)]);
};

function readVersion(versionFile: string): Versions {
const versionPath = path.resolve(process.cwd(), versionFile);
const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' }));
return {
stableVersion: contents.version,
alphaVersion: contents.alphaVersion,
};
}

function getProjectPackageInfos(): PackageInfo[] {
const packages = lerna_project.Project.getPackagesSync();

Expand Down
2 changes: 1 addition & 1 deletion tools/@aws-cdk/cdk-release/lib/private/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'fs';

interface WriteFileOpts {
export interface WriteFileOpts {
readonly dryRun?: boolean;
}

Expand Down
68 changes: 68 additions & 0 deletions tools/@aws-cdk/cdk-release/lib/release-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
import parseChangelog = require('changelog-parser');
import { WriteFileOpts, writeFile } from './private/files';
import { debugObject, LoggingOptions } from './private/print';
import { Versions } from './types';
import { readVersion } from './versions';

export interface ReleaseNotesOpts {
/** path to the version file for the current branch (e.g., version.v2.json) */
versionFile: string;
/** path to the primary changelog file (e.g., 'CHANGELOG.v2.md') */
changelogFile: string;
/** (optional) path to the independent alpha changelog file (e.g., 'CHANGELOG.v2.alpha.md') */
alphaChangelogFile?: string;
/** path to write out the final release notes (e.g., 'RELEASE_NOTES.md'). */
releaseNotesFile: string;
}

/**
* Creates a release notes file from one (or more) changelog files for the current version.
* If an alpha version and alpha changelog file aren't present, this is identical to the contents
* of the (main) changelog for the current version. Otherwise, a combined release is put together
* from the contents of the stable and alpha changelogs.
*/
export async function createReleaseNotes(opts: ReleaseNotesOpts & LoggingOptions & WriteFileOpts) {
const currentVersion = readVersion(opts.versionFile);
debugObject(opts, 'Current version info', currentVersion);

writeFile(opts, opts.releaseNotesFile, await releaseNoteContents(currentVersion, opts));
}

async function releaseNoteContents(currentVersion: Versions, opts: ReleaseNotesOpts) {
const stableChangelogContents = await readChangelogSection(opts.changelogFile, currentVersion.stableVersion);
// If we don't have an alpha version and distinct alpha changelog, the release notes are just the main changelog section.
if (!opts.alphaChangelogFile || !currentVersion.alphaVersion) { return stableChangelogContents; }

const alphaChangelogContents = await readChangelogSection(opts.alphaChangelogFile, currentVersion.alphaVersion);

// See https://github.com/aws/aws-cdk-rfcs/blob/master/text/0249-v2-experiments.md#changelog--release-notes for format
return [
'## aws-cdk-lib', // Assumption: we only have stable + alpha changelogs on v2+, where aws-cdk-lib is the stable module.
stableChangelogContents,
'---',
`## Alpha modules (${currentVersion.alphaVersion})`,
alphaChangelogContents,
].join('\n');
}

async function readChangelogSection(changelogFile: string, version: string) {
const changelog = await parseChangelog(changelogFile) as Changelog;
const entry = (changelog.versions || []).find(section => section.version === version);
if (!entry) {
throw new Error(`No changelog entry found for version ${version} in ${changelogFile}`);
}
return entry.body;
}

/** @types/changelog-parser only returns `object`; this is slightly more helpful */
interface Changelog {
title: string;
description: string;
versions?: ChangelogVersion[];
}
interface ChangelogVersion {
version: string;
title: string;
body: string;
}
12 changes: 12 additions & 0 deletions tools/@aws-cdk/cdk-release/lib/versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { Versions } from './types';

export function readVersion(versionFile: string): Versions {
const versionPath = path.resolve(process.cwd(), versionFile);
const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' }));
return {
stableVersion: contents.version,
alphaVersion: contents.alphaVersion,
};
}
9 changes: 5 additions & 4 deletions tools/@aws-cdk/cdk-release/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@
},
"dependencies": {
"@lerna/project": "^4.0.0",
"changelog-parser": "^2.8.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.2",
"conventional-changelog-writer": "^4.1.0",
"conventional-commits-parser": "^3.2.2",
"detect-indent": "^6.1.0",
"detect-newline": "^3.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"
"stringify-package": "^1.0.1"
},
"keywords": [
"aws",
Expand Down
62 changes: 62 additions & 0 deletions tools/@aws-cdk/cdk-release/test/release-notes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as files from '../lib/private/files';
import { createReleaseNotes } from '../lib/release-notes';
import * as versions from '../lib/versions';

/** MOCKS */
const mockWriteFile = jest.spyOn(files, 'writeFile').mockImplementation(() => jest.fn());
const mockReadVersion = jest.spyOn(versions, 'readVersion');
jest.mock('changelog-parser', () => { return jest.fn(); });
// eslint-disable-next-line @typescript-eslint/no-require-imports
const changelogParser = require('changelog-parser');
/** MOCKS */

beforeEach(() => { jest.resetAllMocks(); });

const DEFAULT_OPTS = {
changelogFile: 'CHANGELOG.md',
releaseNotesFile: 'RELEASE_NOTES.md',
versionFile: 'versions.json',
};

test('without alpha releases, only the stable changelog is returned', async () => {
mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3' }; });
mockChangelogOnceForVersion('1.2.3', 'foo');

await createReleaseNotes(DEFAULT_OPTS);

expectReleaseNotes('foo');
});

test('with alpha releases the contents of both are returned as separate sections', async () => {
mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3', alphaVersion: '1.2.3-alpha' }; });
mockChangelogOnceForVersion('1.2.3', 'foo'); // stable
mockChangelogOnceForVersion('1.2.3-alpha', 'bar'); // alpha

await createReleaseNotes({ ...DEFAULT_OPTS, alphaChangelogFile: 'CHANGELOG.alpha.md' });

expectReleaseNotes([
'## aws-cdk-lib',
'foo',
'---',
'## Alpha modules (1.2.3-alpha)',
'bar',
]);
});

test('throws if no matching version is found in the changelog', async () => {
mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3' }; });
mockChangelogOnceForVersion('4.5.6', 'foo');

await expect(createReleaseNotes(DEFAULT_OPTS))
.rejects
.toThrow(/No changelog entry found for version 1.2.3 in CHANGELOG.md/);
});

function mockChangelogOnceForVersion(version: string, body: string) {
changelogParser.mockImplementationOnce((_: string) => { return { versions: [{ version, body }] }; });
}

function expectReleaseNotes(contents: string | string[]) {
const data = (typeof contents === 'string') ? contents : contents.join('\n');
expect(mockWriteFile).toBeCalledWith(expect.any(Object), 'RELEASE_NOTES.md', data);
}
Loading

0 comments on commit fb23802

Please sign in to comment.