Commit 0cd99b90cc5 for woocommerce

commit 0cd99b90cc5d168fdc6404511c2e6682a97b00a3
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Thu Sep 3 15:35:41 2026 +0300

    Replace abandoned node12 reviewer actions with a composite action (#68126)

diff --git a/.github/actions/assign-reviewers/action.yml b/.github/actions/assign-reviewers/action.yml
new file mode 100644
index 00000000000..e129311a59c
--- /dev/null
+++ b/.github/actions/assign-reviewers/action.yml
@@ -0,0 +1,28 @@
+name: 'Assign reviewers'
+description: 'Request reviews on a pull request, either from the owners of the files it changes or from the teams its author belongs to. Expects a Linux runner: glob matching follows the runner platform, and is case-insensitive on macOS and Windows.'
+
+inputs:
+  mode:
+    description: 'How to pick reviewers: `changed-files` matches the config keys as globs against the changed files, `author-team` matches them as team slugs against the author.'
+    required: true
+  config:
+    description: 'Path to the reviewer config, relative to the repository root. A JSON object mapping each key to a reviewer, or a list of them: a team slug, or `@login` for a person.'
+    required: true
+  github-token:
+    description: 'Token used to look up teams and request reviews. Needs to be able to request team reviews.'
+    required: true
+
+runs:
+  using: 'composite'
+  steps:
+    - name: Request the reviews
+      uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+      env:
+        CONFIG_FILE: ${{ inputs.config }}
+        MODE: ${{ inputs.mode }}
+        SCRIPT: ${{ github.action_path }}/assign.js
+      with:
+        github-token: ${{ inputs.github-token }}
+        retries: 2
+        script: |
+          await require( process.env.SCRIPT ).assignReviewers( { github, context, core } );
diff --git a/.github/actions/assign-reviewers/assign.js b/.github/actions/assign-reviewers/assign.js
new file mode 100644
index 00000000000..a04e028b19b
--- /dev/null
+++ b/.github/actions/assign-reviewers/assign.js
@@ -0,0 +1,222 @@
+/**
+ * Reviewer assignment for .github/workflows/automate-team-review-assignment.yml.
+ *
+ * Run the checks with:
+ *   node .github/actions/assign-reviewers/test.js
+ */
+
+const fs = require( 'fs' );
+const path = require( 'path' );
+
+// path.matchesGlob() has no `dot` option, so a leaf such as
+// `packages/js/components/.npmrc` would never match `**/*`. Renaming every
+// segment-leading dot on both sides makes those segments ordinary again.
+// The one case this cannot reproduce is a wildcard matching the empty string
+// before a literal dot, so a file named exactly `.js` does not match
+// `*{.js,.ts}`. Beware too when testing a pattern locally: path.matchesGlob()
+// is case-insensitive on macOS and Windows, and case-sensitive on Linux.
+const undot = ( value ) => value.replace( /(^|\/)\./g, '$1__dot__' );
+
+// Whether a dot inside `{...}` starts a segment depends on where the group
+// sits, so expand the groups first and rename each result on its own.
+const expand = ( pattern ) => {
+	const group = pattern.match( /\{([^{}]*)\}/ );
+
+	if ( ! group ) {
+		return [ undot( pattern ) ];
+	}
+
+	return group[ 1 ].split( ',' ).flatMap( ( alternative ) => expand(
+		pattern.slice( 0, group.index ) + alternative + pattern.slice( group.index + group[ 0 ].length )
+	) );
+};
+
+/**
+ * Whether a pattern matches any of the given paths.
+ *
+ * @param {string}   pattern  A config key, as a glob.
+ * @param {string[]} undotted Paths already passed through undot().
+ * @return {boolean} True when at least one path matches.
+ */
+const matchesAny = ( pattern, undotted ) => {
+	const globs = expand( pattern );
+
+	return undotted.some( ( file ) => globs.some( ( glob ) => path.matchesGlob( file, glob ) ) );
+};
+
+/**
+ * Reads the config and returns a lookup for the reviewers a key names.
+ *
+ * A value is one reviewer or a list of them. A bare value is a team slug,
+ * `@login` marks a person. Every key is checked on load, so a bad edit fails
+ * on the first run rather than when a pull request first touches its area.
+ *
+ * @param {string} file Path to the config, relative to the workspace.
+ * @return {{ config: Object, owners: Function }} The parsed config and its lookup.
+ */
+const readConfig = ( file ) => {
+	const config = JSON.parse( fs.readFileSync( file, 'utf8' ) );
+
+	if ( ! config || typeof config !== 'object' || Array.isArray( config ) ) {
+		throw new Error( 'The reviewer config must be a JSON object.' );
+	}
+
+	const owners = ( key ) => {
+		const list = [].concat( config[ key ] );
+
+		if ( ! list.length || list.some( ( owner ) => typeof owner !== 'string' || ! owner ) ) {
+			throw new Error( `Config key '${ key }' does not name a reviewer.` );
+		}
+
+		return list;
+	};
+
+	Object.keys( config ).forEach( owners );
+
+	return { config, owners };
+};
+
+/**
+ * Requests reviews on the pull request that triggered the workflow.
+ *
+ * @param {Object} toolkit         The github-script toolkit.
+ * @param {Object} toolkit.github  A configured octokit.
+ * @param {Object} toolkit.context The workflow context.
+ * @param {Object} toolkit.core    The actions core helpers.
+ * @return {Promise<void>} Resolves once the reviews are requested.
+ */
+const assignReviewers = async ( { github, context, core } ) => {
+	const { config, owners } = readConfig( process.env.CONFIG_FILE );
+	const author = context.payload.pull_request.user.login;
+	const pull = {
+		owner: context.repo.owner,
+		repo: context.repo.repo,
+		pull_number: context.payload.pull_request.number,
+	};
+
+	// Both routers return the config values of the keys they matched. Every
+	// match counts, so a pull request spanning several areas collects every
+	// owner.
+	const byChangedFiles = async () => {
+		const files = await github.paginate( github.rest.pulls.listFiles, { ...pull, per_page: 100 } );
+		const changed = files.map( ( file ) => undot( file.filename ) );
+
+		return Object.keys( config )
+			.filter( ( pattern ) => matchesAny( pattern, changed ) )
+			.flatMap( owners );
+	};
+
+	const byAuthorTeam = async () => {
+		const isOnTeam = async ( team ) => {
+			try {
+				const { data } = await github.rest.teams.getMembershipForUserInOrg( {
+					org: context.repo.owner,
+					team_slug: team,
+					username: author,
+				} );
+
+				return data.state !== 'pending';
+			} catch ( error ) {
+				const message = `Could not read ${ author }'s membership of ${ team }: ${ error.message }`;
+
+				// 404 just means "not a member". Anything else means the lookup
+				// failed, so the routing below would quietly miss a team. Say so
+				// rather than assigning nobody on a green run.
+				if ( error.status === 404 ) {
+					core.info( message );
+				} else {
+					core.setFailed( message );
+				}
+
+				return false;
+			}
+		};
+
+		const matched = [];
+
+		for ( const team of Object.keys( config ) ) {
+			if ( await isOnTeam( team ) ) {
+				matched.push( ...owners( team ) );
+			}
+		}
+
+		return matched;
+	};
+
+	const routers = { 'changed-files': byChangedFiles, 'author-team': byAuthorTeam };
+	const mode = process.env.MODE;
+
+	if ( ! Object.hasOwn( routers, mode ) ) {
+		throw new Error( `Unknown mode '${ mode }'.` );
+	}
+
+	const reviewers = new Set();
+	const teams = new Set();
+
+	for ( const owner of await routers[ mode ]() ) {
+		if ( ! owner.startsWith( '@' ) ) {
+			teams.add( owner );
+		} else if ( owner.slice( 1 ).toLowerCase() !== author.toLowerCase() ) {
+			reviewers.add( owner.slice( 1 ) );
+		}
+	}
+
+	const matched = [ ...teams, ...[ ...reviewers ].map( ( reviewer ) => `@${ reviewer }` ) ];
+
+	core.info( `Matched reviewers: ${ matched.join( ', ' ) || 'nobody' }.` );
+
+	if ( ! reviewers.size && ! teams.size ) {
+		return;
+	}
+
+	const request = ( body ) => github.rest.pulls.requestReviewers( { ...pull, ...body } );
+
+	try {
+		await request( { reviewers: [ ...reviewers ], team_reviewers: [ ...teams ] } );
+
+		return;
+	} catch ( error ) {
+		// 422 is the API rejecting the whole batch over a single bad entry, so
+		// ask one at a time instead of losing every reviewer to it. Any other
+		// status is about the token or the rate limit, and asking again would
+		// only spend more of the rate limit on the same answer.
+		if ( error.status !== 422 ) {
+			throw error;
+		}
+
+		core.warning( `Could not request the reviews together: ${ error.message }` );
+	}
+
+	const rejected = [];
+	const failed = [];
+
+	const alone = async ( body, name ) => request( body ).then(
+		() => {},
+		( error ) => {
+			// 422 is the API saying it will not accept this reviewer, which means
+			// the config names somebody who no longer exists or lost access. Every
+			// other status is about the token or the rate limit, not the config.
+			( error.status === 422 ? rejected : failed ).push( name );
+
+			core.warning( `Could not request a review from ${ name }: ${ error.message }` );
+		}
+	);
+
+	for ( const team of teams ) {
+		await alone( { team_reviewers: [ team ] }, team );
+	}
+
+	for ( const reviewer of reviewers ) {
+		await alone( { reviewers: [ reviewer ] }, reviewer );
+	}
+
+	if ( rejected.length ) {
+		core.setFailed( `The API rejected these reviewers, check the config: ${ rejected.join( ', ' ) }.` );
+	}
+
+	if ( failed.length ) {
+		core.setFailed( `Could not request a review from: ${ failed.join( ', ' ) }.` );
+	}
+};
+
+module.exports = { assignReviewers, undot, matchesAny };
diff --git a/.github/actions/assign-reviewers/test.js b/.github/actions/assign-reviewers/test.js
new file mode 100644
index 00000000000..7c50e22d6b3
--- /dev/null
+++ b/.github/actions/assign-reviewers/test.js
@@ -0,0 +1,306 @@
+/**
+ * Checks for assign.js. Plain node, no dependencies:
+ *   node .github/actions/assign-reviewers/test.js
+ *
+ * The glob cases below were all confirmed against minimatch 3.1.2 with
+ * { dot: true }, the call the replaced shufo/auto-assign-reviewer-by-files
+ * action made. Run this on Linux: path.matchesGlob() is case-insensitive on
+ * macOS and Windows, so a case-sensitive pattern passes there when it should
+ * not.
+ */
+
+const assert = require( 'assert' );
+const fs = require( 'fs' );
+const os = require( 'os' );
+const path = require( 'path' );
+
+const { assignReviewers, undot, matchesAny } = require( './assign.js' );
+
+const workspace = fs.mkdtempSync( path.join( os.tmpdir(), 'assign-reviewers-' ) );
+let configs = 0;
+const configFile = ( contents ) => {
+	const file = path.join( workspace, `config-${ ( configs += 1 ) }.json` );
+	fs.writeFileSync( file, JSON.stringify( contents ) );
+
+	return file;
+};
+
+const matches = ( pattern, file ) => matchesAny( pattern, [ undot( file ) ] );
+
+const ran = [];
+const check = ( name, run ) => ran.push( { name, run } );
+
+/* Glob matching. */
+
+check( 'a wildcard matches a dotfile leaf, as minimatch did with { dot: true }', () => {
+	assert.ok( matches( 'packages/js/**/*', 'packages/js/components/.npmrc' ) );
+	assert.ok( matches( 'docs/**/*', 'docs/_docu-tools/static/.nojekyll' ) );
+} );
+
+check( 'a dot opening a brace alternative still matches', () => {
+	// Regression: renaming dots after `{` and `,` broke this pattern.
+	const pattern = 'plugins/woocommerce/{.wordpress-org,i18n}/**/*';
+	assert.ok( matches( pattern, 'plugins/woocommerce/.wordpress-org/icon-128x128.png' ) );
+	assert.ok( matches( pattern, 'plugins/woocommerce/i18n/states.php' ) );
+} );
+
+check( 'a brace group of extensions is not treated as a segment boundary', () => {
+	// Regression: the fix for the case above must not break this one.
+	assert.ok( matches( 'src/**/*{.js,.ts}', 'src/a.js' ) );
+	assert.ok( matches( 'src/**/*{.js,.ts}', 'src/.hidden.js' ) );
+	assert.ok( ! matches( 'src/**/*{.js,.ts}', 'src/a.css' ) );
+} );
+
+check( 'a single star does not cross a directory boundary', () => {
+	assert.ok( matches( 'plugins/woocommerce/templates/*', 'plugins/woocommerce/templates/a.php' ) );
+	assert.ok( ! matches( 'plugins/woocommerce/templates/*', 'plugins/woocommerce/templates/cart/a.php' ) );
+} );
+
+check( 'a double star matches at any depth, including none', () => {
+	assert.ok( matches( 'a/**/*', 'a/b' ) );
+	assert.ok( matches( 'a/**/*', 'a/b/c/d' ) );
+	assert.ok( ! matches( 'a/**/*', 'a' ) );
+} );
+
+check( 'nested brace groups expand', () => {
+	assert.ok( matches( 'a/{b,{c,d}}/*', 'a/d/e.php' ) );
+	assert.ok( ! matches( 'a/{b,{c,d}}/*', 'a/e/f.php' ) );
+} );
+
+check( 'the shipped configs still parse and name reviewers', () => {
+	const community = path.join( __dirname, '../../project-community-pr-assigner.json' );
+	const teams = path.join( __dirname, '../../automate-team-review-assignment-config.json' );
+
+	for ( const file of [ community, teams ] ) {
+		const config = JSON.parse( fs.readFileSync( file, 'utf8' ) );
+		assert.ok( Object.keys( config ).length, `${ file } is empty` );
+
+		for ( const [ key, value ] of Object.entries( config ) ) {
+			for ( const owner of [].concat( value ) ) {
+				assert.ok( typeof owner === 'string' && owner, `${ key } does not name a reviewer` );
+			}
+		}
+	}
+} );
+
+/* Requesting the reviews. */
+
+const run = async ( { config, mode = 'changed-files', changed = [], requestReviewers, membership = {} } ) => {
+	const calls = [], warnings = [];
+	let failed = null;
+
+	process.env.CONFIG_FILE = config;
+	process.env.MODE = mode;
+
+	const github = {
+		paginate: async () => changed.map( ( filename ) => ( { filename } ) ),
+		rest: {
+			pulls: {
+				listFiles() {},
+				requestReviewers: async ( body ) => {
+					calls.push( { reviewers: body.reviewers, team_reviewers: body.team_reviewers } );
+
+					if ( requestReviewers ) {
+						await requestReviewers( body );
+					}
+				},
+			},
+			teams: {
+				// `membership` maps a team slug to the state the API reports, or to a
+				// status code the lookup should fail with. An absent slug is a 404.
+				getMembershipForUserInOrg: async ( { team_slug: team } ) => {
+					const state = membership[ team ];
+
+					if ( typeof state !== 'string' ) {
+						reject( state ?? 404, `lookup says ${ state ?? 404 }` );
+					}
+
+					return { data: { state } };
+				},
+			},
+		},
+	};
+
+	await assignReviewers( {
+		github,
+		context: {
+			repo: { owner: 'woocommerce', repo: 'woocommerce' },
+			payload: { pull_request: { number: 1, user: { login: 'alice' } } },
+		},
+		core: { info() {}, warning: ( m ) => warnings.push( m ), setFailed: ( m ) => { failed = m; } },
+	} );
+
+	return { calls, warnings, failed };
+};
+
+const reject = ( status, message ) => {
+	const error = new Error( message );
+	error.status = status;
+
+	throw error;
+};
+
+const routing = configFile( { 'a/**/*': [ 'rubik', '@Alice' ], 'b/**/*': 'ballade' } );
+
+check( 'a key may name several reviewers, and the author is never one of them', async () => {
+	// `@Alice` is the author `alice`; GitHub logins are case-insensitive.
+	const { calls, failed } = await run( { config: routing, changed: [ 'a/x.js' ] } );
+	assert.deepStrictEqual( calls, [ { reviewers: [], team_reviewers: [ 'rubik' ] } ] );
+	assert.strictEqual( failed, null );
+} );
+
+check( 'nothing matched asks for nothing and does not fail', async () => {
+	const { calls, failed } = await run( { config: routing, changed: [ 'unrouted/z.txt' ] } );
+	assert.strictEqual( calls.length, 0 );
+	assert.strictEqual( failed, null );
+} );
+
+check( 'a rejected batch is retried one reviewer at a time', async () => {
+	const { calls, failed } = await run( {
+		config: routing,
+		changed: [ 'a/x.js', 'b/y.js' ],
+		requestReviewers: ( { team_reviewers: requested } ) =>
+			requested.length > 1 && reject( 422, 'batch rejected' ),
+	} );
+
+	assert.deepStrictEqual( calls.slice( 1 ), [
+		{ reviewers: undefined, team_reviewers: [ 'rubik' ] },
+		{ reviewers: undefined, team_reviewers: [ 'ballade' ] },
+	] );
+	assert.strictEqual( failed, null, 'both retries landed, so stay green' );
+} );
+
+check( 'a retry that fails for another reason still fails the job', async () => {
+	// The first retry landing must not hide the second one being dropped.
+	let attempts = 0;
+
+	const { failed } = await run( {
+		config: routing,
+		changed: [ 'a/x.js', 'b/y.js' ],
+		requestReviewers: () => {
+			attempts += 1;
+
+			if ( attempts === 1 ) {
+				reject( 422, 'batch rejected' );
+			}
+
+			if ( attempts === 3 ) {
+				reject( 403, 'rate limited' );
+			}
+		},
+	} );
+
+	assert.match( failed, /ballade/ );
+} );
+
+check( 'a reviewer the API will not accept fails the job and is named', async () => {
+	const { failed } = await run( {
+		config: routing,
+		changed: [ 'b/y.js' ],
+		requestReviewers: () => reject( 422, 'no such team' ),
+	} );
+
+	assert.match( failed, /ballade/ );
+	assert.match( failed, /check the config/ );
+} );
+
+check( 'a batch failure that is not a rejected reviewer is not asked again', async () => {
+	// Only a 422 can be narrowed down to one entry. Asking again after a 403
+	// spends the rate limit to be told the same thing once per reviewer.
+	let attempts = 0;
+
+	await assert.rejects(
+		() => run( {
+			config: routing,
+			changed: [ 'a/x.js', 'b/y.js' ],
+			requestReviewers: () => {
+				attempts += 1;
+				reject( 403, 'rate limited' );
+			},
+		} ),
+		/rate limited/
+	);
+
+	assert.strictEqual( attempts, 1, 'only the batch is attempted' );
+} );
+
+const teamRouting = configFile( { rubik: 'rubik', ballade: 'ballade' } );
+
+check( 'author-team asks the teams the author belongs to', async () => {
+	const { calls, failed } = await run( {
+		config: teamRouting,
+		mode: 'author-team',
+		membership: { rubik: 'active' },
+	} );
+
+	assert.deepStrictEqual( calls, [ { reviewers: [], team_reviewers: [ 'rubik' ] } ] );
+	assert.strictEqual( failed, null );
+} );
+
+check( 'a pending team membership is not a membership', async () => {
+	const { calls, failed } = await run( {
+		config: teamRouting,
+		mode: 'author-team',
+		membership: { rubik: 'pending' },
+	} );
+
+	assert.strictEqual( calls.length, 0 );
+	assert.strictEqual( failed, null );
+} );
+
+check( 'a membership lookup that fails for any other reason fails the job', async () => {
+	// An expired token answers 401 to every team, which used to look exactly
+	// like an author who is on none of them.
+	const { calls, failed } = await run( {
+		config: teamRouting,
+		mode: 'author-team',
+		membership: { rubik: 401, ballade: 401 },
+	} );
+
+	assert.strictEqual( calls.length, 0 );
+	assert.match( failed, /Could not read alice's membership/ );
+} );
+
+check( 'a config that is not an object map is refused', async () => {
+	const file = path.join( workspace, 'not-a-map.json' );
+
+	for ( const contents of [ '"rubik"', '[ "rubik" ]', 'null', '42' ] ) {
+		fs.writeFileSync( file, contents );
+
+		await assert.rejects(
+			() => run( { config: file, changed: [ 'a/x.js' ] } ),
+			/must be a JSON object/,
+			`${ contents } should be rejected`
+		);
+	}
+} );
+
+check( 'a malformed config value names the offending key', async () => {
+	for ( const value of [ null, '', [], { team: 'rubik' } ] ) {
+		await assert.rejects(
+			() => run( { config: configFile( { 'a/**/*': value } ), changed: [ 'a/x.js' ] } ),
+			/Config key 'a\/\*\*\/\*' does not name a reviewer/,
+			`${ JSON.stringify( value ) } should be rejected`
+		);
+	}
+} );
+
+/* Runner. */
+
+( async () => {
+	let failures = 0;
+
+	for ( const { name, run: body } of ran ) {
+		try {
+			await body();
+			console.log( `ok    ${ name }` );
+		} catch ( error ) {
+			failures += 1;
+			console.log( `FAIL  ${ name }\n      ${ error.message.split( '\n' )[ 0 ] }` );
+		}
+	}
+
+	fs.rmSync( workspace, { recursive: true, force: true } );
+	console.log( failures ? `\n${ failures } of ${ ran.length } failed` : `\nall ${ ran.length } checks passed` );
+	process.exit( failures ? 1 : 0 );
+} )();
diff --git a/.github/automate-team-review-assignment-config.json b/.github/automate-team-review-assignment-config.json
new file mode 100644
index 00000000000..fbc0287bba1
--- /dev/null
+++ b/.github/automate-team-review-assignment-config.json
@@ -0,0 +1,5 @@
+{
+	"rubik": "rubik",
+	"kirigami": "kirigami",
+	"ballade": "ballade"
+}
diff --git a/.github/automate-team-review-assignment-config.yml b/.github/automate-team-review-assignment-config.yml
deleted file mode 100644
index e65781ec2e1..00000000000
--- a/.github/automate-team-review-assignment-config.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-when:
-  - author:
-      teamIs:
-        - rubik
-      ignore:
-        nameIs:
-    assign:
-      teams:
-        - rubik
-  - author:
-      teamIs:
-        - kirigami
-      ignore:
-        nameIs:
-    assign:
-      teams:
-        - kirigami
-  - author:
-      teamIs:
-        - ballade
-      ignore:
-        nameIs:
-    assign:
-      teams:
-        - ballade
diff --git a/.github/project-community-pr-assigner.json b/.github/project-community-pr-assigner.json
new file mode 100644
index 00000000000..4b939200f53
--- /dev/null
+++ b/.github/project-community-pr-assigner.json
@@ -0,0 +1,169 @@
+{
+	".github/**/*": "monorepo",
+	"docs/**/*": "developer-advocacy",
+	"{CODE_OF_CONDUCT,CONTRIBUTING,DEVELOPMENT,README,SECURITY}.md": "developer-advocacy",
+	"tools/**/*": "monorepo",
+	"packages/js/{create-woo-extension,dependency-extraction-webpack-plugin,eslint-plugin,internal-build,internal-js-tests}/**/*": "monorepo",
+	"packages/js/e2e-utils-playwright/**/*": "monorepo",
+	"packages/php/{monorepo-plugin,remote-specs-validation}/**/*": "monorepo",
+	"plugins/woocommerce/tests/{Tools,bin,cli,e2e,metrics,performance}/**/*": "monorepo",
+	"packages/js/{block-templates,components,experimental,expression-evaluation,sanitize}/**/*": "kirigami",
+	"plugins/woocommerce/{.wordpress-org,i18n}/**/*": "developer-advocacy",
+	"plugins/woocommerce-beta-tester/**/*": "kirigami",
+	"plugins/woocommerce/patterns/**/*": "kirigami",
+	"plugins/woocommerce/client/blocks/**/*": "kirigami",
+	"plugins/woocommerce/client/admin/client/{components,error-boundary,hooks,lib,stylesheets,typings,utils,wp-admin-scripts}/**/*": "kirigami",
+	"plugins/woocommerce/client/admin/client/*": "kirigami",
+	"plugins/woocommerce/client/admin/docs/**/*": "developer-advocacy",
+	"plugins/woocommerce/client/legacy/**/*{attribute,product,variation}*": "kirigami",
+	"plugins/woocommerce/src/{Blocks,LayoutTemplates}/**/*": "kirigami",
+	"plugins/woocommerce/src/Admin/BlockTemplates/**/*": "kirigami",
+	"plugins/woocommerce/src/Internal/{CostOfGoodsSold,Customers,ProductAttributes,ProductAttributesLookup,ProductDownloads,ProductFeed,ProductFilters,ProductGallery,ProductImage,VariationGallery}/**/*": "kirigami",
+	"plugins/woocommerce/includes/{blocks,customizer,shortcodes,theme-support,walkers,widgets}/**/*": "kirigami",
+	"plugins/woocommerce/includes/{class-wc-customer*,class-wc-product*,wc-customer*,wc-product*,wc-stock-functions.php}": "kirigami",
+	"plugins/woocommerce/includes/data-stores/class-wc-{customer,product}*": "kirigami",
+	"plugins/woocommerce/templates/{auth,block-notices,brands,global,loop,myaccount,notices,parts,product-form,single-product,templates}/**/*": "kirigami",
+	"plugins/woocommerce/templates/*": "kirigami",
+	"plugins/woocommerce/packages/**/*": "kirigami",
+	"packages/js/{currency,customer-effort-score,date,extend-cart-checkout-block,number,onboarding}/**/*": "rubik",
+	"plugins/woocommerce/client/blocks/assets/js/blocks/{add-to-wishlist-button,cart,cart-checkout-shared,checkout,coupon-code,mini-cart,order-confirmation,payment-method-icons,saved-for-later,shopper-lists,wishlist}/**/*": "rubik",
+	"plugins/woocommerce/client/admin/client/{core-profiler,customize-store,launch-your-store,task-lists}/**/*": "rubik",
+	"plugins/woocommerce/assets/images/{core-profiler,onboarding,task_list}/**/*": "rubik",
+	"plugins/woocommerce/src/{Checkout,StoreApi}/**/*": "rubik",
+	"plugins/woocommerce/src/{Abilities,Api,Caches,Caching,Database,Enums,Proxies,Utilities,Vendor}/**/*": "rubik",
+	"plugins/woocommerce/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Traits,TransientFiles}/**/*": "rubik",
+	"plugins/woocommerce/src/Internal/{AddressProvider,POS}/**/*": "kirigami",
+	"plugins/woocommerce/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*": "rubik",
+	"plugins/woocommerce/src/Internal/Admin/*": "rubik",
+	"plugins/woocommerce/src/Internal/Admin/{ProductForm,ProductReviews}/**/*": "kirigami",
+	"plugins/woocommerce/includes/*": "rubik",
+	"plugins/woocommerce/includes/{abstracts,data-stores,interfaces,legacy,libraries,log-handlers,queue,rest-api,traits}/**/*": "rubik",
+	"plugins/woocommerce/includes/admin/{list-tables,meta-boxes}/**/*": "rubik",
+	"plugins/woocommerce/src/Admin/API/**/*": "rubik",
+	"plugins/woocommerce/templates/{cart,checkout,order}/**/*": "rubik",
+	"plugins/woocommerce/tests/**/*{cart,checkout,order,rest-api,store-api,webhook}*": "rubik",
+	"plugins/woocommerce/tests/php/src/{Api,Caching,Database,Proxies,Utilities}/**/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,LegacyPhpApi,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Telemetry,Traits,TransientFiles}/**/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Internal/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Admin/API/**/*": "rubik",
+	"plugins/woocommerce/tests/php/includes/*": "rubik",
+	"plugins/woocommerce/tests/php/includes/{abstracts,admin,data-stores,rest-api}/**/*": "rubik",
+	"plugins/woocommerce/tests/legacy/unit-tests/{admin,cart,checkout,core,crud,order,order-items,packages,page-functions,privacy,queue,rest-api,session,totals,webhooks}/**/*": "rubik",
+	"plugins/woocommerce/tests/legacy/unit-tests/{formatting,libraries,log}/**/*": "rubik",
+	"packages/js/email-editor/**/*": "ballade",
+	"packages/php/email-editor/**/*": "ballade",
+	"plugins/woocommerce/client/admin/client/{marketing,settings-email}/**/*": "ballade",
+	"plugins/woocommerce/src/Admin/Marketing/**/*": "ballade",
+	"plugins/woocommerce/src/Internal/{Email,EmailEditor}/**/*": "ballade",
+	"plugins/woocommerce/src/Internal/Admin/{EmailImprovements,EmailPreview,Emails,Marketing}/**/*": "ballade",
+	"plugins/woocommerce/includes/{emails,react-admin/emails}/**/*": "ballade",
+	"plugins/woocommerce/includes/{class-wc-coupon*,class-wc-email*,wc-coupon*,wc-email*}": "ballade",
+	"plugins/woocommerce/templates/emails/**/*": "ballade",
+	"plugins/woocommerce/tests/**/*{coupon,email,mailer,marketing}*": "ballade",
+	"plugins/woocommerce/tests/php/src/Internal/{Email,EmailEditor}/**/*": "ballade",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{EmailImprovements,EmailPreview,Emails,Marketing}/**/*": "ballade",
+	"plugins/woocommerce/tests/php/src/Admin/Marketing/**/*": "ballade",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/*Marketing*": "ballade",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/*Marketing*/**/*": "ballade",
+	"plugins/woocommerce/tests/php/includes/emails/**/*": "ballade",
+	"plugins/woocommerce/tests/legacy/unit-tests/{coupon,discounts,email}/**/*": "ballade",
+	"packages/js/settings-ui/**/*": "moltres",
+	"plugins/woocommerce/client/admin/client/{payments,payments-welcome,settings,settings-payments,settings-recommendations}/**/*": "moltres",
+	"plugins/woocommerce/src/Gateways/**/*": "moltres",
+	"plugins/woocommerce/src/Admin/Settings/**/*": "moltres",
+	"plugins/woocommerce/src/Internal/Settings/**/*": "moltres",
+	"plugins/woocommerce/includes/{gateways,payment-tokens}/**/*": "moltres",
+	"plugins/woocommerce/includes/admin/settings/**/*": "moltres",
+	"plugins/woocommerce/includes/admin/*settings*": "moltres",
+	"plugins/woocommerce/tests/**/*{gateway,payment,setting}*": "moltres",
+	"plugins/woocommerce/src/Internal/Admin/{Settings,WCPayPromotion}/**/*": "moltres",
+	"plugins/woocommerce/tests/php/src/Gateways/**/*": "moltres",
+	"plugins/woocommerce/tests/php/src/Internal/Settings/**/*": "moltres",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{Settings,WCPayPromotion}/**/*": "moltres",
+	"plugins/woocommerce/tests/php/src/Admin/Settings/**/*": "moltres",
+	"plugins/woocommerce/assets/images/{payment-methods,payment-methods-cards,payment_methods,settings-payments}/**/*": "moltres",
+	"plugins/woocommerce/tests/php/includes/{gateways,settings}/**/*": "moltres",
+	"plugins/woocommerce/tests/legacy/unit-tests/{gateways,payment-gateways,payment-tokens,settings}/**/*": "moltres",
+	"plugins/woocommerce/client/admin/client/{shipping,tax}/**/*": "escargot",
+	"plugins/woocommerce/src/Internal/Tax/**/*": "escargot",
+	"plugins/woocommerce/includes/shipping/**/*": "escargot",
+	"plugins/woocommerce/includes/{class-wc-shipping*,class-wc-tax*,wc-shipping*,wc-tax*}": "escargot",
+	"plugins/woocommerce/tests/**/*{shipping,tax}*": "escargot",
+	"plugins/woocommerce/tests/php/src/Internal/Tax/**/*": "escargot",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{ShippingPartnerSuggestions,TaxSettingsRecommendations}/**/*": "escargot",
+	"plugins/woocommerce/tests/php/includes/shipping/**/*": "escargot",
+	"plugins/woocommerce/tests/legacy/unit-tests/{countries,shipping,tax}/**/*": "escargot",
+	"plugins/woocommerce/assets/images/{shipping_partners,shipping_providers}/**/*": "escargot",
+	"packages/js/{data,explat,tracks}/**/*": "ventures",
+	"packages/php/woocommerce-analytics/**/*": "ventures",
+	"plugins/woocommerce/client/admin/client/{analytics,order-attribution-install-banner}/**/*": "ventures",
+	"plugins/woocommerce/src/Admin/API/Reports/**/*": "ventures",
+	"plugins/woocommerce/includes/{product-usage,tracks}/**/*": "ventures",
+	"plugins/woocommerce/includes/admin/reports/**/*": "ventures",
+	"plugins/woocommerce/tests/**/*{analytic,report,track}*": "ventures",
+	"plugins/woocommerce/src/Internal/Admin/Reports/**/*": "ventures",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/Reports/**/*": "ventures",
+	"plugins/woocommerce/tests/php/src/Admin/API/Reports/**/*": "ventures",
+	"plugins/woocommerce/tests/php/src/Admin/*Report*": "ventures",
+	"plugins/woocommerce/tests/php/src/Internal/McStatsTest.php": "ventures",
+	"plugins/woocommerce/tests/php/includes/tracks/**/*": "ventures",
+	"packages/js/{admin-layout,csv-export,navigation,notices}/**/*": "somewherewarm",
+	"packages/php/blueprint/**/*": "somewherewarm",
+	"plugins/woocommerce/client/admin/client/{activity-panel,blueprint,dashboard,embedded-body-layout,guided-tours,header,homescreen,inbox-panel,layout,store-management-links}/**/*": "somewherewarm",
+	"plugins/woocommerce/src/Admin/{Composer,DateTimeProvider,Features,Notes,Overrides,RemoteInboxNotifications,Schedulers}/**/*": "somewherewarm",
+	"plugins/woocommerce/src/Internal/{CLI,Integrations,Jetpack,Utilities}/**/*": "somewherewarm",
+	"plugins/woocommerce/src/Internal/Admin/{ImportExport,Notes}/**/*": "somewherewarm",
+	"plugins/woocommerce/includes/{cli,export,import,integrations}/**/*": "somewherewarm",
+	"plugins/woocommerce/includes/admin/{helper,importers,notes,plugin-updates,views}/**/*": "somewherewarm",
+	"plugins/woocommerce/sample-data/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/**/*{export,import,navigation,notice,tool}*": "somewherewarm",
+	"plugins/woocommerce/tests/php/src/Internal/{CLI,Integration,Utilities}/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{ImportExport,Notes}/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/php/src/Admin/Features/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/php/src/Admin/*": "somewherewarm",
+	"plugins/woocommerce/tests/php/includes/{cli,exporter,importer}/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/legacy/unit-tests/{exporter,geolocation,importer,integrations,util}/**/*": "somewherewarm",
+	"plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/**/*": "somewherewarm",
+	"plugins/woocommerce/client/admin/client/marketplace/**/*": "desire",
+	"plugins/woocommerce/src/Admin/{PluginsInstallLoggers,PluginsProvider,RemoteSpecs}/**/*": "desire",
+	"plugins/woocommerce/src/Internal/WCCom/**/*": "desire",
+	"plugins/woocommerce/includes/wccom-site/**/*": "desire",
+	"plugins/woocommerce/includes/admin/marketplace-suggestions/**/*": "desire",
+	"plugins/woocommerce/src/Internal/Admin/{RemoteFreeExtensions,Suggestions}/**/*": "desire",
+	"plugins/woocommerce/tests/php/src/Internal/WCCom/**/*": "desire",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{RemoteFreeExtensions,Suggestions}/**/*": "desire",
+	"plugins/woocommerce/client/blocks/assets/js/base/stores/**/*": "billow",
+	"plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php": "billow",
+	"packages/php/woocommerce-subscriptions-engine/**/*": "chronos",
+	"plugins/woocommerce/client/admin/client/{mobile-app-login,mobile-banner}/**/*": "@jorgemucientes",
+	"packages/js/remote-logging/**/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Blocks/**/*": "kirigami",
+	"plugins/woocommerce/tests/php/src/Blocks/StoreApi/**/*": "rubik",
+	"plugins/woocommerce/tests/php/src/Internal/{AddressProvider,CostOfGoodsSold,Customers,POS,ProductAttributes,ProductAttributesLookup,ProductDownloads,ProductFeed,ProductFilters,ProductGallery,VariationGallery}/**/*": "kirigami",
+	"plugins/woocommerce/tests/php/src/Internal/Admin/{ProductForm,ProductReviews}/**/*": "kirigami",
+	"plugins/woocommerce/tests/php/src/Internal/{AssignDefaultCategoryTest.php,DownloadPermissionsAdjusterTest.php}": "kirigami",
+	"plugins/woocommerce/tests/legacy/unit-tests/{account,attributes,blocks,customer,product,shortcodes,templates,widgets}/**/*": "kirigami",
+	"plugins/woocommerce/tests/php/includes/data-stores/class-wc-{customer,product}*": "kirigami",
+	"plugins/woocommerce/client/legacy/**/*{cart,checkout,order}*": "rubik",
+	"plugins/woocommerce/client/legacy/**/*{coupon,email,marketing}*": "ballade",
+	"plugins/woocommerce/client/legacy/**/*{gateway,payment,setting}*": "moltres",
+	"plugins/woocommerce/client/legacy/**/*{shipping,tax}*": "escargot",
+	"plugins/woocommerce/client/legacy/**/*{analytic,report,track}*": "ventures",
+	"plugins/woocommerce/assets/**/*{cart,checkout,order,onboarding,core-profiler,task-list,task_list,customize-store}*": "rubik",
+	"plugins/woocommerce/assets/**/*{coupon,email,marketing}*": "ballade",
+	"plugins/woocommerce/assets/**/*{gateway,payment,setting}*": "moltres",
+	"plugins/woocommerce/assets/**/*{shipping,tax}*": "escargot",
+	"plugins/woocommerce/assets/**/*{analytic,report,track}*": "ventures",
+	"plugins/woocommerce/assets/**/*{export,import,navigation,notice}*": "somewherewarm",
+	"plugins/woocommerce/assets/**/*{marketplace,wccom}*": "desire",
+	"plugins/woocommerce/assets/images/marketing/**/*": "ballade",
+	"plugins/woocommerce/assets/images/{pattern-placeholders,product_data,template-placeholders}/**/*": "kirigami",
+	"plugins/woocommerce/client/legacy/css/**/*": "kirigami",
+	"plugins/woocommerce/client/legacy/js/frontend/**/*": "kirigami",
+	"plugins/woocommerce/assets/{client,fonts}/**/*": "kirigami",
+	"plugins/woocommerce/assets/images/{icons,pinata,previews}/**/*": "kirigami",
+	"plugins/woocommerce/tests/php/src/{AutoloaderTest.php,CLAUDE.md}": "monorepo",
+	"plugins/woocommerce/tests/{legacy/framework,legacy/data,php/bin,php/framework,php/helpers}/**/*": "monorepo"
+}
diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml
deleted file mode 100644
index 0da693db8d4..00000000000
--- a/.github/project-community-pr-assigner.yml
+++ /dev/null
@@ -1,522 +0,0 @@
-# See https://github.com/shufo/auto-assign-reviewer-by-files/blob/main/README.md for configuration format
-# Community PRs are routed by product area. Rules accumulate when a PR spans areas.
-# Product mappings follow WooCommerce's current triage structure; shared
-# backend and developer-tooling fallbacks use recent contribution activity.
-
-# Repository infrastructure and developer tooling.
-'.github/**/*':
-    - team: monorepo
-
-'docs/**/*':
-    - team: developer-advocacy
-
-'{CODE_OF_CONDUCT,CONTRIBUTING,DEVELOPMENT,README,SECURITY}.md':
-    - team: developer-advocacy
-
-'tools/**/*':
-    - team: monorepo
-
-'packages/js/{create-woo-extension,dependency-extraction-webpack-plugin,eslint-plugin,internal-build,internal-js-tests}/**/*':
-    - team: monorepo
-
-'packages/js/e2e-utils-playwright/**/*':
-    - team: monorepo
-
-'packages/php/{monorepo-plugin,remote-specs-validation}/**/*':
-    - team: monorepo
-
-'plugins/woocommerce/tests/{Tools,bin,cli,e2e,metrics,performance}/**/*':
-    - team: monorepo
-
-# Blocks, patterns, products, inventory, search, and storefront (Kirigami).
-'packages/js/{block-templates,components,experimental,expression-evaluation,sanitize}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/{.wordpress-org,i18n}/**/*':
-    - team: developer-advocacy
-
-'plugins/woocommerce-beta-tester/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/patterns/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/blocks/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/admin/client/{components,error-boundary,hooks,lib,stylesheets,typings,utils,wp-admin-scripts}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/admin/client/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/admin/docs/**/*':
-    - team: developer-advocacy
-
-'plugins/woocommerce/client/legacy/**/*{attribute,product,variation}*':
-    - team: kirigami
-
-'plugins/woocommerce/src/{Blocks,LayoutTemplates}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/src/Admin/BlockTemplates/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/src/Internal/{CostOfGoodsSold,Customers,ProductAttributes,ProductAttributesLookup,ProductDownloads,ProductFeed,ProductFilters,ProductGallery,ProductImage,VariationGallery}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/includes/{blocks,customizer,shortcodes,theme-support,walkers,widgets}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/includes/{class-wc-customer*,class-wc-product*,wc-customer*,wc-product*,wc-stock-functions.php}':
-    - team: kirigami
-
-'plugins/woocommerce/includes/data-stores/class-wc-{customer,product}*':
-    - team: kirigami
-
-'plugins/woocommerce/templates/{auth,block-notices,brands,global,loop,myaccount,notices,parts,product-form,single-product,templates}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/templates/*':
-    - team: kirigami
-
-'plugins/woocommerce/packages/**/*':
-    - team: kirigami
-
-# Cart, checkout, orders, onboarding, Store API, and shared backend (Rubik).
-'packages/js/{currency,customer-effort-score,date,extend-cart-checkout-block,number,onboarding}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/client/blocks/assets/js/blocks/{add-to-wishlist-button,cart,cart-checkout-shared,checkout,coupon-code,mini-cart,order-confirmation,payment-method-icons,saved-for-later,shopper-lists,wishlist}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/client/admin/client/{core-profiler,customize-store,launch-your-store,task-lists}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/assets/images/{core-profiler,onboarding,task_list}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/{Checkout,StoreApi}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/{Abilities,Api,Caches,Caching,Database,Enums,Proxies,Utilities,Vendor}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Traits,TransientFiles}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/Internal/{AddressProvider,POS}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/Internal/Admin/*':
-    - team: rubik
-
-'plugins/woocommerce/src/Internal/Admin/{ProductForm,ProductReviews}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/includes/*':
-    - team: rubik
-
-'plugins/woocommerce/includes/{abstracts,data-stores,interfaces,legacy,libraries,log-handlers,queue,rest-api,traits}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/includes/admin/{list-tables,meta-boxes}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/src/Admin/API/**/*':
-    - team: rubik
-
-'plugins/woocommerce/templates/{cart,checkout,order}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/**/*{cart,checkout,order,rest-api,store-api,webhook}*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/{Api,Caching,Database,Proxies,Utilities}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,LegacyPhpApi,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Telemetry,Traits,TransientFiles}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Internal/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Admin/API/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/includes/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/includes/{abstracts,admin,data-stores,rest-api}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/legacy/unit-tests/{admin,cart,checkout,core,crud,order,order-items,packages,page-functions,privacy,queue,rest-api,session,totals,webhooks}/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/legacy/unit-tests/{formatting,libraries,log}/**/*':
-    - team: rubik
-
-# Email and store marketing (Ballade).
-'packages/js/email-editor/**/*':
-    - team: ballade
-
-'packages/php/email-editor/**/*':
-    - team: ballade
-
-'plugins/woocommerce/client/admin/client/{marketing,settings-email}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/src/Admin/Marketing/**/*':
-    - team: ballade
-
-'plugins/woocommerce/src/Internal/{Email,EmailEditor}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/src/Internal/Admin/{EmailImprovements,EmailPreview,Emails,Marketing}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/includes/{emails,react-admin/emails}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/includes/{class-wc-coupon*,class-wc-email*,wc-coupon*,wc-email*}':
-    - team: ballade
-
-'plugins/woocommerce/templates/emails/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/**/*{coupon,email,mailer,marketing}*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/src/Internal/{Email,EmailEditor}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{EmailImprovements,EmailPreview,Emails,Marketing}/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/src/Admin/Marketing/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/*Marketing*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/*Marketing*/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/php/includes/emails/**/*':
-    - team: ballade
-
-'plugins/woocommerce/tests/legacy/unit-tests/{coupon,discounts,email}/**/*':
-    - team: ballade
-
-# Payments and settings (Moltres).
-'packages/js/settings-ui/**/*':
-    - team: moltres
-
-'plugins/woocommerce/client/admin/client/{payments,payments-welcome,settings,settings-payments,settings-recommendations}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/src/Gateways/**/*':
-    - team: moltres
-
-'plugins/woocommerce/src/Admin/Settings/**/*':
-    - team: moltres
-
-'plugins/woocommerce/src/Internal/Settings/**/*':
-    - team: moltres
-
-'plugins/woocommerce/includes/{gateways,payment-tokens}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/includes/admin/settings/**/*':
-    - team: moltres
-
-'plugins/woocommerce/includes/admin/*settings*':
-    - team: moltres
-
-'plugins/woocommerce/tests/**/*{gateway,payment,setting}*':
-    - team: moltres
-
-'plugins/woocommerce/src/Internal/Admin/{Settings,WCPayPromotion}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/php/src/Gateways/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/php/src/Internal/Settings/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{Settings,WCPayPromotion}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/php/src/Admin/Settings/**/*':
-    - team: moltres
-
-'plugins/woocommerce/assets/images/{payment-methods,payment-methods-cards,payment_methods,settings-payments}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/php/includes/{gateways,settings}/**/*':
-    - team: moltres
-
-'plugins/woocommerce/tests/legacy/unit-tests/{gateways,payment-gateways,payment-tokens,settings}/**/*':
-    - team: moltres
-
-# Shipping and tax (Escargot).
-'plugins/woocommerce/client/admin/client/{shipping,tax}/**/*':
-    - team: escargot
-
-'plugins/woocommerce/src/Internal/Tax/**/*':
-    - team: escargot
-
-'plugins/woocommerce/includes/shipping/**/*':
-    - team: escargot
-
-'plugins/woocommerce/includes/{class-wc-shipping*,class-wc-tax*,wc-shipping*,wc-tax*}':
-    - team: escargot
-
-'plugins/woocommerce/tests/**/*{shipping,tax}*':
-    - team: escargot
-
-'plugins/woocommerce/tests/php/src/Internal/Tax/**/*':
-    - team: escargot
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{ShippingPartnerSuggestions,TaxSettingsRecommendations}/**/*':
-    - team: escargot
-
-'plugins/woocommerce/tests/php/includes/shipping/**/*':
-    - team: escargot
-
-'plugins/woocommerce/tests/legacy/unit-tests/{countries,shipping,tax}/**/*':
-    - team: escargot
-
-'plugins/woocommerce/assets/images/{shipping_partners,shipping_providers}/**/*':
-    - team: escargot
-
-# Reports, analytics, tracking, and experimentation (Ventures).
-'packages/js/{data,explat,tracks}/**/*':
-    - team: ventures
-
-'packages/php/woocommerce-analytics/**/*':
-    - team: ventures
-
-'plugins/woocommerce/client/admin/client/{analytics,order-attribution-install-banner}/**/*':
-    - team: ventures
-
-'plugins/woocommerce/src/Admin/API/Reports/**/*':
-    - team: ventures
-
-'plugins/woocommerce/includes/{product-usage,tracks}/**/*':
-    - team: ventures
-
-'plugins/woocommerce/includes/admin/reports/**/*':
-    - team: ventures
-
-'plugins/woocommerce/tests/**/*{analytic,report,track}*':
-    - team: ventures
-
-'plugins/woocommerce/src/Internal/Admin/Reports/**/*':
-    - team: ventures
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/Reports/**/*':
-    - team: ventures
-
-'plugins/woocommerce/tests/php/src/Admin/API/Reports/**/*':
-    - team: ventures
-
-'plugins/woocommerce/tests/php/src/Admin/*Report*':
-    - team: ventures
-
-'plugins/woocommerce/tests/php/src/Internal/McStatsTest.php':
-    - team: ventures
-
-'plugins/woocommerce/tests/php/includes/tracks/**/*':
-    - team: ventures
-
-# Import/export, store utilities, and wayfinding (SomewhereWarm).
-'packages/js/{admin-layout,csv-export,navigation,notices}/**/*':
-    - team: somewherewarm
-
-'packages/php/blueprint/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/client/admin/client/{activity-panel,blueprint,dashboard,embedded-body-layout,guided-tours,header,homescreen,inbox-panel,layout,store-management-links}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/src/Admin/{Composer,DateTimeProvider,Features,Notes,Overrides,RemoteInboxNotifications,Schedulers}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/src/Internal/{CLI,Integrations,Jetpack,Utilities}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/src/Internal/Admin/{ImportExport,Notes}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/includes/{cli,export,import,integrations}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/includes/admin/{helper,importers,notes,plugin-updates,views}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/sample-data/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/**/*{export,import,navigation,notice,tool}*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/php/src/Internal/{CLI,Integration,Utilities}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{ImportExport,Notes}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/php/src/Admin/Features/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/php/src/Admin/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/php/includes/{cli,exporter,importer}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/legacy/unit-tests/{exporter,geolocation,importer,integrations,util}/**/*':
-    - team: somewherewarm
-
-'plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/**/*':
-    - team: somewherewarm
-
-# WooCommerce.com and Marketplace connections (Desire).
-'plugins/woocommerce/client/admin/client/marketplace/**/*':
-    - team: desire
-
-'plugins/woocommerce/src/Admin/{PluginsInstallLoggers,PluginsProvider,RemoteSpecs}/**/*':
-    - team: desire
-
-'plugins/woocommerce/src/Internal/WCCom/**/*':
-    - team: desire
-
-'plugins/woocommerce/includes/wccom-site/**/*':
-    - team: desire
-
-'plugins/woocommerce/includes/admin/marketplace-suggestions/**/*':
-    - team: desire
-
-'plugins/woocommerce/src/Internal/Admin/{RemoteFreeExtensions,Suggestions}/**/*':
-    - team: desire
-
-'plugins/woocommerce/tests/php/src/Internal/WCCom/**/*':
-    - team: desire
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{RemoteFreeExtensions,Suggestions}/**/*':
-    - team: desire
-
-# Existing specialist ownership that sits outside the core triage table.
-'plugins/woocommerce/client/blocks/assets/js/base/stores/**/*':
-    - team: billow
-
-'plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php':
-    - team: billow
-
-'packages/php/woocommerce-subscriptions-engine/**/*':
-    - team: chronos
-
-'plugins/woocommerce/client/admin/client/{mobile-app-login,mobile-banner}/**/*':
-    - jorgemucientes
-
-# Logging has no named core team in the triage map; recent eligible activity is
-# split between Rubik and Moltres, with Rubik covering the shared backend fallback.
-'packages/js/remote-logging/**/*':
-    - team: rubik
-
-# Product and storefront tests mirror the Kirigami-owned source areas.
-'plugins/woocommerce/tests/php/src/Blocks/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/tests/php/src/Blocks/StoreApi/**/*':
-    - team: rubik
-
-'plugins/woocommerce/tests/php/src/Internal/{AddressProvider,CostOfGoodsSold,Customers,POS,ProductAttributes,ProductAttributesLookup,ProductDownloads,ProductFeed,ProductFilters,ProductGallery,VariationGallery}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/tests/php/src/Internal/Admin/{ProductForm,ProductReviews}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/tests/php/src/Internal/{AssignDefaultCategoryTest.php,DownloadPermissionsAdjusterTest.php}':
-    - team: kirigami
-
-'plugins/woocommerce/tests/legacy/unit-tests/{account,attributes,blocks,customer,product,shortcodes,templates,widgets}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/tests/php/includes/data-stores/class-wc-{customer,product}*':
-    - team: kirigami
-
-# Legacy client assets are routed only when their product area is identifiable.
-'plugins/woocommerce/client/legacy/**/*{cart,checkout,order}*':
-    - team: rubik
-
-'plugins/woocommerce/client/legacy/**/*{coupon,email,marketing}*':
-    - team: ballade
-
-'plugins/woocommerce/client/legacy/**/*{gateway,payment,setting}*':
-    - team: moltres
-
-'plugins/woocommerce/client/legacy/**/*{shipping,tax}*':
-    - team: escargot
-
-'plugins/woocommerce/client/legacy/**/*{analytic,report,track}*':
-    - team: ventures
-
-# Built assets are ancillary; identifiable product-area assets follow the same route.
-'plugins/woocommerce/assets/**/*{cart,checkout,order,onboarding,core-profiler,task-list,task_list,customize-store}*':
-    - team: rubik
-
-'plugins/woocommerce/assets/**/*{coupon,email,marketing}*':
-    - team: ballade
-
-'plugins/woocommerce/assets/**/*{gateway,payment,setting}*':
-    - team: moltres
-
-'plugins/woocommerce/assets/**/*{shipping,tax}*':
-    - team: escargot
-
-'plugins/woocommerce/assets/**/*{analytic,report,track}*':
-    - team: ventures
-
-'plugins/woocommerce/assets/**/*{export,import,navigation,notice}*':
-    - team: somewherewarm
-
-'plugins/woocommerce/assets/**/*{marketplace,wccom}*':
-    - team: desire
-
-# Product-area image assets follow the same ownership as their implementation.
-'plugins/woocommerce/assets/images/marketing/**/*':
-    - team: ballade
-
-'plugins/woocommerce/assets/images/{pattern-placeholders,product_data,template-placeholders}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/legacy/css/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/client/legacy/js/frontend/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/assets/{client,fonts}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/assets/images/{icons,pinata,previews}/**/*':
-    - team: kirigami
-
-'plugins/woocommerce/tests/php/src/{AutoloaderTest.php,CLAUDE.md}':
-    - team: monorepo
-
-# Test harness internals are routed to the active test-infrastructure owner.
-'plugins/woocommerce/tests/{legacy/framework,legacy/data,php/bin,php/framework,php/helpers}/**/*':
-    - team: monorepo
diff --git a/.github/workflows/automate-team-review-assignment.yml b/.github/workflows/automate-team-review-assignment.yml
index e54919decab..04d8c0ae38f 100644
--- a/.github/workflows/automate-team-review-assignment.yml
+++ b/.github/workflows/automate-team-review-assignment.yml
@@ -27,7 +27,7 @@ jobs:
               uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
               with:
                   script: |
-                      const username = '${{ github.event.pull_request.user.login || github.event.issue.user.login }}';
+                      const username = context.payload.pull_request?.user.login ?? context.payload.issue.user.login;
                       const { data: { permission } } = await github.rest.repos.getCollaboratorPermissionLevel( {
                           owner: context.repo.owner,
                           repo: context.repo.repo,
@@ -41,12 +41,11 @@ jobs:
               uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
               with:
                   script: |
-                      github.rest.issues.addLabels({
-                          issue_number: ${{ github.event.pull_request.number || github.event.issue.number }},
-                          owner: context.repo.owner,
-                          repo: context.repo.repo,
-                          labels: [ 'type: community contribution' ]
-                      });
+                      await github.rest.issues.addLabels( {
+                          ...context.repo,
+                          issue_number: context.issue.number,
+                          labels: [ 'type: community contribution' ],
+                      } );

     assign-reviewers:
       name: Assign reviewers
@@ -63,24 +62,35 @@ jobs:
               const { data: { permission } } = await github.rest.repos.getCollaboratorPermissionLevel( {
                   owner: context.repo.owner,
                   repo: context.repo.repo,
-                  username: '${{ github.event.pull_request.user.login }}',
+                  username: context.payload.pull_request.user.login,
               } );
               core.setOutput( 'contributor', ( permission === 'read' || permission === 'none' ) ? 'yes' : 'no' );

+        - name: Check out the composite action and the reviewer configs
+          if: ${{ github.event.pull_request.draft == false }}
+          uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+          with:
+              # Never the pull request head: the configs decide who gets write-scoped review requests.
+              ref: ${{ github.sha }}
+              sparse-checkout: .github
+              persist-credentials: false
+
         - name: Assign reviewers for a community PR
           if: ${{ steps.check.outputs.contributor == 'yes' && github.event.pull_request.draft == false }}
-          uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6
+          uses: ./.github/actions/assign-reviewers
           with:
-              config: '.github/project-community-pr-assigner.yml'
-              token: ${{ secrets.PR_ASSIGN_TOKEN }}
+              mode: changed-files
+              config: .github/project-community-pr-assigner.json
+              github-token: ${{ secrets.PR_ASSIGN_TOKEN }}

         - name: Assign reviewers for a teams PR
           if: ${{ steps.check.outputs.contributor == 'no' && github.event.pull_request.draft == false && ! contains( github.event.pull_request.labels.*.name, 'Release' ) && ( github.event.pull_request.base.ref == 'trunk' || startsWith( github.event.pull_request.base.ref, 'release/' ) ) }}
-          continue-on-error: ${{ ( github.event.pull_request.head.repo.fork && 'true' ) || 'false' }}
-          uses: acq688/Request-Reviewer-For-Team-Action@fca1c60fd0504aef59bdc925f3902c8a2d8bce62 # v1.1
+          continue-on-error: ${{ github.event.pull_request.head.repo.fork }}
+          uses: ./.github/actions/assign-reviewers
           with:
-              config: '.github/automate-team-review-assignment-config.yml'
-              GITHUB_TOKEN: ${{ secrets.PR_ASSIGN_TOKEN }}
+              mode: author-team
+              config: .github/automate-team-review-assignment-config.json
+              github-token: ${{ secrets.PR_ASSIGN_TOKEN }}

     add-testing-instructions-review-comment:
       name: Remind reviewers to also review the testing instructions and test coverage
@@ -97,7 +107,7 @@ jobs:
               const { data: { permission } } = await github.rest.repos.getCollaboratorPermissionLevel( {
                   owner: context.repo.owner,
                   repo: context.repo.repo,
-                  username: '${{ github.event.pull_request.user.login }}',
+                  username: context.payload.pull_request.user.login,
               } );
               core.setOutput( 'contributor', ( permission === 'read' || permission === 'none' ) ? 'yes' : 'no' );

diff --git a/.github/workflows/pr-check-local-actions.yml b/.github/workflows/pr-check-local-actions.yml
new file mode 100644
index 00000000000..173fd6b7e94
--- /dev/null
+++ b/.github/workflows/pr-check-local-actions.yml
@@ -0,0 +1,33 @@
+name: 'Check the local actions'
+
+on:
+    pull_request:
+        paths:
+            - '.github/actions/**'
+            - '.github/*.json'
+            - '.github/workflows/pr-check-local-actions.yml'
+
+permissions:
+    contents: read
+
+jobs:
+    assign-reviewers:
+        name: 'Check the assign-reviewers action'
+        runs-on: ubuntu-latest
+        timeout-minutes: 5
+        steps:
+            - name: Checkout
+              uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+              with:
+                  sparse-checkout: .github
+                  persist-credentials: false
+
+            - name: 'Setup Node'
+              uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+              with:
+                  node-version-file: '.nvmrc'
+
+            # Linux only: path.matchesGlob() is case-insensitive elsewhere, so a
+            # case-sensitive pattern passes on macOS and Windows when it should not.
+            - name: 'Run the checks'
+              run: node .github/actions/assign-reviewers/test.js