virtual-code-owners 5.1.0 → 6.1.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 +15 -5
- package/dist/cli.js +2 -2
- package/dist/generate-codeowners.js +4 -1
- package/dist/generate-labeler-yml.js +1 -1
- package/dist/main.js +30 -35
- package/dist/read-team-map.js +23 -1
- package/dist/read-virtual-code-owners.js +21 -2
- package/dist/version.js +1 -1
- package/dist/virtual-teams.schema.json +15 -0
- package/package.json +26 -10
- /package/dist/{parse.js → parse-virtual-code-owners.js} +0 -0
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ This is where `virtual-code-owners` comes in.
|
|
|
71
71
|
|
|
72
72
|
### VIRTUAL-CODEOWNERS.txt
|
|
73
73
|
|
|
74
|
-
`
|
|
74
|
+
`VIRTUAL-CODEOWNERS.txt` is a regular, valid GitHub [CODEOWNERS](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) file.
|
|
75
75
|
The only difference between VIRTUAL-CODEOWNERS.txt and a CODEOWNERS file is that
|
|
76
76
|
the _teams_ the former uses might not exist yet, except in a `virtual-teams.yml`.
|
|
77
77
|
This enables you to write a _much_ easier to maintain list of code owners.
|
|
@@ -189,10 +189,10 @@ file that will check that for you.
|
|
|
189
189
|
- ~~Currently only works for _user names_ to identify team members - not for e-mail
|
|
190
190
|
addresses.~~
|
|
191
191
|
Works with both user names and e-mail addresses
|
|
192
|
-
- _virtual-code-owners_
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
- Although _virtual-code-owners_ performs basic validations on the CODEOWNER
|
|
193
|
+
format and tries to emit clear and actionable error messages about them, it
|
|
194
|
+
might not catch all of them - e.g. it doesn't check if the users in
|
|
195
|
+
virtual-teams.yml actually exist.
|
|
196
196
|
|
|
197
197
|
### Why the `.txt` extension?
|
|
198
198
|
|
|
@@ -236,3 +236,13 @@ npx virtual-code-owners --emitLabeler
|
|
|
236
236
|
|
|
237
237
|
If you have an alternate file location for the `labeler.yml` you can specify that
|
|
238
238
|
with virtual-code-owner's `--labelerLocation` parameter.
|
|
239
|
+
|
|
240
|
+
### Can I just run virtual-code-owners to validate VIRTUAL-CODEOWNERS.txt & virtual-teams.yml?
|
|
241
|
+
|
|
242
|
+
So _without_ generating any output?
|
|
243
|
+
|
|
244
|
+
Yes. Use the `--dryRun` command line option:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
npx virtual-code-owners --dryRun
|
|
248
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { cli } from "./main.js";
|
|
3
|
+
cli();
|
|
@@ -20,7 +20,7 @@ export default function generateCodeOwners(pVirtualCodeOwners, pTeamMap, pGenera
|
|
|
20
20
|
function generateLine(pCSTLine, pTeamMap) {
|
|
21
21
|
if (pCSTLine.type === "rule") {
|
|
22
22
|
const lUserNames = uniq(pCSTLine.users.flatMap((pUser) => expandTeamToUserNames(pUser, pTeamMap)))
|
|
23
|
-
.sort()
|
|
23
|
+
.sort(compareUserNames)
|
|
24
24
|
.join(" ");
|
|
25
25
|
return pCSTLine.filesPattern + pCSTLine.spaces + lUserNames;
|
|
26
26
|
}
|
|
@@ -41,6 +41,9 @@ function userNameToCodeOwner(pUserName) {
|
|
|
41
41
|
}
|
|
42
42
|
return `@${pUserName}`;
|
|
43
43
|
}
|
|
44
|
+
function compareUserNames(pLeftName, pRightName) {
|
|
45
|
+
return pLeftName.toLowerCase() > pRightName.toLowerCase() ? 1 : -1;
|
|
46
|
+
}
|
|
44
47
|
function uniq(pUserNames) {
|
|
45
48
|
return Array.from(new Set(pUserNames));
|
|
46
49
|
}
|
|
@@ -35,7 +35,7 @@ function transformForYamlAndMinimatch(pOriginalString) {
|
|
|
35
35
|
lReturnValue = "**";
|
|
36
36
|
}
|
|
37
37
|
if (lReturnValue.startsWith("*")) {
|
|
38
|
-
lReturnValue = `
|
|
38
|
+
lReturnValue = `"${lReturnValue}"`;
|
|
39
39
|
}
|
|
40
40
|
if (pOriginalString.endsWith("/")) {
|
|
41
41
|
lReturnValue = `${lReturnValue}**`;
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,6 @@ import generateLabelerYml from "./generate-labeler-yml.js";
|
|
|
6
6
|
import readTeamMap from "./read-team-map.js";
|
|
7
7
|
import readVirtualCodeOwners from "./read-virtual-code-owners.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
|
-
import { getAnomalies } from "./parse.js";
|
|
10
9
|
const HELP_MESSAGE = `Usage: virtual-code-owners [options]
|
|
11
10
|
|
|
12
11
|
Merges a VIRTUAL-CODEOWNERS.txt and a virtual-teams.yml into CODEOWNERS
|
|
@@ -26,8 +25,10 @@ Options:
|
|
|
26
25
|
(default: false)
|
|
27
26
|
--labelerLocation [file-name] The location of the labeler.yml file
|
|
28
27
|
(default: ".github/labeler.yml")
|
|
28
|
+
--dryRun Just validate inputs, don't generate
|
|
29
|
+
outputs (default: false)
|
|
29
30
|
-h, --help display help for command`;
|
|
30
|
-
export function
|
|
31
|
+
export function cli(pArguments = process.argv.slice(2), pOutStream = process.stdout, pErrorStream = process.stderr) {
|
|
31
32
|
try {
|
|
32
33
|
const lOptions = getOptions(pArguments);
|
|
33
34
|
if (lOptions.help) {
|
|
@@ -38,45 +39,13 @@ export function main(pArguments = process.argv.slice(2), pOutStream = process.st
|
|
|
38
39
|
pOutStream.write(`${VERSION}${EOL}`);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
const lVirtualCodeOwners = readVirtualCodeOwners(lOptions.virtualCodeOwners, lTeamMap);
|
|
43
|
-
const lAnomalies = getAnomalies(lVirtualCodeOwners);
|
|
44
|
-
if (lAnomalies.length > 0) {
|
|
45
|
-
throw new Error(`${EOL}${reportAnomalies(lOptions.virtualCodeOwners, lAnomalies)}`);
|
|
46
|
-
}
|
|
47
|
-
const lCodeOwnersContent = generateCodeOwners(lVirtualCodeOwners, lTeamMap);
|
|
48
|
-
writeFileSync(lOptions.codeOwners, lCodeOwnersContent, {
|
|
49
|
-
encoding: "utf-8",
|
|
50
|
-
});
|
|
51
|
-
if (lOptions.emitLabeler) {
|
|
52
|
-
const lLabelerContent = generateLabelerYml(lVirtualCodeOwners, lTeamMap);
|
|
53
|
-
writeFileSync(lOptions.labelerLocation, lLabelerContent, {
|
|
54
|
-
encoding: "utf-8",
|
|
55
|
-
});
|
|
56
|
-
pErrorStream.write(`${EOL}Wrote ${lOptions.codeOwners} AND ${lOptions.labelerLocation}${EOL}${EOL}`);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
pErrorStream.write(`${EOL}Wrote ${lOptions.codeOwners}${EOL}${EOL}`);
|
|
60
|
-
}
|
|
42
|
+
main(lOptions, pErrorStream);
|
|
61
43
|
}
|
|
62
44
|
catch (pError) {
|
|
63
45
|
pErrorStream.write(`${EOL}ERROR: ${pError.message}${EOL}${EOL}`);
|
|
64
46
|
process.exitCode = 1;
|
|
65
47
|
}
|
|
66
48
|
}
|
|
67
|
-
function reportAnomalies(pFileName, pAnomalies) {
|
|
68
|
-
return pAnomalies
|
|
69
|
-
.map((pAnomaly) => {
|
|
70
|
-
if (pAnomaly.type === "invalid-line") {
|
|
71
|
-
return `${pFileName}:${pAnomaly.line}:1 invalid line - neither a rule, comment nor empty: '${pAnomaly.raw}'`;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
return (`${pFileName}:${pAnomaly.line}:1 invalid user or team name '${pAnomaly.raw}' (# ${pAnomaly.userNumberWithinLine} on this line). ` +
|
|
75
|
-
`It should either start with '@' or be an e-mail address.`);
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
.join(EOL);
|
|
79
|
-
}
|
|
80
49
|
function getOptions(pArguments) {
|
|
81
50
|
return parseArgs({
|
|
82
51
|
args: pArguments,
|
|
@@ -105,6 +74,10 @@ function getOptions(pArguments) {
|
|
|
105
74
|
type: "string",
|
|
106
75
|
default: ".github/labeler.yml",
|
|
107
76
|
},
|
|
77
|
+
dryRun: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
default: false,
|
|
80
|
+
},
|
|
108
81
|
help: { type: "boolean", short: "h", default: false },
|
|
109
82
|
version: { type: "boolean", short: "V", default: false },
|
|
110
83
|
},
|
|
@@ -113,3 +86,25 @@ function getOptions(pArguments) {
|
|
|
113
86
|
tokens: false,
|
|
114
87
|
}).values;
|
|
115
88
|
}
|
|
89
|
+
function main(pOptions, pErrorStream) {
|
|
90
|
+
const lTeamMap = readTeamMap(pOptions.virtualTeams);
|
|
91
|
+
const lVirtualCodeOwners = readVirtualCodeOwners(pOptions.virtualCodeOwners, lTeamMap);
|
|
92
|
+
const lCodeOwnersContent = generateCodeOwners(lVirtualCodeOwners, lTeamMap);
|
|
93
|
+
if (!pOptions.dryRun) {
|
|
94
|
+
writeFileSync(pOptions.codeOwners, lCodeOwnersContent, {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (pOptions.emitLabeler) {
|
|
99
|
+
const lLabelerContent = generateLabelerYml(lVirtualCodeOwners, lTeamMap);
|
|
100
|
+
if (!pOptions.dryRun) {
|
|
101
|
+
writeFileSync(pOptions.labelerLocation, lLabelerContent, {
|
|
102
|
+
encoding: "utf-8",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
pErrorStream.write(`${EOL}Wrote '${pOptions.codeOwners}' AND '${pOptions.labelerLocation}'${EOL}${EOL}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
pErrorStream.write(`${EOL}Wrote '${pOptions.codeOwners}'${EOL}${EOL}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/read-team-map.js
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { EOL } from "node:os";
|
|
2
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
+
const TEAM_MAP_SCHEMA = JSON.parse(readFileSync(new URL("./virtual-teams.schema.json", import.meta.url), "utf-8"));
|
|
3
6
|
export default function readTeamMap(pVirtualTeamsFileName) {
|
|
4
7
|
const lVirtualTeamsAsAString = readFileSync(pVirtualTeamsFileName, {
|
|
5
8
|
encoding: "utf-8",
|
|
6
9
|
});
|
|
7
|
-
|
|
10
|
+
const lTeamMap = parseYaml(lVirtualTeamsAsAString);
|
|
11
|
+
validateTeamMap(lTeamMap, pVirtualTeamsFileName);
|
|
12
|
+
return lTeamMap;
|
|
13
|
+
}
|
|
14
|
+
function validateTeamMap(pTeamMap, pVirtualTeamsFileName) {
|
|
15
|
+
const ajv = new Ajv({
|
|
16
|
+
allErrors: true,
|
|
17
|
+
verbose: true,
|
|
18
|
+
});
|
|
19
|
+
if (!ajv.validate(TEAM_MAP_SCHEMA, pTeamMap)) {
|
|
20
|
+
throw new Error(`This is not a valid virtual-teams.yml:${EOL}${formatAjvErrors(ajv.errors, pVirtualTeamsFileName)}.\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function formatAjvErrors(pAjvErrors, pVirtualTeamsFileName) {
|
|
24
|
+
return pAjvErrors
|
|
25
|
+
.map((pAjvError) => formatAjvError(pAjvError, pVirtualTeamsFileName))
|
|
26
|
+
.join(EOL);
|
|
27
|
+
}
|
|
28
|
+
function formatAjvError(pAjvError, pVirtualTeamsFileName) {
|
|
29
|
+
return `${pVirtualTeamsFileName}: ${pAjvError.instancePath} - ${JSON.stringify(pAjvError.data)} ${pAjvError.message}`;
|
|
8
30
|
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
import { getAnomalies, parse as parseVirtualCodeOwners, } from "./parse-virtual-code-owners.js";
|
|
3
4
|
export default function readVirtualCodeOwners(pVirtualCodeOwnersFileName, pTeamMap) {
|
|
4
5
|
const lVirtualCodeOwnersAsAString = readFileSync(pVirtualCodeOwnersFileName, {
|
|
5
6
|
encoding: "utf-8",
|
|
6
7
|
});
|
|
7
|
-
|
|
8
|
+
const lVirtualCodeOwners = parseVirtualCodeOwners(lVirtualCodeOwnersAsAString, pTeamMap);
|
|
9
|
+
const lAnomalies = getAnomalies(lVirtualCodeOwners);
|
|
10
|
+
if (lAnomalies.length > 0) {
|
|
11
|
+
throw new Error(`This is not a valid virtual code-owners file:${EOL}${reportAnomalies(pVirtualCodeOwnersFileName, lAnomalies)}`);
|
|
12
|
+
}
|
|
13
|
+
return lVirtualCodeOwners;
|
|
14
|
+
}
|
|
15
|
+
function reportAnomalies(pFileName, pAnomalies) {
|
|
16
|
+
return pAnomalies
|
|
17
|
+
.map((pAnomaly) => {
|
|
18
|
+
if (pAnomaly.type === "invalid-line") {
|
|
19
|
+
return `${pFileName}:${pAnomaly.line}:1 invalid line - neither a rule, comment nor empty: "${pAnomaly.raw}"`;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return (`${pFileName}:${pAnomaly.line}:1 invalid user or team name "${pAnomaly.raw}" (#${pAnomaly.userNumberWithinLine} on this line). ` +
|
|
23
|
+
`It should either start with "@" or be an e-mail address.`);
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.join(EOL);
|
|
8
27
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "
|
|
1
|
+
export const VERSION = "6.1.0";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
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/6.0.0",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Username or e-mail address of a team member. (Don't prefix usernames with '@')",
|
|
12
|
+
"pattern": "^[^@][^\\s]+$"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virtual-code-owners",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
|
+
"description": "Makes your CODEOWNERS file liveable again",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
"
|
|
7
|
+
"parse": [
|
|
8
8
|
{
|
|
9
|
-
"import": "./dist/parse-
|
|
10
|
-
"types": "./types/parse-
|
|
9
|
+
"import": "./dist/parse-virtual-code-owners.js",
|
|
10
|
+
"types": "./types/parse-virtual-code-owners.d.ts"
|
|
11
11
|
},
|
|
12
|
-
"./dist/parse-
|
|
12
|
+
"./dist/parse-virtual-code-owners.js"
|
|
13
|
+
],
|
|
14
|
+
"generateCodeOwners": [
|
|
15
|
+
{
|
|
16
|
+
"import": "./dist/generate-codeowners.js",
|
|
17
|
+
"types": "./types/generate-codeowners.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./dist/generate-codeowners.js"
|
|
20
|
+
],
|
|
21
|
+
"generateLabelerYml": [
|
|
22
|
+
{
|
|
23
|
+
"import": "./dist/generate-labeler-yml.js",
|
|
24
|
+
"types": "./types/generate-labeler-yml.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./dist/generate-labeler-yml.js"
|
|
13
27
|
]
|
|
14
28
|
},
|
|
15
|
-
"main": "dist/
|
|
29
|
+
"main": "dist/main.js",
|
|
30
|
+
"types": "types/types.d.ts",
|
|
16
31
|
"bin": "dist/cli.js",
|
|
17
32
|
"files": [
|
|
18
33
|
"dist",
|
|
@@ -21,7 +36,7 @@
|
|
|
21
36
|
"LICENSE"
|
|
22
37
|
],
|
|
23
38
|
"scripts": {
|
|
24
|
-
"build": "rm -rf dist && node --no-warnings --loader ts-node/esm tools/get-version.ts > src/version.ts && tsc",
|
|
39
|
+
"build": "rm -rf dist && node --no-warnings --loader ts-node/esm tools/get-version.ts > src/version.ts && tsc && cp -f src/*.json dist/.",
|
|
25
40
|
"check": "npm run format && npm run build && npm run depcruise -- --no-progress && npm test",
|
|
26
41
|
"depcruise": "depcruise src tools",
|
|
27
42
|
"depcruise:graph": "depcruise src --include-only '^(src)' --output-type dot | dot -T svg | depcruise-wrap-stream-in-html > dependency-graph.html",
|
|
@@ -54,8 +69,8 @@
|
|
|
54
69
|
},
|
|
55
70
|
"devDependencies": {
|
|
56
71
|
"@types/mocha": "10.0.1",
|
|
57
|
-
"@types/node": "20.3.
|
|
58
|
-
"c8": "
|
|
72
|
+
"@types/node": "20.3.1",
|
|
73
|
+
"c8": "8.0.0",
|
|
59
74
|
"dependency-cruiser": "13.0.3",
|
|
60
75
|
"husky": "8.0.3",
|
|
61
76
|
"lint-staged": "13.2.2",
|
|
@@ -67,6 +82,7 @@
|
|
|
67
82
|
"watskeburt": "0.11.3"
|
|
68
83
|
},
|
|
69
84
|
"dependencies": {
|
|
85
|
+
"ajv": "8.12.0",
|
|
70
86
|
"yaml": "2.3.1"
|
|
71
87
|
},
|
|
72
88
|
"engines": {
|
|
File without changes
|