Commit 3a94b76167 for woocommerce

commit 3a94b761675c60f371504f517f9b8af82fe1632e
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Thu Dec 18 18:00:51 2025 +0000

    Ensure PRs generated as part of the release process are milestoned (#62344)

diff --git a/.github/workflows/cherry-pick-milestoned-prs.yml b/.github/workflows/cherry-pick-milestoned-prs.yml
index c9a3c5942a..66eb1d2a1c 100644
--- a/.github/workflows/cherry-pick-milestoned-prs.yml
+++ b/.github/workflows/cherry-pick-milestoned-prs.yml
@@ -23,6 +23,7 @@ jobs:
       next_branch: ${{ steps.get-branches.outputs.next_branch }}
       pr_number: ${{ steps.set-vars.outputs.pr_number }}
       milestone: ${{ steps.set-vars.outputs.milestone }}
+      should_cherry_pick: ${{ steps.check-cherry-pick.outputs.should_cherry_pick }}
     steps:
       - name: debug
         run: |
@@ -37,6 +38,25 @@ jobs:
           echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
           echo "milestone=${{ github.event.pull_request.milestone.title }}" >> $GITHUB_OUTPUT

+      # Check if cherry-pick should proceed.
+      # Behavior can be overridden using a hidden comment in HTML. By default, cherry-pick is disabled for PRs with the 'Release' label.
+      - name: Check if cherry-pick should proceed
+        id: check-cherry-pick
+        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1
+        with:
+          script: |
+            const body  = context.payload.pull_request.body || '';
+
+            // Check for override comment first (takes precedence over everything)
+            // Format: <!-- cherry-pick-milestoned-prs: yes --> or <!-- cherry-pick-milestoned-prs: no -->
+            const match = body.match(/^<!--\s*cherry-pick-milestoned-prs:\s*(yes|no)\s*-->$/m);
+            if ( match ) {
+              core.setOutput( 'should_cherry_pick', match[1] === 'yes' ? 'true' : 'false' );
+              return;
+            }
+
+            core.setOutput( 'should_cherry_pick', ${{ ! contains(github.event.pull_request.labels.*.name, 'Release') }} ? 'true' : 'false' );
+
       # Extract base version from milestone (e.g., 9.9 from 9.9.0)
       - name: Extract version from milestone
         id: extract-version
@@ -93,11 +113,11 @@ jobs:

             core.setOutput('milestoned_branch', milestonedExists ? milestonedBranch : '');
             core.setOutput('next_branch', nextExists ? nextBranch : '');
-
+
   # Cherry-pick to milestoned branch
   cherry-pick-milestoned:
     needs: prepare
-    if: needs.prepare.outputs.milestoned_branch != ''
+    if: needs.prepare.outputs.milestoned_branch != '' && needs.prepare.outputs.should_cherry_pick == 'true'
     uses: ./.github/workflows/shared-cherry-pick.yml
     with:
       pr_number: ${{ needs.prepare.outputs.pr_number }}
@@ -107,7 +127,7 @@ jobs:
   # Cherry-pick to next branch
   cherry-pick-next:
     needs: prepare
-    if: needs.prepare.outputs.next_branch != ''
+    if: needs.prepare.outputs.next_branch != '' && needs.prepare.outputs.should_cherry_pick == 'true'
     uses: ./.github/workflows/shared-cherry-pick.yml
     with:
       pr_number: ${{ needs.prepare.outputs.pr_number }}
@@ -167,7 +187,7 @@ jobs:
               issue_number: parseInt('${{ needs.prepare.outputs.pr_number }}'),
               body
             });
-
+
       # Notify Slack about successful cherry-pick to milestoned branch
       - name: Notify Slack on success
         if: always()
@@ -237,7 +257,7 @@ jobs:
               issue_number: parseInt('${{ needs.prepare.outputs.pr_number }}'),
               body
             });
-
+
       # Notify Slack about successful cherry-pick to next branch
       - name: Notify Slack on success
         if: always()
@@ -299,7 +319,7 @@ jobs:
             const targetBranch = process.env.TARGET_BRANCH;
             const workflowLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;

-            const body =
+            const body =
               (merger ? merger + ' ' : '') +
               '❌ **Cherry-pick to `' + targetBranch + '` failed.**\n\n' +
               '**Error:** ' + errorMsg + '\n\n' +
@@ -329,8 +349,8 @@ jobs:
           slack-optional-unfurl_links: false
           slack-channel: ${{ secrets.WOO_RELEASE_SLACK_NOTIFICATION_CHANNEL }}
           slack-text: |
