mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
8084bd640e
commit
976b94ffa5
19 changed files with 960 additions and 0 deletions
|
@ -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
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
166
packages/kbn-dependency-ownership/README.md
Normal file
166
packages/kbn-dependency-ownership/README.md
Normal 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.
|
||||
|
18
packages/kbn-dependency-ownership/bin/index.js
Normal file
18
packages/kbn-dependency-ownership/bin/index.js
Normal 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');
|
14
packages/kbn-dependency-ownership/jest.config.js
Normal file
14
packages/kbn-dependency-ownership/jest.config.js
Normal 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'],
|
||||
};
|
6
packages/kbn-dependency-ownership/kibana.jsonc
Normal file
6
packages/kbn-dependency-ownership/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/dependency-ownership",
|
||||
"owner": "@elastic/kibana-security",
|
||||
"devOnly": true
|
||||
}
|
6
packages/kbn-dependency-ownership/package.json
Normal file
6
packages/kbn-dependency-ownership/package.json
Normal 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"
|
||||
}
|
119
packages/kbn-dependency-ownership/src/cli.test.ts
Normal file
119
packages/kbn-dependency-ownership/src/cli.test.ts
Normal 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();
|
||||
});
|
||||
});
|
117
packages/kbn-dependency-ownership/src/cli.ts
Normal file
117
packages/kbn-dependency-ownership/src/cli.ts
Normal 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();
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
189
packages/kbn-dependency-ownership/src/dependency_ownership.ts
Normal file
189
packages/kbn-dependency-ownership/src/dependency_ownership.ts
Normal 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,
|
||||
};
|
||||
};
|
44
packages/kbn-dependency-ownership/src/parse_config.ts
Normal file
44
packages/kbn-dependency-ownership/src/parse_config.ts
Normal 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;
|
||||
};
|
||||
})();
|
39
packages/kbn-dependency-ownership/src/rule.test.ts
Normal file
39
packages/kbn-dependency-ownership/src/rule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
61
packages/kbn-dependency-ownership/src/rule.ts
Normal file
61
packages/kbn-dependency-ownership/src/rule.ts
Normal 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;
|
||||
}
|
21
packages/kbn-dependency-ownership/tsconfig.json
Normal file
21
packages/kbn-dependency-ownership/tsconfig.json
Normal 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",
|
||||
],
|
||||
}
|
10
scripts/dependency_ownership.js
Normal file
10
scripts/dependency_ownership.js
Normal 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');
|
|
@ -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"],
|
||||
|
|
|
@ -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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue