Add script to check test file code ownership (#173411)

## Summary

This PR adds a script that determines GitHub code ownership for
functional test files in the Kibana repository.

### Why do we need this?

We want to be able to determine test ownership to allow teams to get a
better overview of their tests (number of tests, number of skipped
tests, number of failures in the last x days, etc).

### What does this PR bring?

This PR is a first step on closing the test ownership gaps. It adds
functionality to determine the GitHub code owner for a given file (in
the `@kbn/code-owners` package) and adds a script that makes use of this
to check if all functional test files have a code owner, reporting the
gaps.

### Future plans

The idea is to include the test ownership information in our ingested
test results, such that we can create dashboards, reports, etc based on
it.
At some point (once all ownership gaps are closed), we might consider
running this check on CI to prevent new test files without owners.

### How to run?

```
node scripts/check_ftr_code_owners.js
```
The script lists the functional test files that are not covered by code
owners and also gives a summary like this:
```
ERROR Found 2592 test files without code owner (checked 7550 test files in 12.73 s)
```
This commit is contained in:
Robert Oskamp 2023-12-18 17:41:39 +01:00 committed by GitHub
parent 164427463e
commit 6272d5af6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 214 additions and 4 deletions

1
.github/CODEOWNERS vendored
View file

@ -87,6 +87,7 @@ x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core
x-pack/plugins/cloud @elastic/kibana-core
x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
packages/shared-ux/code_editor @elastic/appex-sharedux
packages/kbn-code-owners @elastic/appex-qa
packages/kbn-coloring @elastic/kibana-visualizations
packages/kbn-config @elastic/kibana-core
packages/kbn-config-mocks @elastic/kibana-core

View file

@ -1176,6 +1176,7 @@
"@kbn/ci-stats-reporter": "link:packages/kbn-ci-stats-reporter",
"@kbn/ci-stats-shipper-cli": "link:packages/kbn-ci-stats-shipper-cli",
"@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode",
"@kbn/code-owners": "link:packages/kbn-code-owners",
"@kbn/core-analytics-browser-mocks": "link:packages/core/analytics/core-analytics-browser-mocks",
"@kbn/core-analytics-server-mocks": "link:packages/core/analytics/core-analytics-server-mocks",
"@kbn/core-application-browser-mocks": "link:packages/core/application/core-application-browser-mocks",
@ -1558,6 +1559,7 @@
"html": "1.0.0",
"html-loader": "^1.3.2",
"http-proxy": "^1.18.1",
"ignore": "^5.3.0",
"is-path-inside": "^3.0.2",
"jest": "^29.6.1",
"jest-axe": "^5.0.0",

View file

@ -0,0 +1,3 @@
# @kbn/code-owners
This package contains utility methods to determine GitHub code ownership for files in the repository.

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { PathWithOwners } from './src/file_code_owner';
export { getPathsWithOwnersReversed, getCodeOwnersForFile } from './src/file_code_owner';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-code-owners'],
};

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/code-owners",
"owner": "@elastic/appex-qa",
"devOnly": true
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/code-owners",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { REPO_ROOT } from '@kbn/repo-info';
import { createFailError } from '@kbn/dev-cli-errors';
import { join as joinPath } from 'path';
import { existsSync, readFileSync } from 'fs';
import type { Ignore } from 'ignore';
import ignore from 'ignore';
export interface PathWithOwners {
path: string;
teams: string;
ignorePattern: Ignore;
}
/**
* Get the .github/CODEOWNERS entries, prepared for path matching.
* The last matching CODEOWNERS entry has highest precedence:
* https://help.github.com/articles/about-codeowners/
* so entries are returned in reversed order to later search for the first match.
*/
export function getPathsWithOwnersReversed(): PathWithOwners[] {
const codeownersPath = joinPath(REPO_ROOT, '.github', 'CODEOWNERS');
if (existsSync(codeownersPath) === false) {
throw createFailError(`Unable to determine code owners: file ${codeownersPath} not found`);
}
const codeownersContent = readFileSync(codeownersPath, { encoding: 'utf8', flag: 'r' });
const codeownersLines = codeownersContent.split(/\r?\n/);
const codeowners = codeownersLines
.map((line) => line.trim())
.filter((line) => line && line[0] !== '#');
const pathsWithOwners: PathWithOwners[] = codeowners.map((c) => {
const [path, ...ghTeams] = c.split(/\s+/);
return {
path,
teams: ghTeams.map((t) => t.replace('@', '')).join(),
// register CODEOWNERS entries with the `ignores` lib for later path matching
ignorePattern: ignore().add([path]),
};
});
return pathsWithOwners.reverse();
}
/**
* Get the GitHub CODEOWNERS for a file in the repository
* @param filePath the file to get code owners for
* @param reversedCodeowners a cached reversed code owners list, use to speed up multiple requests
*/
export function getCodeOwnersForFile(
filePath: string,
reversedCodeowners?: PathWithOwners[]
): string | undefined {
const pathsWithOwners = reversedCodeowners ?? getPathsWithOwnersReversed();
const match = pathsWithOwners.find((p) => p.ignorePattern.test(filePath).ignored);
return match?.teams;
}

