|
| 1 | +import {green, grey} from 'chalk'; |
| 2 | +import {createReadStream, createWriteStream, readFileSync} from 'fs'; |
| 3 | +import {prompt} from 'inquirer'; |
| 4 | +import {join} from 'path'; |
| 5 | +import {Readable} from 'stream'; |
| 6 | + |
| 7 | +// These imports lack type definitions. |
| 8 | +const conventionalChangelog = require('conventional-changelog'); |
| 9 | +const merge2 = require('merge2'); |
| 10 | + |
| 11 | +/** Prompts for a changelog release name and prepends the new changelog. */ |
| 12 | +export async function promptAndGenerateChangelog(changelogPath: string) { |
| 13 | + const releaseName = await promptChangelogReleaseName(); |
| 14 | + await prependChangelogFromLatestTag(changelogPath, releaseName); |
| 15 | +} |
| 16 | + |
| 17 | +/** |
| 18 | + * Writes the changelog from the latest Semver tag to the current HEAD. |
| 19 | + * @param changelogPath Path to the changelog file. |
| 20 | + * @param releaseName Name of the release that should show up in the changelog. |
| 21 | + */ |
| 22 | +export async function prependChangelogFromLatestTag(changelogPath: string, releaseName: string) { |
| 23 | + const outputStream: Readable = conventionalChangelog( |
| 24 | + /* core options */ {preset: 'angular'}, |
| 25 | + /* context options */ {title: releaseName}, |
| 26 | + /* raw-commits options */ null, |
| 27 | + /* commit parser options */ null, |
| 28 | + /* writer options */ createDedupeWriterOptions(changelogPath)); |
| 29 | + |
| 30 | + // Stream for reading the existing changelog. This is necessary because we want to |
| 31 | + // actually prepend the new changelog to the existing one. |
| 32 | + const previousChangelogStream = createReadStream(changelogPath); |
| 33 | + |
| 34 | + return new Promise((resolve, reject) => { |
| 35 | + // Sequentially merge the changelog output and the previous changelog stream, so that |
| 36 | + // the new changelog section comes before the existing versions. Afterwards, pipe into the |
| 37 | + // changelog file, so that the changes are reflected on file system. |
| 38 | + const mergedCompleteChangelog = merge2(outputStream, previousChangelogStream); |
| 39 | + |
| 40 | + // Wait for the previous changelog to be completely read because otherwise we would |
| 41 | + // read and write from the same source which causes the content to be thrown off. |
| 42 | + previousChangelogStream.on('end', () => { |
| 43 | + mergedCompleteChangelog.pipe(createWriteStream(changelogPath)) |
| 44 | + .once('error', (error: any) => reject(error)) |
| 45 | + .once('finish', () => resolve()); |
| 46 | + }); |
| 47 | + |
| 48 | + }); |
| 49 | +} |
| 50 | + |
| 51 | +/** Prompts the terminal for a changelog release name. */ |
| 52 | +export async function promptChangelogReleaseName(): Promise<string> { |
| 53 | + return (await prompt<{releaseName: string}>({ |
| 54 | + type: 'text', |
| 55 | + name: 'releaseName', |
| 56 | + message: 'What should be the name of the release?' |
| 57 | + })).releaseName; |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Creates changelog writer options which ensure that commits are not showing up multiple times. |
| 62 | + * Commits can show up multiple times if a changelog has been generated on a publish branch |
| 63 | + * and has been cherry-picked into "master". In that case, the changelog will already contain |
| 64 | + * commits from master which might be added to the changelog again. This is because usually |
| 65 | + * patch and minor releases are tagged from the publish branches and therefore |
| 66 | + * conventional-changelog tries to build the changelog from last major version to master's HEAD. |
| 67 | + */ |
| 68 | +function createDedupeWriterOptions(changelogPath: string) { |
| 69 | + const existingChangelogContent = readFileSync(changelogPath, 'utf8'); |
| 70 | + |
| 71 | + return { |
| 72 | + // Specify a writer option that can be used to modify the content of a new changelog section. |
| 73 | + // See: conventional-changelog/tree/master/packages/conventional-changelog-writer |
| 74 | + finalizeContext: (context: any) => { |
| 75 | + context.commitGroups.forEach((group: any) => { |
| 76 | + group.commits = group.commits.filter((commit: any) => { |
| 77 | + // NOTE: We cannot compare the SHA's because the commits will have a different SHA |
| 78 | + // if they are being cherry-picked into a different branch. |
| 79 | + if (existingChangelogContent.includes(commit.header)) { |
| 80 | + console.log(grey(`Excluding: "${commit.header}" (${commit.hash})`)); |
| 81 | + return false; |
| 82 | + } |
| 83 | + return true; |
| 84 | + }); |
| 85 | + }); |
| 86 | + return context; |
| 87 | + } |
| 88 | + }; |
| 89 | +} |
| 90 | + |
| 91 | +/** Entry-point for generating the changelog when called through the CLI. */ |
| 92 | +if (require.main === module) { |
| 93 | + promptAndGenerateChangelog(join(__dirname, '../../CHANGELOG.md')).then(() => { |
| 94 | + console.log(green(' ✓ Successfully updated the changelog.')); |
| 95 | + }); |
| 96 | +} |
| 97 | + |
| 98 | + |
0 commit comments