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/esphome/components/api/api.proto b/esphome/components/api/api.proto index 546c498ff3..fd08e87bbf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -203,7 +203,7 @@ message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; - bool uses_password = 1; + bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"]; // The name of the node, given by "App.set_name()" string name = 2; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 602a0256cf..bc0afd49eb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1432,8 +1432,6 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; #ifdef USE_API_PASSWORD resp.uses_password = true; -#else - resp.uses_password = false; #endif resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4cf4b63269..528c581ad7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -80,7 +80,9 @@ void DeviceInfo::calculate_size(uint32_t &total_size) const { } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_API_PASSWORD buffer.encode_bool(1, this->uses_password); +#endif buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -130,7 +132,9 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { +#ifdef USE_API_PASSWORD ProtoSize::add_bool_field(total_size, 1, this->uses_password); +#endif ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->mac_address); ProtoSize::add_string_field(total_size, 1, this->esphome_version); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e241451ec8..7b64bd889f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -474,7 +474,9 @@ class DeviceInfoResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif +#ifdef USE_API_PASSWORD bool uses_password{false}; +#endif std::string name{}; std::string mac_address{}; std::string esphome_version{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index bda5ec5764..4951c6cebf 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -647,10 +647,12 @@ void DeviceInfo::dump_to(std::string &out) const { void DeviceInfoResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfoResponse {\n"); +#ifdef USE_API_PASSWORD out.append(" uses_password: "); out.append(YESNO(this->uses_password)); out.append("\n"); +#endif out.append(" name: "); out.append("'").append(this->name).append("'"); out.append("\n"); 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