diff --git a/dev-infra/commit-message/validate.spec.ts b/dev-infra/commit-message/validate.spec.ts index 24fb996698..92632ed74a 100644 --- a/dev-infra/commit-message/validate.spec.ts +++ b/dev-infra/commit-message/validate.spec.ts @@ -263,5 +263,72 @@ describe('validate-commit-message.js', () => { VALID); }); }); + + describe('breaking change', () => { + it('should allow valid breaking change commit descriptions', () => { + const msgWithSummary = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE: This is a summary of a breaking change.'; + expectValidationResult(validateCommitMessage(msgWithSummary), VALID); + + const msgWithDescription = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE:\n\n' + + 'This is a full description of the breaking change.'; + expectValidationResult(validateCommitMessage(msgWithDescription), VALID); + + const msgWithSummaryAndDescription = + 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE: This is a summary of a breaking change.\n\n' + + 'This is a full description of the breaking change.'; + expectValidationResult(validateCommitMessage(msgWithSummaryAndDescription), VALID); + + const msgWithNonBreaking = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is not a\n' + + 'breaking change commit.'; + expectValidationResult(validateCommitMessage(msgWithNonBreaking), VALID); + }); + + it('should fail for non-valid breaking change commit descriptions', () => { + const msgWithSummary = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE This is a summary of a breaking change.'; + expectValidationResult( + validateCommitMessage(msgWithSummary), INVALID, + [`The commit message body contains an invalid breaking change description.`]); + + const msgWithPlural = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGES: This is a summary of a breaking change.'; + expectValidationResult( + validateCommitMessage(msgWithPlural), INVALID, + [`The commit message body contains an invalid breaking change description.`]); + + const msgWithDescription = 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE:\n' + + 'This is a full description of the breaking change.'; + expectValidationResult( + validateCommitMessage(msgWithDescription), INVALID, + [`The commit message body contains an invalid breaking change description.`]); + + const msgWithSummaryAndDescription = + 'feat(compiler): this is just an usual commit message tile\n\n' + + 'This is a normal commit message body which does not exceed the max length\n' + + 'limit. For more details see the following super long URL:\n\n' + + 'BREAKING CHANGE\n\n' + + 'This is a full description of the breaking change.'; + expectValidationResult( + validateCommitMessage(msgWithSummaryAndDescription), INVALID, + [`The commit message body contains an invalid breaking change description.`]); + }); + }); }); }); diff --git a/dev-infra/commit-message/validate.ts b/dev-infra/commit-message/validate.ts index 578f599973..3fa93c23fb 100644 --- a/dev-infra/commit-message/validate.ts +++ b/dev-infra/commit-message/validate.ts @@ -25,6 +25,16 @@ export interface ValidateCommitMessageResult { /** Regex matching a URL for an entire commit body line. */ const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/; +/** + * Regex matching a breaking change. + * + * - Starts with BREAKING CHANGE + * - Followed by a colon + * - Followed by a single space or two consecutive new lines + * + * NB: Anything after `BREAKING CHANGE` is optional to facilitate the validation. + */ +const COMMIT_BODY_BREAKING_CHANGE_RE = /^BREAKING CHANGE(:( |\n{2}))?/m; /** Validate a commit message against using the local repo's config. */ export function validateCommitMessage( @@ -137,11 +147,24 @@ export function validateCommitMessage( }); if (lineExceedsMaxLength) { - errors.push( - `The commit message body contains lines greater than ${config.maxLineLength} characters`); + errors.push(`The commit message body contains lines greater than ${ + config.maxLineLength} characters.`); return false; } + // Breaking change + // Check if the commit message contains a valid break change description. + // https://github.com/angular/angular/blob/88fbc066775ab1a2f6a8c75f933375b46d8fa9a4/CONTRIBUTING.md#commit-message-footer + const hasBreakingChange = COMMIT_BODY_BREAKING_CHANGE_RE.exec(commit.body); + if (hasBreakingChange !== null) { + const [, breakingChangeDescription] = hasBreakingChange; + if (!breakingChangeDescription) { + // Not followed by :, space or two consecutive new lines, + errors.push(`The commit message body contains an invalid breaking change description.`); + return false; + } + } + return true; } @@ -160,4 +183,9 @@ export function printValidationErrors(errors: string[], print = error) { print(); print('
'); print(); + print(`BREAKING CHANGE: