Skip to content
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

feat: Support generating change files from conventional commits #513

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions change/beachball-38e088f5-413c-44fe-98c1-6fccdbe43be0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Support generating change files from conventional commits",
"packageName": "beachball",
"email": "[email protected]",
"dependentChangeType": "patch"
}
13 changes: 11 additions & 2 deletions src/__e2e__/publishE2E.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -140,6 +141,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -230,6 +232,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -323,6 +326,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -396,6 +400,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -474,6 +479,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -547,6 +553,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -627,6 +634,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -700,6 +708,7 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: false,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -771,14 +780,15 @@ describe('publish command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
retries: 3,
bump: true,
generateChangelog: true,
dependentChangeType: null,
depth: 10
depth: 10,
});

const showResult = npm(['--registry', registry.getUrl(), 'show', 'foo', '--json']);
Expand All @@ -793,5 +803,4 @@ describe('publish command (e2e)', () => {
// no fetch when flag set to false
expect(depthString).toEqual('--depth=10');
});

});
2 changes: 2 additions & 0 deletions src/__e2e__/publishGit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('publish command (git)', () => {
package: 'foo',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -125,6 +126,7 @@ describe('publish command (git)', () => {
package: 'foo',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down
5 changes: 5 additions & 0 deletions src/__e2e__/publishRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('publish command (registry)', () => {
package: 'foo',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -136,6 +137,7 @@ describe('publish command (registry)', () => {
package: 'foo',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -220,6 +222,7 @@ describe('publish command (registry)', () => {
package: 'foopkg',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -300,6 +303,7 @@ describe('publish command (registry)', () => {
package: 'foopkg',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -385,6 +389,7 @@ describe('publish command (registry)', () => {
package: 'foopkg',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down
3 changes: 3 additions & 0 deletions src/__e2e__/syncE2E.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe('sync command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -145,6 +146,7 @@ describe('sync command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down Expand Up @@ -208,6 +210,7 @@ describe('sync command (e2e)', () => {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
disallowedChangeTypes: null,
defaultNpmTag: 'latest',
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/changefile/conventionalCommits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { parseConventionalCommit } from '../../changefile/conventionalCommits';

describe.each<[string, ReturnType<typeof parseConventionalCommit>]>([
['fix: change message\nbody', { type: 'patch', message: 'change message' }],
['chore: change', { type: 'none', message: 'change' }],
['feat: change', { type: 'minor', message: 'change' }],
['feat(scope): change', { type: 'minor', message: 'change' }],
['feat!: change', { type: 'major', message: 'change' }],
['feat(scope)!: change', { type: 'major', message: 'change' }],
['foo', undefined],
['fix(foo-bar): change', { type: 'patch', message: 'change' }],
])('parse(%s)', (s, expected) => {
test('should parse correctly', () => expect(parseConventionalCommit(s)).toEqual(expected));
});
44 changes: 44 additions & 0 deletions src/changefile/conventionalCommits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ChangeType } from '../types/ChangeInfo';

/**
* 1. type
* 2. scope
* 3. breaking
* 4. message
*/
const COMMIT_RE = /([a-z]+)(?:\(([a-z\-]+)\))?(!)?: (.+)/i;

interface ConventionalCommit {
type: string;
scope?: string;
breaking: boolean;
message: string;
}

interface Change {
type: ChangeType;
message: string;
}

export function parseConventionalCommit(commitMessage: string): Change | undefined {
const match = commitMessage.match(COMMIT_RE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: consider doing case insensitive match

const data: ConventionalCommit | undefined = match
? { type: match[1], scope: match[2], breaking: !!match[3], message: match[4] }
: undefined;
return data && map(data);
}

function map(d: ConventionalCommit): Change | undefined {
if (d.breaking) {
return { type: 'major', message: d.message };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a special case here, for semver 0.x.y packages. In that scheme breaking changes are a minor bump, and fixes/feats are a patch bump.

npm, dependabot, are aware of that scheme, it it's used in some common packages like react-native.

}

switch (d.type) {
case 'chore':
Copy link
Member

@tido64 tido64 Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would chore map to patch here? When I think of chore commits, I usually think of things like bumping a dev dependency or other changes that do not affect the published package.

Also, can you add support docs and test as well?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tido64 I've been doing some hacking locally to get it "just right" and I think what would be best would be to let the user provide a dictionary of allowed types and the change type (major/minor/patch/none) it should map to. That has worked well for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that makes sense. Also, I just saw your comment now, asking the exact same question 🤣

return { type: 'none', message: d.message };
case 'fix':
return { type: 'patch', message: d.message };
case 'feat':
return { type: 'minor', message: d.message };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this intersect with prerelease packages?

}
}
28 changes: 20 additions & 8 deletions src/changefile/promptForChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getPackageGroups } from '../monorepo/getPackageGroups';
import { isValidChangeType } from '../validation/isValidChangeType';
import { DefaultPrompt } from '../types/ChangeFilePrompt';
import { getDisallowedChangeTypes } from './getDisallowedChangeTypes';
import { parseConventionalCommit } from './conventionalCommits';

/**
* Uses `prompts` package to prompt for change type and description, fills in git user.email and scope
Expand All @@ -27,6 +28,15 @@ export async function promptForChange(options: BeachballOptions): Promise<Change

const packageGroups = getPackageGroups(packageInfos, options.path, options.groups);

// Check recent commit messages for structured conventional
// commits. If present, and --useConventionalCommits is set, fetch
// change type and description from the first available commit
// message.
const fromConventionalCommits =
(options.useConventionalCommits &&
recentMessages.map(parseConventionalCommit).filter(<T>(obj: T | undefined): obj is T => !!obj)) ||
[];

for (let pkg of changedPackages) {
console.log('');
console.log(`Please describe the changes for: ${pkg}`);
Expand All @@ -47,7 +57,7 @@ export async function promptForChange(options: BeachballOptions): Promise<Change
title: ' None - this change does not affect the published package in any way.',
},
{ value: 'major', title: ' Major - major feature; breaking changes.' },
].filter(choice => !disallowedChangeTypes?.includes(choice.value as ChangeType)),
].filter((choice) => !disallowedChangeTypes?.includes(choice.value as ChangeType)),
};

if (changeTypePrompt.choices!.length === 0) {
Expand All @@ -64,16 +74,18 @@ export async function promptForChange(options: BeachballOptions): Promise<Change
type: 'autocomplete',
name: 'comment',
message: 'Describe changes (type or choose one)',
suggest: input => {
return Promise.resolve([...recentMessages.filter(msg => msg.startsWith(input)), input]);
suggest: (input) => {
return Promise.resolve([...recentMessages.filter((msg) => msg.startsWith(input)), input]);
},
};

const showChangeTypePrompt = !options.type && changeTypePrompt.choices!.length > 1;
// Only include structured commit messages that map to an allowed change type.
const allowedConventionalCommit = fromConventionalCommits.find((c) => !disallowedChangeTypes?.includes(c.type));
const showChangeTypePrompt = !options.type && !allowedConventionalCommit && changeTypePrompt.choices!.length > 1;

const defaultPrompt: DefaultPrompt = {
changeType: showChangeTypePrompt ? changeTypePrompt : undefined,
description: !options.message ? descriptionPrompt : undefined,
description: !options.message && !allowedConventionalCommit ? descriptionPrompt : undefined,
};

let questions = [defaultPrompt.changeType, defaultPrompt.description];
Expand All @@ -86,11 +98,11 @@ export async function promptForChange(options: BeachballOptions): Promise<Change
questions = packageInfo.combinedOptions.changeFilePrompt?.changePrompt(defaultPrompt, pkg);
}

questions = questions.filter(q => !!q);
questions = questions.filter((q) => !!q);

let response: { comment: string; type: ChangeType } = {
type: options.type || 'none',
comment: options.message || '',
type: options.type || allowedConventionalCommit?.type || 'none',
comment: options.message || allowedConventionalCommit?.message || '',
};

if (questions.length > 0) {
Expand Down
10 changes: 9 additions & 1 deletion src/options/getCliOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ function getCliOptionsUncached(argv: string[]): CliOptions {
const args = parser(trimmedArgv, {
string: ['branch', 'tag', 'message', 'package', 'since', 'dependent-change-type', 'config'],
array: ['scope', 'disallowed-change-types'],
boolean: ['git-tags', 'keep-change-files', 'force', 'disallow-deleted-change-files', 'no-commit', 'fetch'],
boolean: [
'git-tags',
'keep-change-files',
'force',
'disallow-deleted-change-files',
'no-commit',
'fetch',
'use-conventional-commits',
],
number: ['depth'],
alias: {
authType: ['a'],
Expand Down
1 change: 1 addition & 0 deletions src/options/getDefaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function getDefaultOptions() {
package: '',
changehint: 'Run "beachball change" to create a change file',
type: null,
useConventionalCommits: false,
fetch: true,
version: false,
disallowedChangeTypes: null,
Expand Down
3 changes: 2 additions & 1 deletion src/types/BeachballOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface CliOptions
timeout?: number;
token: string;
type?: ChangeType | null;
useConventionalCommits: boolean;
verbose?: boolean;
version?: boolean;
yes: boolean;
Expand Down Expand Up @@ -141,7 +142,7 @@ export interface HooksOptions {
* Runs for each package, before writing changelog and package.json updates
* to the filesystem. May be called multiple times during publish.
*/
prebump?: (packagePath: string, name: string, version: string) => void | Promise<void>;
prebump?: (packagePath: string, name: string, version: string) => void | Promise<void>;

/**
* Runs for each package, after writing changelog and package.json updates
Expand Down