nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
1// @ts-check
2const { classify } = require('../supportedBranches.js')
3const { getCommitDetailsForPR } = require('./get-pr-commit-details.js')
4
5/**
6 * @param {{
7 * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
8 * context: import('@actions/github/lib/context').Context,
9 * core: import('@actions/core'),
10 * repoPath?: string,
11 * }} CheckCommitMessagesProps
12 */
13async function checkCommitMessages({ github, context, core, repoPath }) {
14 // This check should only be run when we have the pull_request context.
15 const pull_number = context.payload.pull_request?.number
16 if (!pull_number) {
17 core.info('This is not a pull request. Skipping checks.')
18 return
19 }
20
21 const pr = (
22 await github.rest.pulls.get({
23 ...context.repo,
24 pull_number,
25 })
26 ).data
27
28 const baseBranchType = classify(
29 pr.base.ref.replace(/^refs\/heads\//, ''),
30 ).type
31 const headBranchType = classify(
32 pr.head.ref.replace(/^refs\/heads\//, ''),
33 ).type
34
35 if (
36 baseBranchType.includes('development') &&
37 headBranchType.includes('development') &&
38 pr.base.repo.id === pr.head.repo?.id
39 ) {
40 // This matches, for example, PRs from NixOS:staging-next to NixOS:master, or vice versa.
41 // Ignore them: we should only care about PRs introducing *new* commits.
42 // We still want to run on PRs from, e.g., Someone:master to NixOS:master, though.
43 core.info(
44 'This PR is from one development branch to another. Skipping checks.',
45 )
46 return
47 }
48
49 const commits = await getCommitDetailsForPR({ core, pr, repoPath })
50
51 const failures = new Set()
52
53 const conventionalCommitTypes = [
54 'build',
55 'chore',
56 'ci',
57 'doc',
58 'docs',
59 'feat',
60 'feature',
61 'fix',
62 'perf',
63 'refactor',
64 'style',
65 'test',
66 ]
67
68 /**
69 * @param {string[]} types e.g. ["fix", "feat"]
70 * @param {string?} sha commit hash
71 */
72 function makeConventionalCommitRegex(types, sha = null) {
73 core.info(
74 `${
75 sha
76 ? `Conventional commit types for ${sha?.slice(0, 16)}`
77 : 'Default conventional commit types'
78 }: ${JSON.stringify(types)}`,
79 )
80
81 return new RegExp(`^(${types.join('|')})!?(\\(.*\\))?!?:`)
82 }
83
84 // Optimize for the common case that we don't have path segments with the
85 // same name as a conventional commit type.
86 const fullConventionalCommitRegex = makeConventionalCommitRegex(
87 conventionalCommitTypes,
88 )
89
90 for (const commit of commits) {
91 const logMsgStart = `Commit ${commit.sha}'s message's subject ("${commit.subject}")`
92
93 // If we have a commit `perf: ...`, and we touch a file containing the path
94 // segment "perf", we don't want to flag this.
95 const filteredTypes = conventionalCommitTypes.filter(
96 (type) => !commit.changedPathSegments.has(type),
97 )
98 const conventionalCommitRegex =
99 filteredTypes.length === conventionalCommitTypes.length
100 ? fullConventionalCommitRegex
101 : makeConventionalCommitRegex(filteredTypes, commit.sha)
102
103 if (!commit.subject.includes(': ')) {
104 core.error(
105 `${logMsgStart} was detected as not meeting our guidelines because ` +
106 'it does not contain a colon followed by a whitespace. ' +
107 'There are likely other issues as well.',
108 )
109 failures.add(commit.sha)
110 }
111
112 if (commit.subject.endsWith('.')) {
113 core.error(
114 `${logMsgStart} was detected as not meeting our guidelines because ` +
115 'it ends in a period. There may be other issues as well.',
116 )
117 failures.add(commit.sha)
118 }
119
120 const fixups = ['amend!', 'fixup!', 'squash!']
121 if (fixups.some((s) => commit.subject.startsWith(s))) {
122 core.error(
123 `${logMsgStart} was detected as not meeting our guidelines because ` +
124 `it begins with "${fixups.find((s) => commit.subject.startsWith(s))}". ` +
125 'Did you forget to run `git rebase -i --autosquash`?',
126 )
127 failures.add(commit.sha)
128 }
129
130 if (conventionalCommitRegex.test(commit.subject)) {
131 core.error(
132 `${logMsgStart} was detected as not meeting our guidelines because ` +
133 'it seems to use conventional commit (conventionalcommits.org) ' +
134 'formatting. Nixpkgs has its own, different, commit message ' +
135 'formatting standards.',
136 )
137 failures.add(commit.sha)
138 }
139
140 if (!failures.has(commit.sha)) {
141 core.info(`${logMsgStart} passed our automated checks!`)
142 }
143 }
144
145 if (failures.size !== 0) {
146 core.error(
147 'Please review the guidelines at ' +
148 '<https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#commit-conventions>, ' +
149 'as well as the applicable area-specific guidelines linked there.',
150 )
151 core.setFailed('Committers: merging is discouraged.')
152 }
153}
154
155module.exports = checkCommitMessages