forked from nodejs/core-validate-commit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsigned-off-by.js
More file actions
219 lines (200 loc) · 6.67 KB
/
signed-off-by.js
File metadata and controls
219 lines (200 loc) · 6.67 KB
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
const id = 'signed-off-by'
// Matches the name and email from a Signed-off-by line
const signoffParts = /^Signed-off-by: (.*) <([^>]+)>/i
// Bot/AI patterns: name ending in [bot], or GitHub bot noreply emails
// This is an imperfect heuristic, but there's no standard way to identify
// bot commits in a reliable, generic way.
const botNamePattern = /\[bot\]$/i
const botEmailPattern = /(?:\[bot\]@users\.noreply\.github\.com|github-bot@iojs\.org)$/i
// Matches "Name <email>" author strings
const authorPattern = /^(.*?)\s*<([^>]+)>/
const signoffPattern = /^Signed-off-by: /i
const validSignoff = /^Signed-off-by: .+ <[^@]+@[^@]+\.[^@]+>/i
const backportPattern = /^Backport-PR-URL:/i
// Parse name and email from an author string like "Name <email>"
function parseAuthor (authorStr) {
if (!authorStr) return null
const match = authorStr.match(authorPattern)
if (!match) return null // Purely defensive check here
return { name: match[1].trim(), email: match[2].toLowerCase() }
}
// Check if an author string looks like a bot
function isBotAuthor (authorStr) {
const author = parseAuthor(authorStr)
if (!author) return false
return botNamePattern.test(author.name) || botEmailPattern.test(author.email)
}
export default {
id,
meta: {
description: 'enforce DCO sign-off',
recommended: true
},
defaults: {},
options: {},
validate: (context, rule) => {
const parsed = context.toJSON()
// Release commits generally won't have sign-offs
if (parsed.release) {
context.report({
id,
message: 'skipping sign-off for release commit',
string: '',
level: 'skip'
})
return
}
// Deps commits are cherry-picks/backports/updates from upstream projects
// (V8, etc.) and are not expected to have a Signed-off-by. When deps
// is mixed with other subsystems, a sign-off might be required but we'll
// downgrade to a warn instead of fail in this case.
const hasDeps = parsed.subsystems.includes('deps')
if (hasDeps && parsed.subsystems.every((s) => s === 'deps')) {
context.report({
id,
message: 'skipping sign-off for deps commit',
string: '',
level: 'skip'
})
return
}
const body = parsed.trailers ?? parsed.body
// Backport commits (identified by a Backport-PR-URL trailer) are
// cherry-picks of existing commits into release branches. The
// original commit was already validated.
if (body.some((line) => backportPattern.test(line))) {
context.report({
id,
message: 'skipping sign-off for backport commit',
string: '',
level: 'skip'
})
return
}
const signoffs = body
.filter(line => signoffPattern.test(line))
.map((line, i) => [line, i + parsed.body.length - body.length])
// Bot-authored commits don't need a sign-off.
// If they have one, warn; otherwise pass.
if (isBotAuthor(parsed.author)) {
if (signoffs.length === 0) {
context.report({
id,
message: 'skipping sign-off for bot commit',
string: '',
level: 'pass'
})
} else {
for (const [line, lineNum] of signoffs) {
context.report({
id,
message: 'bot commit should not have a "Signed-off-by" trailer',
string: line,
line: lineNum,
column: 0,
level: 'warn'
})
}
}
return
}
// Assume it's not a bot commit... a Signed-off-by trailer is required.
// For mixed deps commits (deps + other subsystems), downgrade to warn
// since the deps portion may legitimately lack a sign-off.
if (signoffs.length === 0) {
context.report({
id,
message: hasDeps
? 'Commit with non-deps changes should have a "Signed-off-by" trailer'
: 'Commit must have a "Signed-off-by" trailer',
string: '',
level: hasDeps ? 'warn' : 'fail'
})
return
}
// Flag every sign-off that has an invalid email format.
// Collect valid sign-offs for further checks.
const valid = []
for (const [line, lineNum] of signoffs) {
if (validSignoff.test(line)) {
valid.push([line, lineNum])
} else {
context.report({
id,
message: '"Signed-off-by" trailer has invalid email',
string: line,
line: lineNum,
column: 0,
level: 'fail'
})
}
}
if (valid.length === 0) {
// All sign-offs had invalid emails; already reported above.
return
}
// Flag any sign-off that appears to be from a bot or AI agent.
// Bots and AI agents are not permitted to sign off on commits.
// Collect non-bot sign-offs for further checks. If the commit
// itself appears to be from a bot, the case is handled above.
const human = []
for (const [line, lineNum] of valid) {
const { 1: name, 2: email } = line.match(signoffParts)
if (botNamePattern.test(name) || botEmailPattern.test(email)) {
context.report({
id,
message: '"Signed-off-by" must be from a human author, ' +
'not a bot or AI agent',
string: line,
line: lineNum,
column: 0,
level: 'warn'
})
} else {
human.push([line, lineNum])
}
}
// All sign-offs appear to be from bots; already reported above.
// If there are no human sign-offs, fail (or warn for mixed deps).
if (human.length === 0) {
context.report({
id,
message: hasDeps
? 'Commit with non-deps changes should have a "Signed-off-by" ' +
'trailer from a human author'
: 'Commit must have a "Signed-off-by" trailer from a human author',
string: '',
level: hasDeps ? 'warn' : 'fail'
})
return
}
// When author info is available, warn if none of the human sign-off
// emails match the commit author email. This may indicate an automated
// tool signed off on behalf of the author.
const authorEmail = parseAuthor(parsed.author)?.email
if (authorEmail) {
const authorMatch = human.some(([line]) => {
const { 2: email } = line.match(signoffParts)
return email.toLowerCase() === authorEmail
})
if (!authorMatch) {
context.report({
id,
message: '"Signed-off-by" email does not match the ' +
'commit author email',
string: human[0][0],
line: human[0][1],
column: 0,
level: 'warn'
})
return
}
}
context.report({
id,
message: 'has valid Signed-off-by',
string: '',
level: 'pass'
})
}
}