mirror of
https://github.com/esphome/esphome.git
synced 2026-09-14 08:38:39 +00:00
pull_request_review checks out the PR merge commit by default, which means require() would load the PR author's version of shared scripts. Explicitly checkout the base branch SHA instead.
162 lines
6.1 KiB
YAML
162 lines
6.1 KiB
YAML
# This workflow adds/removes a 'code-owner-approved' label when a
|
|
# component-specific codeowner approves (or dismisses) a PR.
|
|
# This helps maintainers prioritize PRs that have codeowner sign-off.
|
|
#
|
|
# Only component-specific codeowners count — the catch-all @esphome/core
|
|
# team is excluded so the label reflects domain-expert approval.
|
|
|
|
name: Codeowner Approved Label
|
|
|
|
on:
|
|
pull_request_review:
|
|
types: [submitted, dismissed]
|
|
|
|
permissions:
|
|
pull-requests: write
|
|
contents: read
|
|
|
|
jobs:
|
|
codeowner-approved:
|
|
name: Run
|
|
if: ${{ github.repository == 'esphome/esphome' }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout base branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ github.event.pull_request.base.sha }}
|
|
|
|
- name: Check codeowner approval and update label
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
with:
|
|
script: |
|
|
const { fetchCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
|
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const pr_number = context.payload.pull_request.number;
|
|
const LABEL_NAME = 'code-owner-approved';
|
|
|
|
console.log(`Processing PR #${pr_number} for codeowner approval label`);
|
|
|
|
try {
|
|
// Get the list of changed files in this PR (with pagination)
|
|
const prFiles = await github.paginate(
|
|
github.rest.pulls.listFiles,
|
|
{
|
|
owner,
|
|
repo,
|
|
pull_number: pr_number
|
|
}
|
|
);
|
|
|
|
const changedFiles = prFiles.map(file => file.filename);
|
|
console.log(`Found ${changedFiles.length} changed files`);
|
|
|
|
if (changedFiles.length === 0) {
|
|
console.log('No changed files found, skipping');
|
|
return;
|
|
}
|
|
|
|
// Fetch and parse CODEOWNERS from base branch
|
|
const codeownersPatterns = await fetchCodeowners(
|
|
github, owner, repo,
|
|
context.payload.pull_request.base.sha
|
|
);
|
|
|
|
// Get effective owners using last-match-wins semantics
|
|
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
|
|
|
// Only keep individual component-specific codeowners (exclude teams)
|
|
const componentCodeowners = effective.users;
|
|
|
|
console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
|
|
|
|
if (componentCodeowners.size === 0) {
|
|
console.log('No component-specific codeowners found for changed files');
|
|
// Remove label if present since there are no component codeowners
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner,
|
|
repo,
|
|
issue_number: pr_number,
|
|
name: LABEL_NAME
|
|
});
|
|
console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`);
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
console.log(`Failed to remove label: ${error.message}`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Get all reviews on the PR
|
|
const reviews = await github.paginate(
|
|
github.rest.pulls.listReviews,
|
|
{
|
|
owner,
|
|
repo,
|
|
pull_number: pr_number
|
|
}
|
|
);
|
|
|
|
// Get the latest review per user (reviews are returned chronologically)
|
|
const latestReviewByUser = new Map();
|
|
for (const review of reviews) {
|
|
// Skip bot reviews and comment-only reviews
|
|
if (!review.user || review.state === 'COMMENTED') continue;
|
|
latestReviewByUser.set(review.user.login, review);
|
|
}
|
|
|
|
// Check if any component-specific codeowner has an active approval
|
|
let hasCodeownerApproval = false;
|
|
for (const [login, review] of latestReviewByUser) {
|
|
if (review.state === 'APPROVED' && componentCodeowners.has(login)) {
|
|
console.log(`Codeowner '${login}' has approved`);
|
|
hasCodeownerApproval = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get current labels to check if label is already present
|
|
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
|
owner,
|
|
repo,
|
|
issue_number: pr_number
|
|
});
|
|
const hasLabel = currentLabels.some(label => label.name === LABEL_NAME);
|
|
|
|
if (hasCodeownerApproval && !hasLabel) {
|
|
// Add the label
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: pr_number,
|
|
labels: [LABEL_NAME]
|
|
});
|
|
console.log(`Added '${LABEL_NAME}' label`);
|
|
} else if (!hasCodeownerApproval && hasLabel) {
|
|
// Remove the label
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner,
|
|
repo,
|
|
issue_number: pr_number,
|
|
name: LABEL_NAME
|
|
});
|
|
console.log(`Removed '${LABEL_NAME}' label`);
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
console.log(`Failed to remove label: ${error.message}`);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log('Failed to process codeowner approval label:', error.message);
|
|
console.error(error);
|
|
}
|