virtual-code-owners 8.0.3 → 8.0.4
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 +23 -30
- package/dist/cli.js +57 -57
- package/dist/codeowners/generate.js +45 -45
- package/dist/labeler-yml/generate.js +45 -45
- package/dist/main.js +26 -26
- package/dist/team-map/read.js +19 -19
- package/dist/team-map/virtual-teams.schema.js +14 -14
- package/dist/utensils.js +2 -2
- package/dist/version.js +1 -1
- package/dist/virtual-code-owners/anomalies.js +28 -28
- package/dist/virtual-code-owners/parse.js +55 -55
- package/dist/virtual-code-owners/read.js +28 -28
- package/package.json +36 -36
package/README.md
CHANGED
|
@@ -43,21 +43,20 @@ For example a CODEOWNERS file can look like this:
|
|
|
43
43
|
|
|
44
44
|
# admin & ci stuff => transversal
|
|
45
45
|
|
|
46
|
-
.github/
|
|
46
|
+
.github/ @ch/transversal
|
|
47
47
|
|
|
48
48
|
# generic stuff
|
|
49
49
|
|
|
50
|
-
apps/framework/
|
|
51
|
-
apps/ux-portal/
|
|
52
|
-
libs/components/
|
|
50
|
+
apps/framework/ @ch/transversal
|
|
51
|
+
apps/ux-portal/ @ch/ux @ch/transversal
|
|
52
|
+
libs/components/ @ch/ux
|
|
53
53
|
|
|
54
54
|
# specific functionality
|
|
55
55
|
|
|
56
|
-
libs/
|
|
57
|
-
libs/
|
|
58
|
-
libs/
|
|
59
|
-
libs/
|
|
60
|
-
libs/ubc-baarden/ @ch/mannen-met-baarden
|
|
56
|
+
libs/sales/ @ch/sales
|
|
57
|
+
libs/after-sales/ @ch/after-sales
|
|
58
|
+
libs/refund/ @ch/sales @ch/after-sales
|
|
59
|
+
libs/baarden/ @ch/mannen-met-baarden
|
|
61
60
|
```
|
|
62
61
|
|
|
63
62
|
... where only the @cloud-heroes-all is a 'real' team on GitHub level. The other
|
|
@@ -74,7 +73,7 @@ to keep CODEOWNERS current.
|
|
|
74
73
|
ch/after-sales:
|
|
75
74
|
- john-doe-ch
|
|
76
75
|
- pete-peterson-ch
|
|
77
|
-
-
|
|
76
|
+
- john-galt-ch
|
|
78
77
|
- daisy-duck
|
|
79
78
|
- donald-duck
|
|
80
79
|
ch/sales:
|
|
@@ -83,11 +82,6 @@ ch/sales:
|
|
|
83
82
|
- abraham-ableton-ch
|
|
84
83
|
- dagny-taggert-ch
|
|
85
84
|
- karl-marx-ch
|
|
86
|
-
ch/pre-sales:
|
|
87
|
-
- jean-claude-ch
|
|
88
|
-
- valerie-valerton-ch
|
|
89
|
-
- averel-dalton-ch
|
|
90
|
-
- john-galt-ch
|
|
91
85
|
ch/ux:
|
|
92
86
|
- davy-davidson-ch
|
|
93
87
|
- john-johnson-ch
|
|
@@ -129,21 +123,20 @@ Running `npx virtual-code-owners` will combine these into a CODEOWNERS file like
|
|
|
129
123
|
|
|
130
124
|
# admin & ci stuff => transversal
|
|
131
125
|
|
|
132
|
-
.github/
|
|
126
|
+
.github/ @abraham-lincoln @benjamin-franklin @koos-koets @luke-the-lucky-ch @mary-the-merry-ch
|
|
133
127
|
|
|
134
128
|
# generic stuff
|
|
135
129
|
|
|
136
|
-
apps/framework/
|
|
137
|
-
apps/ux-portal/
|
|
138
|
-
libs/components/
|
|
130
|
+
apps/framework/ @abraham-lincoln @benjamin-franklin @koos-koets @luke-the-lucky-ch @mary-the-merry-ch
|
|
131
|
+
apps/ux-portal/ @abraham-lincoln @benjamin-franklin @davy-davidson-ch @joe-dalton-ch @john-johnson-ch @koos-koets @luke-the-lucky-ch @mary-the-merry-ch
|
|
132
|
+
libs/components/ @davy-davidson-ch @joe-dalton-ch @john-johnson-ch @koos-koets
|
|
139
133
|
|
|
140
134
|
# specific functionality
|
|
141
135
|
|
|
142
|
-
libs/
|
|
143
|
-
libs/
|
|
144
|
-
libs/
|
|
145
|
-
libs/
|
|
146
|
-
libs/ubc-baarden/ jan@example.com korneel@example.com pier@example.com tjorus@example.com
|
|
136
|
+
libs/sales/ @abraham-ableton-ch @dagny-taggert-ch @gregory-gregson-ch @jane-doe-ch @karl-marx-ch
|
|
137
|
+
libs/after-sales/ @daisy-duck @donald-duck @john-doe-ch @john-galt @pete-peterson-ch
|
|
138
|
+
libs/refund/ @abraham-ableton-ch @dagny-taggert-ch @daisy-duck @donald-duck @gregory-gregson-ch @jane-doe-ch @john-doe-ch @john-galt @karl-marx-ch @pete-peterson-ch
|
|
139
|
+
libs/baarden/ jan@example.com korneel@example.com pier@example.com tjorus@example.com
|
|
147
140
|
```
|
|
148
141
|
|
|
149
142
|
## FAQ
|
|
@@ -252,16 +245,16 @@ the formatting of _those_.
|
|
|
252
245
|
|
|
253
246
|
You should _totally_ use GitHub teams! If you can.
|
|
254
247
|
|
|
255
|
-
Organizations sometimes have large mono repositories with
|
|
256
|
-
They or their bureaucracy haven't landed on actually using GitHub teams to
|
|
248
|
+
Organizations sometimes have large mono repositories with many code owners.
|
|
249
|
+
They or their bureaucracy haven't landed on actually using GitHub teams to
|
|
257
250
|
demarcate that. Or you're working on a cross-functional team that doesn't follow
|
|
258
251
|
the organization chart (and hence the GitHub teams). Teams in those organizations
|
|
259
|
-
who want to have clear code ownership
|
|
252
|
+
who want to have clear code ownership can either:
|
|
260
253
|
|
|
261
254
|
- Wrestle the bureaucracy.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
Recommended! It might take a while, though - and even though there are good
|
|
256
|
+
people on many levels in bureaucracies, it might eventually not pan out
|
|
257
|
+
because #reasons.
|
|
265
258
|
- Maintain a CODEOWNERS file with code assigned to large lists of individuals.
|
|
266
259
|
An option, but laborious to maintain, even for smaller projects
|
|
267
260
|
|
package/dist/cli.js
CHANGED
|
@@ -25,64 +25,64 @@ Options:
|
|
|
25
25
|
outputs (default: false)
|
|
26
26
|
-h, --help display help for command`;
|
|
27
27
|
export function cli(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
pArguments = process.argv.slice(2),
|
|
29
|
+
pOutStream = process.stdout,
|
|
30
|
+
pErrorStream = process.stderr,
|
|
31
|
+
pErrorExitCode = 1,
|
|
32
32
|
) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
try {
|
|
34
|
+
const lOptions = getOptions(pArguments);
|
|
35
|
+
if (lOptions.help) {
|
|
36
|
+
pOutStream.write(`${HELP_MESSAGE}${EOL}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (lOptions.version) {
|
|
40
|
+
pOutStream.write(`${VERSION}${EOL}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
main(lOptions, pErrorStream);
|
|
44
|
+
} catch (pError) {
|
|
45
|
+
pErrorStream.write(`${EOL}ERROR: ${pError.message}${EOL}${EOL}`);
|
|
46
|
+
process.exitCode = pErrorExitCode;
|
|
47
|
+
}
|
|
48
48
|
}
|
|
49
49
|
function getOptions(pArguments) {
|
|
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
|
-
|
|
50
|
+
return parseArgs({
|
|
51
|
+
args: pArguments,
|
|
52
|
+
options: {
|
|
53
|
+
virtualCodeOwners: {
|
|
54
|
+
type: "string",
|
|
55
|
+
short: "v",
|
|
56
|
+
default: ".github/VIRTUAL-CODEOWNERS.txt",
|
|
57
|
+
},
|
|
58
|
+
virtualTeams: {
|
|
59
|
+
type: "string",
|
|
60
|
+
short: "t",
|
|
61
|
+
default: ".github/virtual-teams.yml",
|
|
62
|
+
},
|
|
63
|
+
codeOwners: {
|
|
64
|
+
type: "string",
|
|
65
|
+
short: "c",
|
|
66
|
+
default: ".github/CODEOWNERS",
|
|
67
|
+
},
|
|
68
|
+
emitLabeler: {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
short: "l",
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
73
|
+
labelerLocation: {
|
|
74
|
+
type: "string",
|
|
75
|
+
default: ".github/labeler.yml",
|
|
76
|
+
},
|
|
77
|
+
dryRun: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
default: false,
|
|
80
|
+
},
|
|
81
|
+
help: { type: "boolean", short: "h", default: false },
|
|
82
|
+
version: { type: "boolean", short: "V", default: false },
|
|
83
|
+
},
|
|
84
|
+
strict: true,
|
|
85
|
+
allowPositionals: true,
|
|
86
|
+
tokens: false,
|
|
87
|
+
}).values;
|
|
88
88
|
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
import { EOL } from "node:os";
|
|
2
2
|
import { isEmailIshUsername } from "../utensils.js";
|
|
3
3
|
const DEFAULT_WARNING =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
`#${EOL}` +
|
|
5
|
+
`# DO NOT EDIT - this file is generated and your edits will be overwritten${EOL}` +
|
|
6
|
+
`#${EOL}` +
|
|
7
|
+
`# To make changes:${EOL}` +
|
|
8
|
+
`#${EOL}` +
|
|
9
|
+
`# - edit .github/VIRTUAL-CODEOWNERS.txt${EOL}` +
|
|
10
|
+
`# - and/ or add team members to .github/virtual-teams.yml${EOL}` +
|
|
11
|
+
`# - run 'npx virtual-code-owners' (or 'npx virtual-code-owners --emitLabeler' if you also${EOL}` +
|
|
12
|
+
`# want to generate a .github/labeler.yml)${EOL}` +
|
|
13
|
+
`#${EOL}${EOL}`;
|
|
14
14
|
export default function generateCodeOwners(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
pVirtualCodeOwners,
|
|
16
|
+
pTeamMap,
|
|
17
|
+
pGeneratedWarning = DEFAULT_WARNING,
|
|
18
18
|
) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
return (
|
|
20
|
+
pGeneratedWarning +
|
|
21
|
+
pVirtualCodeOwners
|
|
22
|
+
.filter((pLine) => pLine.type !== "ignorable-comment")
|
|
23
|
+
.map((pLine) => generateLine(pLine, pTeamMap))
|
|
24
|
+
.join(EOL)
|
|
25
|
+
);
|
|
26
26
|
}
|
|
27
27
|
function generateLine(pCSTLine, pTeamMap) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
if (pCSTLine.type === "rule") {
|
|
29
|
+
const lUserNames = uniq(
|
|
30
|
+
pCSTLine.users.flatMap((pUser) => expandTeamToUserNames(pUser, pTeamMap)),
|
|
31
|
+
)
|
|
32
|
+
.sort(compareUserNames)
|
|
33
|
+
.join(" ");
|
|
34
|
+
return (
|
|
35
|
+
pCSTLine.filesPattern +
|
|
36
|
+
pCSTLine.spaces +
|
|
37
|
+
lUserNames +
|
|
38
|
+
(pCSTLine.inlineComment ? ` #${pCSTLine.inlineComment}` : "")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return pCSTLine.raw;
|
|
42
42
|
}
|
|
43
43
|
function expandTeamToUserNames(pUser, pTeamMap) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (pUser.type === "virtual-team-name") {
|
|
45
|
+
return stringifyTeamMembers(pTeamMap, pUser.bareName);
|
|
46
|
+
}
|
|
47
|
+
return [pUser.raw];
|
|
48
48
|
}
|
|
49
49
|
function stringifyTeamMembers(pTeamMap, pTeamName) {
|
|
50
|
-
|
|
50
|
+
return (pTeamMap[pTeamName] ?? []).map(userNameToCodeOwner);
|
|
51
51
|
}
|
|
52
52
|
function userNameToCodeOwner(pUserName) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (isEmailIshUsername(pUserName)) {
|
|
54
|
+
return pUserName;
|
|
55
|
+
}
|
|
56
|
+
return `@${pUserName}`;
|
|
57
57
|
}
|
|
58
58
|
function compareUserNames(pLeftName, pRightName) {
|
|
59
|
-
|
|
59
|
+
return pLeftName.toLowerCase() > pRightName.toLowerCase() ? 1 : -1;
|
|
60
60
|
}
|
|
61
61
|
function uniq(pUserNames) {
|
|
62
|
-
|
|
62
|
+
return Array.from(new Set(pUserNames));
|
|
63
63
|
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
import { EOL } from "node:os";
|
|
2
2
|
const DEFAULT_WARNING =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
`#${EOL}` +
|
|
4
|
+
`# DO NOT EDIT - this file is generated and your edits will be overwritten${EOL}` +
|
|
5
|
+
`#${EOL}` +
|
|
6
|
+
`# To make changes:${EOL}` +
|
|
7
|
+
`#${EOL}` +
|
|
8
|
+
`# - edit .github/VIRTUAL-CODEOWNERS.txt${EOL}` +
|
|
9
|
+
`# - and/ or add teams (& members) to .github/virtual-teams.yml${EOL}` +
|
|
10
|
+
`# - run 'npx virtual-code-owners --emitLabeler'${EOL}` +
|
|
11
|
+
`#${EOL}${EOL}`;
|
|
12
12
|
export default function generateLabelerYml(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
pCodeOwners,
|
|
14
|
+
pTeamMap,
|
|
15
|
+
pGeneratedWarning = DEFAULT_WARNING,
|
|
16
16
|
) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
let lReturnValue = pGeneratedWarning;
|
|
18
|
+
for (const lTeamName in pTeamMap) {
|
|
19
|
+
const lPatternsForTeam = getPatternsForTeam(pCodeOwners, lTeamName)
|
|
20
|
+
.map(
|
|
21
|
+
(pPattern) =>
|
|
22
|
+
` - any-glob-to-any-file: ${transformForYamlAndMinimatch(pPattern)}${EOL}`,
|
|
23
|
+
)
|
|
24
|
+
.join("");
|
|
25
|
+
if (lPatternsForTeam) {
|
|
26
|
+
lReturnValue += `${lTeamName}:${EOL} - changed-files:${EOL}${lPatternsForTeam}${EOL}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lReturnValue;
|
|
30
30
|
}
|
|
31
31
|
function getPatternsForTeam(pCodeOwners, pTeamName) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
return pCodeOwners
|
|
33
|
+
.filter((pLine) => {
|
|
34
|
+
return pLine.type === "rule" && lineContainsTeamName(pLine, pTeamName);
|
|
35
|
+
})
|
|
36
|
+
.map((pLine) => pLine.filesPattern);
|
|
37
37
|
}
|
|
38
38
|
function transformForYamlAndMinimatch(pOriginalString) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
let lReturnValue = pOriginalString;
|
|
40
|
+
if (pOriginalString === "*") {
|
|
41
|
+
lReturnValue = "**";
|
|
42
|
+
}
|
|
43
|
+
if (lReturnValue.startsWith("*")) {
|
|
44
|
+
lReturnValue = `"${lReturnValue}"`;
|
|
45
|
+
}
|
|
46
|
+
if (pOriginalString.endsWith("/")) {
|
|
47
|
+
lReturnValue = `${lReturnValue}**`;
|
|
48
|
+
}
|
|
49
|
+
return lReturnValue;
|
|
50
50
|
}
|
|
51
51
|
function lineContainsTeamName(pLine, pTeamName) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
return pLine.users.some(
|
|
53
|
+
(pUser) =>
|
|
54
|
+
pUser.type === "virtual-team-name" && pUser.bareName === pTeamName,
|
|
55
|
+
);
|
|
56
56
|
}
|
package/dist/main.js
CHANGED
|
@@ -5,30 +5,30 @@ import generateLabelerYml from "./labeler-yml/generate.js";
|
|
|
5
5
|
import readTeamMap from "./team-map/read.js";
|
|
6
6
|
import readVirtualCodeOwners from "./virtual-code-owners/read.js";
|
|
7
7
|
export function main(pOptions, pErrorStream) {
|
|
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
|
-
|
|
8
|
+
const lTeamMap = readTeamMap(pOptions.virtualTeams);
|
|
9
|
+
const lVirtualCodeOwners = readVirtualCodeOwners(
|
|
10
|
+
pOptions.virtualCodeOwners,
|
|
11
|
+
lTeamMap,
|
|
12
|
+
);
|
|
13
|
+
const lCodeOwnersContent = generateCodeOwners(lVirtualCodeOwners, lTeamMap);
|
|
14
|
+
if (!pOptions.dryRun) {
|
|
15
|
+
writeFileSync(pOptions.codeOwners, lCodeOwnersContent, {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
flag: "w",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (pOptions.emitLabeler) {
|
|
21
|
+
const lLabelerContent = generateLabelerYml(lVirtualCodeOwners, lTeamMap);
|
|
22
|
+
if (!pOptions.dryRun) {
|
|
23
|
+
writeFileSync(pOptions.labelerLocation, lLabelerContent, {
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
flag: "w",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
pErrorStream.write(
|
|
29
|
+
`${EOL}Wrote '${pOptions.codeOwners}' AND '${pOptions.labelerLocation}'${EOL}${EOL}`,
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
pErrorStream.write(`${EOL}Wrote '${pOptions.codeOwners}'${EOL}${EOL}`);
|
|
33
|
+
}
|
|
34
34
|
}
|
package/dist/team-map/read.js
CHANGED
|
@@ -4,29 +4,29 @@ import { EOL } from "node:os";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import virtualTeamsSchema from "./virtual-teams.schema.js";
|
|
6
6
|
export default function readTeamMap(pVirtualTeamsFileName) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const lVirtualTeamsAsAString = readFileSync(pVirtualTeamsFileName, {
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
});
|
|
10
|
+
const lTeamMap = parseYaml(lVirtualTeamsAsAString);
|
|
11
|
+
assertTeamMapValid(lTeamMap, pVirtualTeamsFileName);
|
|
12
|
+
return lTeamMap;
|
|
13
13
|
}
|
|
14
14
|
function assertTeamMapValid(pTeamMap, pVirtualTeamsFileName) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const ajv = new Ajv({
|
|
16
|
+
allErrors: true,
|
|
17
|
+
verbose: true,
|
|
18
|
+
});
|
|
19
|
+
if (!ajv.validate(virtualTeamsSchema, pTeamMap)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`This is not a valid virtual-teams.yml:${EOL}${formatAjvErrors(ajv.errors, pVirtualTeamsFileName)}.\n`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
24
|
}
|
|
25
25
|
function formatAjvErrors(pAjvErrors, pVirtualTeamsFileName) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
return pAjvErrors
|
|
27
|
+
.map((pAjvError) => formatAjvError(pAjvError, pVirtualTeamsFileName))
|
|
28
|
+
.join(EOL);
|
|
29
29
|
}
|
|
30
30
|
function formatAjvError(pAjvError, pVirtualTeamsFileName) {
|
|
31
|
-
|
|
31
|
+
return `${pVirtualTeamsFileName}: ${pAjvError.instancePath} - ${JSON.stringify(pAjvError.data)} ${pAjvError.message}`;
|
|
32
32
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
16
|
};
|
package/dist/utensils.js
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "8.0.
|
|
1
|
+
export const VERSION = "8.0.4";
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
export function getAnomalies(pVirtualCodeOwners) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
const weirdLines = pVirtualCodeOwners
|
|
3
|
+
.filter((pLine) => pLine.type === "unknown")
|
|
4
|
+
.map((pLine) => ({
|
|
5
|
+
...pLine,
|
|
6
|
+
type: "invalid-line",
|
|
7
|
+
}));
|
|
8
|
+
const weirdUsers = pVirtualCodeOwners.flatMap((pLine) => {
|
|
9
|
+
if (pLine.type === "rule") {
|
|
10
|
+
return pLine.users
|
|
11
|
+
.filter((pUser) => pUser.type === "invalid")
|
|
12
|
+
.map((pUser) => ({
|
|
13
|
+
...pUser,
|
|
14
|
+
line: pLine.line,
|
|
15
|
+
type: "invalid-user",
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
});
|
|
20
|
+
return weirdLines.concat(weirdUsers).sort(orderAnomaly);
|
|
21
21
|
}
|
|
22
22
|
function orderAnomaly(pLeft, pRight) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
if (
|
|
24
|
+
pLeft.line === pRight.line &&
|
|
25
|
+
pLeft.type === "invalid-user" &&
|
|
26
|
+
pRight.type === "invalid-user"
|
|
27
|
+
) {
|
|
28
|
+
return pLeft.userNumberWithinLine > pRight.userNumberWithinLine ? 1 : -1;
|
|
29
|
+
} else {
|
|
30
|
+
return pLeft.line > pRight.line ? 1 : -1;
|
|
31
|
+
}
|
|
32
32
|
}
|
|
@@ -1,67 +1,67 @@
|
|
|
1
1
|
import { EOL } from "node:os";
|
|
2
2
|
import { isEmailIshUsername } from "../utensils.js";
|
|
3
3
|
export function parse(pVirtualCodeOwnersAsString, pTeamMap = {}) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
return pVirtualCodeOwnersAsString
|
|
5
|
+
.split(EOL)
|
|
6
|
+
.map((pUntreatedLine, pLineNo) =>
|
|
7
|
+
parseLine(pUntreatedLine, pTeamMap, pLineNo + 1),
|
|
8
|
+
);
|
|
9
9
|
}
|
|
10
10
|
function parseLine(pUntreatedLine, pTeamMap, pLineNo) {
|
|
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
|
-
|
|
11
|
+
const lTrimmedLine = pUntreatedLine.trim();
|
|
12
|
+
const lCommentSplitLine = lTrimmedLine.split(/\s*#/);
|
|
13
|
+
const lRule = lCommentSplitLine[0]?.match(
|
|
14
|
+
/^(?<filesPattern>[^\s]+)(?<spaces>\s+)(?<userNames>.*)$/,
|
|
15
|
+
);
|
|
16
|
+
if (lTrimmedLine.startsWith("#!")) {
|
|
17
|
+
return { type: "ignorable-comment", line: pLineNo, raw: pUntreatedLine };
|
|
18
|
+
}
|
|
19
|
+
if (lTrimmedLine.startsWith("#")) {
|
|
20
|
+
return { type: "comment", line: pLineNo, raw: pUntreatedLine };
|
|
21
|
+
}
|
|
22
|
+
if (!lRule?.groups) {
|
|
23
|
+
if (lTrimmedLine === "") {
|
|
24
|
+
return { type: "empty", line: pLineNo, raw: pUntreatedLine };
|
|
25
|
+
}
|
|
26
|
+
return { type: "unknown", line: pLineNo, raw: pUntreatedLine };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
type: "rule",
|
|
30
|
+
line: pLineNo,
|
|
31
|
+
filesPattern: lRule.groups.filesPattern,
|
|
32
|
+
spaces: lRule.groups.spaces,
|
|
33
|
+
users: parseUsers(lRule.groups.userNames, pTeamMap),
|
|
34
|
+
inlineComment: lCommentSplitLine[1] ?? "",
|
|
35
|
+
raw: pUntreatedLine,
|
|
36
|
+
};
|
|
37
37
|
}
|
|
38
38
|
function parseUsers(pUserNamesString, pTeamMap) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
const lUserNames = pUserNamesString.split(/\s+/);
|
|
40
|
+
return lUserNames.map((pUserName, pIndex) => {
|
|
41
|
+
const lBareName = getBareUserName(pUserName);
|
|
42
|
+
return {
|
|
43
|
+
type: getUserNameType(pUserName, lBareName, pTeamMap),
|
|
44
|
+
userNumberWithinLine: pIndex + 1,
|
|
45
|
+
bareName: lBareName,
|
|
46
|
+
raw: pUserName,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
49
|
}
|
|
50
50
|
function getUserNameType(pUserName, pBareName, pTeamMap) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
if (isEmailIshUsername(pUserName)) {
|
|
52
|
+
return "e-mail-address";
|
|
53
|
+
}
|
|
54
|
+
if (pUserName.startsWith("@")) {
|
|
55
|
+
if (pTeamMap.hasOwnProperty(pBareName)) {
|
|
56
|
+
return "virtual-team-name";
|
|
57
|
+
}
|
|
58
|
+
return "other-user-or-team";
|
|
59
|
+
}
|
|
60
|
+
return "invalid";
|
|
61
61
|
}
|
|
62
62
|
function getBareUserName(pUserName) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
if (pUserName.startsWith("@")) {
|
|
64
|
+
return pUserName.slice(1);
|
|
65
|
+
}
|
|
66
|
+
return pUserName;
|
|
67
67
|
}
|
|
@@ -3,35 +3,35 @@ import { EOL } from "node:os";
|
|
|
3
3
|
import { getAnomalies } from "./anomalies.js";
|
|
4
4
|
import { parse as parseVirtualCodeOwners } from "./parse.js";
|
|
5
5
|
export default function readVirtualCodeOwners(
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
pVirtualCodeOwnersFileName,
|
|
7
|
+
pTeamMap,
|
|
8
8
|
) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
const lVirtualCodeOwnersAsAString = readFileSync(pVirtualCodeOwnersFileName, {
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
});
|
|
12
|
+
const lVirtualCodeOwners = parseVirtualCodeOwners(
|
|
13
|
+
lVirtualCodeOwnersAsAString,
|
|
14
|
+
pTeamMap,
|
|
15
|
+
);
|
|
16
|
+
const lAnomalies = getAnomalies(lVirtualCodeOwners);
|
|
17
|
+
if (lAnomalies.length > 0) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`This is not a valid virtual code-owners file:${EOL}${reportAnomalies(pVirtualCodeOwnersFileName, lAnomalies)}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return lVirtualCodeOwners;
|
|
23
23
|
}
|
|
24
24
|
function reportAnomalies(pFileName, pAnomalies) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
return pAnomalies
|
|
26
|
+
.map((pAnomaly) => {
|
|
27
|
+
if (pAnomaly.type === "invalid-line") {
|
|
28
|
+
return `${pFileName}:${pAnomaly.line}:1 invalid line - neither a rule, comment nor empty: "${pAnomaly.raw}"`;
|
|
29
|
+
} else {
|
|
30
|
+
return (
|
|
31
|
+
`${pFileName}:${pAnomaly.line}:1 invalid user or team name "${pAnomaly.raw}" (#${pAnomaly.userNumberWithinLine} on this line). ` +
|
|
32
|
+
`It should either start with "@" or be an e-mail address.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.join(EOL);
|
|
37
37
|
}
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
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
|
-
|
|
2
|
+
"name": "virtual-code-owners",
|
|
3
|
+
"version": "8.0.4",
|
|
4
|
+
"description": "CODEOWNERS with teams for teams that can't use GitHub teams",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"virtual-code-owners": "dist/run-cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"package.json",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"CODEOWNERS"
|
|
17
|
+
],
|
|
18
|
+
"author": "Sander Verweij (https://sverweij.github.io/)",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/sverweij/virtual-code-owners",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/sverweij/virtual-code-owners.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/sverweij/virtual-code-owners/issues"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ajv": "8.12.0",
|
|
30
|
+
"yaml": "2.4.1"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": "^18.11.0||>=20.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "echo for test, build and static analysis scripts: see the github repository"
|
|
37
|
+
}
|
|
38
38
|
}
|