forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[kbn-code-owners] General improvements (elastic#204023)
## Summary The following improvements have been made: - Added `--json` flag to CLI command to output result as JSON - Terminology updated to more accurately reflect object contents - Code owner teams are always returned as an array - Search path validation (is under repo root, exists) - Proper handling of inline comments - Better logging for `scripts/check_ftr_code_owners.js` Existing usage of the `@kbn/code-owners` package has been updated accordingly, without modifying outcomes.
- Loading branch information
Showing
10 changed files
with
267 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
import { run } from '@kbn/dev-cli-runner'; | ||
import { findCodeOwnersEntryForPath } from './code_owners'; | ||
import { throwIfPathIsMissing, throwIfPathNotInRepo } from './path'; | ||
|
||
/** | ||
* CLI entrypoint for finding code owners for a given path. | ||
*/ | ||
export async function findCodeOwnersForPath() { | ||
await run( | ||
async ({ flagsReader, log }) => { | ||
const targetPath = flagsReader.requiredPath('path'); | ||
throwIfPathIsMissing(targetPath, 'Target path', true); | ||
throwIfPathNotInRepo(targetPath, true); | ||
|
||
const codeOwnersEntry = findCodeOwnersEntryForPath(targetPath); | ||
|
||
if (!codeOwnersEntry) { | ||
log.warning(`No matching code owners entry found for path ${targetPath}`); | ||
return; | ||
} | ||
|
||
if (flagsReader.boolean('json')) { | ||
// Replacer function that hides irrelevant fields in JSON output | ||
const hideIrrelevantFields = (k: string, v: any) => { | ||
return ['matcher'].includes(k) ? undefined : v; | ||
}; | ||
|
||
log.write(JSON.stringify(codeOwnersEntry, hideIrrelevantFields, 2)); | ||
return; | ||
} | ||
|
||
log.write(`Matching pattern: ${codeOwnersEntry.pattern}`); | ||
log.write('Teams:', codeOwnersEntry.teams); | ||
}, | ||
{ | ||
description: `Find code owners for a given path in this local Kibana repository`, | ||
flags: { | ||
string: ['path'], | ||
boolean: ['json'], | ||
help: ` | ||
--path Path to find owners for (required) | ||
--json Output result as JSON`, | ||
}, | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
import { REPO_ROOT } from '@kbn/repo-info'; | ||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
|
||
import ignore, { Ignore } from 'ignore'; | ||
import { CODE_OWNERS_FILE, throwIfPathIsMissing, throwIfPathNotInRepo } from './path'; | ||
|
||
export interface CodeOwnersEntry { | ||
pattern: string; | ||
matcher: Ignore; | ||
teams: string[]; | ||
comment?: string; | ||
} | ||
|
||
/** | ||
* Generator function that yields lines from the CODEOWNERS file | ||
*/ | ||
export function* getCodeOwnersLines(): Generator<string> { | ||
const codeOwnerLines = fs | ||
.readFileSync(CODE_OWNERS_FILE, { encoding: 'utf8', flag: 'r' }) | ||
.split(/\r?\n/); | ||
|
||
for (const line of codeOwnerLines) { | ||
// Empty line | ||
if (line.length === 0) continue; | ||
|
||
// Comment | ||
if (line.startsWith('#')) continue; | ||
|
||
// Assignment override on backport branches to avoid review requests | ||
if (line.includes('@kibanamachine')) continue; | ||
|
||
yield line.trim(); | ||
} | ||
} | ||
|
||
/** | ||
* Get all code owner entries from the CODEOWNERS file | ||
* | ||
* Entries are ordered in reverse relative to how they're defined in the CODEOWNERS file | ||
* as patterns defined lower in the CODEOWNERS file can override earlier entries. | ||
*/ | ||
export function getCodeOwnersEntries(): CodeOwnersEntry[] { | ||
const entries: CodeOwnersEntry[] = []; | ||
|
||
for (const line of getCodeOwnersLines()) { | ||
const comment = line | ||
.match(/#(.+)$/) | ||
?.at(1) | ||
?.trim(); | ||
|
||
const [rawPathPattern, ...rawTeams] = line | ||
.replace(/#.+$/, '') // drop comment | ||
.split(/\s+/); | ||
|
||
const pathPattern = rawPathPattern.replace(/\/$/, ''); | ||
|
||
entries.push({ | ||
pattern: pathPattern, | ||
teams: rawTeams.map((t) => t.replace('@', '')).filter((t) => t.length > 0), | ||
comment, | ||
|
||
// Register code owner entry with the `ignores` lib for easy pattern matching later on | ||
matcher: ignore().add(pathPattern), | ||
}); | ||
} | ||
|
||
// Reverse entry order as patterns defined lower in the CODEOWNERS file can override earlier entries | ||
entries.reverse(); | ||
|
||
return entries; | ||
} | ||
|
||
/** | ||
* Get a list of matching code owners for a given path | ||
* | ||
* Tip: | ||
* If you're making a lot of calls to this function, fetch the code owner paths once using | ||
* `getCodeOwnersEntries` and pass it in the `getCodeOwnersEntries` parameter to speed up your queries.. | ||
* | ||
* @param searchPath The path to find code owners for | ||
* @param codeOwnersEntries Pre-defined list of code owner paths to search in | ||
* | ||
* @returns Code owners entry if a match is found. | ||
* @throws Error if `searchPath` does not exist or is not part of this repository | ||
*/ | ||
export function findCodeOwnersEntryForPath( | ||
searchPath: string, | ||
codeOwnersEntries?: CodeOwnersEntry[] | ||
): CodeOwnersEntry | undefined { | ||
throwIfPathIsMissing(CODE_OWNERS_FILE, 'Code owners file'); | ||
throwIfPathNotInRepo(searchPath); | ||
const searchPathRelativeToRepo = path.relative(REPO_ROOT, searchPath); | ||
|
||
return (codeOwnersEntries || getCodeOwnersEntries()).find( | ||
(p) => p.matcher.test(searchPathRelativeToRepo).ignored | ||
); | ||
} | ||
|
||
/** | ||
* Get a list of matching code owners for a given path | ||
* | ||
* Tip: | ||
* If you're making a lot of calls to this function, fetch the code owner paths once using | ||
* `getCodeOwnersEntries` and pass it in the `getCodeOwnersEntries` parameter to speed up your queries. | ||
* | ||
* @param searchPath The path to find code owners for | ||
* @param codeOwnersEntries Pre-defined list of code owner entries | ||
* | ||
* @returns List of code owners for the given path. Empty list if no matching entry is found. | ||
* @throws Error if `searchPath` does not exist or is not part of this repository | ||
*/ | ||
export function getOwningTeamsForPath( | ||
searchPath: string, | ||
codeOwnersEntries?: CodeOwnersEntry[] | ||
): string[] { | ||
return findCodeOwnersEntryForPath(searchPath, codeOwnersEntries)?.teams || []; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import { createFailError } from '@kbn/dev-cli-errors'; | ||
import { REPO_ROOT } from '@kbn/repo-info'; | ||
|
||
/** CODEOWNERS file path **/ | ||
export const CODE_OWNERS_FILE = path.join(REPO_ROOT, '.github', 'CODEOWNERS'); | ||
|
||
/** | ||
* Throw an error if the given path does not exist | ||
* | ||
* @param targetPath Path to check | ||
* @param description Path description used in the error message if an exception is thrown | ||
* @param cli Whether this function is called from a CLI context | ||
*/ | ||
export function throwIfPathIsMissing( | ||
targetPath: fs.PathLike, | ||
description = 'File', | ||
cli: boolean = false | ||
) { | ||
if (fs.existsSync(targetPath)) return; | ||
const msg = `${description} ${targetPath} does not exist`; | ||
throw cli ? createFailError(msg) : new Error(msg); | ||
} | ||
|
||
/** | ||
* Throw an error if the given path does not reside in this repo | ||
* | ||
* @param targetPath Path to check | ||
* @param cli Whether this function is called from a CLI context | ||
*/ | ||
export function throwIfPathNotInRepo(targetPath: fs.PathLike, cli: boolean = false) { | ||
const relativePath = path.relative(REPO_ROOT, targetPath.toString()); | ||
|
||
if (relativePath.includes('../')) { | ||
const msg = `Path ${targetPath} is not part of this repository.`; | ||
throw cli ? createFailError(msg) : new Error(msg); | ||
} | ||
} |
Oops, something went wrong.