Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.

Commit 8631512

Browse files
authored
providing some validation on codeowners to help detect invalid rules (#29)
1 parent a1de07f commit 8631512

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Things it does:
1212
* Output ownership information of staged files
1313
* Outputs lots of lovely stats
1414
* Outputs handy formats for integrations (CSV and JSONL)
15+
* Validates that the provided owners are in the correct format for github
1516

1617
## Installation
1718
Install via npm globally then run

src/lib/OwnershipEngine.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import * as fs from 'fs';
2+
13
import { OwnershipEngine } from './OwnershipEngine';
24
import { FileOwnershipMatcher } from './types';
35

6+
jest.mock('fs');
7+
const readFileSyncMock = fs.readFileSync as jest.Mock;
8+
49
describe('OwnershipEngine', () => {
10+
afterEach(() => {
11+
jest.resetAllMocks();
12+
});
13+
514
describe('calcFileOwnership', () => {
615
const createFileOwnershipMatcher = (path: string, owners: string[]): FileOwnershipMatcher => {
716
return {
@@ -50,4 +59,81 @@ describe('OwnershipEngine', () => {
5059
expect(result).not.toContainEqual(unexpectedOwner);
5160
});
5261
});
62+
63+
describe('FromCodeownersFile', () => {
64+
it('should not throw when provided valid owners', () => {
65+
// Arrange
66+
const codeowners = 'some/path @global-owner1 @org/octocat docs@example.com';
67+
68+
readFileSyncMock.mockReturnValue(Buffer.from(codeowners));
69+
70+
// Assert
71+
expect(() => OwnershipEngine.FromCodeownersFile('some/codeowners/file')).not.toThrow();
72+
});
73+
74+
it('should throw when provided an invalid owner', () => {
75+
// Arrange
76+
const rulePath = 'some/path';
77+
const owner = '.not@valid-owner';
78+
79+
const expectedError = new Error(`${owner} is not a valid owner name in rule ${rulePath} ${owner}`);
80+
81+
const codeowners = `${rulePath} ${owner}`;
82+
83+
readFileSyncMock.mockReturnValue(Buffer.from(codeowners));
84+
85+
// Assert
86+
expect(() => OwnershipEngine.FromCodeownersFile('some/codeowners/file'))
87+
.toThrowError(expectedError);
88+
});
89+
90+
it('should throw when provided an invalid github user as an owner', () => {
91+
// Arrange
92+
const rulePath = 'some/path';
93+
const owner = 'invalid-owner';
94+
95+
const expectedError = new Error(`${owner} is not a valid owner name in rule ${rulePath} ${owner}`);
96+
97+
const codeowners = `${rulePath} ${owner}`;
98+
99+
readFileSyncMock.mockReturnValue(Buffer.from(codeowners));
100+
101+
// Assert
102+
expect(() => OwnershipEngine.FromCodeownersFile('some/codeowners/file'))
103+
.toThrowError(expectedError);
104+
});
105+
106+
it('should throw when provided an invalid email address as an owner', () => {
107+
// Arrange
108+
const rulePath = 'some/path';
109+
const owner = 'invalid-owner@nowhere';
110+
111+
const expectedError = new Error(`${owner} is not a valid owner name in rule ${rulePath} ${owner}`);
112+
113+
const codeowners = `${rulePath} ${owner}`;
114+
115+
readFileSyncMock.mockReturnValue(Buffer.from(codeowners));
116+
117+
// Assert
118+
expect(() => OwnershipEngine.FromCodeownersFile('some/codeowners/file'))
119+
.toThrowError(expectedError);
120+
});
121+
122+
it('should throw when provided at least one invalid owner', () => {
123+
// Arrange
124+
const rulePath = 'some/path';
125+
const valid = 'valid@owner.com';
126+
const owner = '@invalid-owner*';
127+
128+
const expectedError = new Error(`${owner} is not a valid owner name in rule ${rulePath} ${valid} ${owner}`);
129+
130+
const codeowners = `${rulePath} ${valid} ${owner}`;
131+
132+
readFileSyncMock.mockReturnValue(Buffer.from(codeowners));
133+
134+
// Assert
135+
expect(() => OwnershipEngine.FromCodeownersFile('some/codeowners/file'))
136+
.toThrowError(expectedError);
137+
});
138+
});
53139
});

src/lib/OwnershipEngine.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,15 @@ const createMatcherCodeownersRule = (rule: string) => {
6161
// Remaining parts are expected to be team names (if any)
6262
if (parts.length > 1) {
6363
teamNames = parts.slice(1, parts.length);
64+
for(const name of teamNames){
65+
if(!codeOwnerRegex.test(name)){
66+
throw new Error(`${name} is not a valid owner name in rule ${rule}`);
67+
}
68+
}
6469
}
6570

6671
// Create an `ignore` matcher to ape github behaviour
67-
const match : any = ignore().add(path);
72+
const match: any = ignore().add(path);
6873

6974
// Return our complete matcher
7075
return {
@@ -73,3 +78,6 @@ const createMatcherCodeownersRule = (rule: string) => {
7378
match: match.ignores.bind(match),
7479
};
7580
};
81+
82+
// ensures that only the following patterns are allowed @octocat @octocat/kitty docs@example.com
83+
const codeOwnerRegex = /(^@[a-zA-Z0-9_\-/]*$)|(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;

0 commit comments

Comments
 (0)