-            :warning: *Cherry pick from `trunk` to `${{ env.TARGET_BRANCH }}` <${{ env.WORKFLOW_RUN_URL }}|failed>*
-            Please resolve before releasing.
+            :warning: *Cherry pick from `trunk` to `${{ env.TARGET_BRANCH }}` <${{ env.WORKFLOW_RUN_URL }}|failed>*
+            Please resolve before releasing.
             Source PR: _<${{ env.SOURCE_PR_URL }}|#${{ env.SOURCE_PR_NUMBER }} - ${{ env.SOURCE_PR_TITLE }}>_.

   # Handle failed cherry-pick to next branch
@@ -379,7 +399,7 @@ jobs:
             const targetBranch = process.env.TARGET_BRANCH;
             const workflowLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;

-            const body =
+            const body =
               (merger ? merger + ' ' : '') +
               '❌ **Cherry-pick to `' + targetBranch + '` failed.**\n\n' +
               '**Error:** ' + errorMsg + '\n\n' +
@@ -409,6 +429,6 @@ jobs:
           slack-optional-unfurl_links: false
           slack-channel: ${{ secrets.WOO_RELEASE_SLACK_NOTIFICATION_CHANNEL }}
           slack-text: |
-            :warning: *Cherry pick from `trunk` to `${{ env.TARGET_BRANCH }}` <${{ env.WORKFLOW_RUN_URL }}|failed>*
-            Please resolve before releasing.
+            :warning: *Cherry pick from `trunk` to `${{ env.TARGET_BRANCH }}` <${{ env.WORKFLOW_RUN_URL }}|failed>*
+            Please resolve before releasing.
             Source PR: _<${{ env.SOURCE_PR_URL }}|#${{ env.SOURCE_PR_NUMBER }} - ${{ env.SOURCE_PR_TITLE }}>_.
diff --git a/.github/workflows/release-bump-version.yml b/.github/workflows/release-bump-version.yml
index f598b5520b..033db20514 100644
--- a/.github/workflows/release-bump-version.yml
+++ b/.github/workflows/release-bump-version.yml
@@ -27,10 +27,6 @@ on:
         description: Type of version bump to perform
         type: string
         required: true
-      milestone:
-        description: Milestone to set on the PR (optional)
-        type: string
-        required: false

 permissions:
   contents: write
@@ -119,6 +115,7 @@ jobs:
             core.setOutput( 'nextVersion', newVersion );
             core.setOutput( 'nextStable', newVersion.replace( /-(dev|(beta|rc)\.\d+)/, '' ) );
             core.setOutput( 'clearChangelog', '' === prerelease );
+            core.setOutput( 'prMilestone', `${ baseVersion }.0` );

       - name: Bump version in files
         env:
@@ -175,16 +172,19 @@ jobs:
             reviewer_arg="--reviewer ${{ github.actor }}"
           fi

-          # Set milestone argument (empty if not provided).
+          # Check milestone exists.
+          milestone="${{ steps.compute-new-version.outputs.prMilestone }}"
           milestone_arg=""
-          if [[ -n "${{ inputs.milestone }}" ]]; then
-            milestone_arg="--milestone ${{ inputs.milestone }}"
+          if [[ -n "$milestone" ]]; then
+            if [ "$(gh api repos/${{ github.repository }}/milestones --jq "any(.[]; .title==\"$milestone\")")" = "true" ]; then
+              milestone_arg="--milestone $milestone"
+            fi
           fi

           # Create PR
           gh pr create \
-            --title 'Bump WooCommerce version to ${{ steps.compute-new-version.outputs.nextVersion }}' \
-            --body 'This PR updates the versions in ${{ inputs.branch }} to ${{ steps.compute-new-version.outputs.nextVersion }}.' \
+            --title 'Bump WooCommerce version to `${{ steps.compute-new-version.outputs.nextVersion }}` on `${{ inputs.branch }}`' \
+            --body 'This PR updates the versions in ${{ inputs.branch }} to ${{ steps.compute-new-version.outputs.nextVersion }}.'$'\n\n''<!-- [x] This Pull Request does not require a changelog -->' \
             --base ${{ inputs.branch }} \
             --head ${branch_name} \
             --label Release \
diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml
index b9cd27ffef..1062dca44f 100644
--- a/.github/workflows/release-code-freeze.yml
+++ b/.github/workflows/release-code-freeze.yml
@@ -210,7 +210,6 @@ jobs:
     with:
       branch: trunk
       bump-type: dev
-      milestone: ${{ needs.prepare-for-feature-freeze.outputs.nextReleaseVersion }}

   build-dev-release:
     name: 'Build WooCommerce -dev release'
diff --git a/.github/workflows/release-new-release-published.yml b/.github/workflows/release-new-release-published.yml
index 068362b84e..7dd4a1b6a5 100644
--- a/.github/workflows/release-new-release-published.yml
+++ b/.github/workflows/release-new-release-published.yml
@@ -56,10 +56,10 @@ jobs:
           git fetch origin ${BRANCH_NAME}
           git checkout ${BRANCH_NAME}
           current_version=$( cat plugins/woocommerce/woocommerce.php | grep -oP '(?<=Version: )(.+)' | head -n1 )
-
+
           echo "Current version from woocommerce.php: '$current_version'"
           echo "Release tag version: '$RELEASE_TAG'"
-
+
           if [[ "$current_version" != "$RELEASE_TAG" ]]; then
             echo "::warning::The version in woocommerce.php ($current_version) does not match the release tag version ($RELEASE_TAG). Skipping notification."
             echo "versions_match=false" >> $GITHUB_OUTPUT
@@ -82,6 +82,10 @@ jobs:
     name: 'Update changelog.txt after any stable release'
     runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }}
     if: ${{ github.event.action == 'published' && ! ( contains( inputs.release_tag_name, '-dev' ) || contains( inputs.release_tag_name, '-beta' ) || contains( inputs.release_tag_name, '-rc' ) ) }}
+    permissions:
+      contents: write
+      pull-requests: write
+      issues: write
     steps:
       - name: 'Fetch release readme.txt'
         env:
@@ -110,12 +114,15 @@ jobs:
           php ./trunk/.github/workflows/scripts/release-readme-to-changelog.php ./woocommerce/readme.txt ./trunk/changelog.txt
       - name: 'Push changes and open PR'
         env:
-          GH_TOKEN: ${{ github.token }}
+          GH_TOKEN: ${{ secrets.WC_BOT_PR_CREATE_TOKEN || secrets.GITHUB_TOKEN }}
           GH_REPO: ${{ github.repository }}
         run: |
+          # Determine milestone from release version.
+          milestone=$( cat ./woocommerce/woocommerce.php | grep -oP '(?<=Version: )(.+)' | head -n1 | sed 's/\.[0-9]\+$/.0/' )
+
           # Configure Git.
-          git config --global user.name "github-actions[bot]"
-          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
+          git config --global user.name "woocommercebot"
+          git config --global user.email "woocommercebot@users.noreply.github.com"

           # Push new changelog.txt.
           branch_name="update-changelog-${{ inputs.release_tag_name }}-$(date +%s)"
@@ -132,4 +139,6 @@ jobs:
             --body 'This PR updates the global changelog.txt file following the release of version ${{ inputs.release_tag_name }}.' \
             --base trunk \
             --head "$branch_name" \
-            --reviewer "${{ github.actor }}"
+            --reviewer "${{ github.actor }}" \
+            --label "Release" \
+            --milestone "$milestone"
diff --git a/.github/workflows/release-update-stable-tag.yml b/.github/workflows/release-update-stable-tag.yml
index 715f622d21..b07a5edc60 100644
--- a/.github/workflows/release-update-stable-tag.yml
+++ b/.github/workflows/release-update-stable-tag.yml
@@ -275,15 +275,20 @@ jobs:
             git commit -m "Update stable tag to $TARGET_VERSION on $BRANCH_NAME"
             git push origin "$UPDATE_BRANCH"

+            # Determine milestone from woocommerce.php version.
+            milestone=$( cat ./plugins/woocommerce/woocommerce.php | grep -oP '(?<=Version: )(.+)' | head -n1 | sed 's/\.[0-9]\+\(-dev\)\?$/.0/' )
+            echo "Using milestone: '$milestone'."
+
             gh pr create \
               --title "Update stable tag to $TARGET_VERSION ($BRANCH_NAME)" \
               --body "This PR updates the stable tag to $TARGET_VERSION." \
               --base "$BRANCH_NAME" \
               --head "$UPDATE_BRANCH" \
               --reviewer ${{ github.actor }} \
-              --label Release
+              --label Release \
+              --milestone "$milestone"

-            echo "✅ Successfully created PR to update stable tag to $TARGET_VERSION on $BRANCH_NAME"
+            echo "✅ Successfully created PR to update stable tag to $TARGET_VERSION on $BRANCH_NAME with milestone $milestone"
           fi

   notify-release:
diff --git a/tools/monorepo-utils/src/code-freeze/commands/changelog/lib/index.ts b/tools/monorepo-utils/src/code-freeze/commands/changelog/lib/index.ts
index 5c5e385f33..23bef09f4a 100644
--- a/tools/monorepo-utils/src/code-freeze/commands/changelog/lib/index.ts
+++ b/tools/monorepo-utils/src/code-freeze/commands/changelog/lib/index.ts
@@ -14,6 +14,7 @@ import { Logger } from '../../../../core/logger';
 import { checkoutRemoteBranch } from '../../../../core/git';
 import {
 	addLabelsToIssue,
+	addMilestoneToIssue,
 	createPullRequest,
 } from '../../../../core/github/repo';
 import { Options } from '../types';
@@ -296,6 +297,18 @@ export const updateReleaseBranchChangelogs = async (
 			);
 		}

+		try {
+			await addMilestoneToIssue(
+				options,
+				pullRequest.number,
+				`${ mainVersion }.0`
+			);
+		} catch {
+			Logger.warn(
+				`Could not add milestone "${ mainVersion }.0" to PR ${ pullRequest.number }`
+			);
+		}
+
 		return {
 			deletionCommitHash,
 			prNumber: pullRequest.number,
@@ -350,6 +363,18 @@ export const updateBranchChangelog = async (
 			[ branch ]: null,
 		} );

+		// Read plugin file version in branch to determine milestone.
+		let milestone = '';
+		const pluginFile = readFileSync(
+			path.join( tmpRepoPath, 'plugins/woocommerce/woocommerce.php' ),
+			'utf8'
+		);
+		const m = pluginFile.match( /\*\s+Version:\s+(\d+\.\d+)\.\d+/ );
+
+		if ( m ) {
+			milestone = `${ m[ 1 ] }.0`;
+		}
+
 		try {
 			await git.raw( [ 'cherry-pick', deletionCommitHash ] );
 		} catch ( e ) {
@@ -390,6 +415,14 @@ export const updateBranchChangelog = async (
 			);
 		}

+		try {
+			await addMilestoneToIssue( options, pullRequest.number, milestone );
+		} catch {
+			Logger.warn(
+				`Could not add milestone "${ milestone }" to PR ${ pullRequest.number }`
+			);
+		}
+
 		return pullRequest.number;
 	} catch ( e ) {
 		if ( e.message.includes( `No commits between ${ releaseBranch }` ) ) {
diff --git a/tools/monorepo-utils/src/core/github/repo.ts b/tools/monorepo-utils/src/core/github/repo.ts
index 25dca3ec1c..3d14897d41 100644
--- a/tools/monorepo-utils/src/core/github/repo.ts
+++ b/tools/monorepo-utils/src/core/github/repo.ts
@@ -249,18 +249,55 @@ export const addLabelsToIssue = async (
 	);
 };

+export const addMilestoneToIssue = async (
+	options: {
+		owner?: string;
+		name?: string;
+	},
+	issueNumber: number,
+	milestoneName: string
+): Promise< void > => {
+	const { owner, name } = options;
+
+	// Try to find milestone by name.
+	const { data } = await octokitWithAuth().request(
+		'GET /repos/{owner}/{repo}/milestones',
+		{
+			owner,
+			repo: name,
+			state: 'all',
+			direction: 'desc',
+			per_page: 100,
+		}
+	);
+
+	const milestone = data.find( ( m ) => m.title === milestoneName );
+
+	if ( milestone ) {
+		await octokitWithAuth().request(
+			'PATCH /repos/{owner}/{repo}/issues/{issue_number}',
+			{
+				owner,
+				repo: name,
+				issue_number: issueNumber,
+				milestone: milestone.number,
+			}
+		);
+	}
+};
+
 /**
  * Create a pull request from branches on GitHub.
  *
- * @param {Object} options       pull request options.
- * @param {string} options.head  branch name containing the changes you want to merge.
- * @param {string} options.base  branch name you want the changes pulled into.
- * @param {string} options.owner repository owner.
- * @param {string} options.name  repository name.
- * @param {string} options.title pull request title.
- * @param {string} options.body  pull request body.
- * @return {Promise<object>}     pull request data.
+ * @param {Object}   options           pull request options.
+ * @param {string}   options.head      branch name containing the changes you want to merge.
+ * @param {string}   options.base      branch name you want the changes pulled into.
+ * @param {string}   options.owner     repository owner.
+ * @param {string}   options.name      repository name.
+ * @param {string}   options.title     pull request title.
+ * @param {string}   options.body      pull request body.
  * @param {string[]} options.reviewers list of GitHub usernames to request a review from.
+ * @return {Promise<object>}     pull request data.
  */
 export const createPullRequest = async ( options: {
 	head: string;
@@ -284,14 +321,18 @@ export const createPullRequest = async ( options: {
 		}
 	);

-	if ( reviewers && reviewers.length > 0 ) {
+	const filteredReviewers = reviewers?.filter(
+		( reviewer ) => reviewer !== pullRequest.data.user.login
+	);
+
+	if ( filteredReviewers && filteredReviewers.length > 0 ) {
 		await octokitWithAuth().request(
 			'POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers',
 			{
 				owner,
 				repo: name,
 				pull_number: pullRequest.data.number,
-				reviewers,
+				reviewers: filteredReviewers,
 			}
 		);
 	}