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