Skip to content

Commit 65f5c04

Browse files
gkalpakmatsko
authored andcommitted
ci: check code-ownership on CI (angular#32577)
This commit expands the `lint` CircleCI job to also run the `tools/verify-codeownership.js` script. This script verifies that some important files/directories in the codebase have code-owners assigned in `.github/CODEOWNERS`. The main purpose of this change is to prevent adding new directories (e.g. packages or docs guides/examples) without assigning appropriate code-owners. When no code-owner is explicitly assigned, corresponding PRs will automatically request reviews from @IgorMinar, who is the "fall-back" code-owner. PR Close angular#32577
1 parent d0dd69f commit 65f5c04

File tree

2 files changed

+54
-36
lines changed

2 files changed

+54
-36
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ jobs:
234234
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
235235

236236
- run: yarn gulp lint
237+
- run: node tools/verify-codeownership
237238

238239
test:
239240
<<: *job_defaults

aio/scripts/verify-codeownership.js renamed to tools/verify-codeownership.js

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
#!/usr/bin/env node
2-
'use strict';
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
38

49
/**
510
* **Usage:**
611
* ```
7-
* node aio/scripts/verify-codeownership
12+
* node tools/verify-codeownership
813
* ```
914
*
10-
* Verify whether there are directories in the codebase that don't have a codeowner (in `.github/CODEOWNERS`) and vice
11-
* versa (that there are no patterns in `CODEOWNERS` that do not correspond to actual directories).
15+
* Verify whether there are directories in the codebase that don't have a codeowner (in
16+
* `.github/CODEOWNERS`) and vice versa (that there are no patterns in `CODEOWNERS` that do not
17+
* correspond to actual directories).
1218
*
13-
* The script does not aim to be exhaustive and highly accurate, checking all files and directories (since that would be
14-
* too complicated). Instead, it does a coarse check on some important (or frequently changing) directories.
19+
* The script does not aim to be exhaustive and highly accurate, checking all files and directories
20+
* (since that would be too complicated). Instead, it does a coarse check on some important (or
21+
* frequently changing) directories.
1522
*
1623
* Currently, it checks the following:
1724
* - **Packages**: Top-level directories in `packages/`.
@@ -20,13 +27,15 @@
2027
* - **Guide images**: Top-level directories in `aio/content/images/guide/`.
2128
* - **Guide examples**: Top-level directories in `aio/content/examples/`.
2229
*/
30+
'use strict';
2331

2432
// Imports
33+
const chalk = require('chalk');
2534
const fs = require('fs');
2635
const path = require('path');
2736

2837
// Constants
29-
const PROJECT_ROOT_DIR = path.resolve(__dirname, '../..');
38+
const PROJECT_ROOT_DIR = path.resolve(__dirname, '..');
3039
const CODEOWNERS_PATH = path.resolve(PROJECT_ROOT_DIR, '.github/CODEOWNERS');
3140
const PKG_DIR = path.resolve(PROJECT_ROOT_DIR, 'packages');
3241
const PKG_EXAMPLES_DIR = path.resolve(PKG_DIR, 'examples');
@@ -45,7 +54,11 @@ _main();
4554
// Functions - Definitions
4655
function _main() {
4756
const {packages: pkgPackagePaths, examples: pkgExamplePaths} = getPathsFromPkg();
48-
const {guides: aioGuidePaths, images: aioGuideImagesPaths, examples: aioExamplePaths} = getPathsFromAioContent();
57+
const {
58+
guides: aioGuidePaths,
59+
images: aioGuideImagesPaths,
60+
examples: aioExamplePaths,
61+
} = getPathsFromAioContent();
4962
const {
5063
pkgPackages: coPkgPackagePaths,
5164
pkgExamples: coPkgExamplePaths,
@@ -60,7 +73,8 @@ function _main() {
6073
const aioImagesDiff = arrayDiff(aioGuideImagesPaths, coAioGuideImagesPaths);
6174
const aioExamplesDiff = arrayDiff(aioExamplePaths, coAioExamplePaths);
6275
const hasDiff = (pkgPackagesDiff.diffCount > 0) || (pkgExamplesDiff.diffCount > 0) ||
63-
(aioGuidesDiff.diffCount > 0) || (aioImagesDiff.diffCount> 0) || (aioExamplesDiff.diffCount > 0);
76+
(aioGuidesDiff.diffCount > 0) || (aioImagesDiff.diffCount > 0) ||
77+
(aioExamplesDiff.diffCount > 0);
6478

6579
if (hasDiff) {
6680
const expectedPkgPackagesSrc = path.relative(PROJECT_ROOT_DIR, PKG_DIR);
@@ -75,6 +89,16 @@ function _main() {
7589
reportDiff(aioGuidesDiff, expectedAioGuidesSrc, actualSrc);
7690
reportDiff(aioImagesDiff, expectedAioImagesSrc, actualSrc);
7791
reportDiff(aioExamplesDiff, expectedAioExamplesSrc, actualSrc);
92+
93+
// tslint:disable-next-line: no-console
94+
console.log(chalk.red(
95+
'\nCode-ownership verification failed.\n' +
96+
'Please update \'.github/CODEOWNERS\' to ensure that all necessary files/directories ' +
97+
'have code-owners and all patterns that appear in the file correspond to actual ' +
98+
'files/directories in the repo.'));
99+
} else {
100+
// tslint:disable-next-line: no-console
101+
console.log(chalk.green('\nCode-ownership verification succeeded!'));
78102
}
79103

80104
process.exit(hasDiff ? 1 : 0);
@@ -88,16 +112,16 @@ function arrayDiff(expected, actual) {
88112
}
89113

90114
function findDirectories(parentDir) {
91-
return fs.readdirSync(parentDir).
92-
filter(name => fs.statSync(`${parentDir}/${name}`).isDirectory());
115+
return fs.readdirSync(parentDir).filter(
116+
name => fs.statSync(`${parentDir}/${name}`).isDirectory());
93117
}
94118

95119
function getPathsFromAioContent() {
96120
return {
97121
guides: fs.readdirSync(AIO_GUIDES_DIR),
98122
images: fs.readdirSync(AIO_GUIDE_IMAGES_DIR),
99-
examples: fs.readdirSync(AIO_GUIDE_EXAMPLES_DIR).
100-
filter(name => fs.statSync(`${AIO_GUIDE_EXAMPLES_DIR}/${name}`).isDirectory()),
123+
examples: fs.readdirSync(AIO_GUIDE_EXAMPLES_DIR)
124+
.filter(name => fs.statSync(`${AIO_GUIDE_EXAMPLES_DIR}/${name}`).isDirectory()),
101125
};
102126
}
103127

@@ -106,7 +130,8 @@ function getPathsFromCodeowners() {
106130
const pkgExamplesPathRe = /^\/packages\/examples\/([^\s\*/]+)/;
107131
// Use capturing groups for `images/` and `examples` to be able to differentiate between the
108132
// different kinds of matches (guide, image, example) later (see `isImage`/`isExample` below).
109-
const aioGuidesImagesExamplesPathRe = /^\/aio\/content\/(?:(images\/)?guide|(examples))\/([^\s\*/]+)/;
133+
const aioGuidesImagesExamplesPathRe =
134+
/^\/aio\/content\/(?:(images\/)?guide|(examples))\/([^\s\*/]+)/;
110135
const manualGlobExpansions = {
111136
// `CODEOWNERS` has a glob to match all `testing/` directories, so no specific glob for
112137
// `packages/examples/testing/` is necessary.
@@ -120,10 +145,7 @@ function getPathsFromCodeowners() {
120145
const aioExamples = [];
121146

122147
// Read `CODEOWNERS` and split into lines.
123-
const lines = fs.
124-
readFileSync(CODEOWNERS_PATH, 'utf8').
125-
split('\n').
126-
map(l => l.trim());
148+
const lines = fs.readFileSync(CODEOWNERS_PATH, 'utf8').split('\n').map(l => l.trim());
127149

128150
// Manually expand globs to known matching patterns.
129151
for (const [glob, expansions] of Object.entries(manualGlobExpansions)) {
@@ -134,27 +156,22 @@ function getPathsFromCodeowners() {
134156
}
135157

136158
// Collect packages (`packages/`).
137-
lines.
138-
map(l => l.match(pkgPackagesPathRe)).
139-
filter(m => m).
140-
forEach(([, path]) => pkgPackages.push(path));
159+
lines.map(l => l.match(pkgPackagesPathRe)).filter(m => m).forEach(([
160+
, path
161+
]) => pkgPackages.push(path));
141162

142163
// Collect API docs examples (`packages/examples/`).
143-
lines.
144-
map(l => l.match(pkgExamplesPathRe)).
145-
filter(m => m).
146-
forEach(([, path]) => pkgExamples.push(path));
164+
lines.map(l => l.match(pkgExamplesPathRe)).filter(m => m).forEach(([
165+
, path
166+
]) => pkgExamples.push(path));
147167

148168
// Collect `aio/` guides/images/examples.
149-
lines.
150-
map(l => l.match(aioGuidesImagesExamplesPathRe)).
151-
filter(m => m).
152-
forEach(([, isImage, isExample, path]) => {
153-
const list = isExample ? aioExamples :
154-
isImage ? aioImages :
155-
aioGuides;
156-
list.push(path);
157-
});
169+
lines.map(l => l.match(aioGuidesImagesExamplesPathRe))
170+
.filter(m => m)
171+
.forEach(([, isImage, isExample, path]) => {
172+
const list = isExample ? aioExamples : isImage ? aioImages : aioGuides;
173+
list.push(path);
174+
});
158175

159176
return {pkgPackages, pkgExamples, aioGuides, aioImages, aioExamples};
160177
}

0 commit comments

Comments
 (0)