Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1,121 changes: 718 additions & 403 deletions .github/CODEOWNERS

Large diffs are not rendered by default.

1,090 changes: 360 additions & 730 deletions codeowners.ts

Large diffs are not rendered by default.

259 changes: 259 additions & 0 deletions scripts/manage-codeowners/assemble.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { assembleCodeownersSections } from './assemble.js';
import type { CodeownersConfig } from './types.js';

describe('assembleCodeownersSections', () => {
it('groups all package rules in one alphabetized Packages section', () => {
const config: CodeownersConfig = {
packages: {
zebra: { teams: ['@MetaMask/z-team'] },
alpha: {
teams: ['@MetaMask/b-team', '@MetaMask/a-team'],
initializationPath: 'alpha',
},
},
overrides: [],
};

const sections = assembleCodeownersSections(config);

expect(sections).toStrictEqual([
{
title: 'Packages',
rules: [],
subsections: [
{
title: 'alpha',
rules: [
{
pattern: '/packages/alpha',
owners: ['@MetaMask/a-team', '@MetaMask/b-team'],
},
{
pattern: '/packages/alpha/CHANGELOG.md',
owners: [
'@MetaMask/a-team',
'@MetaMask/b-team',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/alpha/package.json',
owners: [
'@MetaMask/a-team',
'@MetaMask/b-team',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/alpha/tsconfig.*',
owners: [
'@MetaMask/a-team',
'@MetaMask/b-team',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/alpha/typedoc.json',
owners: [
'@MetaMask/a-team',
'@MetaMask/b-team',
'@MetaMask/core-platform',
],
},
],
},
{
title: 'zebra',
rules: [
{
pattern: '/packages/zebra',
owners: ['@MetaMask/z-team'],
},
{
pattern: '/packages/zebra/CHANGELOG.md',
owners: ['@MetaMask/core-platform', '@MetaMask/z-team'],
},
{
pattern: '/packages/zebra/package.json',
owners: ['@MetaMask/core-platform', '@MetaMask/z-team'],
},
{
pattern: '/packages/zebra/tsconfig.*',
owners: ['@MetaMask/core-platform', '@MetaMask/z-team'],
},
{
pattern: '/packages/zebra/typedoc.json',
owners: ['@MetaMask/core-platform', '@MetaMask/z-team'],
},
],
},
],
},
{
title: 'Overrides',
rules: [],
},
]);
});

it('does not list the Core Platform team twice when it already owns a package', () => {
const config: CodeownersConfig = {
packages: {
'platform-package': { teams: ['@MetaMask/core-platform'] },
},
overrides: [],
};

const sections = assembleCodeownersSections(config);

expect(sections[0]?.subsections?.[0]).toStrictEqual({
title: 'platform-package',
rules: [
{
pattern: '/packages/platform-package',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/platform-package/CHANGELOG.md',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/platform-package/package.json',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/platform-package/tsconfig.*',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/platform-package/typedoc.json',
owners: ['@MetaMask/core-platform'],
},
],
});
});

it('appends overrides after Packages', () => {
const config: CodeownersConfig = {
packages: {},
overrides: [
{
pattern: '/packages/zebra/specific',
owners: ['@MetaMask/team'],
},
],
};

const sections = assembleCodeownersSections(config);

expect(sections).toStrictEqual([
{ title: 'Packages', rules: [], subsections: [] },
{ title: 'Overrides', rules: config.overrides },
]);
});

it('emits initialization rules in the wallet package section', () => {
const config: CodeownersConfig = {
packages: {
'accounts-controller': {
teams: ['@MetaMask/accounts-engineers'],
initializationPath: 'accounts-controller',
},
wallet: { teams: ['@MetaMask/core-platform'] },
},
overrides: [
{
pattern: '/packages/eth-json-rpc-middleware/src/methods',
owners: ['@MetaMask/confirmations', '@MetaMask/core-platform'],
},
],
};

const sections = assembleCodeownersSections(config);

expect(sections).toStrictEqual([
{
title: 'Packages',
rules: [],
subsections: [
{
title: 'accounts-controller',
rules: [
{
pattern: '/packages/accounts-controller',
owners: ['@MetaMask/accounts-engineers'],
},
{
pattern: '/packages/accounts-controller/CHANGELOG.md',
owners: [
'@MetaMask/accounts-engineers',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/accounts-controller/package.json',
owners: [
'@MetaMask/accounts-engineers',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/accounts-controller/tsconfig.*',
owners: [
'@MetaMask/accounts-engineers',
'@MetaMask/core-platform',
],
},
{
pattern: '/packages/accounts-controller/typedoc.json',
owners: [
'@MetaMask/accounts-engineers',
'@MetaMask/core-platform',
],
},
],
},
{
title: 'wallet',
rules: [
{
pattern: '/packages/wallet',
owners: ['@MetaMask/core-platform'],
},
{
pattern:
'/packages/wallet/src/initialization/instances/accounts-controller/',
owners: ['@MetaMask/accounts-engineers'],
},
{
pattern: '/packages/wallet/CHANGELOG.md',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/wallet/package.json',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/wallet/tsconfig.*',
owners: ['@MetaMask/core-platform'],
},
{
pattern: '/packages/wallet/typedoc.json',
owners: ['@MetaMask/core-platform'],
},
],
},
],
},
{
title: 'Overrides',
rules: [
{
pattern: '/packages/eth-json-rpc-middleware/src/methods',
owners: ['@MetaMask/confirmations', '@MetaMask/core-platform'],
},
],
},
]);
});
});
125 changes: 125 additions & 0 deletions scripts/manage-codeowners/assemble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type {
CodeownersConfig,
CodeownersRule,
CodeownersSection,
PackageInfo,
} from './types.js';

const CORE_PLATFORM_TEAM = '@MetaMask/core-platform';

/**
* Assembles the sections from the codeowners configuration file that will be
* used to (re-)generate CODEOWNERS.
*
* @param config - The configuration specified in `codeowners.ts`.
* @returns The assembled sections.
*/
export function assembleCodeownersSections(
config: CodeownersConfig,
): CodeownersSection[] {
return [
buildPackagesSection(config.packages),
{
title: 'Overrides',
rules: config.overrides,
},
];
}

/**
* Builds the Packages section with one nested section for each package, sorted
* by package name.
*
* @param packages - Package ownership metadata, as defined in the codeowners
* configuration file.
* @returns The Packages section.
*/
function buildPackagesSection(
packages: Record<string, PackageInfo>,
): CodeownersSection {
const initializationRules = buildInitializationRules(packages);

return {
title: 'Packages',
rules: [],
subsections: Object.entries(packages)
.sort(([firstPackageName], [secondPackageName]) =>
firstPackageName.localeCompare(secondPackageName),
)
.map(([packageName, packageInfo]) => ({
title: packageName,
rules: buildPackageRules(packageName, packageInfo, initializationRules),
})),
};
}

/**
* Builds all CODEOWNERS rules associated with one package. As specified in the
* codeowners configuration file, one or more teams will own the whole directory
* for the package, and, optionally, certain initialization files in the Wallet
* Library. The README, changelog, manifest, and certain configuration files
* will be co-owned by the Core Platform team.
*
* @param packageDirectoryName - The directory in `packages/` where the package
* is located.
* @param packageInfo - The package ownership metadata, as defined in the
* codeowners configuration file.
* @param initializationRules - Rules for Wallet package initialization code.
* @returns The package's rules.
*/
function buildPackageRules(
packageDirectoryName: string,
packageInfo: PackageInfo,
initializationRules: CodeownersRule[],
): CodeownersRule[] {
const owners = [...packageInfo.teams].sort();

const rules: CodeownersRule[] = [
{ pattern: `/packages/${packageDirectoryName}`, owners },
];

if (packageDirectoryName === 'wallet') {
rules.push(...initializationRules);
}

const releaseOwners = [
...new Set([...packageInfo.teams, CORE_PLATFORM_TEAM]),
].sort();
const workspacePath = `/packages/${packageDirectoryName}`;
rules.push(
{ pattern: `${workspacePath}/CHANGELOG.md`, owners: releaseOwners },
{ pattern: `${workspacePath}/package.json`, owners: releaseOwners },
{ pattern: `${workspacePath}/tsconfig.*`, owners: releaseOwners },
{ pattern: `${workspacePath}/typedoc.json`, owners: releaseOwners },
);

return rules;
}

/**
* Builds rules for package initialization code in the Wallet package. These are
* emitted directly after the generic Wallet package rule so they take precedence
* in GitHub's last-match-wins CODEOWNERS evaluation.
*
* @param packages - Package ownership metadata, as defined in the codeowners
* configuration file.
* @returns The initialization rules, sorted by package name.
*/
function buildInitializationRules(
packages: Record<string, PackageInfo>,
): CodeownersRule[] {
return Object.entries(packages)
.sort(([firstPackageName], [secondPackageName]) =>
firstPackageName.localeCompare(secondPackageName),
)
.flatMap(([, packageInfo]) => {
if (packageInfo.initializationPath === undefined) {
return [];
}

return {
pattern: `/packages/wallet/src/initialization/instances/${packageInfo.initializationPath}/`,
owners: [...packageInfo.teams].sort(),
};
});
}
Loading
Loading