Commit 40f2857d316 for woocommerce
commit 40f2857d316ed2d5c75b9602f29158c17daa3095
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Wed Sep 2 15:05:57 2026 +0300
Fix theme rewrite rules lost when WP-CLI skips the active theme (#68146)
* fix(post-types): keep the queued rewrite flush when WP-CLI skips the theme
maybe_flush_rewrite_rules() consumed woocommerce_queue_flush_rewrite_rules
on any request that was not installing, WP-CLI runs started with
--skip-themes included.
Those runs never load the active theme, so consuming the queue regenerates
the whole rewrite graph without the theme's post types and taxonomies,
persists it, and sets the queue back to no. Nothing retries, and no later
request has a reason to rebuild the rules, so every theme-owned rewrite is
lost permanently. Measured on a store with one theme-registered CPT: 219
rules and 13 theme rules before, 190 and 0 after a single unrelated
--skip-themes command, unchanged by a normal request afterwards.
Skip the queue on those requests and leave it for the next normal one.
register_post_types() already asks the same question twice, at :349 to keep
the product archive registered and at :360 to avoid persisting theme
support, which is why WooCommerce's own rules survived while the theme's
did not.
Refs #68145
* Update plugins/woocommerce/includes/class-wc-post-types.php
Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
* fix(order-reviews): preserve rewrite queue when theme is skipped
The Review Order endpoint has a separate deferred rewrite queue from WC_Post_Types. A WP-CLI request that skipped the active theme still consumed that queue, persisted an incomplete rewrite graph, and removed the only retry signal.
Apply the same active-theme skip detection inside the endpoint and keep its queue pending for the next normal request. Keep the predicate private so the fix does not add a public WC_Post_Types contract or a new bootstrap dependency.
Refs #68145
* test(order-reviews): cover WP-CLI rewrite queue preservation
The active-theme skip guard depends on WP-CLI global configuration that unit tests cannot reproduce faithfully.
Exercise bare, active-theme, and inactive-theme skip modes through real WP-CLI child processes so regressions in configuration parsing fail in CI. Keep setup and inspection isolated from WooCommerce and restore rewrite state after the test.
Refs #68145
* test(e2e): pass dynamic WP-CLI values as arguments
The shared E2E helper previously accepted only command strings, so dynamically discovered theme identifiers entered a host shell before wp-env received them.
Keep the existing string path for current callers and add an execFile-backed argument-array path. Run the Review Order WP-CLI coverage entirely through argv so theme values retain their argument boundaries.
Refs #68145
* test(e2e): Stabilize WP-CLI rewrite coverage
The WP-CLI rewrite coverage runs in serial matrices and the parallel
HPOS-off project.
Its shared pending option raced with other HPOS-off workers, while an
unrestricted cleanup flush loaded every installed extension and exhausted
CI memory.
Skip the HPOS-independent case in HPOS-off and invalidate persisted rewrite
rules with core-only WP-CLI cleanup. The next normal request rebuilds the
complete rule set.
* test(e2e): Isolate WP-CLI coverage from extensions
Serial E2E setup leaves several extensions active before this test runs. Booting all of them under WP-CLI can exhaust the 128 MB CI memory limit before the rewrite assertion.
Discover active plugins without loading them and skip every plugin except WooCommerce for the commands under test. This preserves the theme-skip behavior and its inactive-theme control while removing unrelated plugin state from the process.
Refs #68145
---------
Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/fix-68145-skip-themes-rewrite-flush b/plugins/woocommerce/changelog/fix-68145-skip-themes-rewrite-flush
new file mode 100644
index 00000000000..84c5c73b9c8
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-68145-skip-themes-rewrite-flush
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent WP-CLI runs that skip the active theme from consuming queued rewrite flushes and dropping theme-registered rewrite rules.
diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php
index d65d77e02c8..ea6c5275b7d 100644
--- a/plugins/woocommerce/includes/class-wc-post-types.php
+++ b/plugins/woocommerce/includes/class-wc-post-types.php
@@ -725,10 +725,15 @@ class WC_Post_Types {
/**
* Flush rules if the event is queued.
*
+ * Consuming the queue regenerates and persists the whole rewrite graph. A request that skips
+ * the active theme would write the theme's rules away with nothing left to retry, so it
+ * leaves the queue for the next normal request. This mirrors the `$theme_unavailable` check
+ * in register_post_types().
+ *
* @since 3.3.0
*/
public static function maybe_flush_rewrite_rules() {
- if ( wp_installing() ) {
+ if ( wp_installing() || self::wp_cli_skips_active_theme() ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index 4fa8a52c8d2..73b38798ae0 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -405,12 +405,13 @@ class Endpoint {
* flush by setting `woocommerce_review_order_flush_rewrite_pending`;
* `add_rewrite_rule()` doesn't fire until `init` priority 10, so the
* flush has to happen later. `wp_loaded` runs after every `init`
- * callback, which is the earliest safe moment. Installing requests leave
- * the endpoint-specific queue intact for the next normal request, when the
- * complete rewrite graph is available.
+ * callback, which is the earliest safe moment. Installing requests and
+ * WP-CLI requests that skip the active theme leave the endpoint-specific
+ * queue intact for the next normal request, when the complete rewrite graph
+ * is available.
*/
public function maybe_flush_pending_rewrite(): void {
- if ( wp_installing() || 'yes' !== get_option( 'woocommerce_review_order_flush_rewrite_pending' ) ) {
+ if ( wp_installing() || $this->wp_cli_skips_active_theme() || 'yes' !== get_option( 'woocommerce_review_order_flush_rewrite_pending' ) ) {
return;
}
@@ -418,6 +419,31 @@ class Endpoint {
delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
}
+ /**
+ * Determine whether WP-CLI skipped the active theme.
+ *
+ * @return bool
+ */
+ private function wp_cli_skips_active_theme(): bool {
+ $get_wp_cli_config = array( 'WP_CLI', 'get_config' );
+
+ if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) || ! is_callable( $get_wp_cli_config ) ) {
+ return false;
+ }
+
+ $skipped_themes = $get_wp_cli_config( 'skip-themes' );
+
+ if ( true === $skipped_themes ) {
+ return true;
+ }
+
+ if ( ! is_array( $skipped_themes ) ) {
+ $skipped_themes = explode( ',', (string) $skipped_themes );
+ }
+
+ return in_array( get_stylesheet(), $skipped_themes, true );
+ }
+
/**
* Queue the endpoint-specific soft flush for `maybe_flush_pending_rewrite()`.
*
diff --git a/plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts b/plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts
index dd33eb19ce6..0042f59c1d7 100644
--- a/plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts
@@ -20,6 +20,7 @@ import {
import { tags, expect, test } from '../../fixtures/fixtures';
import { setOption, deleteOption } from '../../utils/options';
import { random } from '../../utils/helpers';
+import { wpCLI } from '../../utils/cli';
type SeededOrder = {
id: number;
@@ -273,6 +274,131 @@ test.describe(
}
};
+ test(
+ 'WP-CLI preserves the pending rewrite flush only when the active theme is skipped',
+ { tag: '@skip-on-external-env' },
+ async () => {
+ test.skip(
+ process.env.DISABLE_HPOS === '1',
+ 'WP-CLI theme skipping is independent of HPOS and is covered by the core serial projects.'
+ );
+
+ // Setup and inspection must not load WooCommerce, or those commands could consume the queue they are measuring.
+ const coreOnlyFlags = [ '--skip-plugins', '--skip-themes' ];
+ const pendingOption =
+ 'woocommerce_review_order_flush_rewrite_pending';
+ const seedPendingRewrite = () =>
+ wpCLI( [
+ 'wp',
+ 'eval',
+ `update_option( "${ pendingOption }", "yes" );`,
+ ...coreOnlyFlags,
+ ] );
+ const readPendingRewrite = async () =>
+ (
+ await wpCLI( [
+ 'wp',
+ 'eval',
+ `echo get_option( "${ pendingOption }", "missing" );`,
+ ...coreOnlyFlags,
+ ] )
+ ).stdout.trim();
+ const unrelatedPlugins = (
+ await wpCLI( [
+ 'wp',
+ 'plugin',
+ 'list',
+ '--status=active',
+ '--field=name',
+ ...coreOnlyFlags,
+ ] )
+ ).stdout
+ .split( /\r?\n/ )
+ .filter( ( plugin ) => plugin && plugin !== 'woocommerce' );
+ const skipUnrelatedPlugins = unrelatedPlugins.length
+ ? [ `--skip-plugins=${ unrelatedPlugins.join( ',' ) }` ]
+ : [];
+ const activeTheme = (
+ await wpCLI( [
+ 'wp',
+ 'option',
+ 'get',
+ 'stylesheet',
+ ...coreOnlyFlags,
+ ] )
+ ).stdout.trim();
+ const inactiveTheme =
+ (
+ await wpCLI( [
+ 'wp',
+ 'theme',
+ 'list',
+ '--status=inactive',
+ '--field=name',
+ ...coreOnlyFlags,
+ ] )
+ ).stdout
+ .split( /\r?\n/ )
+ .find( Boolean ) ?? '';
+
+ expect(
+ inactiveTheme,
+ 'Expected an inactive theme for the WP-CLI control.'
+ ).not.toBe( '' );
+
+ try {
+ for ( const skipThemes of [
+ '--skip-themes',
+ `--skip-themes=${ activeTheme }`,
+ ] ) {
+ await seedPendingRewrite();
+ await wpCLI( [
+ 'wp',
+ 'option',
+ 'get',
+ 'siteurl',
+ skipThemes,
+ ...skipUnrelatedPlugins,
+ ] );
+ expect(
+ await readPendingRewrite(),
+ `Expected the queue to survive ${ skipThemes }.`
+ ).toBe( 'yes' );
+ }
+
+ await seedPendingRewrite();
+ await wpCLI( [
+ 'wp',
+ 'option',
+ 'get',
+ 'siteurl',
+ `--skip-themes=${ inactiveTheme }`,
+ ...skipUnrelatedPlugins,
+ ] );
+ expect(
+ await readPendingRewrite(),
+ 'Expected the inactive-theme control to consume the queue.'
+ ).toBe( 'missing' );
+ } finally {
+ await wpCLI( [
+ 'wp',
+ 'eval',
+ `delete_option( "${ pendingOption }" );`,
+ ...coreOnlyFlags,
+ ] );
+ // Let the next normal request regenerate the complete ruleset without loading extension-heavy state under WP-CLI.
+ await wpCLI( [
+ 'wp',
+ 'option',
+ 'update',
+ 'rewrite_rules',
+ '',
+ ...coreOnlyFlags,
+ ] );
+ }
+ }
+ );
+
test( 'Scenario 1 — happy path: rate a product, submit, see thank-you in place', async ( {
page,
restApi,
diff --git a/plugins/woocommerce/tests/e2e/utils/cli.ts b/plugins/woocommerce/tests/e2e/utils/cli.ts
index ce9fa249e8c..9e3ce6251a9 100644
--- a/plugins/woocommerce/tests/e2e/utils/cli.ts
+++ b/plugins/woocommerce/tests/e2e/utils/cli.ts
@@ -2,14 +2,29 @@
* External dependencies
*/
import { promisify } from 'util';
-import { exec } from 'child_process';
+import { exec, execFile } from 'child_process';
const execAsync = promisify( exec );
+const execFileAsync = promisify( execFile );
-const wpCLI = async ( command: string ) => {
- const { stdout, stderr } = await execAsync(
- `pnpm exec wp-env --config .wp-env.e2e.json run cli -- ${ command }`
- );
+/**
+ * Runs a command in the E2E CLI container. Use an argument array when the command contains dynamic values.
+ */
+const wpCLI = async ( command: string | string[] ) => {
+ const { stdout, stderr } = Array.isArray( command )
+ ? await execFileAsync( 'pnpm', [
+ 'exec',
+ 'wp-env',
+ '--config',
+ '.wp-env.e2e.json',
+ 'run',
+ 'cli',
+ '--',
+ ...command,
+ ] )
+ : await execAsync(
+ `pnpm exec wp-env --config .wp-env.e2e.json run cli -- ${ command }`
+ );
return { stdout, stderr };
};