forked from dso-toolkit/dso-toolkit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dangerfile.ts
242 lines (207 loc) · 9.91 KB
/
dangerfile.ts
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { danger, fail } from "danger";
(async function main() {
const types = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Docs", "Task"] as const;
type TypeMap = { [K in (typeof types)[number]]: string };
const groupMap: TypeMap = {
Added: "Added",
Changed: "Changed",
Deprecated: "Deprecated",
Docs: "Docs",
Fixed: "Fixed",
Removed: "Removed",
Task: "Tasks",
};
const labelMap: TypeMap = {
Added: "feature",
Changed: "change",
Deprecated: "deprecate",
Docs: "docs",
Fixed: "bug",
Removed: "remove",
Task: "task",
};
// Commit message check
const [firstCommit, ...remainingCommits] = danger.git.commits;
const firstCommitMessageLine = firstCommit.message.split("\n")[0];
const firstCommitMessage = parseFirstCommitMessage(firstCommitMessageLine);
if (!firstCommitMessage) {
fail(
`De eerste commit ('${firstCommitMessageLine}') volgt niet de vereiste formule. Lees de [Change management notatie](https://www.dso-toolkit.nl/master/voor-maintainers/change-management-notatie) voor voorbeelden van wat het moet zijn, en uitleg waarom.`,
);
}
// Changelog check
const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md");
if (!hasChangelog && firstCommitMessage?.issueId !== 2316) {
fail(
`Het lijkt erop dat je geen aantekening hebt toegevoegd aan het CHANGELOG. Lees de [Change management notatie](https://www.dso-toolkit.nl/master/voor-maintainers/change-management-notatie) om te lezen wat dit inhoudt en hoe je dit op de juiste manier doet.`,
);
}
if (firstCommitMessage) {
if (firstCommitMessage.scope.split(" ").some((w) => w[0].toLocaleUpperCase() !== w[0])) {
fail(
`Het scopegedeelte van het eerste commit-bericht ('${firstCommitMessage.scope}') is niet geschreven in gespatieerde Pascal-casing. Alle woorden in de scope moeten met een hoofdletter beginnen.`,
);
}
for (let i = 0; i < remainingCommits.length, i++; ) {
const commitMessage = remainingCommits[i];
const commitMessageLine = commitMessage.message.split("\n")[0];
if (!commitMessageLine.startsWith(`#${firstCommitMessage.issueId}`)) {
fail(
`Commit ${i + 1} (${
commitMessage.sha
}) met commit-bericht '${commitMessageLine}' moet beginnen met het nummer van de GitHub issue waar je aan gewerkt hebt.`,
);
}
}
const githubIssue = await getGithubIssue(firstCommitMessage.issueId);
if (!githubIssue) {
fail(
`Ik kan GitHub issue '${firstCommitMessage.issueId}' niet vinden. Controleer of het issuenummer klopt en of de issue bestaat.`,
);
} else {
if (firstCommitMessage.issueTitle !== githubIssue.title) {
fail(
`Het scopegedeelte en de samenvatting in je eerste commit-bericht moeten overeenkomen met de titel van het GitHub-issue, maar dat doen ze momenteel niet: Vergelijk '${firstCommitMessage.issueTitle}' met de titel van het issue: '${githubIssue.title}'.`,
);
}
const githubLabel = labelMap[firstCommitMessage.type];
if (!githubIssue.labels.some((l) => l.includes(githubLabel))) {
fail(
`Het gerelateerde GitHub-issue mist het juiste label. Ik denk dat dat '${githubLabel}' moet zijn. Kun je deze alsjeblieft toevoegen?`,
);
}
}
}
if (hasChangelog) {
const diff = await danger.git.diffForFile("CHANGELOG.md");
if (!diff) {
fail(`Ik heb geen wijzigingen aan de CHANGELOG.md aangetroffen.`);
} else {
const changelogEntry = parseChangelogEntry(diff.after, firstCommitMessage.issueId);
if (!changelogEntry) {
fail(
`De aantekening in het CHANGELOG volgt niet de juiste formule. Een aantekening in het CHANGELOG moet de volgende formule volgen: "#issue [changelog entry group] scope: samenvatting". Bijvoorbeeld: "#2241 [Task] Packages: Dependency updates". Raadpleeg voor meer informatie en probleemoplossing de documentatie: [Change management notatie](https://www.dso-toolkit.nl/master/voor-maintainers/change-management-notatie).`,
);
} else {
if (firstCommitMessage?.issueId !== changelogEntry.linkIssueId) {
fail(
`Het issuenummer dat is gebruikt in je eerste commit-bericht verschilt van het issuenummer waarnaar je hebt verwezen in je CHANGELOG-aantekening: vergelijk '${firstCommitMessage.issueId}' met '${changelogEntry.linkIssueId}'. Zorg ervoor dat deze twee hetzelfde zijn.`,
);
}
if (firstCommitMessage?.summary !== changelogEntry.summary) {
fail(
`Er is een verschil in de samenvatting (van het werk dat je hebt gedaan) zoals beschreven in je eerste commit-bericht ('${firstCommitMessage.summary}'), en de aantekening in het CHANGELOG ('${changelogEntry.summary}'). Zorg ervoor dat ze exact hetzelfde zijn.`,
);
}
if (firstCommitMessage?.scope !== changelogEntry.scope) {
fail(
`Het scopegedeelte van je eerste commit-bericht ('${firstCommitMessage.scope}') verschilt van de scope in het CHANGELOG ('${changelogEntry.scope}'). Het toevoegen van het juiste scopegedeelte maakt het gemakkelijker om later in het CHANGELOG te zien welke wijzigingen er zijn aangebracht aan een specifiek aspect van de codebase. Voor meer informatie, raadpleeg de [Change management-notatie](https://www.dso-toolkit.nl/master/voor-maintainers/change-management-notatie).`,
);
}
if (firstCommitMessage?.group !== changelogEntry.group) {
fail(
`Het groupgedeelte in het eerste commit-bericht ('${firstCommitMessage.group}') verschilt van de group in de CHANGELOG-aantekening ('${changelogEntry.group}'). Met 'group' bedoelen we het type wijzigingen dat is aangebracht. Dit moet een van de volgende trefwoorden zijn: 'Added', 'Changed', 'Deprecated', 'Docs', 'Fixed', 'Removed' of 'Tasks', en moet worden gebruikt in het commit-bericht zoals in dit voorbeeld: "#2241 [Task] Packages: Dependency updates". In het CHANGELOG zijn deze trefwoorden subkoppen binnen de 'Volgende' release, zodat alle wijzigingen in de resulterende release gemakkelijk te zien zijn gegroepeerd op soort wijziging.`,
);
}
if (changelogEntry.release !== "Next") {
fail(
`Je CHANGELOG-aantekening staat niet onder het kopje van de eerstvolgende release ('Next'), maar onder '${changelogEntry.release}'. Verplaats deze naar de juiste locatie, zodat wanneer je pull-verzoek wordt samengevoegd, de CHANGELOG-geschiedenis correct je werk weergeeft in de juiste release.`,
);
}
}
}
}
// Lorem check
[...danger.git.modified_files, ...danger.git.created_files]
.filter((file) => file !== "dangerfile.ts")
.forEach((file) =>
danger.git.diffForFile(file).then((diff) => {
if (diff?.after.includes("Lorem")) {
fail(
`Gebruik alsjeblieft geen 'Lorem ipsum' als content. Het lijkt er op dat je dat hebt gedaan in het volgende bestand: ${file}`,
);
}
}),
);
function parseFirstCommitMessage(commitMessage: string) {
const commitMessageRegex = new RegExp(
`^#(?<issueId>\\d+) \\[(?<type>${types.join("|")})\\] (?<scope>[^\\s][^:].+?[^\\s]): (?<summary>[^\\s].+[^\\s])`,
);
const match = commitMessage.match(commitMessageRegex);
if (!match) {
return null;
}
const issueId = parseInt(match.groups["issueId"], 10);
const { type, scope, summary } = match.groups;
if (isNaN(issueId) || typeof type !== "string" || typeof scope !== "string" || typeof summary !== "string") {
return null;
}
return {
issueId,
group: groupMap[type] ?? type,
type,
scope,
summary,
issueTitle: `${scope}: ${summary}`,
};
}
function parseChangelogEntry(changelog: string, issueId: number) {
const changelogRegex = new RegExp(
`^\\* (?:\\*\\*(?<breaking>BREAKING)\\*\\* )?(?<scope>[^\\s][^:].+?[^\\s]): (?<summary>[^\\s].+[^\\s]) \\(\\[#${issueId}\\]\\(https:\\/\\/github\\.com\\/dso-toolkit\\/dso-toolkit\\/issues\\/(?<linkIssueId>\\d+)\\)\\)$`,
"m",
);
const match = changelog.match(changelogRegex);
if (!match) {
return null;
}
const isBreaking = match.groups["breaking"] === "BREAKING";
const linkIssueId = parseInt(match.groups["linkIssueId"], 10);
const { scope, summary } = match.groups;
const { index } = match;
if (isNaN(linkIssueId) || typeof scope !== "string" || typeof summary !== "string" || typeof index !== "number") {
return null;
}
const groupIndex = changelog.lastIndexOf("\n### ", index) + 1;
if (groupIndex === 0) {
return null;
}
const group = changelog.slice(groupIndex + 4, changelog.indexOf("\n", groupIndex));
const releaseIndex = changelog.lastIndexOf("\n## ", groupIndex) + 1;
if (releaseIndex === 0) {
return null;
}
const release = changelog.slice(releaseIndex + 3, changelog.indexOf("\n", releaseIndex));
return {
index,
linkIssueId,
isBreaking,
scope,
summary,
group,
release,
};
}
async function getGithubIssue(issueId: number) {
function isLabel(label: unknown): label is { name: string } {
return typeof label === "object" && label !== null && "name" in label && typeof label.name === "string";
}
const results = await fetch(`https://api.github.com/repos/dso-toolkit/dso-toolkit/issues/${issueId}`, {
headers:
typeof process.env.DANGER_GITHUB_API_TOKEN === "string"
? {
Authorization: `Bearer ${process.env.DANGER_GITHUB_API_TOKEN}`,
}
: {},
});
const data = await results.json();
if (!data || typeof data.title !== "string" || !Array.isArray(data.labels)) {
return null;
}
const { title, labels } = data;
return {
issueId,
title,
labels: Array.isArray(labels) ? labels.filter(isLabel).map((l) => l.name) : [],
};
}
})();