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

Commit 46f5cf7

Browse files
authored
add validate command (#31)
* adding a validate command to help validate CODEOWNERS file * fixing vuln in dev dependency
1 parent 355446d commit 46f5cf7

17 files changed

Lines changed: 277 additions & 29 deletions

README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
A CLI tool for working with GitHub CODEOWNERS.
77

88
Things it does:
9-
* Output who owns each file in your repo (ignoring files listed in `.gitignore`)
10-
* Output who owns a single file
11-
* Output ownership information at a specific git commit
12-
* Output ownership information of staged files
13-
* Outputs lots of lovely stats
14-
* Outputs handy formats for integrations (CSV and JSONL)
15-
* Validates that the provided owners are in the correct format for github
9+
* Calculate ownership stats
10+
* Find out who owns each and every file (ignoring files listed in `.gitignore`)
11+
* Find out who owns a single file
12+
* Find out who owns your staged files
13+
* Outputs in a bunch of script friendly handy formats for integrations (CSV and JSONL)
14+
* Validates that your CODEOWNERS file is valid
1615

1716
## Installation
1817
Install via npm globally then run
@@ -82,7 +81,7 @@ Usage: github-codeowners audit [options]
8281
list the owners for all files
8382

8483
Options:
85-
-d, --dir <dirPath> path to VCS directory (default: "/Users/jjmschofield/projects/github/github-codeowners")
84+
-d, --dir <dirPath> path to VCS directory (default: "<current working directory>")
8685
-c, --codeowners <filePath> path to codeowners file (default: "<dir>/.github/CODEOWNERS")
8786
-o, --output <outputFormat> how to output format eg: simple, jsonl, csv (default: "simple")
8887
-u, --unloved unowned files only (default: false)
@@ -152,6 +151,30 @@ Options:
152151
-h, --help output usage information
153152
```
154153

154+
### Validate
155+
Validates your CODEOWNERS file to find common mistakes, will throw on errors (such as malformed owners).
156+
```shell script
157+
$ cd <your awesome project>
158+
$ github-codeowners validate
159+
Found duplicate rules [ 'some/duplicate/rule @octocat' ]
160+
Found rules which did not match any files [ 'some/non-existent/path @octocat' ]
161+
...
162+
```
163+
164+
Full usage information:
165+
```shell script
166+
$ github-codeowners validate --help
167+
Usage: github-codeowners validate [options]
168+
169+
Validates a CODOWNER file and files in dir
170+
171+
Options:
172+
-d, --dir <dirPath> path to VCS directory (default: "<current working directory>")
173+
-c, --codeowners <filePath> path to codeowners file (default: "<dir>/.github/CODEOWNERS")
174+
-r, --root <rootPath> the root path to filter files by (default: "")
175+
-h, --help output usage information
176+
```
177+
155178
## Output Formats
156179
Check `github-codeowners <command> --help` for support for a given command, however generally the following outputs are supported:
157180
* `simple` - tab delimited - terminal friendly output

package-lock.json

Lines changed: 25 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"jest-junit": "^11.0.1",
3838
"ts-jest": "^26.1.3",
3939
"ts-node": "^8.6.2",
40-
"tslint": "^6.0.0",
40+
"tslint": "^6.1.3",
4141
"tslint-config-airbnb-base": "^0.3.0",
4242
"typescript": "^3.8.2",
4343
"uuid": "^8.2.0"

src/cli.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { git } from './commands/git';
99
import { log } from './lib/logger';
1010

1111
import { OUTPUT_FORMAT } from './lib/writers/types';
12+
import { validate } from './commands/validate';
1213

1314
commander.command('audit')
1415
.description('list the owners for all files')
@@ -35,6 +36,29 @@ commander.command('audit')
3536
}
3637
});
3738

39+
commander.command('validate')
40+
.description('Validates a CODOWNER file and files in dir')
41+
.option('-d, --dir <dirPath>', 'path to VCS directory', process.cwd())
42+
.option('-c, --codeowners <filePath>', 'path to codeowners file (default: "<dir>/.github/CODEOWNERS")')
43+
.option('-r, --root <rootPath>', 'the root path to filter files by', '')
44+
.action(async (options) => {
45+
try {
46+
if (!options.codeowners) {
47+
options.codeowners = path.resolve(options.dir, '.github/CODEOWNERS');
48+
}
49+
50+
if (options.root) {
51+
options.dir = path.resolve(options.dir, options.root);
52+
}
53+
54+
await validate(options);
55+
} catch (error) {
56+
log.error('failed to run validate command', error);
57+
process.exit(1);
58+
}
59+
});
60+
61+
3862
commander.command('who <file>')
3963
.description('lists owners of a specific file')
4064
.option('-d, --dir <dirPath>', 'path to VCS directory', process.cwd())
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"files" : [
3+
{ "path": "valid.js" },
4+
{ "path": "duplicate-rule.js" }
5+
]
6+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import path from 'path';
2+
3+
import { FixturePaths } from '../types';
4+
5+
const paths: FixturePaths = {
6+
files: path.resolve(__dirname, 'files.json'),
7+
codeowners: path.resolve(__dirname, 'owners'),
8+
gitignores: [],
9+
};
10+
11+
export const invalidOwnerFixtures: FixturePaths = {
12+
...paths,
13+
codeowners: path.resolve(__dirname, 'owners-invalid-format'),
14+
};
15+
16+
export default paths;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
valid.js @some-owner
2+
duplicate-rule.js @some-owner
3+
file-does-not-exist.md @some-owner
4+
duplicate-rule.js @some-owner
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file-exists-duplicate-rule.md @valid-owner not-a-valid-owner
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`validate when all owners are valid output the result of all validation checks: stderr 1`] = `
4+
"Found duplicate rules [ 'duplicate-rule.js @some-owner' ]
5+
Found rules which did not match any files [ 'file-does-not-exist.md @some-owner' ]
6+
"
7+
`;
8+
9+
exports[`validate when all owners are valid output the result of all validation checks: stdout 1`] = `""`;

src/commands/validate.test.int.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { v4 as uuidv4 } from 'uuid';
2+
import fixtures, { invalidOwnerFixtures } from './__fixtures__/validate';
3+
import { generateProject } from './__fixtures__/project-builder.test.helper';
4+
5+
import util from 'util';
6+
7+
const exec = util.promisify(require('child_process').exec);
8+
9+
describe('validate', () => {
10+
describe('when all owners are valid', () => {
11+
const testId = uuidv4();
12+
13+
let testDir = 'not set';
14+
15+
beforeAll(async () => {
16+
testDir = await generateProject(testId, fixtures);
17+
// tslint:disable-next-line:no-console
18+
console.log(`test scratch dir: ${testDir}`);
19+
});
20+
21+
const runCli = async (args: string) => {
22+
return exec(`node ../../../dist/cli.js ${args}`, { cwd: testDir });
23+
};
24+
25+
it('output the result of all validation checks', async () => {
26+
const { stdout, stderr } = await runCli('validate');
27+
expect(stdout).toMatchSnapshot('stdout');
28+
expect(stderr).toMatchSnapshot('stderr');
29+
});
30+
});
31+
32+
33+
describe('when owners are invalid', () => {
34+
const testId = uuidv4();
35+
36+
let testDir = 'not set';
37+
38+
beforeAll(async () => {
39+
testDir = await generateProject(testId, invalidOwnerFixtures);
40+
// tslint:disable-next-line:no-console
41+
console.log(`test scratch dir: ${testDir}`);
42+
});
43+
44+
const runCli = async (args: string) => {
45+
return exec(`node ../../../dist/cli.js ${args}`, { cwd: testDir });
46+
};
47+
48+
it('should throw on invalid users', async () => {
49+
await expect(() => runCli('validate')).rejects.toThrow();
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)