virtual-code-owners 8.1.0 → 8.2.1

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 CHANGED
@@ -5,12 +5,12 @@ This generates your `CODEOWNERS` file (_patterns_ x _users_) from
5
5
  - a `VIRTUAL-CODEOWNERS.txt` (_patterns_ x _teams_)
6
6
  - a `virtual-teams.yml` (_teams_ x _users_)
7
7
 
8
- ... which makes it easier to keep `CODEOWNERS` in sync on multi-team mono repos.
9
- When those teams are not defined on GitHub level.
8
+ ... which makes it easier to keep `CODEOWNERS` in sync on multi-team mono repos
9
+ when you don't have (enough) 'real' GitHub or GitLab teams.
10
10
 
11
11
  ## Usage
12
12
 
13
- - Rename your `.github/CODEOWNERS` to `.github/VIRTUAL-CODEOWNERS.txt` and put team names in them.
13
+ - Rename `.github/CODEOWNERS` to `.github/VIRTUAL-CODEOWNERS.txt` and put team names in them.
14
14
  - Define teams that don't (yet) exist on GitHub level in `.github/virtual-teams.yml`
15
15
  - Run this:
16
16
 
@@ -144,9 +144,6 @@ libs/baarden/ jan@example.com korneel@example.com pier@example.com tjorus@
144
144
  ### Any gotcha's?
145
145
 
146
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.
150
147
 
151
148
  ### Do I have to run this each time I edit `VIRTUAL-CODEOWNERS.txt`?
152
149
 
@@ -210,11 +207,17 @@ user/team names but doesn't verify their existence in the project.
210
207
 
211
208
  - valid user/team names start with an `@` or are an e-mail address
212
209
  - valid rules have a file pattern and at least one user/team name
