From 21a3006c5fb95475a9084393c1c52c08c5f7de38 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 18:55:27 +0000 Subject: [PATCH] Add CODEOWNERS authorization check to Python publish workflow Only allows the workflow to proceed if the actor triggering it is listed as a CODEOWNERS owner for the selected package path. Falls back to the root (*) owners if no specific rule matches. https://claude.ai/code/session_01JT4yq4v53Coq865SaxX4Er --- .github/workflows/publish-python-package.yml | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index 233a74aab..c560dd1a6 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -33,6 +33,75 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Authorize actor against CODEOWNERS + env: + ACTOR: ${{ github.actor }} + PACKAGE: ${{ inputs.package }} + run: | + node << 'SCRIPT' + const fs = require('fs'); + const actor = process.env.ACTOR; + const pkg = process.env.PACKAGE; + + // Parse CODEOWNERS + const lines = fs.readFileSync('.github/CODEOWNERS', 'utf-8').split('\n'); + const rules = []; + let rootOwners = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const parts = trimmed.split(/\s+/); + const pattern = parts[0]; + const owners = parts.slice(1).map(o => o.replace('@', '')); + if (pattern === '*') { + rootOwners = owners; + } else { + rules.push({ pattern, owners }); + } + } + + // Find the most specific matching rule for this package path + // Strip trailing /python to match CODEOWNERS entries like "integrations/adk-middleware" + const pathsToCheck = [pkg]; + const pythonSuffix = '/python'; + if (pkg.endsWith(pythonSuffix)) { + pathsToCheck.push(pkg.slice(0, -pythonSuffix.length)); + } + + let authorized = false; + let matchedRule = null; + + for (const checkPath of pathsToCheck) { + for (const rule of rules) { + const pattern = rule.pattern.replace(/\/$/, ''); + if (checkPath === pattern || checkPath.startsWith(pattern + '/')) { + matchedRule = rule; + authorized = rule.owners.includes(actor); + break; + } + } + if (matchedRule) break; + } + + // Fall back to root owners if no specific rule matched + if (!matchedRule) { + matchedRule = { pattern: '*', owners: rootOwners }; + authorized = rootOwners.includes(actor); + } + + console.log(`Actor: ${actor}`); + console.log(`Package: ${pkg}`); + console.log(`Matched rule: ${matchedRule.pattern} -> ${matchedRule.owners.join(', ')}`); + console.log(`Authorized: ${authorized}`); + + if (!authorized) { + console.error(`\nERROR: ${actor} is not a CODEOWNERS owner for ${pkg}`); + console.error(`Allowed users: ${matchedRule.owners.join(', ')}`); + process.exit(1); + } + SCRIPT + - name: Install uv uses: astral-sh/setup-uv@v4 with: