Commit 3d301a5a17 for woocommerce

commit 3d301a5a1769dd1b05c7c10bb4be15e15b266657
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Thu Jan 8 08:58:23 2026 +0000

    Improve notification about open items in release milestones (#62437)

diff --git a/.github/workflows/release-open-issue-warning.yml b/.github/workflows/release-open-issue-warning.yml
index 4137f66327..abb67c6ba2 100644
--- a/.github/workflows/release-open-issue-warning.yml
+++ b/.github/workflows/release-open-issue-warning.yml
@@ -12,6 +12,12 @@ jobs:
   check-upcoming-release-events:
     name: Check for upcoming release events
     runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }}
+    outputs:
+      version: ${{ steps.check-upcoming-release-build.outputs.version }}
+      milestone: ${{ steps.check-upcoming-release-build.outputs.milestone }}
+      release-event-found: ${{ steps.check-upcoming-release-build.outputs.release-event-found }}
+      release-event-date: ${{ steps.check-upcoming-release-build.outputs.release-event-date }}
+      release-event-title: ${{ steps.check-upcoming-release-build.outputs.release-event-title }}
     steps:
       - name: Install node-ical
         run: npm install node-ical
@@ -33,15 +39,15 @@ jobs:
               const upcomingReleaseEvents = Object.values(events)
                 .filter(e => {
                   if (e.type !== 'VEVENT') return false;
-
+
                   if (!e.start) return false;
-
+
                   const startDate = new Date(e.start);
                   if (isNaN(startDate.getTime())) return false;
-
+
                   const diff = startDate - now;
                   if (diff < 0 || diff > eventWindowInMs) return false; // Must be between now and 72h from now
-
+
                   return pattern.test(e.summary || '');
                 })
                 .sort((a, b) => new Date(a.start) - new Date(b.start));
@@ -49,11 +55,14 @@ jobs:
               if (upcomingReleaseEvents.length > 0) {
                 const date = upcomingReleaseEvents[0].start.toISOString();
                 const version = upcomingReleaseEvents[0].summary.match(/\d+\.\d+/)[0];
+                const milestone = `${version}.0`;
                 console.log(`Found Release event: ${upcomingReleaseEvents[0].summary}`);
                 console.log(`Date: ${date}`);
                 console.log(`Version: ${version}`);
+                console.log(`Milestone: ${milestone}`);
                 core.setOutput('release-event-found', 'true');
                 core.setOutput('version', version);
+                core.setOutput('milestone', milestone);
                 core.setOutput('release-event-date', date);
                 core.setOutput('release-event-title', upcomingReleaseEvents[0].summary);
               } else {
@@ -63,104 +72,210 @@ jobs:
             } catch (error) {
               core.setFailed(`Failed to fetch calendar events: ${error.message}`);
             }
-      - name: Build Slack Message with Open Milestoned Issues
-        id: get-milestone-items
-        if: ${{ steps.check-upcoming-release-build.outputs.release-event-found == 'true' }}
+
+  check-milestones:
+    name: Check all open milestones
+    needs: check-upcoming-release-events
+    runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }}
+    steps:
+      - name: Get and process open milestones
+        id: process-milestones
         uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1
         with:
           script: |
