Skip to content

build: generate changelog as part of staging script #14250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"api": "gulp api-docs",
"breaking-changes": "gulp breaking-changes",
"gulp": "gulp",
"stage-release": "bash ./tools/release/stage-release-bin.sh",
"stage-release": "ts-node --project tools/release/ tools/release/stage-release.ts",
"preinstall": "node ./tools/npm/check-npm.js"
},
"version": "7.1.0",
Expand Down Expand Up @@ -78,20 +78,19 @@
"axe-webdriverjs": "^1.1.1",
"chalk": "^1.1.3",
"codelyzer": "^4.5.0",
"conventional-changelog": "^3.0.5",
"dgeni": "^0.4.10",
"dgeni-packages": "^0.26.12",
"firebase": "^5.5.2",
"firebase-admin": "^5.0.0",
"firebase-tools": "^4.1.0",
"fs-extra": "^3.0.1",
"git-semver-tags": "^2.0.0",
"glob": "^7.1.2",
"gulp": "^3.9.1",
"gulp-clean": "^0.3.2",
"gulp-clean-css": "^3.3.1",
"gulp-cli": "^2.0.1",
"gulp-connect": "^5.0.0",
"gulp-conventional-changelog": "^1.1.3",
"gulp-dom": "^0.9.17",
"gulp-flatten": "^0.3.1",
"gulp-highlight-files": "^0.0.5",
Expand All @@ -117,6 +116,7 @@
"karma-sourcemap-loader": "^0.3.7",
"magic-string": "^0.22.4",
"marked": "^0.5.1",
"merge2": "^1.2.3",
"minimatch": "^3.0.4",
"minimist": "^1.2.0",
"mock-fs": "^4.7.0",
Expand Down
1 change: 0 additions & 1 deletion tools/gulp/gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ createPackageBuildTasks(momentAdapterPackage);

import './tasks/aot';
import './tasks/breaking-changes';
import './tasks/changelog';
import './tasks/ci';
import './tasks/clean';
import './tasks/default';
Expand Down
99 changes: 0 additions & 99 deletions tools/gulp/tasks/changelog.ts

This file was deleted.

25 changes: 0 additions & 25 deletions tools/release/BUILD.bazel

This file was deleted.

98 changes: 98 additions & 0 deletions tools/release/changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {green, grey} from 'chalk';
import {createReadStream, createWriteStream, readFileSync} from 'fs';
import {prompt} from 'inquirer';
import {join} from 'path';
import {Readable} from 'stream';

// These imports lack type definitions.
const conventionalChangelog = require('conventional-changelog');
const merge2 = require('merge2');

/** Prompts for a changelog release name and prepends the new changelog. */
export async function promptAndGenerateChangelog(changelogPath: string) {
const releaseName = await promptChangelogReleaseName();
await prependChangelogFromLatestTag(changelogPath, releaseName);
}

/**
* Writes the changelog from the latest Semver tag to the current HEAD.
* @param changelogPath Path to the changelog file.
* @param releaseName Name of the release that should show up in the changelog.
*/
export async function prependChangelogFromLatestTag(changelogPath: string, releaseName: string) {
const outputStream: Readable = conventionalChangelog(
/* core options */ {preset: 'angular'},
/* context options */ {title: releaseName},
/* raw-commits options */ null,
/* commit parser options */ null,
/* writer options */ createDedupeWriterOptions(changelogPath));

// Stream for reading the existing changelog. This is necessary because we want to
// actually prepend the new changelog to the existing one.
const previousChangelogStream = createReadStream(changelogPath);

return new Promise((resolve, reject) => {
// Sequentially merge the changelog output and the previous changelog stream, so that
// the new changelog section comes before the existing versions. Afterwards, pipe into the
// changelog file, so that the changes are reflected on file system.
const mergedCompleteChangelog = merge2(outputStream, previousChangelogStream);

// Wait for the previous changelog to be completely read because otherwise we would
// read and write from the same source which causes the content to be thrown off.
previousChangelogStream.on('end', () => {
mergedCompleteChangelog.pipe(createWriteStream(changelogPath))
.once('error', (error: any) => reject(error))
.once('finish', () => resolve());
});

});
}

/** Prompts the terminal for a changelog release name. */
export async function promptChangelogReleaseName(): Promise<string> {
return (await prompt<{releaseName: string}>({
type: 'text',
name: 'releaseName',
message: 'What should be the name of the release?'
})).releaseName;
}

/**
* Creates changelog writer options which ensure that commits are not showing up multiple times.
* Commits can show up multiple times if a changelog has been generated on a publish branch
* and has been cherry-picked into "master". In that case, the changelog will already contain
* commits from master which might be added to the changelog again. This is because usually
* patch and minor releases are tagged from the publish branches and therefore
* conventional-changelog tries to build the changelog from last major version to master's HEAD.
*/
function createDedupeWriterOptions(changelogPath: string) {
const existingChangelogContent = readFileSync(changelogPath, 'utf8');

return {
// Specify a writer option that can be used to modify the content of a new changelog section.
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
finalizeContext: (context: any) => {
context.commitGroups.forEach((group: any) => {
group.commits = group.commits.filter((commit: any) => {
// NOTE: We cannot compare the SHA's because the commits will have a different SHA
// if they are being cherry-picked into a different branch.
if (existingChangelogContent.includes(commit.header)) {
console.log(grey(`Excluding: "${commit.header}" (${commit.hash})`));
return false;
}
return true;
});
});
return context;
}
};
}

/** Entry-point for generating the changelog when called through the CLI. */
if (require.main === module) {
promptAndGenerateChangelog(join(__dirname, '../../CHANGELOG.md')).then(() => {
console.log(green(' ✓ Successfully updated the changelog.'));
});
}


2 changes: 1 addition & 1 deletion tools/release/prompt/new-version-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function promptForNewVersion(currentVersion: Version): Promise<Vers
message: `What's the type of the new release?`,
choices: versionChoices,
}, {
type: 'prompt',
type: 'confirm',
name: 'isPrerelease',
message: 'Should this be a pre-release?',
// Prompt whether this should a pre-release if the current release is not a pre-release
Expand Down
10 changes: 0 additions & 10 deletions tools/release/stage-release-bin.sh

This file was deleted.

30 changes: 12 additions & 18 deletions tools/release/stage-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import {bold, cyan, green, italic, red, yellow} from 'chalk';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {prompt} from 'inquirer';
import {join} from 'path';
import {promptAndGenerateChangelog} from './changelog';
import {GitClient} from './git/git-client';
import {promptForNewVersion} from './prompt/new-version-prompt';
import {parseVersionName, Version} from './version-name/parse-version';
import {getExpectedPublishBranch} from './version-name/publish-branch';

/** Default filename for the changelog. */
const CHANGELOG_FILE_NAME = 'CHANGELOG.md';

/**
* Class that can be instantiated in order to stage a new release. The tasks requires user
* interaction/input through command line prompts.
Expand Down Expand Up @@ -92,13 +96,15 @@ class StageReleaseTask {

console.log(green(` ✓ Updated the version to "${bold(newVersionName)}" inside of the ` +
`${italic('package.json')}`));
console.log();

// TODO(devversion): run changelog script w/prompts in the future.
// For now, we just let users make modifications and stage the changes.
await promptAndGenerateChangelog(join(this.projectDir, CHANGELOG_FILE_NAME));

console.log(yellow(` ⚠ Please generate the ${bold('CHANGELOG')} for the new version. ` +
`You can also make other unrelated modifications. After the changes have been made, ` +
`just continue here.`));
console.log();
console.log(green(` ✓ Updated the changelog in ` +
`"${bold(CHANGELOG_FILE_NAME)}"`));
console.log(yellow(` ⚠ You can also make other unrelated modifications. After the ` +
`changes have been made, just continue here.`));
console.log();

const {shouldContinue} = await prompt<{shouldContinue: boolean}>({
Expand Down Expand Up @@ -166,19 +172,7 @@ class StageReleaseTask {
}

/** Entry-point for the release staging script. */
async function main() {
const projectDir = process.argv.slice(2)[0];

if (!projectDir) {
console.error(red(`You specified no project directory. Cannot run stage release script.`));
console.error(red(`Usage: bazel run //tools/release:stage-release <project-directory>`));
process.exit(1);
}

return new StageReleaseTask(projectDir).run();
}

if (require.main === module) {
main();
new StageReleaseTask(join(__dirname, '../../')).run();
}

Loading