diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 488a72ffb3..b30f6cf28a 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -11,49 +11,6 @@ permissions: contents: read env: - TARGET_PLATFORMS: | - esp32 - esp8266 - rp2040 - libretiny - bk72xx - rtl87xx - ln882x - nrf52 - host - PLATFORM_COMPONENTS: | - alarm_control_panel - audio_adc - audio_dac - binary_sensor - button - canbus - climate - cover - datetime - display - event - fan - light - lock - media_player - microphone - number - one_wire - ota - output - packet_transport - select - sensor - speaker - stepper - switch - text - text_sensor - time - touchscreen - update - valve SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 @@ -101,6 +58,9 @@ jobs: const { owner, repo } = context.repo; const pr_number = context.issue.number; + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + // Get current labels const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ owner, @@ -143,9 +103,25 @@ jobs: const labels = new Set(); + // Fetch TARGET_PLATFORMS and PLATFORM_COMPONENTS from API + let targetPlatforms = []; + let platformComponents = []; + + try { + const response = await fetch('https://data.esphome.io/components.json'); + const componentsData = await response.json(); + + // Extract target platforms and platform components directly from API + targetPlatforms = componentsData.target_platforms || []; + platformComponents = componentsData.platform_components || []; + + console.log('Target platforms from API:', targetPlatforms.length, targetPlatforms); + console.log('Platform components from API:', platformComponents.length, platformComponents); + } catch (error) { + console.log('Failed to fetch components data from API:', error.message); + } + // Get environment variables - const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); - const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const maxLabels = parseInt('${{ env.MAX_LABELS }}'); const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); @@ -225,6 +201,14 @@ jobs: const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + // Calculate changes excluding root tests directory for too-big calculation + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + const nonTestChanges = totalChanges - testChanges; + console.log(`Test changes: ${testChanges}, Non-test changes: ${nonTestChanges}`); + // Strategy: New Component detection for (const file of addedFiles) { // Check for new component files: esphome/components/{component}/__init__.py @@ -404,16 +388,30 @@ jobs: console.log('Computed labels:', finalLabels.join(', ')); - // Check if PR is allowed to be too big - const allowedTooBig = currentLabels.includes('mega-pr'); + // Check if PR has mega-pr label + const isMegaPR = currentLabels.includes('mega-pr'); // Check if PR is too big (either too many labels or too many line changes) const tooManyLabels = finalLabels.length > maxLabels; - const tooManyChanges = totalChanges > tooBigThreshold; + const tooManyChanges = nonTestChanges > tooBigThreshold; - if ((tooManyLabels || tooManyChanges) && !allowedTooBig) { + if ((tooManyLabels || tooManyChanges) && !isMegaPR) { const originalLength = finalLabels.length; - console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges}`); + console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges} (non-test: ${nonTestChanges})`); + + // Get all reviews on this PR to check for existing bot reviews + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Check if there's already an active bot review requesting changes + const existingBotReview = reviews.find(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); // If too big due to line changes only, keep original labels and add too-big // If too big due to too many labels, replace with just too-big @@ -423,24 +421,69 @@ jobs: finalLabels = ['too-big']; } - // Create appropriate review message - let reviewBody; - if (tooManyLabels && tooManyChanges) { - reviewBody = `This PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; - } else if (tooManyLabels) { - reviewBody = `This PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; - } else { - reviewBody = `This PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; - } + // Only create a new review if there isn't already an active bot review + if (!existingBotReview) { + // Create appropriate review message + let reviewBody; + if (tooManyLabels && tooManyChanges) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else if (tooManyLabels) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests). Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } - // Request changes on the PR - await github.rest.pulls.createReview({ - owner, - repo, - pull_number: pr_number, - body: reviewBody, - event: 'REQUEST_CHANGES' - }); + // Request changes on the PR + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: reviewBody, + event: 'REQUEST_CHANGES' + }); + console.log('Created new "too big" review requesting changes'); + } else { + console.log('Skipping review creation - existing bot review already requesting changes'); + } + } else { + // Check if PR was previously too big but is now acceptable + const wasPreviouslyTooBig = currentLabels.includes('too-big'); + + if (wasPreviouslyTooBig || isMegaPR) { + console.log('PR is no longer too big or has mega-pr label - dismissing bot reviews'); + + // Get all reviews on this PR to find reviews to dismiss + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Find bot reviews that requested changes + const botReviews = reviews.filter(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); + + // Dismiss bot reviews + for (const review of botReviews) { + try { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: isMegaPR ? + 'Review dismissed: mega-pr label was added' : + 'Review dismissed: PR size is now acceptable' + }); + console.log(`Dismissed review ${review.id}`); + } catch (error) { + console.log(`Failed to dismiss review ${review.id}:`, error.message); + } + } + } } // Add new labels diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 9a0b43a51d..121619e049 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -34,6 +34,9 @@ jobs: console.log(`Processing PR #${pr_number} for codeowner review requests`); + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + try { // Get the list of changed files in this PR const { data: files } = await github.rest.pulls.listFiles({ @@ -84,9 +87,9 @@ jobs: const allMentions = [...reviewerMentions, ...teamMentions].join(', '); if (isSuccessful) { - return `šŸ‘‹ Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! šŸ™`; + return `${BOT_COMMENT_MARKER}\nšŸ‘‹ Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! šŸ™`; } else { - return `šŸ‘‹ Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! šŸ™\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; + return `${BOT_COMMENT_MARKER}\nšŸ‘‹ Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! šŸ™\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; } } @@ -188,11 +191,11 @@ jobs: const previouslyPingedUsers = new Set(); const previouslyPingedTeams = new Set(); - // Look for comments from github-actions bot that contain codeowner pings + // Look for comments from github-actions bot that contain our bot marker const workflowComments = comments.filter(comment => comment.user.type === 'Bot' && comment.user.login === 'github-actions[bot]' && - comment.body.includes("I've automatically requested reviews from codeowners") + comment.body.includes(BOT_COMMENT_MARKER) ); // Extract previously mentioned users and teams from workflow comments diff --git a/requirements.txt b/requirements.txt index 6cc821e74c..69d40587ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.0.2 +aioesphomeapi==37.0.3 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import