From 769031b72481e6f374162b00d767286ea9f9f6b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 16:01:23 -1000 Subject: [PATCH] reduce api calls --- .github/scripts/codeowners.js | 27 ++++++++++++++++--- .../workflows/codeowner-approved-label.yml | 9 +++---- .../workflows/codeowner-review-request.yml | 21 +++------------ 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index f0121802c2..9a10391699 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -13,8 +13,9 @@ function globToRegex(pattern) { let regexStr = pattern .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') - .replace(/\*\*/g, '.*') - .replace(/\*/g, '[^/]*') + .replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace + .replace(/\*/g, '[^/]*') // single star + .replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar .replace(/\?/g, '.'); return new RegExp('^' + regexStr + '$'); } @@ -91,11 +92,12 @@ function classifyOwners(rawOwners) { * * @param {string[]} files - list of file paths * @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners - * @returns {{ users: Set, teams: Set }} + * @returns {{ users: Set, teams: Set, matchedFileCount: number }} */ function getEffectiveOwners(files, codeownersPatterns) { const users = new Set(); const teams = new Set(); + let matchedFileCount = 0; for (const file of files) { // Last matching pattern wins for each file @@ -106,19 +108,36 @@ function getEffectiveOwners(files, codeownersPatterns) { } } if (effectiveOwners) { + matchedFileCount++; const classified = classifyOwners(effectiveOwners); for (const u of classified.users) users.add(u); for (const t of classified.teams) teams.add(t); } } - return { users, teams }; + return { users, teams, matchedFileCount }; +} + +/** + * Read and parse the CODEOWNERS file from disk. + * + * Use this when the repo is already checked out (avoids an API call). + * + * @param {string} [repoRoot='.'] - path to the repo root + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +function loadCodeowners(repoRoot = '.') { + const fs = require('fs'); + const path = require('path'); + const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8'); + return parseCodeowners(content); } module.exports = { globToRegex, parseCodeowners, fetchCodeowners, + loadCodeowners, classifyOwners, getEffectiveOwners }; diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml index b45cdd82ec..217ae06419 100644 --- a/.github/workflows/codeowner-approved-label.yml +++ b/.github/workflows/codeowner-approved-label.yml @@ -30,7 +30,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | - const { fetchCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); const owner = context.repo.owner; const repo = context.repo.repo; @@ -58,11 +58,8 @@ jobs: return; } - // Fetch and parse CODEOWNERS from base branch - const codeownersPatterns = await fetchCodeowners( - github, owner, repo, - context.payload.pull_request.base.sha - ); + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); // Get effective owners using last-match-wins semantics const effective = getEffectiveOwners(changedFiles, codeownersPatterns); diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index c6f5864983..abe90836f7 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -33,7 +33,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | - const { fetchCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); const owner = context.repo.owner; const repo = context.repo.repo; @@ -60,11 +60,8 @@ jobs: return; } - // Fetch and parse CODEOWNERS file from base branch - const codeownersPatterns = await fetchCodeowners( - github, owner, repo, - context.payload.pull_request.base.sha - ); + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); @@ -85,17 +82,7 @@ jobs: const effective = getEffectiveOwners(changedFiles, codeownersPatterns); const matchedOwners = effective.users; const matchedTeams = effective.teams; - - // Count matched files for the comment - let matchedFileCount = 0; - for (const file of changedFiles) { - for (const { regex } of codeownersPatterns) { - if (regex.test(file)) { - matchedFileCount++; - break; - } - } - } + const matchedFileCount = effective.matchedFileCount; if (matchedOwners.size === 0 && matchedTeams.size === 0) { console.log('No codeowners found for any changed files');