Skip to content

Commit

Permalink
[kbn-code-owners] General improvements (elastic#204023)
Browse files Browse the repository at this point in the history
## 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
dolaru authored Dec 12, 2024
1 parent 5e69fd1 commit 0dabc52
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 159 deletions.
11 changes: 6 additions & 5 deletions packages/kbn-code-owners/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export type { PathWithOwners, CodeOwnership } from './src/file_code_owner';
export type { CodeOwnersEntry } from './src/code_owners';
export * as cli from './src/cli';
export {
getPathsWithOwnersReversed,
getCodeOwnersForFile,
runGetOwnersForFileCli,
} from './src/file_code_owner';
getCodeOwnersEntries,
findCodeOwnersEntryForPath,
getOwningTeamsForPath,
} from './src/code_owners';
55 changes: 55 additions & 0 deletions packages/kbn-code-owners/src/cli.ts
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`,
},
}
);
}
127 changes: 127 additions & 0 deletions packages/kbn-code-owners/src/code_owners.ts
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 || [];
}
106 changes: 0 additions & 106 deletions packages/kbn-code-owners/src/file_code_owner.ts

This file was deleted.

48 changes: 48 additions & 0 deletions packages/kbn-code-owners/src/path.ts
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);
}
}
Loading

0 comments on commit 0dabc52

Please sign in to comment.