Package Exports
- codeowners-util
Readme
codeowners-util
Generate GitHub CODEOWNERS files from a typed TypeScript config.
Instead of hand-editing a flat CODEOWNERS file, define ownership rules in TypeScript with full type safety, then generate the file.
Install
npm install codeowners-utilQuick Start
Create a codeowners.config.ts at your repo root:
import { team, own, match } from "codeowners-util";
import type { CodeOwnersConfig } from "codeowners-util";
const bot = team("@ci-bot");
const platform = team("@org/platform");
const teamA = team("@org/team-a");
const i18n = team("@org/i18n");
const config: CodeOwnersConfig = {
always: [bot],
own: [
own(platform, ["*", "apps/web", "libs/config"]),
own(teamA, ["libs/auth", "libs/search"]),
],
match: [
match("**/locales/**/*.json", { only: [i18n] }),
match("**/locales/en-US/**/*.json", { add: [i18n] }),
],
};
export default config;Generate the file:
npx codeowners-utilThis writes .github/CODEOWNERS with rules sorted by specificity so GitHub's "last matching rule wins" semantics work correctly.
CLI
codeowners-util [options]
-c, --config <path> Config file (default: codeowners.config.ts)
-o, --output <path> Output file (default: .github/CODEOWNERS)
--check Check if output is up to date (exit 1 if stale)
--stdout Print generated output to stdout
-h, --help Show helpThe --check flag is useful in CI to ensure the CODEOWNERS file stays in sync:
npx codeowners-util --checkProgrammatic API
import { team, own, match, generate, write } from "codeowners-util";team(name)
Creates a typed team handle.
const platform = team("@org/platform");own(owners, paths)
Declares ownership. Accepts a single team or array of teams, and a single path or array of paths.
own(platform, "libs/config");
own([teamA, teamB], ["libs/shared", "libs/utils"]);When multiple own() calls declare the same path, their owners are merged (implicit co-ownership).
match(pattern, opts)
Creates pattern-based rules that apply across all owned paths.
add — adds owners on top of inherited ownership:
match("**/locales/en-US/**/*.json", { add: [i18n] });
// libs/auth/locales/en-US/**/*.json → @org/team-a @org/i18nonly — replaces inherited ownership entirely:
match("**/locales/**/*.json", { only: [i18n] });
// libs/auth/locales/**/*.json → @org/i18n (team-a is NOT inherited)Patterns starting with **/ are scoped under each owned path. The **/ prefix is stripped and the rest is appended to the owned path. Patterns without **/ are appended directly.
generate(config)
Returns the generated CODEOWNERS file content as a string.
const content = generate(config);write(config, options)
Generates and writes the CODEOWNERS file to disk.
import { write } from "codeowners-util";
// Write to file
write(config, { outputPath: ".github/CODEOWNERS" });
// Check mode — compare without writing
const result = write(config, {
outputPath: ".github/CODEOWNERS",
check: true,
});
console.log(result.upToDate); // true or falseConfig Reference
interface CodeOwnersConfig {
/** Teams appended to every generated rule (e.g. a bot account) */
always?: Team[];
/** Ownership declarations */
own: OwnershipRule[];
/** Pattern-based rules applied across all owned paths */
match?: MatchRule[];
}How rules are resolved
- Direct ownership rules are sorted by specificity (ascending)
- Match rules are expanded against every owned path
- When multiple match rules resolve to the same path, the more specific pattern wins; equal specificity uses last-declared
- Match rules are sorted by specificity and placed after direct rules
alwaysteams are appended to every rule
This ordering ensures GitHub's "last matching rule wins" semantics produce the correct result.
License
MIT