virtual-code-owners 8.0.5 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/codeowners/generate.js +18 -6
- package/dist/labeler-yml/generate.js +6 -1
- package/dist/team-map/read.js +66 -16
- package/dist/version.js +1 -1
- package/dist/virtual-code-owners/parse.js +71 -15
- package/dist/virtual-code-owners/read.js +1 -1
- package/package.json +2 -3
- package/dist/team-map/virtual-teams.schema.js +0 -16
package/README.md
CHANGED
|
@@ -143,7 +143,10 @@ libs/baarden/ jan@example.com korneel@example.com pier@example.com tjorus@
|
|
|
143
143
|
|
|
144
144
|
### Any gotcha's?
|
|
145
145
|
|
|
146
|
-
It won't check whether the users or teams you entered exist.
|
|
146
|
+
- It won't check whether the users or teams you entered exist.
|
|
147
|
+
- Only relevant when you're on GitLab: Section heading support is experimental
|
|
148
|
+
and when generating labeler.yml default section owners aren't expanded to
|
|
149
|
+
section rules.
|
|
147
150
|
|
|
148
151
|
### Do I have to run this each time I edit `VIRTUAL-CODEOWNERS.txt`?
|
|
149
152
|
|
|
@@ -207,6 +210,16 @@ user/team names but doesn't verify their existence in the project.
|
|
|
207
210
|
|
|
208
211
|
- valid user/team names start with an `@` or are an e-mail address
|
|
209
212
|
- valid rules have a file pattern and at least one user/team name
|
|
213
|
+
(unless they're in a _section_ that has default owners `[sales related] @ch/sales`
|
|
214
|
+
in which case the rule inherits the default owners of that section)
|
|
215
|
+
- valid sections headings comply with the syntax described over at [GitLab](https://docs.gitlab.com/ee/user/project/codeowners/reference.html#sections)
|
|
216
|
+
> different from GitLab's syntax the line `[bla @group` is not interpreted
|
|
217
|
+
> as a rule, but as an erroneous section heading. This behaviour might change
|
|
218
|
+
> to be the same as GitLab's in future releases without a major version bump.
|
|
219
|
+
|
|
220
|
+
### Does virtual-code-owners support GitLab style sections?
|
|
221
|
+
|
|
222
|
+
Yes.
|
|
210
223
|
|
|
211
224
|
### I want to specify different locations for the files (e.g. because I'm using GitLab)
|
|
212
225
|
|
|
@@ -221,8 +234,6 @@ npx virtual-code-owners \
|
|
|
221
234
|
|
|
222
235
|
### Can I just validate VIRTUAL-CODEOWNERS.txt & virtual-teams.yml without generating output?
|
|
223
236
|
|
|
224
|
-
So _without_ generating any output?
|
|
225
|
-
|
|
226
237
|
Sure thing. Use `--dryRun`:
|
|
227
238
|
|
|
228
239
|
```
|
|
@@ -234,16 +245,15 @@ npx virtual-code-owners --dryRun
|
|
|
234
245
|
It keeps editors and IDE's from messing up your formatting.
|
|
235
246
|
|
|
236
247
|
Various editors assume an ALL_CAPS file name with `#` characters on various lines
|
|
237
|
-
to be markdown, and will auto format them as such.
|
|
238
|
-
|
|
239
|
-
present on text files.
|
|
248
|
+
to be markdown, and will auto format them as such. Usually such autoformatting is
|
|
249
|
+
not present on text files.
|
|
240
250
|
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
Often these editors know about CODEOWNERS, so they won't confuse _those_ with
|
|
252
|
+
markdown.
|
|
243
253
|
|
|
244
254
|
### Why does this exist at all? Why not just use GitHub teams?
|
|
245
255
|
|
|
246
|
-
|
|
256
|
+
If you can you should _totally_ use GitHub teams!
|
|
247
257
|
|
|
248
258
|
Organizations sometimes have large mono repositories with many code owners.
|
|
249
259
|
They or their bureaucracy haven't landed on actually using GitHub teams to
|
|
@@ -252,10 +262,9 @@ the organization chart (and hence the GitHub teams). Teams in those organization
|
|
|
252
262
|
who want to have clear code ownership can either:
|
|
253
263
|
|
|
254
264
|
- Wrestle the bureaucracy.
|
|
255
|
-
Recommended! It
|
|
256
|
-
|
|
257
|
-
because #reasons.
|
|
265
|
+
Recommended! It will often require patience though, and in the mean time
|
|
266
|
+
you might want to have some clarity on code ownership.
|
|
258
267
|
- Maintain a CODEOWNERS file with code assigned to large lists of individuals.
|
|
259
|
-
|
|
268
|
+
That's a lotta work, even for smaller projects
|
|
260
269
|
|
|
261
270
|
This is where `virtual-code-owners` comes in.
|
|
@@ -26,20 +26,32 @@ export default function generateCodeOwners(
|
|
|
26
26
|
}
|
|
27
27
|
function generateLine(pCSTLine, pTeamMap) {
|
|
28
28
|
if (pCSTLine.type === "rule") {
|
|
29
|
-
const lUserNames = uniq(
|
|
30
|
-
pCSTLine.users.flatMap((pUser) => expandTeamToUserNames(pUser, pTeamMap)),
|
|
31
|
-
)
|
|
32
|
-
.sort(compareUserNames)
|
|
33
|
-
.join(" ");
|
|
34
29
|
return (
|
|
35
30
|
pCSTLine.filesPattern +
|
|
36
31
|
pCSTLine.spaces +
|
|
37
|
-
|
|
32
|
+
expandTeamsToUsersString(pCSTLine.users, pTeamMap) +
|
|
33
|
+
(pCSTLine.inlineComment ? ` #${pCSTLine.inlineComment}` : "")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (pCSTLine.type === "section-heading") {
|
|
37
|
+
return (
|
|
38
|
+
(pCSTLine.optional ? "^" : "") +
|
|
39
|
+
"[" +
|
|
40
|
+
pCSTLine.name +
|
|
41
|
+
"]" +
|
|
42
|
+
(pCSTLine.minApprovers ? `[${pCSTLine.minApprovers}]` : "") +
|
|
43
|
+
pCSTLine.spaces +
|
|
44
|
+
expandTeamsToUsersString(pCSTLine.users, pTeamMap) +
|
|
38
45
|
(pCSTLine.inlineComment ? ` #${pCSTLine.inlineComment}` : "")
|
|
39
46
|
);
|
|
40
47
|
}
|
|
41
48
|
return pCSTLine.raw;
|
|
42
49
|
}
|
|
50
|
+
function expandTeamsToUsersString(pUsers, pTeamMap) {
|
|
51
|
+
return uniq(pUsers.flatMap((pUser) => expandTeamToUserNames(pUser, pTeamMap)))
|
|
52
|
+
.sort(compareUserNames)
|
|
53
|
+
.join(" ");
|
|
54
|
+
}
|
|
43
55
|
function expandTeamToUserNames(pUser, pTeamMap) {
|
|
44
56
|
if (pUser.type === "virtual-team-name") {
|
|
45
57
|
return stringifyTeamMembers(pTeamMap, pUser.bareName);
|
|
@@ -49,8 +49,13 @@ function transformForYamlAndMinimatch(pOriginalString) {
|
|
|
49
49
|
return lReturnValue;
|
|
50
50
|
}
|
|
51
51
|
function lineContainsTeamName(pLine, pTeamName) {
|
|
52
|
-
|
|
52
|
+
const lHasTeamNameInRegularUsers = pLine.users.some(
|
|
53
53
|
(pUser) =>
|
|
54
54
|
pUser.type === "virtual-team-name" && pUser.bareName === pTeamName,
|
|
55
55
|
);
|
|
56
|
+
const lHasTeamNameInInheritedUsers = (pLine.inheritedUsers ?? []).some(
|
|
57
|
+
(pUser) =>
|
|
58
|
+
pUser.type === "virtual-team-name" && pUser.bareName === pTeamName,
|
|
59
|
+
);
|
|
60
|
+
return lHasTeamNameInRegularUsers || lHasTeamNameInInheritedUsers;
|
|
56
61
|
}
|
package/dist/team-map/read.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import Ajv from "ajv";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { EOL } from "node:os";
|
|
4
2
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import
|
|
3
|
+
import { EOL } from "node:os";
|
|
6
4
|
export default function readTeamMap(pVirtualTeamsFileName) {
|
|
7
5
|
const lVirtualTeamsAsAString = readFileSync(pVirtualTeamsFileName, {
|
|
8
6
|
encoding: "utf-8",
|
|
@@ -11,22 +9,74 @@ export default function readTeamMap(pVirtualTeamsFileName) {
|
|
|
11
9
|
assertTeamMapValid(lTeamMap, pVirtualTeamsFileName);
|
|
12
10
|
return lTeamMap;
|
|
13
11
|
}
|
|
14
|
-
function assertTeamMapValid(
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
verbose: true,
|
|
18
|
-
});
|
|
19
|
-
if (!ajv.validate(virtualTeamsSchema, pTeamMap)) {
|
|
12
|
+
function assertTeamMapValid(pTeamMapCandidate, pVirtualTeamsFileName) {
|
|
13
|
+
const [lValid, lError] = validateTeamMap(pTeamMapCandidate);
|
|
14
|
+
if (!lValid) {
|
|
20
15
|
throw new Error(
|
|
21
|
-
`
|
|
16
|
+
`'${pVirtualTeamsFileName}' is not a valid virtual-teams.yml:${EOL} ${lError}`,
|
|
22
17
|
);
|
|
23
18
|
}
|
|
24
19
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
function validateTeamMap(pCandidateTeamMap) {
|
|
21
|
+
if (
|
|
22
|
+
typeof pCandidateTeamMap !== "object" ||
|
|
23
|
+
pCandidateTeamMap === null ||
|
|
24
|
+
Array.isArray(pCandidateTeamMap)
|
|
25
|
+
) {
|
|
26
|
+
return [false, "The team map is not an object"];
|
|
27
|
+
}
|
|
28
|
+
const lTeamNameResults = Object.keys(pCandidateTeamMap).map(validateTeamName);
|
|
29
|
+
const lErrors = lTeamNameResults.filter((result) => !result[0]);
|
|
30
|
+
if (lErrors.length > 0) {
|
|
31
|
+
return [
|
|
32
|
+
false,
|
|
33
|
+
`These team names are not valid: ${lErrors.map((error) => error[1]).join(", ")}`,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
const lTeamResults = Object.keys(pCandidateTeamMap).map((pKey) =>
|
|
37
|
+
validateTeam(pCandidateTeamMap[pKey], pKey),
|
|
38
|
+
);
|
|
39
|
+
const lTeamErrors = lTeamResults.filter((result) => !result[0]);
|
|
40
|
+
if (lTeamErrors.length > 0) {
|
|
41
|
+
return [false, lTeamErrors.map((error) => error[1]).join(`, ${EOL} `)];
|
|
42
|
+
}
|
|
43
|
+
return [true];
|
|
29
44
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
45
|
+
function validateTeamName(pTeamNameCandidate) {
|
|
46
|
+
if (typeof pTeamNameCandidate !== "string") {
|
|
47
|
+
return [false, "not a string"];
|
|
48
|
+
}
|
|
49
|
+
if (pTeamNameCandidate === "") {
|
|
50
|
+
return [false, "'' (empty string)"];
|
|
51
|
+
}
|
|
52
|
+
if (pTeamNameCandidate.includes(" ")) {
|
|
53
|
+
return [false, `'${pTeamNameCandidate}' (contains spaces)`];
|
|
54
|
+
}
|
|
55
|
+
return [true];
|
|
56
|
+
}
|
|
57
|
+
function validateTeam(pCandidateTeam, pTeamName) {
|
|
58
|
+
if (!Array.isArray(pCandidateTeam)) {
|
|
59
|
+
return [false, `This team is not an array: '${pTeamName}'`];
|
|
60
|
+
}
|
|
61
|
+
const lTeamMemberResults = pCandidateTeam.map(validateTeamMember);
|
|
62
|
+
const lErrors = lTeamMemberResults.filter((result) => !result[0]);
|
|
63
|
+
if (lErrors.length > 0) {
|
|
64
|
+
return [false, lErrors.map((error) => error[1]).join(", ")];
|
|
65
|
+
}
|
|
66
|
+
return [true];
|
|
67
|
+
}
|
|
68
|
+
function validateTeamMember(pTeamMemberCandidate) {
|
|
69
|
+
if (typeof pTeamMemberCandidate !== "string") {
|
|
70
|
+
return [
|
|
71
|
+
false,
|
|
72
|
+
`This username is not a string: '${pTeamMemberCandidate.toString()}'`,
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
if (!/^[^@][^\s]+$/.test(pTeamMemberCandidate)) {
|
|
76
|
+
return [
|
|
77
|
+
false,
|
|
78
|
+
`This username doesn't match /^[^@][^\\s]+$/: '${pTeamMemberCandidate}'`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
return [true];
|
|
32
82
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "8.0
|
|
1
|
+
export const VERSION = "8.2.0";
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { EOL } from "node:os";
|
|
2
2
|
import { isEmailIshUsername } from "../utensils.js";
|
|
3
|
+
let STATE = {
|
|
4
|
+
currentSection: "",
|
|
5
|
+
inheritedUsers: [],
|
|
6
|
+
};
|
|
3
7
|
export function parse(pVirtualCodeOwnersAsString, pTeamMap = {}) {
|
|
8
|
+
STATE = {
|
|
9
|
+
currentSection: "",
|
|
10
|
+
inheritedUsers: [],
|
|
11
|
+
};
|
|
4
12
|
return pVirtualCodeOwnersAsString
|
|
5
13
|
.split(EOL)
|
|
6
14
|
.map((pUntreatedLine, pLineNo) =>
|
|
@@ -9,34 +17,82 @@ export function parse(pVirtualCodeOwnersAsString, pTeamMap = {}) {
|
|
|
9
17
|
}
|
|
10
18
|
function parseLine(pUntreatedLine, pTeamMap, pLineNo) {
|
|
11
19
|
const lTrimmedLine = pUntreatedLine.trim();
|
|
12
|
-
const lCommentSplitLine = lTrimmedLine.split(/\s*#/);
|
|
13
|
-
const lRule = lCommentSplitLine[0]?.match(
|
|
14
|
-
/^(?<filesPattern>[^\s]+)(?<spaces>\s+)(?<userNames>.*)$/,
|
|
15
|
-
);
|
|
16
20
|
if (lTrimmedLine.startsWith("#!")) {
|
|
17
21
|
return { type: "ignorable-comment", line: pLineNo, raw: pUntreatedLine };
|
|
18
22
|
}
|
|
19
23
|
if (lTrimmedLine.startsWith("#")) {
|
|
20
24
|
return { type: "comment", line: pLineNo, raw: pUntreatedLine };
|
|
21
25
|
}
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
if (lTrimmedLine === "") {
|
|
27
|
+
return { type: "empty", line: pLineNo, raw: pUntreatedLine };
|
|
28
|
+
}
|
|
29
|
+
if (lTrimmedLine.startsWith("[") || lTrimmedLine.startsWith("^[")) {
|
|
30
|
+
return parseSection(pUntreatedLine, pLineNo, pTeamMap);
|
|
31
|
+
}
|
|
32
|
+
return parseRule(pUntreatedLine, pLineNo, pTeamMap);
|
|
33
|
+
}
|
|
34
|
+
function parseRule(pUntreatedLine, pLineNo, pTeamMap) {
|
|
35
|
+
const lTrimmedLine = pUntreatedLine.trim();
|
|
36
|
+
const lCommentSplitLine = lTrimmedLine.split(/\s*#/);
|
|
37
|
+
const lRule = lCommentSplitLine[0]?.match(
|
|
38
|
+
/^(?<filesPattern>[^\s]+)(?<spaces>\s+)?(?<userNames>.+)?$/,
|
|
39
|
+
);
|
|
40
|
+
const ruleIsValid =
|
|
41
|
+
lRule?.groups &&
|
|
42
|
+
(lRule.groups.userNames || STATE.inheritedUsers.length > 0);
|
|
43
|
+
if (ruleIsValid) {
|
|
44
|
+
let lReturnValue = {
|
|
45
|
+
type: "rule",
|
|
46
|
+
line: pLineNo,
|
|
47
|
+
raw: pUntreatedLine,
|
|
48
|
+
filesPattern: lRule.groups.filesPattern,
|
|
49
|
+
spaces: lRule.groups?.spaces ?? "",
|
|
50
|
+
users: parseUsers(lRule.groups?.userNames ?? "", pTeamMap),
|
|
51
|
+
inlineComment: lCommentSplitLine[1] ?? "",
|
|
52
|
+
};
|
|
53
|
+
if (STATE.currentSection) {
|
|
54
|
+
lReturnValue.inheritedUsers = STATE.inheritedUsers;
|
|
55
|
+
lReturnValue.currentSection = STATE.currentSection;
|
|
25
56
|
}
|
|
26
|
-
return
|
|
57
|
+
return lReturnValue;
|
|
27
58
|
}
|
|
28
|
-
return {
|
|
29
|
-
|
|
59
|
+
return { type: "unknown", line: pLineNo, raw: pUntreatedLine };
|
|
60
|
+
}
|
|
61
|
+
function parseSection(pUntreatedLine, pLineNo, pTeamMap) {
|
|
62
|
+
const lTrimmedLine = pUntreatedLine.trim();
|
|
63
|
+
const lCommentSplitLine = lTrimmedLine.split(/\s*#/);
|
|
64
|
+
const lSection = lCommentSplitLine[0]?.match(
|
|
65
|
+
/^(?<optionalIndicator>\^)?\[(?<name>[^\]]+)\](\[(?<minApprovers>[0-9]+)\])?(?<spaces>\s+)?(?<userNames>.+)?$/,
|
|
66
|
+
);
|
|
67
|
+
if (!lSection?.groups) {
|
|
68
|
+
return {
|
|
69
|
+
type: "unknown",
|
|
70
|
+
line: pLineNo,
|
|
71
|
+
raw: pUntreatedLine,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const lUsers = parseUsers(lSection.groups?.userNames ?? "", pTeamMap);
|
|
75
|
+
STATE = {
|
|
76
|
+
currentSection: lSection.groups.name,
|
|
77
|
+
inheritedUsers: lUsers,
|
|
78
|
+
};
|
|
79
|
+
const lReturnValue = {
|
|
80
|
+
type: "section-heading",
|
|
30
81
|
line: pLineNo,
|
|
31
|
-
filesPattern: lRule.groups.filesPattern,
|
|
32
|
-
spaces: lRule.groups.spaces,
|
|
33
|
-
users: parseUsers(lRule.groups.userNames, pTeamMap),
|
|
34
|
-
inlineComment: lCommentSplitLine[1] ?? "",
|
|
35
82
|
raw: pUntreatedLine,
|
|
83
|
+
optional: lSection.groups.optionalIndicator === "^",
|
|
84
|
+
name: lSection.groups.name,
|
|
85
|
+
spaces: lSection.groups?.spaces ?? "",
|
|
86
|
+
users: parseUsers(lSection.groups?.userNames ?? "", pTeamMap),
|
|
87
|
+
inlineComment: lCommentSplitLine[1] ?? "",
|
|
36
88
|
};
|
|
89
|
+
if (lSection.groups.minApprovers) {
|
|
90
|
+
lReturnValue.minApprovers = parseInt(lSection.groups.minApprovers, 10);
|
|
91
|
+
}
|
|
92
|
+
return lReturnValue;
|
|
37
93
|
}
|
|
38
94
|
function parseUsers(pUserNamesString, pTeamMap) {
|
|
39
|
-
const lUserNames = pUserNamesString.split(/\s+/);
|
|
95
|
+
const lUserNames = pUserNamesString ? pUserNamesString.split(/\s+/) : [];
|
|
40
96
|
return lUserNames.map((pUserName, pIndex) => {
|
|
41
97
|
const lBareName = getBareUserName(pUserName);
|
|
42
98
|
return {
|
|
@@ -25,7 +25,7 @@ function reportAnomalies(pFileName, pAnomalies) {
|
|
|
25
25
|
return pAnomalies
|
|
26
26
|
.map((pAnomaly) => {
|
|
27
27
|
if (pAnomaly.type === "invalid-line") {
|
|
28
|
-
return `${pFileName}:${pAnomaly.line}:1 invalid line - neither a rule, comment nor empty: "${pAnomaly.raw}"`;
|
|
28
|
+
return `${pFileName}:${pAnomaly.line}:1 invalid line - neither a rule, section heading, comment nor empty: "${pAnomaly.raw}"`;
|
|
29
29
|
} else {
|
|
30
30
|
return (
|
|
31
31
|
`${pFileName}:${pAnomaly.line}:1 invalid user or team name "${pAnomaly.raw}" (#${pAnomaly.userNumberWithinLine} on this line). ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virtual-code-owners",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "CODEOWNERS with teams for teams that can't use GitHub teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
"url": "https://github.com/sverweij/virtual-code-owners/issues"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"
|
|
30
|
-
"yaml": "2.4.2"
|
|
29
|
+
"yaml": "2.4.3"
|
|
31
30
|
},
|
|
32
31
|
"engines": {
|
|
33
32
|
"node": "^18.11.0||>=20.0.0"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
title: "virtual teams schema for virtual-code-owners",
|
|
4
|
-
description: "a list of teams and their team members",
|
|
5
|
-
$id: "org.js.virtual-code-owners/7.0.0",
|
|
6
|
-
type: "object",
|
|
7
|
-
additionalProperties: {
|
|
8
|
-
type: "array",
|
|
9
|
-
items: {
|
|
10
|
-
type: "string",
|
|
11
|
-
description:
|
|
12
|
-
"Username or e-mail address of a team member. (Don't prefix usernames with '@')",
|
|
13
|
-
pattern: "^[^@][^\\s]+$",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
};
|