-
Notifications
You must be signed in to change notification settings - Fork 693
ci: validate commit messages instead of PR title #18462
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
Draft
Christopher Co (christopherco)
wants to merge
1
commit into
microsoft:4.0
Choose a base branch
from
christopherco:chrco/fix-conventional-commit-checker
base: 4.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+177
−71
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| # | ||
| # Validates that every commit entering the permanent 4.0 history through | ||
| # rebase-merge follows the Conventional Commits header format documented in | ||
| # CONTRIBUTING.md. | ||
| # | ||
| # Workflows that run on 'pull_request_target' trigger need to be carefully | ||
| # reviewed since they run in the context of the PR target and consume unvalidated | ||
| # input controlled by a PR submitter. We've reviewed this workflow and | ||
| # allow-listed it via the 'zizmor' comment below. This workflow reads commit | ||
| # metadata exclusively through the GitHub API and never checks out or executes | ||
| # any code from the PR branch, so it is safe to use pull_request_target. | ||
| # | ||
|
|
||
| name: "Check Commit Messages" | ||
|
|
||
| on: | ||
| pull_request_target: # zizmor: ignore[dangerous-triggers] | ||
|
christopherco marked this conversation as resolved.
|
||
| # Only run on PRs targeting 4.0. pull_request_target always uses the workflow | ||
| # from the default branch. This workflow does not require any untrusted | ||
| # scripts from the PR branch, so it is safe to use pull_request_target. | ||
| branches: | ||
| - "4.0" | ||
| types: | ||
| - opened | ||
| - edited | ||
| - reopened | ||
| - synchronize | ||
| - ready_for_review | ||
|
|
||
| # Cancel in-progress runs of this workflow if a new run is triggered. | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| main: | ||
| name: Validate commit messages | ||
| # Prevent forks from running a stale/vulnerable copy of this workflow with Actions enabled | ||
| if: github.repository == 'microsoft/azurelinux' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write # Needed to post comments on PR | ||
| steps: | ||
| - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||
| id: validate_commits | ||
| with: | ||
| script: | | ||
| // Conventional Commit types documented in CONTRIBUTING.md. | ||
| const types = [ | ||
| 'feat', 'fix', 'docs', 'style', 'refactor', | ||
| 'perf', 'test', 'build', 'ci', 'chore', 'revert', | ||
| ]; | ||
| // <type>(<optional scope>)<optional !>: <summary> | ||
| const headerRegex = new RegExp( | ||
| '^(' + types.join('|') + ')' + // type | ||
| '(\\([^)\\r\\n]+\\))?' + // optional (scope) | ||
| '!?' + // optional breaking-change marker | ||
| ': .+' // ': ' followed by a non-empty summary | ||
| ); | ||
|
|
||
| const pr = context.payload.pull_request; | ||
| const commits = await github.paginate( | ||
| github.rest.pulls.listCommits, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: pr.number, | ||
| per_page: 100, | ||
| } | ||
| ); | ||
|
|
||
| // pulls.listCommits is hard-capped at 250 commits even with | ||
| // pagination. Fail closed if we did not receive the full set the PR | ||
| // reports, so a large PR cannot pass by validating only a prefix. | ||
| const expected = pr.commits; | ||
| if (typeof expected !== 'number' || commits.length !== expected) { | ||
| const message = | ||
| `Unable to validate all commits: received ${commits.length} ` + | ||
| `of ${expected} reported by the pull request. The commit-message ` + | ||
| `check cannot guarantee coverage of every commit; please split ` + | ||
| `this pull request.`; | ||
| core.setOutput('error_message', message); | ||
| core.setFailed(message); | ||
| return; | ||
| } | ||
|
|
||
| // Encode an untrusted commit subject at the rendering boundary: keep | ||
| // a conservative allowlist verbatim and emit every other character | ||
| // (including Markdown link/image syntax, HTML, mentions, backslashes, | ||
| // and Unicode format characters) as an HTML numeric entity. Wrapped | ||
| // in a <code> element, the result is inert text in the bot comment. | ||
| const encodeSubject = (text) => { | ||
| const firstLine = String(text || '').split('\n')[0].slice(0, 200); | ||
| let out = ''; | ||
| for (const ch of firstLine) { | ||
| out += /[A-Za-z0-9 _.\-/:]/.test(ch) | ||
| ? ch | ||
| : `&#${ch.codePointAt(0)};`; | ||
| } | ||
| return out; | ||
| }; | ||
|
|
||
| const invalid = []; | ||
| for (const c of commits) { | ||
| const shortSha = c.sha.substring(0, 8); | ||
| const subject = String(c.commit.message || '').split('\n')[0]; | ||
| if (!headerRegex.test(subject)) { | ||
| invalid.push({ sha: shortSha, subject }); | ||
| } | ||
| } | ||
|
|
||
| if (invalid.length === 0) { | ||
| core.setOutput('error_message', ''); | ||
| core.info(`All ${commits.length} commit message(s) are valid.`); | ||
| return; | ||
| } | ||
|
|
||
| // Comment body: HTML-encoded subjects inside <code> elements. | ||
| const list = invalid | ||
| .map((c) => `- \`${c.sha}\` <code>${encodeSubject(c.subject)}</code>`) | ||
| .join('\n'); | ||
| const commentMessage = | ||
| `The following commit(s) do not follow the Conventional Commits ` + | ||
| `header format:\n\n${list}`; | ||
| core.setOutput('error_message', commentMessage); | ||
|
|
||
| // Failure annotation/log: SHAs only, so no untrusted subject text is | ||
| // echoed into plain-text log output. | ||
| core.setFailed( | ||
| `The following commit(s) do not follow the Conventional Commits ` + | ||
| `header format: ${invalid.map((c) => c.sha).join(', ')}. ` + | ||
| `See the pull request comment for details.` | ||
| ); | ||
|
|
||
| - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | ||
| # When the previous step fails, the workflow would stop. By adding this | ||
| # condition you can continue the execution with the populated error message. | ||
| if: always() && (steps.validate_commits.outputs.error_message != '') | ||
| with: | ||
| header: commit-message-lint-error | ||
| message: | | ||
| Hello, and thank you for opening this pull request! 👋🏼 We appreciate the contribution. | ||
|
|
||
| Because this repository uses **rebase-merge**, every commit you push becomes part of the permanent `4.0` history. We require each commit message header to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/), as described in [`CONTRIBUTING.md`](https://github.com/microsoft/azurelinux/blob/4.0/CONTRIBUTING.md#conventional-commits). PR titles do **not** need to follow this format. | ||
|
|
||
| A valid header looks like: | ||
|
|
||
| ``` | ||
| feat(component): add capability | ||
| fix(kernel)!: change incompatible behavior | ||
| ``` | ||
|
|
||
| Use one of the standard types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`. The scope in parentheses and the `!` breaking-change marker are optional. | ||
|
|
||
| Please fix the offending commit(s) below by amending or rebasing: | ||
|
|
||
| - To fix the most recent commit: `git commit --amend`, then `git push --force-with-lease`. | ||
| - To fix earlier commits (including `fixup!` / "address review feedback" commits): `git rebase -i`, reword the offending commits, then `git push --force-with-lease`. | ||
|
|
||
| Details: | ||
|
|
||
| ${{ steps.validate_commits.outputs.error_message }} | ||
|
|
||
| # Delete the previous comment once every commit message is valid. | ||
| - if: steps.validate_commits.outputs.error_message == '' | ||
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | ||
| with: | ||
| header: commit-message-lint-error | ||
| delete: true | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question(non-blocking): This generally looks reasonable, but I'll acknowledge that my JavaScript skills aren't my best. My main question is around general approach.
Did we consider using an alternative action (e.g., https://github.com/wagoid/commitlint-github-action)? I do recognize that we want to be careful in what we select.
Even if we're not comfortable with some of these other actions, I think we should seriously look at a commit linting approach that can be replicated locally too -- even if that's a follow-up here. commitlint has 18k GitHub stars and seems to be a dominant player. What I like about an approach that works equally well locally is that we could later look at
pre-commithooks using it, or other options that enable a human or copilot to validate.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this didn't come up in my original search, but I'll get a prototype of it going to see how it looks. Then we can decide which direction we want to go.