mirror of
https://github.com/esphome/esphome.git
synced 2026-03-04 11:48:21 -07:00
159 lines
6.0 KiB
YAML
159 lines
6.0 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 { loadCodeowners, 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;
|
|
}
|
|
|
|
// Parse CODEOWNERS from the checked-out base branch
|
|
const codeownersPatterns = loadCodeowners();
|
|
|
|
// 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.user.type === 'Bot' || 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.error(error);
|
|
core.setFailed(`Failed to process codeowner approval label: ${error.message}`);
|
|
}
|