From 7cf59724104ef43e73e96176acbb8a5be79b1921 Mon Sep 17 00:00:00 2001 From: Michael Daniels Date: Fri, 12 Dec 2025 17:36:53 -0500 Subject: [PATCH] {workflows/lint,ci/github-script}: lint commit messages --- .github/workflows/lint.yml | 22 ++++++++ ci/github-script/lint-commits.js | 90 ++++++++++++++++++++++++++++++++ ci/github-script/run | 11 ++++ 3 files changed, 123 insertions(+) create mode 100644 ci/github-script/lint-commits.js diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 95ad536571a8..ff037c038eba 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -124,3 +124,25 @@ jobs: echo "If you're having trouble, ping @NixOS/nixpkgs-vet" exit "$exitCode" fi + + commits: + runs-on: ubuntu-slim + timeout-minutes: 5 + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: trusted + sparse-checkout: | + ci/github-script + - name: Check commit messages + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const checkCommitMessages = require('./trusted/ci/github-script/lint-commits.js') + + checkCommitMessages({ + github, + context, + core, + }) diff --git a/ci/github-script/lint-commits.js b/ci/github-script/lint-commits.js new file mode 100644 index 000000000000..1824c538b22f --- /dev/null +++ b/ci/github-script/lint-commits.js @@ -0,0 +1,90 @@ +// @ts-check +const { classify } = require('../supportedBranches.js') + +/** + * @param {{ + * github: InstanceType, + * context: import('@actions/github/lib/context').Context + * core: import('@actions/core') + * }} CheckCommitMessagesProps + */ +async function checkCommitMessages({ github, context, core }) { + // Do not run on merge groups: it'd be much harder, and there is no need + // (changes to the target branch can't change commit messages on the base branch). + const pull_number = context.payload.pull_request?.number + if (!pull_number) { + core.info('This is not a pull request. Skipping checks.') + return + } + + const pr = ( + await github.rest.pulls.get({ + ...context.repo, + pull_number, + }) + ).data + + const baseBranchType = classify( + pr.base.ref.replace(/^refs\/heads\//, ''), + ).type + const headBranchType = classify( + pr.head.ref.replace(/^refs\/heads\//, ''), + ).type + + if ( + baseBranchType.includes('development') && + headBranchType.includes('development') + ) { + // This matches, for example, PRs from staging-next to master, or vice versa. + // Ignore them: we should only care about PRs introducing *new* commits. + core.info( + 'This PR is from one development branch to another. Skipping checks.', + ) + return + } + + const commits = await github.paginate(github.rest.pulls.listCommits, { + ...context.repo, + pull_number, + }) + + const failures = new Set() + + for (const commit of commits) { + const message = commit.commit.message + const firstLine = message.split('\n')[0] + + const logMsgStart = `Commit ${commit.sha}'s message's subject ("${firstLine}")` + + if (!firstLine.includes(':')) { + core.error( + `${logMsgStart} was detected as not meeting our guidelines because ` + + 'it does not contain a colon. There are likely other issues as well.', + ) + failures.add(commit.sha) + } + + if (firstLine.endsWith('.')) { + core.error( + `${logMsgStart} was detected as not meeting our guidelines because ` + + 'it ends in a period. There may be other issues as well.', + ) + failures.add(commit.sha) + } + + if (!failures.has(commit.sha)) { + core.info(`${logMsgStart} passed our automated checks!`) + } + } + + if (failures.size !== 0) { + core.error( + 'Please review the guidelines at ' + + 'https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#commit-conventions, ' + + 'as well as the applicable area-specific guidelines linked there.', + ) + core.setFailed('Committers: merging is discouraged.') + } +} + +module.exports = checkCommitMessages diff --git a/ci/github-script/run b/ci/github-script/run index 8a2906606cac..095768f317da 100755 --- a/ci/github-script/run +++ b/ci/github-script/run @@ -94,4 +94,15 @@ program await run(getTeams, owner, repo, undefined, { ...options, outFile }) }) +program + .command('lint-commits') + .description('Lint for common errors in commit messages') + .argument('', 'Owner of the GitHub repository to run on (Example: NixOS)') + .argument('', 'Name of the GitHub repository to run on (Example: nixpkgs)') + .argument('', 'Number of the Pull Request to run on') + .action(async (owner, repo, pr, options) => { + const checkCommitMessages = (await import('./lint-commits.js')).default + await run(checkCommitMessages, owner, repo, pr, options) + }) + await program.parse()