import * as globby from 'tinyglobby'; import * as path from 'path'; import * as fs from 'fs'; import * as octokit from 'octokit'; import { output } from '@nx/devkit'; import { Octokit } from 'octokit'; async function main() { const codeowners = fs.readFileSync( path.join(__dirname, '../CODEOWNERS'), 'utf-8' ); const codeownersLines = codeowners .split('\n') .filter((line) => line.trim().length > 0 && !line.startsWith('#')); const mismatchedPatterns: string[] = []; const mismatchedOwners: string[] = []; const gh = new octokit.Octokit({ auth: process.env.GITHUB_TOKEN, }).rest; const cache: Map = new Map(); for (const line of codeownersLines) { // This is perhaps a bit naive, but it should // work for all paths and patterns that do not // contain spaces. const [specifiedPattern, ...owners] = line.split(' '); let foundMatchingFiles = false; const patternsToCheck = specifiedPattern.startsWith('/') ? [`.${specifiedPattern}`] : [`./${specifiedPattern}`, `**/${specifiedPattern}`]; for (const pattern of patternsToCheck) { foundMatchingFiles ||= globby.globSync(pattern, { ignore: ['node_modules', 'dist', 'build', '.git'], cwd: path.join(__dirname, '..'), onlyFiles: false, }).length > 0; } if (!foundMatchingFiles) { mismatchedPatterns.push(specifiedPattern); } if (process.env.GITHUB_TOKEN) { for (let owner of owners) { owner = owner.substring(1); // Remove the @ if (owner.includes('/')) { // Its a team. const [org, team] = owner.split('/'); let res = cache.get(owner); if (res === undefined) { res = await validateTeam(gh, org, team); cache.set(owner, res); } if (res === false) { mismatchedOwners.push(`${specifiedPattern}: ${owner}`); } } else { let res = cache.get(owner); if (res === undefined) { res = await validateUser(gh, owner); cache.set(owner, res); } if (res === false) { mismatchedOwners.push(`${specifiedPattern}: ${owner}`); } } } } else { output.warn({ title: `Skipping owner validation because GITHUB_TOKEN is not set.`, }); } } if (mismatchedPatterns.length > 0) { output.error({ title: `The following patterns in CODEOWNERS do not match any files:`, bodyLines: mismatchedPatterns.map((e) => `- ${e}`), }); } if (mismatchedOwners.length > 0) { output.error({ title: `The following owners in CODEOWNERS do not exist:`, bodyLines: mismatchedOwners.map((e) => `- ${e}`), }); } process.exit(mismatchedPatterns.length + mismatchedOwners.length > 0 ? 1 : 0); } main(); async function validateTeam( gh: Octokit['rest'], org: string, team: string ): Promise { try { await gh.teams.getByName({ org, team_slug: team, }); return true; } catch { return false; } } async function validateUser( gh: Octokit['rest'], username: string ): Promise { try { await gh.users.getByUsername({ username, }); return true; } catch { return false; } }