-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvalidatePrTitle.js
127 lines (89 loc) · 3.2 KB
/
validatePrTitle.js
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
const { closest } = require("fastest-levenshtein");
const { getAllNodesDisplayNames } = require("./getAllNodesDisplayNames");
const { TYPES, SCOPES, NO_CHANGELOG, ERRORS, REGEXES } = require("./constants");
/**
* Validate that a pull request title matches n8n's version of the Conventional Commits spec.
*
* See: https://www.notion.so/n8n/Release-Process-fce65faea3d5403a85210f7e7a60d0f8
*/
async function validatePrTitle(title) {
const match = title.match(REGEXES.CONVENTIONAL_SCHEMA);
// general validation
if (!match) return [ERRORS.CONVENTIONAL_SCHEMA_MISMATCH];
if (containsTicketNumber(title)) return [ERRORS.TICKET_NUMBER_PRESENT];
const issues = [];
// type validation
if (!match?.groups?.type) {
issues.push(ERRORS.TYPE_NOT_FOUND);
}
const { type } = match.groups;
if (isInvalidType(type)) {
issues.push(ERRORS.INVALID_TYPE);
}
// scope validation
const { scope } = match.groups;
if (scope) {
if (/,\S/.test(scope)) {
issues.push(ERRORS.MISSING_WHITESPACE_AFTER_COMMA);
} else {
const scopeIssues = await Promise.all(
scope.split(", ").map(getScopeIssue),
);
issues.push(...scopeIssues.filter((scopeIssue) => scopeIssue !== null));
}
}
// subject validation
const { subject } = match.groups;
if (startsWithLowerCase(subject)) {
issues.push(ERRORS.LOWERCASE_INITIAL_IN_SUBJECT);
}
if (endsWithPeriod(subject)) {
issues.push(ERRORS.FINAL_PERIOD_IN_SUBJECT);
}
if (doesNotUsePresentTense(subject)) {
issues.push(ERRORS.NO_PRESENT_TENSE_IN_SUBJECT);
}
if (hasSkipChangelog(subject) && skipChangelogIsNotInFinalPosition(subject)) {
issues.push(ERRORS.SKIP_CHANGELOG_NOT_IN_FINAL_POSITION);
}
return issues;
}
/**
* Helpers
*/
const isInvalidType = (str) => !TYPES.includes(str);
const isInvalidNodeScope = (str, allNodesDisplayNames) =>
!allNodesDisplayNames.some((name) => str.startsWith(name));
const getScopeIssue = async (scope) => {
if (scope.endsWith(" Node")) {
const names = await getAllNodesDisplayNames();
if (names.length === 0) {
console.log("Failed to find all nodes display names. Skipping check...");
return null;
}
if (isInvalidNodeScope(scope, names)) {
const closest = getClosestMatch(scope, names);
const supplement = `. Did you mean \`${closest} Node\`?`;
return ERRORS.INVALID_SCOPE + supplement;
}
} else if (!SCOPES.includes(scope)) {
return ERRORS.INVALID_SCOPE;
}
return null;
};
const startsWithLowerCase = (str) => /^[a-z]/.test(str);
const endsWithPeriod = (str) => /\.$/.test(str);
const containsTicketNumber = (str) => REGEXES.TICKET.test(str);
const doesNotUsePresentTense = (str) => {
const verb = str.split(" ").shift();
return verb.endsWith("ed"); // naive check
};
const hasSkipChangelog = (str) => str.includes(NO_CHANGELOG);
const skipChangelogIsNotInFinalPosition = (str) => {
const suffixPattern = [" ", escapeForRegex(NO_CHANGELOG), "$"].join("");
return !new RegExp(suffixPattern).test(str);
};
const escapeForRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const getClosestMatch = (str, names) =>
closest(str.split(" Node").shift(), names);
module.exports = { validatePrTitle };