ci/github-script/lint-commits: error when conventional commit format is used

E.g. https://redirect.github.com/NixOS/nixpkgs/pull/495442
This commit is contained in:
Michael Daniels
2026-03-01 10:14:27 -05:00
parent d03b81d689
commit 58f002f950
2 changed files with 75 additions and 6 deletions

View File

@@ -23,6 +23,11 @@ async function runGit({ args, repoPath, core, quiet }) {
}
/**
* Gets the SHA, subject and changed files for each commit in the given PR.
*
* Don't use GitHub API at all: the "list commits on PR" endpoint has a limit
* of 250 commits and doesn't return the changed files.
*
* @param {{
* core: import('@actions/core'),
* pr: Awaited<ReturnType<InstanceType<import('@actions/github/lib/utils').GitHub>["rest"]["pulls"]["get"]>>["data"]
@@ -32,6 +37,8 @@ async function runGit({ args, repoPath, core, quiet }) {
* @returns {Promise<{
* subject: string,
* sha: string,
* changedPaths: string[],
* changedPathSegments: Set<string>,
* }[]>}
*/
async function getCommitDetailsForPR({ core, pr, repoPath }) {
@@ -63,9 +70,10 @@ async function getCommitDetailsForPR({ core, pr, repoPath }) {
return Promise.all(
shas.map(async (sha) => {
// Subject first, then a blank line, then filenames.
const result = (
await runGit({
args: ['log', '--format=%s', '--numstat', '-1', sha],
args: ['log', '--format=%s', '--name-only', '-1', sha],
repoPath,
core,
quiet: true,
@@ -74,9 +82,17 @@ async function getCommitDetailsForPR({ core, pr, repoPath }) {
const subject = result[0]
const changedPaths = result.slice(2, -1)
const changedPathSegments = new Set(
changedPaths.flatMap((path) => path.split('/')),
)
return {
sha,
subject,
changedPaths,
changedPathSegments,
}
}),
)

View File

@@ -46,17 +46,60 @@ async function checkCommitMessages({ github, context, core, repoPath }) {
return
}
const commits = await getCommitDetailsForPR({
core,
pr,
repoPath,
})
const commits = await getCommitDetailsForPR({ core, pr, repoPath })
const failures = new Set()
const conventionalCommitTypes = [
'build',
'chore',
'ci',
'doc',
'docs',
'feat',
'feature',
'fix',
'perf',
'refactor',
'style',
'test',
]
/**
* @param {string[]} types e.g. ["fix", "feat"]
* @param {string?} sha commit hash
*/
function makeConventionalCommitRegex(types, sha = null) {
core.info(
`${
sha
? `Conventional commit types for ${sha?.slice(0, 16)}`
: 'Default conventional commit types'
}: ${JSON.stringify(types)}`,
)
return new RegExp(`^(${types.join('|')})!?(\\(.*\\))?!?:`)
}
// Optimize for the common case that we don't have path segments with the
// same name as a conventional commit type.
const fullConventionalCommitRegex = makeConventionalCommitRegex(
conventionalCommitTypes,
)
for (const commit of commits) {
const logMsgStart = `Commit ${commit.sha}'s message's subject ("${commit.subject}")`
// If we have a commit `perf: ...`, and we touch a file containing the path
// segment "perf", we don't want to flag this.
const filteredTypes = conventionalCommitTypes.filter(
(type) => !commit.changedPathSegments.has(type),
)
const conventionalCommitRegex =
filteredTypes.length === conventionalCommitTypes.length
? fullConventionalCommitRegex
: makeConventionalCommitRegex(filteredTypes, commit.sha)
if (!commit.subject.includes(': ')) {
core.error(
`${logMsgStart} was detected as not meeting our guidelines because ` +
@@ -84,6 +127,16 @@ async function checkCommitMessages({ github, context, core, repoPath }) {
failures.add(commit.sha)
}
if (conventionalCommitRegex.test(commit.subject)) {
core.error(
`${logMsgStart} was detected as not meeting our guidelines because ` +
'it seems to use conventional commit (conventionalcommits.org) ' +
'formatting. Nixpkgs has its own, different, commit message ' +
'formatting standards.',
)
failures.add(commit.sha)
}
if (!failures.has(commit.sha)) {
core.info(`${logMsgStart} passed our automated checks!`)
}