1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
|
import { getCommitDetailsForPR } from './get-pr-commit-details.js'
import { classify } from './supportedBranches.js'
/** @typedef {import('./get-pr-commit-details.js').Commit} Commit */
/**
* @param {{
* github: InstanceType<typeof import('@actions/github/lib/utils').GitHub>,
* context: typeof import('@actions/github').context,
* core: typeof import('@actions/core'),
* repoPath?: string,
* }} LintCommitsProps
*/
export default async function lintCommits({ github, context, core, repoPath }) {
// This check should only be run when we have the pull_request context.
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') &&
pr.base.repo.id === pr.head.repo?.id
) {
// This matches, for example, PRs from NixOS:staging-next to NixOS:master, or vice versa.
// Ignore them: we should only care about PRs introducing *new* commits.
// We still want to run on PRs from, e.g., Someone:master to NixOS:master, though.
core.info(
'This PR is from one development branch to another. Skipping checks.',
)
return
}
const commits = await getCommitDetailsForPR({ core, pr, repoPath })
await checkCommitMessages({ commits, core })
await checkCommitMetadata({ commits, core })
}
/**
* @param {{
* commits: Commit[],
* core: typeof import('@actions/core'),
* }} CheckCommitMessagesProps
*/
async function checkCommitMessages({ commits, core }) {
const failures = new Set()
const conventionalCommitTypes = [
'build',
'chore',
'ci',
'doc',
'docs',
'feat',
'feature',
'fix',
'perf',
'refactor',
'services',
'style',
'test',
'update',
]
/**
* @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 ` +
'it does not contain a colon followed by a whitespace. ' +
'There are likely other issues as well.',
)
failures.add(commit.sha)
}
if (commit.subject.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)
}
const fixups = ['amend!', 'fixup!', 'squash!']
if (fixups.some((s) => commit.subject.startsWith(s))) {
core.error(
`${logMsgStart} was detected as not meeting our guidelines because ` +
`it begins with "${fixups.find((s) => commit.subject.startsWith(s))}". ` +
'Did you forget to run `git rebase -i --autosquash`?',
)
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!`)
}
}
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.')
}
}
/**
* @param {{
* commits: Commit[],
* core: typeof import('@actions/core'),
* }} CheckGitFieldsProps
*/
async function checkCommitMetadata({ commits, core }) {
const failures = new Set()
/** @type {(s: string) => boolean} */
const isEmail = (s) => /^.+@.*$/.test(s)
for (const commit of commits) {
if (!commit.author.name) {
core.error(`Commit ${commit.sha} author's name field is missing`)
failures.add(commit.sha)
}
if (!commit.author.email || !isEmail(commit.author.email)) {
core.error(
`Commit ${commit.sha} author's email field is missing or invalid`,
)
failures.add(commit.sha)
}
if (!commit.committer.name) {
core.error(`Commit ${commit.sha} committer's name field is missing`)
failures.add(commit.sha)
}
if (!commit.committer.email || !isEmail(commit.committer.email)) {
core.error(
`Commit ${commit.sha} committer's email field is missing or invalid`,
)
failures.add(commit.sha)
}
if (!failures.has(commit.sha)) {
core.info(
`Commit ${commit.sha}'s git fields passed our automated checks!`,
)
}
}
if (failures.size !== 0) {
core.error(
'Please add the missing commit fields. ' +
'You can use the noreply email address generated for you by GitHub ' +
'(https://docs.github.com/en/account-and-profile/reference/email-addresses-reference#your-noreply-email-address) ' +
"if you'd like.",
)
core.setFailed('Committers: merging is discouraged.')
}
}
|