Dependency Ownership CLI (#201773)

## Summary

1. Show all packages owned by a specific team
```
node scripts/dependency_ownership -o <owner>
```
2. Identify owners of a specific dependency
```
node scripts/dependency_ownership -d <dependency>
```

3. List dependencies without an owner
```
node scripts/dependency_ownership --missing-owner
```

4. Generate a full dependency ownership report
```
node scripts/dependency_ownership
```

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)


__Closes: https://github.com/elastic/kibana/issues/196767__

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Elena Shostak 2024-11-29 17:18:36 +01:00 committed by GitHub
parent 8084bd640e
commit 976b94ffa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 960 additions and 0 deletions

View file

@ -2027,6 +2027,13 @@ module.exports = {
'@kbn/imports/uniform_imports': 'off',
},
},
{
files: ['packages/kbn-dependency-ownership/**/*.{ts,tsx}'],
rules: {
// disabling it since package is a CLI tool
'no-console': 'off',
},
},
],
};

1
.github/CODEOWNERS vendored
View file

@ -329,6 +329,7 @@ packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-di
packages/kbn-data-stream-adapter @elastic/security-threat-hunting
packages/kbn-data-view-utils @elastic/kibana-data-discovery
packages/kbn-datemath @elastic/kibana-data-discovery
packages/kbn-dependency-ownership @elastic/kibana-security
packages/kbn-dependency-usage @elastic/kibana-security
packages/kbn-dev-cli-errors @elastic/kibana-operations
packages/kbn-dev-cli-runner @elastic/kibana-operations

View file

@ -1430,6 +1430,7 @@
"@kbn/core-ui-settings-server-mocks": "link:packages/core/ui-settings/core-ui-settings-server-mocks",
"@kbn/core-usage-data-server-mocks": "link:packages/core/usage-data/core-usage-data-server-mocks",
"@kbn/cypress-config": "link:packages/kbn-cypress-config",
"@kbn/dependency-ownership": "link:packages/kbn-dependency-ownership",
"@kbn/dependency-usage": "link:packages/kbn-dependency-usage",
"@kbn/dev-cli-errors": "link:packages/kbn-dev-cli-errors",
"@kbn/dev-cli-runner": "link:packages/kbn-dev-cli-runner",

View file

@ -0,0 +1,166 @@
# @kbn/dependency-ownership
A CLI tool for analyzing package ownership.
---
## Table of Contents
1. [Show all packages owned by a specific team](#show-all-packages-owned-by-a-specific-team)
2. [Show who owns specific dependency](#show-who-owns-specific-dependency)
3. [List all dependencies with without owner](#list-all-dependencies-with-without-owner)
4. [Generate dependency ownership report](#generate-dependency-ownership-report)
---
### 1. Show all packages owned by a specific team
Use this command to list all packages or plugins within a directory that use a specified dependency.
```sh
node scripts/dependency_ownership -o <owner>
```
or
```sh
node scripts/dependency_ownership --owner <owner>
```
**Example**:
```sh
node scripts/dependency_ownership -o @elastic/kibana-core
```
- `-o @elastic/kibana-core`: Specifies the team.
**Output**: Lists dev and prod dependencies.
```json
{
"prodDependencies": [
"<dependency_1>",
"<dependency_2>",
"<dependency_3>",
//...
],
"devDependencies": [
"<dependency_1>",
"<dependency_2>",
//...
]
}
```
---
### 2. Show who owns specific dependency
Get the owner for a specific dependency.
```sh
node scripts/dependency_ownership -d <dependency>
```
or
```sh
node scripts/dependency_ownership --dependency <dependency>
```
**Example**:
```sh
node scripts/dependency_ownership -d rxjs
```
- `-d rxjs`: Specifies the dependency.
**Output**: Lists owners for `rxjs`.
```json
[
"@elastic/kibana-core"
]
```
---
### 3. List all dependencies with without owner
To display all dependencies that do not have owner defined.
```sh
node scripts/dependency_ownership --missing-owner
```
**Example**:
```sh
node scripts/dependency_ownership --missing-owner
```
**Output**: Lists all dev and prod dependencies without owner.
```json
{
"prodDependencies": [
"<dependency_1>",
"<dependency_2>",
//...
],
"devDependencies": [
"<dependency_1>",
"<dependency_2>",
//...
]
}
```
---
### 4. Generate dependency ownership report
Generates a comprehensive report with all dependencies with and without owner.
```sh
node scripts/dependency_ownership --missing-owner
```
**Example**:
```sh
node scripts/dependency_ownership --missing-owner
```
**Output**: Lists all covered dev and prod dependencies, uncovered dev and prod dependencies, dependencies aggregated by owner.
```json
{
"coveredProdDependencies": [ // Prod dependencies with owner
"<dependency_1>",
"<dependency_2>",
//...
],
"coveredDevDependencies": [ // Dev dependencies with owner
"<dependency_1>",
"<dependency_2>",
//...
],
"uncoveredProdDependencies": [ // Prod dependencies without owner
"<dependency_1>",
"<dependency_2>",
//...
],
"uncoveredDevDependencies": [ // Dev dependencies without owner
"<dependency_1>",
"<dependency_2>",
//...
],
"prodDependenciesByOwner": { // Prod dependencies aggregated by owner
"@elastic/team_1": ["<dependency_1>"],
"@elastic/team_2": ["<dependency_1>"],
},
"devDependenciesByOwner": { // Dev dependencies aggregated by owner
"@elastic/team_1": ["<dependency_1>"],
"@elastic/team_2": ["<dependency_1>"],
},
}
```
---
For further information on additional flags and options, refer to the script's help command.

View file

@ -0,0 +1,18 @@
#!/usr/bin/env node
/*
* 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".
*/
// eslint-disable-next-line import/no-extraneous-dependencies
require('@babel/register')({
extensions: ['.ts', '.js'],
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
});
require('../src/cli');

View file

@ -0,0 +1,14 @@
/*
* 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".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-dependency-ownership'],
};

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/dependency-ownership",
"owner": "@elastic/kibana-security",
"devOnly": true
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/dependency-ownership",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,119 @@
/*
* 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 { configureYargs } from './cli';
import { identifyDependencyOwnership } from './dependency_ownership';
jest.mock('chalk', () => ({
green: jest.fn((str) => str),
yellow: jest.fn((str) => str),
cyan: jest.fn((str) => str),
magenta: jest.fn((str) => str),
blue: jest.fn((str) => str),
bold: { magenta: jest.fn((str) => str), blue: jest.fn((str) => str) },
}));
jest.mock('./dependency_ownership', () => ({
identifyDependencyOwnership: jest.fn(),
}));
jest.mock('./cli', () => ({
...jest.requireActual('./cli'),
runCLI: jest.fn(),
}));
describe('dependency-ownership CLI', () => {
const parser = configureYargs()
.fail((message: string) => {
throw new Error(message);
})
.exitProcess(false);
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should parse the dependency option correctly', () => {
const argv = parser.parse(['--dependency', 'lodash']);
expect(argv).toMatchObject({
dependency: 'lodash',
});
expect(identifyDependencyOwnership).toHaveBeenCalledWith(
expect.objectContaining({ dependency: 'lodash' })
);
});
it('should parse the owner option correctly', () => {
const argv = parser.parse(['--owner', '@elastic/kibana-core']);
expect(argv).toMatchObject({
owner: '@elastic/kibana-core',
});
expect(identifyDependencyOwnership).toHaveBeenCalledWith(
expect.objectContaining({ owner: '@elastic/kibana-core' })
);
});
it('should parse the missing-owner option correctly', () => {
const argv = parser.parse(['--missing-owner']);
expect(argv).toMatchObject({
missingOwner: true,
});
expect(identifyDependencyOwnership).toHaveBeenCalledWith(
expect.objectContaining({ missingOwner: true })
);
});
it('should parse the output-path option correctly', () => {
const argv = parser.parse([
'--output-path',
'./output.json',
'--owner',
'@elastic/kibana-core',
]);
expect(argv).toMatchObject({
owner: '@elastic/kibana-core',
outputPath: './output.json',
});
expect(identifyDependencyOwnership).toHaveBeenCalledWith(
expect.objectContaining({ owner: '@elastic/kibana-core' })
);
});
it('should support aliases for options', () => {
const argv1 = parser.parse(['-d', 'lodash', '-f', './out.json']);
expect(argv1).toMatchObject({
dependency: 'lodash',
outputPath: './out.json',
});
const argv2 = parser.parse(['-o', '@elastic/kibana-core', '-f', './out.json']);
expect(argv2).toMatchObject({
owner: '@elastic/kibana-core',
outputPath: './out.json',
});
});
it('should throw an error for invalid flag combinations', () => {
expect(() => {
parser.parse(['--dependency', 'lodash', '--missing-owner']);
}).toThrow('You must provide either a dependency, owner, or missingOwner flag to search for');
expect(identifyDependencyOwnership).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,117 @@
/*
* 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 nodePath from 'path';
import yargs from 'yargs';
import chalk from 'chalk';
import fs from 'fs';
import { identifyDependencyOwnership } from './dependency_ownership';
interface CLIArgs {
dependency?: string;
owner?: string;
missingOwner?: boolean;
outputPath?: string;
}
export const configureYargs = () => {
return yargs(process.argv.slice(2))
.command(
'*',
chalk.green('Identify the dependency ownership'),
(y) => {
y.version(false)
.option('dependency', {
alias: 'd',
describe: chalk.yellow('Show who owns the given dependency'),
type: 'string',
})
.option('owner', {
alias: 'o',
type: 'string',
describe: chalk.magenta('Show dependencies owned by the given owner'),
})
.option('missing-owner', {
describe: chalk.cyan('Show dependencies that are not owned by any team'),
type: 'boolean',
})
.option('output-path', {
alias: 'f',
describe: chalk.blue('Specify the output file to save results as JSON'),
type: 'string',
})
.check(({ dependency, owner, missingOwner }: Partial<CLIArgs>) => {
const notProvided = [dependency, owner, missingOwner].filter(
(arg) => arg === undefined
);
if (notProvided.length === 1) {
throw new Error(
'You must provide either a dependency, owner, or missingOwner flag to search for'
);
}
return true;
})
.example(
'--owner @elastic/kibana-core',
chalk.blue('Searches for all dependencies owned by the Kibana Core team')
);
},
async (argv: CLIArgs) => {
const { dependency, owner, missingOwner, outputPath } = argv;
if (owner) {
console.log(chalk.yellow(`Searching for dependencies owned by ${owner}...\n`));
}
try {
const result = identifyDependencyOwnership({ dependency, owner, missingOwner });
if (outputPath) {
const isJsonFile = nodePath.extname(outputPath) === '.json';
const outputFile = isJsonFile
? outputPath
: nodePath.join(outputPath, 'dependency-ownership.json');
const outputDir = nodePath.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFile(outputFile, JSON.stringify(result, null, 2), (err) => {
if (err) {
console.error(chalk.red(`Failed to save results to ${outputFile}: ${err.message}`));
} else {
console.log(chalk.green(`Results successfully saved to ${outputFile}`));
}
});
} else {
console.log(chalk.yellow('No output file specified, displaying results below:\n'));
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
console.error('Error fetching dependency ownership:', error.message);
}
}
)
.help();
};
export const runCLI = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
configureYargs().argv;
};
if (!process.env.JEST_WORKER_ID) {
runCLI();
}

View file

@ -0,0 +1,135 @@
/*
* 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 { identifyDependencyOwnership } from './dependency_ownership';
import { parseConfig } from './parse_config';
jest.mock('./parse_config', () => ({
parseConfig: jest.fn(),
}));
describe('identifyDependencyOwnership', () => {
const mockConfig = {
renovateRules: [
{
reviewers: ['team:elastic', 'team:infra'],
matchPackageNames: ['lodash', 'react'],
enabled: true,
},
{
reviewers: ['team:ui'],
matchPackageNames: ['@testing-library/react'],
enabled: true,
},
{
reviewers: ['team:disabled-team'],
matchPackageNames: ['disabled-package'],
enabled: false, // Disabled rule
},
],
packageDependencies: ['lodash', 'react'],
packageDevDependencies: ['jest', '@testing-library/react'],
};
beforeEach(() => {
(parseConfig as jest.Mock).mockReturnValue(mockConfig);
});
it('returns prod and dev dependencies for a specific owner, considering only enabled rules', () => {
const result = identifyDependencyOwnership({ owner: '@elastic/elastic' });
expect(result).toEqual({
prodDependencies: ['lodash', 'react'],
devDependencies: [],
});
const resultInfra = identifyDependencyOwnership({ owner: '@elastic/infra' });
expect(resultInfra).toEqual({
prodDependencies: ['lodash', 'react'],
devDependencies: [],
});
const resultUi = identifyDependencyOwnership({ owner: '@elastic/ui' });
expect(resultUi).toEqual({
prodDependencies: [],
devDependencies: ['@testing-library/react'],
});
// Disabled team should have no dependencies
const resultDisabled = identifyDependencyOwnership({ owner: 'team:disabled-team' });
expect(resultDisabled).toEqual({
prodDependencies: [],
devDependencies: [],
});
});
it('returns owners of a specific dependency, considering only enabled rules', () => {
const result = identifyDependencyOwnership({ dependency: 'lodash' });
expect(result).toEqual(['@elastic/elastic', '@elastic/infra']);
const resultUi = identifyDependencyOwnership({ dependency: '@testing-library/react' });
expect(resultUi).toEqual(['@elastic/ui']);
const resultDisabled = identifyDependencyOwnership({ dependency: 'disabled-package' });
expect(resultDisabled).toEqual([]); // Disabled rule, no owners
});
it('returns uncovered dependencies when missingOwner is true', () => {
const result = identifyDependencyOwnership({ missingOwner: true });
expect(result).toEqual({
prodDependencies: [],
devDependencies: ['jest'],
});
});
it('returns comprehensive ownership coverage, considering only enabled rules', () => {
const result = identifyDependencyOwnership({});
expect(result).toEqual({
prodDependenciesByOwner: {
'@elastic/elastic': ['lodash', 'react'],
'@elastic/infra': ['lodash', 'react'],
'@elastic/ui': [],
'@elastic/disabled-team': [],
},
devDependenciesByOwner: {
'@elastic/elastic': [],
'@elastic/infra': [],
'@elastic/ui': ['@testing-library/react'],
'@elastic/disabled-team': [],
},
uncoveredProdDependencies: [],
uncoveredDevDependencies: ['jest'],
coveredProdDependencies: ['lodash', 'react'],
coveredDevDependencies: ['@testing-library/react'],
});
});
it('handles scenarios with no matching rules or dependencies', () => {
(parseConfig as jest.Mock).mockReturnValue({
renovateRules: [],
packageDependencies: ['lodash', 'react'],
packageDevDependencies: ['jest'],
});
const result = identifyDependencyOwnership({});
expect(result).toEqual({
prodDependenciesByOwner: {},
devDependenciesByOwner: {},
uncoveredProdDependencies: ['lodash', 'react'],
uncoveredDevDependencies: ['jest'],
coveredProdDependencies: [],
coveredDevDependencies: [],
});
});
it('ignores disabled rules in coverage calculations', () => {
const result = identifyDependencyOwnership({});
// @ts-expect-error
expect(result.coveredProdDependencies).not.toContain('disabled-package');
});
});

View file

@ -0,0 +1,189 @@
/*
* 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 { RenovatePackageRule, ruleCoversDependency } from './rule';
import { parseConfig } from './parse_config';
type DependencyOwners = string[];
interface GetDependencyOwnershipParams {
dependency?: string;
owner?: string;
missingOwner?: boolean;
}
interface DependenciesByOwner {
prodDependencies: string[];
devDependencies: string[];
}
interface DependenciesByOwners {
prodDependencies: Record<string, string[]>;
devDependencies: Record<string, string[]>;
}
interface DependenciesUncovered {
uncoveredProdDependencies: string[];
uncoveredDevDependencies: string[];
}
interface DependenciesCovered {
coveredProdDependencies: string[];
coveredDevDependencies: string[];
}
type DependenciesCoverage = DependenciesUncovered & DependenciesCovered;
interface DependenciesOwnershipReport extends DependenciesCoverage {
prodDependenciesByOwner: Record<string, string[]>;
devDependenciesByOwner: Record<string, string[]>;
}
type GetDependencyOwnershipResponse =
| DependencyOwners
| DependenciesUncovered
| DependenciesByOwners
| DependenciesByOwner
| DependenciesOwnershipReport;
const normalizeOwnerName = (owner: string): string => {
return owner.replace('team:', '@elastic/');
};
const getDependencyOwners = (dependency: string): string[] => {
const { renovateRules } = parseConfig();
const owners =
renovateRules.find((rule) => rule.enabled && ruleCoversDependency(rule, dependency))
?.reviewers ?? [];
return owners.map(normalizeOwnerName);
};
const getDependenciesByOwner = (): DependenciesByOwners => {
const { renovateRules, packageDependencies, packageDevDependencies } = parseConfig();
const dependenciesByOwner = renovateRules.reduce(
(acc, rule: RenovatePackageRule) => {
const { reviewers = [] } = rule;
const ruleDependencies = packageDependencies.filter((dependency) =>
ruleCoversDependency(rule, dependency)
);
const ruleDevDependencies = packageDevDependencies.filter((dependency) =>
ruleCoversDependency(rule, dependency)
);
for (const owner of reviewers) {
if (!owner.startsWith('team:')) {
continue;
}
const normalizedOwner = normalizeOwnerName(owner);
if (!acc.prodDependencies[normalizedOwner]) {
acc.prodDependencies[normalizedOwner] = [];
}
if (!acc.devDependencies[normalizedOwner]) {
acc.devDependencies[normalizedOwner] = [];
}
acc.prodDependencies[normalizedOwner].push(...ruleDependencies);
acc.devDependencies[normalizedOwner].push(...ruleDevDependencies);
}
return acc;
},
{ prodDependencies: {}, devDependencies: {} } as DependenciesByOwners
);
return dependenciesByOwner;
};
const getDependenciesCoverage = (): DependenciesCoverage => {
const { renovateRules, packageDependencies, packageDevDependencies } = parseConfig();
const aggregateDependencies = (dependencies: string[]) => {
return dependencies.reduce(
(acc, dependency) => {
const isCovered = renovateRules.some((rule: any) => ruleCoversDependency(rule, dependency));
if (isCovered) {
acc.covered.push(dependency);
} else {
acc.uncovered.push(dependency);
}
return acc;
},
{ covered: [] as string[], uncovered: [] as string[] }
);
};
const { covered: coveredProdDependencies, uncovered: uncoveredProdDependencies } =
aggregateDependencies(packageDependencies);
const { covered: coveredDevDependencies, uncovered: uncoveredDevDependencies } =
aggregateDependencies(packageDevDependencies);
return {
coveredProdDependencies,
coveredDevDependencies,
uncoveredProdDependencies,
uncoveredDevDependencies,
};
};
export const identifyDependencyOwnership = ({
dependency,
owner,
missingOwner,
}: GetDependencyOwnershipParams): GetDependencyOwnershipResponse => {
if (owner) {
const dependenciesByOwner = getDependenciesByOwner();
const prodDependencies = dependenciesByOwner.prodDependencies[owner] ?? [];
const devDependencies = dependenciesByOwner.devDependencies[owner] ?? [];
return {
prodDependencies,
devDependencies,
};
}
if (dependency) {
return getDependencyOwners(dependency);
}
const {
uncoveredDevDependencies,
uncoveredProdDependencies,
coveredDevDependencies,
coveredProdDependencies,
} = getDependenciesCoverage();
if (missingOwner) {
return {
prodDependencies: uncoveredProdDependencies,
devDependencies: uncoveredDevDependencies,
};
}
const { prodDependencies: prodDependenciesByOwner, devDependencies: devDependenciesByOwner } =
getDependenciesByOwner();
return {
prodDependenciesByOwner,
devDependenciesByOwner,
uncoveredProdDependencies,
uncoveredDevDependencies,
coveredDevDependencies,
coveredProdDependencies,
};
};

View file

@ -0,0 +1,44 @@
/*
* 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 { resolve } from 'path';
import { readFileSync } from 'fs';
import { REPO_ROOT } from '@kbn/repo-info';
import { RenovatePackageRule, ruleFilter, packageFilter } from './rule';
export const parseConfig = (() => {
let cache: {
renovateRules: RenovatePackageRule[];
packageDependencies: string[];
packageDevDependencies: string[];
} | null = null;
return () => {
if (cache) {
return cache;
}
const renovateFile = resolve(REPO_ROOT, 'renovate.json');
const packageFile = resolve(REPO_ROOT, 'package.json');
const renovateConfig = JSON.parse(readFileSync(renovateFile, 'utf8'));
const packageConfig = JSON.parse(readFileSync(packageFile, 'utf8'));
const renovateRules = (renovateConfig?.packageRules || []).filter(ruleFilter);
const packageDependencies = Object.keys(packageConfig?.dependencies || {}).filter(
packageFilter
);
const packageDevDependencies = Object.keys(packageConfig?.devDependencies || {}).filter(
packageFilter
);
cache = { renovateRules, packageDependencies, packageDevDependencies };
return cache;
};
})();

View file

@ -0,0 +1,39 @@
/*
* 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 { ruleCoversDependency } from './rule';
describe('ruleCoversDependency', () => {
const mockRule = {
matchPackageNames: ['lodash'],
matchPackagePatterns: ['^react'],
matchDepNames: ['@testing-library/react'],
matchDepPatterns: ['^jest'],
excludePackageNames: ['lodash'],
excludePackagePatterns: ['^react-dom'],
};
it('returns true when a dependency is included and not excluded', () => {
expect(ruleCoversDependency(mockRule, '@testing-library/react')).toBe(true);
expect(ruleCoversDependency(mockRule, 'jest-mock')).toBe(true);
});
it('returns false when a dependency is excluded', () => {
expect(ruleCoversDependency(mockRule, 'lodash')).toBe(false); // Excluded by name
expect(ruleCoversDependency(mockRule, 'react-dom')).toBe(false); // Excluded by pattern
});
it('returns true for included dependencies by pattern', () => {
expect(ruleCoversDependency(mockRule, 'react-redux')).toBe(true); // Matches ^react
});
it('returns false when no match is found', () => {
expect(ruleCoversDependency(mockRule, 'unknown-package')).toBe(false);
});
});

View file

@ -0,0 +1,61 @@
/*
* 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".
*/
export interface RenovatePackageRule {
matchPackageNames?: string[];
matchDepNames?: string[];
matchPackagePatterns?: string[];
matchDepPatterns?: string[];
excludePackageNames?: string[];
excludePackagePatterns?: string[];
enabled?: boolean;
reviewers?: string[];
}
export function ruleFilter(rule: RenovatePackageRule) {
return (
// Only include rules that are enabled
rule.enabled !== false &&
// Only include rules that have a team reviewer
rule.reviewers?.some((reviewer) => reviewer.startsWith('team:'))
);
}
// Filter packages that do not require ownership.
export function packageFilter(pkg: string) {
return (
// @kbn-* packages are internal to this repo, and do not require ownership via renovate
!pkg.startsWith('@kbn/') &&
// The EUI team owns the EUI package, and it is not covered by renovate
pkg !== '@elastic/eui'
);
}
export function ruleCoversDependency(rule: RenovatePackageRule, dependency: string): boolean {
const {
matchPackageNames = [],
matchPackagePatterns = [],
matchDepNames = [],
matchDepPatterns = [],
excludePackageNames = [],
excludePackagePatterns = [],
} = rule;
const packageIncluded =
matchPackageNames.includes(dependency) ||
matchDepNames.includes(dependency) ||
matchPackagePatterns.some((pattern) => new RegExp(pattern).test(dependency)) ||
matchDepPatterns.some((pattern) => new RegExp(pattern).test(dependency));
const packageExcluded =
excludePackageNames.includes(dependency) ||
excludePackagePatterns.some((pattern) => new RegExp(pattern).test(dependency));
return packageIncluded && !packageExcluded;
}

View file

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

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", 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".
*/
require('@kbn/dependency-ownership/bin');

View file

@ -756,6 +756,8 @@
"@kbn/default-nav-management/*": ["packages/default-nav/management/*"],
"@kbn/default-nav-ml": ["packages/default-nav/ml"],
"@kbn/default-nav-ml/*": ["packages/default-nav/ml/*"],
"@kbn/dependency-ownership": ["packages/kbn-dependency-ownership"],
"@kbn/dependency-ownership/*": ["packages/kbn-dependency-ownership/*"],
"@kbn/dependency-usage": ["packages/kbn-dependency-usage"],
"@kbn/dependency-usage/*": ["packages/kbn-dependency-usage/*"],
"@kbn/dev-cli-errors": ["packages/kbn-dev-cli-errors"],

View file

@ -5320,6 +5320,10 @@
version "0.0.0"
uid ""
"@kbn/dependency-ownership@link:packages/kbn-dependency-ownership":
version "0.0.0"
uid ""
"@kbn/dependency-usage@link:packages/kbn-dependency-usage":
version "0.0.0"
uid ""