Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kbn-code-owners] General improvements #204023

Merged
merged 3 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
54 changes: 54 additions & 0 deletions packages/kbn-code-owners/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 } 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);

const codeOwnersEntry = await 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`,
},
}
);
}
125 changes: 125 additions & 0 deletions packages/kbn-code-owners/src/code_owners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 { throwIfPathIsMissing } from './path';
const CODE_OWNERS_FILE = path.join(REPO_ROOT, '.github', 'CODEOWNERS');

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 file 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.
*/
export function findCodeOwnersEntryForPath(
searchPath: string,
codeOwnersEntries?: CodeOwnersEntry[]
): CodeOwnersEntry | undefined {
throwIfPathIsMissing(CODE_OWNERS_FILE, 'Code owners file');
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 file 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.
*/
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.

28 changes: 28 additions & 0 deletions packages/kbn-code-owners/src/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { createFailError } from '@kbn/dev-cli-errors';

/**
* 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);
}
21 changes: 6 additions & 15 deletions packages/kbn-scout-reporting/src/reporting/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { SCOUT_REPORT_OUTPUT_ROOT } from '@kbn/scout-info';
import stripANSI from 'strip-ansi';
import { REPO_ROOT } from '@kbn/repo-info';
import {
type PathWithOwners,
getPathsWithOwnersReversed,
getCodeOwnersForFile,
type CodeOwnersEntry,
getCodeOwnersEntries,
getOwningTeamsForPath,
} from '@kbn/code-owners';
import { generateTestRunId, getTestIDForTitle, ScoutReport, ScoutReportEventAction } from '.';
import { environmentMetadata } from '../datasources';
Expand All @@ -47,7 +47,7 @@ export class ScoutPlaywrightReporter implements Reporter {
readonly name: string;
readonly runId: string;
private report: ScoutReport;
private readonly pathsWithOwners: PathWithOwners[];
private codeOwnersEntries: CodeOwnersEntry[];

constructor(private reporterOptions: ScoutPlaywrightReporterOptions = {}) {
this.log = new ToolingLog({
Expand All @@ -60,20 +60,11 @@ export class ScoutPlaywrightReporter implements Reporter {
this.log.info(`Scout test run ID: ${this.runId}`);

this.report = new ScoutReport(this.log);
this.pathsWithOwners = getPathsWithOwnersReversed();
this.codeOwnersEntries = getCodeOwnersEntries();
}

private getFileOwners(filePath: string): string[] {
const concatenatedOwners = getCodeOwnersForFile(filePath, this.pathsWithOwners)?.teams;

if (concatenatedOwners === undefined) {
return [];
}

return concatenatedOwners
.replace(/#.+$/, '')
.split(',')
.filter((value) => value.length > 0);
return getOwningTeamsForPath(filePath, this.codeOwnersEntries);
}

/**
Expand Down
Loading