-            const version = '${{ steps.check-upcoming-release-build.outputs.version }}';
-            const milestone = `${version}.0`;
-
-            console.log(`Searching for open issues and PRs in milestone: ${milestone}`);
-
-            try {
-              // Search for issues in the milestone
-              const searchQuery = `milestone:"${milestone}" state:open repo:${context.repo.owner}/${context.repo.repo}`;
-
-              const searchResults = await github.rest.search.issuesAndPullRequests({
-                q: searchQuery,
-                per_page: 20,
-                advanced_search: true
-              });
-
-              console.log(`Found ${searchResults.data.total_count} open items in milestone ${milestone}`);
-
-              if (searchResults.data.total_count === 0) {
-                core.setOutput('slack-message', '');
-                core.setOutput('has-items', 'false');
-                return;
+            /**
+              * Formats an item (issue or PR) into a string for a Slack message.
+              *
+              * @param {Object} item - The item to format.
+              * @returns {string} The formatted item.
+              */
+            const formatItem = async (item) => {
+              let waitingOn = [];
+
+              // Get assignees.
+              if (item.assignees && item.assignees.length > 0) {
+                waitingOn.push(...item.assignees.map(a => a.login));
               }
-
-              const items = [];
-
-              // Process each item to get detailed information
-              for (const item of searchResults.data.items) {
-                let waitingOn = [];
-
-                // Get assignees
-                if (item.assignees && item.assignees.length > 0) {
-                  waitingOn.push(...item.assignees.map(a => a.login));
+
+              // For PRs, also get requested reviewers.
+              if (item.pull_request) {
+                try {
+                  const reviewers = await github.rest.pulls.listRequestedReviewers({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    pull_number: item.number
+                  });
+
+                  if (reviewers.data.users && reviewers.data.users.length > 0) {
+                    waitingOn.push(...reviewers.data.users.map(r => r.login));
+                  }
+                } catch (error) {
+                  console.log(`Failed to get reviewers for PR #${item.number}: ${error.message}`);
                 }
-
-                // For PRs, also get requested reviewers
-                if (item.pull_request) {
-                  try {
-                    const reviewers = await github.rest.pulls.listRequestedReviewers({
-                      owner: context.repo.owner,
-                      repo: context.repo.repo,
-                      pull_number: item.number
-                    });
-
-                    if (reviewers.data.users && reviewers.data.users.length > 0) {
-                      waitingOn.push(...reviewers.data.users.map(r => r.login));
-                    }
-                  } catch (error) {
-                    console.log(`Failed to get reviewers for PR #${item.number}: ${error.message}`);
+              }
+
+              const uniqueWaitingOn = [...new Set(waitingOn)];
+              const createdDate = new Date(item.created_at).toISOString().split('T')[0];
+              const author = item.user ? item.user.login : 'unknown';
+
+              const waitingOnText = uniqueWaitingOn.length > 0 ?
+                `Waiting on ${uniqueWaitingOn.join(', ')}` :
+                'No assignees/reviewers';
+
+              const itemUrl = item.pull_request
+                ? `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${item.number}`
+                : `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${item.number}`;
+
+              return `• <${itemUrl}|#${item.number}: ${item.title}> (${author})\n  Created ${createdDate} · ${waitingOnText}`;
+            }
+
+            const releaseEventFound = '${{ needs.check-upcoming-release-events.outputs.release-event-found }}' === 'true';
+            const upcomingMilestone = '${{ needs.check-upcoming-release-events.outputs.milestone }}';
+            const releaseEventTitle = '${{ needs.check-upcoming-release-events.outputs.release-event-title }}';
+            const releaseEventDate  = '${{ needs.check-upcoming-release-events.outputs.release-event-date }}';
+
+            /**
+             * Fetches and filters milestones to only include those with a release branch and valid version.
+             *
+             * @returns {Array} The filtered milestones.
+             */
+            const versionPattern = /^(\d+\.\d+)\.0$/;
+            const getReleaseMilestones = async () => {
+              const milestones = await github.rest.issues.listMilestones( {
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                state: 'open',
+                per_page: 10
+              } );
+
+              const result = [];
+
+              for ( const milestone of milestones.data ) {
+                // Skip if not in format "X.Y.0".
+                if ( ! versionPattern.test( milestone.title ) ) {
+                  continue;
+                }
+
+                // Extract X.Y from milestone title.
+                const version = milestone.title.match( versionPattern )[1];
+
+                // Skip if there isn't a release branch associated.
+                try {
+                  await github.rest.repos.getBranch( {
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    branch: `release/${version}`
+                  } );
+                } catch ( error ) {
+                  if ( 404 === error.status ) {
+                    console.log( `Skipping milestone ${milestone.title} - release branch does not exist.` );
+                    continue;
+                  } else {
+                    throw error;
                   }
                 }
-
-                const uniqueWaitingOn = [...new Set(waitingOn)];
-                const createdDate = new Date(item.created_at).toISOString().split('T')[0];
-                const author = item.user ? item.user.login : 'unknown';
-
-                const waitingOnText = uniqueWaitingOn.length > 0 ?
-                  `Waiting on ${uniqueWaitingOn.join(', ')}` :
-                  'No assignees/reviewers';
-
-                const itemUrl = item.pull_request
-                  ? `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${item.number}`
-                  : `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${item.number}`;
-
-                const formattedItem = `• [#${item.number}] <${itemUrl}|${item.title}> (${author})\n  Created ${createdDate} · ${waitingOnText}`;
-
-                items.push(formattedItem);
+
+                // Check the plugin version on the release branch.
+                try {
+                  const fileResponse = await github.rest.repos.getContent( {
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    path: 'plugins/woocommerce/woocommerce.php',
+                    ref: `release/${version}`
+                  } );
+
+                  const content = Buffer.from( fileResponse.data.content, 'base64' ).toString( 'utf8' );
+                  const versionMatch = content.match( /^\s*\*\s*Version:\s*(.+)$/m );
+
+                  if ( ! versionMatch ) {
+                    console.log( `Could not find version in ${milestone.title}` );
+                    continue;
+                  }
+
+                  const pluginVersion = versionMatch[1].trim().toLowerCase();
+                  const isPreRelease = /dev|rc|beta/.test( pluginVersion );
+
+                  // Skip pre-release versions unless they match the upcoming milestone.
+                  if ( isPreRelease && milestone.title !== upcomingMilestone ) {
+                    console.log( `Skipping milestone ${milestone.title} - pre-release version (${pluginVersion}) and not upcoming.` );
+                    continue;
+                  }
+                } catch ( error ) {
+                  console.log( `Could not check plugin version for ${milestone.title}: ${error.message}` );
+                  continue;
+                }
+
+                result.push( {
+                  title: milestone.title,
+                  number: milestone.number,
+                  version: parseFloat( version )
+                } );
               }

-              const releaseEventTitle = '${{ steps.check-upcoming-release-build.outputs.release-event-title }}';
-              const releaseEventDate = '${{ steps.check-upcoming-release-build.outputs.release-event-date }}';
-              const formattedDate = new Date(releaseEventDate).toISOString().split('T')[0];
-
-              // Build the complete Slack message
-              const slackMessage = [
-                `🚨 Milestone ${milestone} has ${searchResults.data.total_count} open items for ${releaseEventTitle} on ${formattedDate}\n`,
-                ...items
-              ].join('\n');
-
-              core.setOutput('slack-message', slackMessage);
-              core.setOutput('has-items', 'true');
-              console.log(`slack-message: ${slackMessage}`);
-
-            } catch (error) {
-              console.error(`Error fetching milestone items: ${error.message}`);
-              core.setFailed(`Failed to fetch milestone items: ${error.message}`);
+              result.sort( (a, b) => b.version - a.version );
+
+              return result;
+            };
+
+            const releaseMilestones = await getReleaseMilestones();
+
+            // Build report.
+            const milestoneReports = [];
+            for ( const milestone of releaseMilestones ) {
+              const milestoneResults = await github.rest.search.issuesAndPullRequests( {
+                q: `milestone:"${milestone.title}" state:open repo:${context.repo.owner}/${context.repo.repo}`,
+                per_page: 6
+              } );
+
+              const totalCount = milestoneResults.data.total_count;
+              const milestoneItems = milestoneResults.data.items;
+              console.log( `Found ${totalCount} open items for milestone ${milestone.title}` );
+
+              if ( totalCount === 0 ) {
+                continue;
+              }
+
+              // Build the report for this milestone.
+              let reportLines = [];
+
+              reportLines.push( `*${milestone.title}*` );
+
+              if ( releaseEventFound && milestone.title === upcomingMilestone ) {
+                // Milestone matches upcoming release.
+                const formattedDate = new Date( releaseEventDate ).toISOString().split( 'T' )[0];
+                reportLines.push( `  _*${releaseEventTitle}* coming up on *${formattedDate}*_` );
+              }
+
+              const shouldPaginate = totalCount >= 6;
+              const itemsToShow    = shouldPaginate ? milestoneItems.slice( 0, 5 ) : milestoneItems;
+              reportLines.push( ...( await Promise.all( itemsToShow.map( formatItem ) ) ) );
+
+              if ( shouldPaginate ) {
+                const remainingCount = totalCount - 5;
+                const milestoneUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/milestone/${milestone.number}`;
+                reportLines.push( `• <${milestoneUrl}|... and *${remainingCount}* more items>.` );
+              }
+
+              milestoneReports.push( reportLines.join( '\n' ) );
+            }
+
+            if ( milestoneReports.length > 0 ) {
+              const fullReport = [];
+              fullReport.push( `:fyi: The following release milestones have open items:` );
+              fullReport.push( '\n\n' );
+              fullReport.push( milestoneReports.join( '\n\n' ) );
+
+              core.setOutput( 'slack-message', fullReport.join( '' ) );
+            } else {
+              core.setOutput( 'slack-message', '' );
             }

+
       - name: Send Slack Message
-        if: ${{ steps.get-milestone-items.outputs.has-items == 'true' }}
+        if: ${{ steps.process-milestones.outputs.slack-message != '' }}
         uses: archive/github-actions-slack@c643e5093620d65506466f2c9b317d5d29a5e517 # v2.10.1
         env:
-          SLACK_MESSAGE: ${{ steps.get-milestone-items.outputs.slack-message }}
+          SLACK_MESSAGE: ${{ steps.process-milestones.outputs.slack-message }}
         with:
           slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
           slack-optional-unfurl_links: false