From 668007707da498e12fd9904a152761245821f258 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:13:22 +1200 Subject: [PATCH] [CI] Add org fork detection warning to auto-label PR workflow (#15588) --- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 19 +++++++ .github/scripts/auto-label-pr/index.js | 16 ++++-- .github/scripts/auto-label-pr/reviews.js | 62 +++++++++++++++++++++- 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 1c33772c4c..e02b450bf0 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -4,6 +4,7 @@ module.exports = { CODEOWNERS_MARKER: '', TOO_BIG_MARKER: '', DEPRECATED_COMPONENT_MARKER: '', + ORG_FORK_MARKER: '', MANAGED_LABELS: [ 'new-component', diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index fb9dadc6a0..25c0ba49af 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) { return { labels, deprecatedInfo }; } +// Strategy: Detect when maintainers cannot modify the PR branch +function detectMaintainerAccess(context) { + const pr = context.payload.pull_request; + + // Only relevant for cross-repo PRs (forks) + if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) { + return null; + } + + if (pr.maintainer_can_modify) { + return null; + } + + const isOrgFork = pr.head.repo.owner.type === 'Organization'; + console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`); + return { isOrgFork, orgName: pr.head.repo.owner.login }; +} + // Strategy: Requirements detection async function detectRequirements(allLabels, prFiles, context) { const labels = new Set(); @@ -329,5 +347,6 @@ module.exports = { detectTests, detectPRTemplateCheckboxes, detectDeprecatedComponents, + detectMaintainerAccess, detectRequirements }; diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 42588c0bc8..021e91a9ee 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -12,9 +12,10 @@ const { detectTests, detectPRTemplateCheckboxes, detectDeprecatedComponents, + detectMaintainerAccess, detectRequirements } = require('./detectors'); -const { handleReviews } = require('./reviews'); +const { handleReviews, handleMaintainerAccessComment } = require('./reviews'); const { applyLabels, removeOldLabels } = require('./labels'); // Fetch API data @@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => { codeOwnerLabels, testLabels, checkboxLabels, - deprecatedResult + deprecatedResult, + maintainerAccess ] = await Promise.all([ detectMergeBranch(context), detectComponentPlatforms(changedFiles, apiData), @@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => { detectCodeOwner(github, context, changedFiles), detectTests(changedFiles), detectPRTemplateCheckboxes(context), - detectDeprecatedComponents(github, context, changedFiles) + detectDeprecatedComponents(github, context, changedFiles), + detectMaintainerAccess(context) ]); // Extract deprecated component info @@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => { console.log('Computed labels:', finalLabels.join(', ')); - // Handle reviews - await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD); + // Handle reviews and org fork comment + await Promise.all([ + handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD), + handleMaintainerAccessComment(github, context, maintainerAccess) + ]); // Apply labels await applyLabels(github, context, finalLabels); diff --git a/.github/scripts/auto-label-pr/reviews.js b/.github/scripts/auto-label-pr/reviews.js index 906e2c456a..7ac136515d 100644 --- a/.github/scripts/auto-label-pr/reviews.js +++ b/.github/scripts/auto-label-pr/reviews.js @@ -2,7 +2,8 @@ const { BOT_COMMENT_MARKER, CODEOWNERS_MARKER, TOO_BIG_MARKER, - DEPRECATED_COMPONENT_MARKER + DEPRECATED_COMPONENT_MARKER, + ORG_FORK_MARKER } = require('./constants'); // Generate review messages @@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d } } +// Handle maintainer access warning comment +async function handleMaintainerAccessComment(github, context, maintainerAccess) { + if (!maintainerAccess) { + return; + } + + const { owner, repo } = context.repo; + const pr_number = context.issue.number; + const prAuthor = context.payload.pull_request.user.login; + + // Check if we already posted the warning (iterate pages to exit early) + let existingComment; + for await (const { data: comments } of github.paginate.iterator( + github.rest.issues.listComments, + { owner, repo, issue_number: pr_number } + )) { + existingComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body && comment.body.includes(ORG_FORK_MARKER) + ); + if (existingComment) { + break; + } + } + + if (existingComment) { + console.log('Maintainer access warning comment already exists, skipping'); + return; + } + + let body; + if (maintainerAccess.isOrgFork) { + body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` + + `Hey there @${prAuthor},\n` + + `It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` + + `GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` + + `This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` + + `To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` + + `See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`; + } else { + body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` + + `Hey there @${prAuthor},\n` + + `It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` + + `This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` + + `Please enable this option in the PR sidebar to allow maintainer collaboration.`; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body + }); + console.log('Created maintainer access warning comment'); +} + module.exports = { - handleReviews + handleReviews, + handleMaintainerAccessComment };