From a8263cb79f2aec2c9f39ac8d9960f93483812bea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:54:00 +1200 Subject: [PATCH] [CI] Add ``by-code-owner`` labelling (#9589) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index b22364210b..83d66bb140 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -121,6 +121,7 @@ jobs: 'small-pr', 'dashboard', 'github-actions', + 'by-code-owner', 'has-tests', 'needs-tests', 'too-big', @@ -297,6 +298,71 @@ jobs: labels.add('github-actions'); } + // Strategy: Code Owner detection + try { + // Fetch CODEOWNERS file from the repository (in case it was changed in this PR) + const { data: codeownersFile } = await github.rest.repos.getContent({ + owner, + repo, + path: '.github/CODEOWNERS', + ref: context.payload.pull_request.head.sha + }); + + const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const prAuthor = context.payload.pull_request.user.login; + + // Parse CODEOWNERS file + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + let isCodeOwner = false; + + // Precompile CODEOWNERS patterns into regex objects + const codeownersRegexes = codeownersLines.map(line => { + const parts = line.split(/\s+/); + const pattern = parts[0]; + const owners = parts.slice(1); + + let regex; + if (pattern.endsWith('*')) { + // Directory pattern like "esphome/components/api/*" + const dir = pattern.slice(0, -1); + regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); + } else if (pattern.includes('*')) { + // Glob pattern + const regexPattern = pattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\\*/g, '.*'); + regex = new RegExp(`^${regexPattern}$`); + } else { + // Exact match + regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); + } + + return { regex, owners }; + }); + + for (const file of changedFiles) { + for (const { regex, owners } of codeownersRegexes) { + if (regex.test(file)) { + // Check if PR author is in the owners list + if (owners.some(owner => owner === `@${prAuthor}`)) { + isCodeOwner = true; + break; + } + } + } + if (isCodeOwner) break; + } + + if (isCodeOwner) { + labels.add('by-code-owner'); + } + } catch (error) { + console.log('Failed to read or parse CODEOWNERS file:', error.message); + } + // Strategy: Test detection const testFiles = changedFiles.filter(file => file.startsWith('tests/')