210
+ (unless they're in a _section_ that has default owners `[sales related] @ch/sales`
211
+ in which case the rule inherits the default owners of that section)
213
212
  - valid sections headings comply with the syntax described over at [GitLab](https://docs.gitlab.com/ee/user/project/codeowners/reference.html#sections)
214
213
  > different from GitLab's syntax the line `[bla @group` is not interpreted
215
214
  > as a rule, but as an erroneous section heading. This behaviour might change
216
215
  > to be the same as GitLab's in future releases without a major version bump.
217
216
 
217
+ ### Does virtual-code-owners support GitLab style sections?
218
+
219
+ Yes.
220
+
218
221
  ### I want to specify different locations for the files (e.g. because I'm using GitLab)
219
222
 
220
223
  Here you go:
@@ -228,8 +231,6 @@ npx virtual-code-owners \
228
231
 
229
232
  ### Can I just validate VIRTUAL-CODEOWNERS.txt & virtual-teams.yml without generating output?
230
233
 
231
- So _without_ generating any output?
232
-
233
234
  Sure thing. Use `--dryRun`:
234
235
 
235
236
  ```
@@ -241,16 +242,15 @@ npx virtual-code-owners --dryRun
241
242
  It keeps editors and IDE's from messing up your formatting.
242
243
 
243
244
  Various editors assume an ALL_CAPS file name with `#` characters on various lines
244
- to be markdown, and will auto format them as such. This makes for either very ugly
245
- or in worst cases invalid CODEOWNERS files. Usually such autoformatting is not
246
- present on text files.
245
+ to be markdown, and will auto format them as such. Usually such autoformatting is
246
+ not present on text files.
247
247
 
248
- Apparently these editors know about CODEOWNERS, though, so they don't mess with
249
- the formatting of _those_.
248
+ Often these editors know about CODEOWNERS, so they won't confuse _those_ with
249
+ markdown.
250
250
 
251
251
  ### Why does this exist at all? Why not just use GitHub teams?
252
252
 
253
- You should _totally_ use GitHub teams! If you can.
253
+ If you can you should _totally_ use GitHub teams!
254
254
 
255
255
  Organizations sometimes have large mono repositories with many code owners.
256
256
  They or their bureaucracy haven't landed on actually using GitHub teams to
@@ -259,10 +259,9 @@ the organization chart (and hence the GitHub teams). Teams in those organization
259
259
  who want to have clear code ownership can either:
260
260
 
261
261
  - Wrestle the bureaucracy.
262
- Recommended! It might take a while, though - and even though there are good
263
- people on many levels in bureaucracies, it might eventually not pan out
264
- because #reasons.
262
+ Recommended! It will often require patience though, and in the mean time
263
+ you might want to have some clarity on code ownership.
265
264
  - Maintain a CODEOWNERS file with code assigned to large lists of individuals.
266
- An option, but laborious to maintain, even for smaller projects
265
+ That's a lotta work, even for smaller projects
267
266
 
268
267
  This is where `virtual-code-owners` comes in.
@@ -37,7 +37,7 @@ function generateLine(pCSTLine, pTeamMap) {
37
37
  return (
38
38
  (pCSTLine.optional ? "^" : "") +
39
39
  "[" +
40
- pCSTLine.sectionName +
40
+ pCSTLine.name +
41
41
  "]" +
42
42
  (pCSTLine.minApprovers ? `[${pCSTLine.minApprovers}]` : "") +
43
43
  pCSTLine.spaces +
@@ -49,8 +49,13 @@ function transformForYamlAndMinimatch(pOriginalString) {
49
49
  return lReturnValue;
50
50
  }
51
51
  function lineContainsTeamName(pLine, pTeamName) {
52
- return pLine.users.some(
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
  }
@@ -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 virtualTeamsSchema from "./virtual-teams.schema.js";
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(pTeamMap, pVirtualTeamsFileName) {
15
- const ajv = new Ajv({
16
- allErrors: true,
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
- `This is not a valid virtual-teams.yml:${EOL}${formatAjvErrors(ajv.errors, pVirtualTeamsFileName)}.\n`,
16
+ `'${pVirtualTeamsFileName}' is not a valid virtual-teams.yml:${EOL} ${lError}`,
22
17
  );
23
18
  }
24
19
  }
25
- function formatAjvErrors(pAjvErrors, pVirtualTeamsFileName) {
26
- return pAjvErrors
27
- .map((pAjvError) => formatAjvError(pAjvError, pVirtualTeamsFileName))
28
- .join(EOL);
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 formatAjvError(pAjvError, pVirtualTeamsFileName) {
31
- return `${pVirtualTeamsFileName}: ${pAjvError.instancePath} - ${JSON.stringify(pAjvError.data)} ${pAjvError.message}`;
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.1.0";
1
+ export const VERSION = "8.2.1";
@@ -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,63 +17,74 @@ 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
  }
26
+ if (lTrimmedLine === "") {
27
+ return { type: "empty", line: pLineNo, raw: pUntreatedLine };
28
+ }
22
29
  if (lTrimmedLine.startsWith("[") || lTrimmedLine.startsWith("^[")) {
23
30
  return parseSection(pUntreatedLine, pLineNo, pTeamMap);
24
31
  }
25
- if (!lRule?.groups) {
26
- if (lTrimmedLine === "") {
27
- return { type: "empty", line: pLineNo, raw: pUntreatedLine };
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;
28
56
  }
29
- return { type: "unknown", line: pLineNo, raw: pUntreatedLine };
57
+ return lReturnValue;
30
58
  }
31
- return {
32
- type: "rule",
33
- line: pLineNo,
34
- filesPattern: lRule.groups.filesPattern,
35
- spaces: lRule.groups.spaces,
36
- users: parseUsers(lRule.groups.userNames, pTeamMap),
37
- inlineComment: lCommentSplitLine[1] ?? "",
38
- raw: pUntreatedLine,
39
- };
59
+ return { type: "unknown", line: pLineNo, raw: pUntreatedLine };
40
60
  }
41
61
  function parseSection(pUntreatedLine, pLineNo, pTeamMap) {
42
62
  const lTrimmedLine = pUntreatedLine.trim();
43
63
  const lCommentSplitLine = lTrimmedLine.split(/\s*#/);
44
64
  const lSection = lCommentSplitLine[0]?.match(
45
- /^(?<optionalIndicator>\^)?\[(?<sectionName>[^\]]+)\](\[(?<minApprovers>[0-9]+)\])?(?<spaces>\s+)(?<userNames>.*)$/,
65
+ /^(?<optionalIndicator>\^)?\[(?<name>[^\]]+)\](\[(?<minApprovers>[0-9]+)\])?(?<spaces>\s+)?(?<userNames>.+)?$/,
46
66
  );
47
67
  if (!lSection?.groups) {
48
- return lTrimmedLine.endsWith("]")
49
- ? {
50
- type: "section-without-users",
51
- line: pLineNo,
52
- raw: pUntreatedLine,
53
- }
54
- : {
55
- type: "unknown",
56
- line: pLineNo,
57
- raw: pUntreatedLine,
58
- };
68
+ return {
69
+ type: "unknown",
70
+ line: pLineNo,
71
+ raw: pUntreatedLine,
72
+ };
59
73
  }
74
+ const lUsers = parseUsers(lSection.groups?.userNames ?? "", pTeamMap);
75
+ STATE = {
76
+ currentSection: lSection.groups.name,
77
+ inheritedUsers: lUsers,
78
+ };
60
79
  const lReturnValue = {
61
80
  type: "section-heading",
62
81
  line: pLineNo,
63
- optional: lSection.groups.optionalIndicator === "^",
64
- sectionName: lSection.groups.sectionName,
65
- spaces: lSection.groups.spaces,
66
- users: parseUsers(lSection.groups.userNames, pTeamMap),
67
- inlineComment: lTrimmedLine.split(/\s*#/)[1] ?? "",
68
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] ?? "",
69
88
  };
70
89
  if (lSection.groups.minApprovers) {
71
90
  lReturnValue.minApprovers = parseInt(lSection.groups.minApprovers, 10);
@@ -73,7 +92,7 @@ function parseSection(pUntreatedLine, pLineNo, pTeamMap) {
73
92
  return lReturnValue;
74
93
  }
75
94
  function parseUsers(pUserNamesString, pTeamMap) {
76
- const lUserNames = pUserNamesString.split(/\s+/);
95
+ const lUserNames = pUserNamesString ? pUserNamesString.split(/\s+/) : [];
77
96
  return lUserNames.map((pUserName, pIndex) => {
78
97
  const lBareName = getBareUserName(pUserName);
79
98
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-code-owners",
3
- "version": "8.1.0",
3
+ "version": "8.2.1",
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
- "ajv": "8.13.0",
30
- "yaml": "2.4.2"
29
+ "yaml": "2.5.0"
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
- };