Skip to content
Draft
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
171 changes: 171 additions & 0 deletions .github/workflows/check-commit-messages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#

Copy link
Copy Markdown
Member

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-commit hooks using it, or other options that enable a human or copilot to validate.

Copy link
Copy Markdown
Collaborator Author

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.

# 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]
Comment thread
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
70 changes: 0 additions & 70 deletions .github/workflows/check-pr-title.yml

This file was deleted.

7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ PR. Concretely:

### Validating your commits

Before pushing, validate each commit, not just the tip of the branch. CI enforces that
Before pushing, validate each commit, not just the tip of the branch. CI validates the
Conventional Commit header of **every** commit in the PR (not the PR title), so make
sure each commit's summary line follows the format above. PR titles only need to be
descriptive — they are not required to follow Conventional Commits. Clean up any invalid
or `fixup!` commits (see [Responding to review feedback](#responding-to-review-feedback))
before your PR is approved. CI also enforces that
rendered specs match the committed state, so re-render any components you touched.
For changes that affect RPM output, build and smoke-test the result. Pure documentation
or metadata changes don't require a rebuild. See the [`README.md`](README.md) for
Expand Down
Loading