View file

@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/repo-info",
"@kbn/dev-cli-errors"
]
}

View file

@ -52,6 +52,8 @@ export { getUrl } from './src/jest/get_url';
export { runCheckJestConfigsCli } from './src/jest/run_check_jest_configs_cli';
export { runCheckFtrCodeOwnersCli } from './src/functional_test_runner/run_check_ftr_code_owners';
export { runJest } from './src/jest/run';
export * from './src/kbn_archiver_cli';

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { run } from '@kbn/dev-cli-runner';
import { createFailError } from '@kbn/dev-cli-errors';
import { getRepoFiles } from '@kbn/get-repo-files';
import { getCodeOwnersForFile, getPathsWithOwnersReversed } from '@kbn/code-owners';
const TEST_DIRECTORIES = ['test', 'x-pack/test', 'x-pack/test_serverless'];
const fmtMs = (ms: number) => {
if (ms < 1000) {
return `${Math.round(ms)} ms`;
}
return `${(Math.round(ms) / 1000).toFixed(2)} s`;
};
const fmtList = (list: Iterable<string>) => [...list].map((i) => ` - ${i}`).join('\n');
export async function runCheckFtrCodeOwnersCli() {
run(
async ({ log }) => {
const start = performance.now();
const missingOwners = new Set<string>();
// cache codeowners for quicker lookup
const reversedCodeowners = getPathsWithOwnersReversed();
const testFiles = await getRepoFiles(TEST_DIRECTORIES);
for (const { repoRel } of testFiles) {
const owners = getCodeOwnersForFile(repoRel, reversedCodeowners);
if (owners === undefined) {
missingOwners.add(repoRel);
}
}
const timeSpent = fmtMs(performance.now() - start);
if (missingOwners.size) {
log.error(
`The following test files do not have a GitHub code owner:\n${fmtList(missingOwners)}`
);
throw createFailError(
`Found ${missingOwners.size} test files without code owner (checked ${testFiles.length} test files in ${timeSpent})`
);
}
log.success(
`All test files have a code owner (checked ${testFiles.length} test files in ${timeSpent})`
);
},
{
description: 'Check that all test files are covered by GitHub CODEOWNERS',
}
);
}

View file

@ -33,5 +33,6 @@
"@kbn/repo-packages",
"@kbn/core-saved-objects-api-server",
"@kbn/mock-idp-plugin",
"@kbn/code-owners",
]
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
require('../src/setup_node_env');
require('@kbn/test').runCheckFtrCodeOwnersCli();

View file

@ -168,6 +168,8 @@
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
"@kbn/code-editor": ["packages/shared-ux/code_editor"],
"@kbn/code-editor/*": ["packages/shared-ux/code_editor/*"],
"@kbn/code-owners": ["packages/kbn-code-owners"],
"@kbn/code-owners/*": ["packages/kbn-code-owners/*"],
"@kbn/coloring": ["packages/kbn-coloring"],
"@kbn/coloring/*": ["packages/kbn-coloring/*"],
"@kbn/config": ["packages/kbn-config"],

View file

@ -3271,6 +3271,10 @@
version "0.0.0"
uid ""
"@kbn/code-owners@link:packages/kbn-code-owners":
version "0.0.0"
uid ""
"@kbn/coloring@link:packages/kbn-coloring":
version "0.0.0"
uid ""
@ -18734,10 +18738,10 @@ ignore@^4.0.3:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.0.5, ignore@^5.1.1, ignore@^5.1.4, ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
ignore@^5.0.5, ignore@^5.1.1, ignore@^5.1.4, ignore@^5.2.0, ignore@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
immediate@~3.0.5:
version "3.0.6"