From af447ebd0828bf65acd156bd17103be639506467 Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 13:44:06 +0200 Subject: [PATCH 01/30] fix: improve snippet filtering for multisite support --- src/php/class-db.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/php/class-db.php b/src/php/class-db.php index 84ab399c..f88bdea6 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -325,8 +325,15 @@ public function fetch_active_snippets( array $scopes ): array { foreach ( $ms_snippets as $snippet ) { $id = intval( $snippet['id'] ); + $active_value = intval( $snippet['active'] ); - if ( ! $snippet['active'] && ! in_array( $id, $active_shared_ids, true ) ) { + // Skip if snippet is trashed. + if ( -1 === $active_value ) { + continue; + } + + // Skip if snippet is not active and not in the list of active shared snippets. + if ( 1 !== $active_value && ! in_array( $id, $active_shared_ids, true ) ) { continue; } From 750af7371b0123365201fa9b7472e46c627b81ff Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 13:50:26 +0200 Subject: [PATCH 02/30] fix: enhance multisite snippet handling by ensuring local snippets use the correct table and filtering out trashed snippets --- .../classes/class-snippet-files.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index ae75156a..1700fc67 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -245,7 +245,8 @@ public static function get_active_snippets_from_flat_files( $active_snippets = []; $db = code_snippets()->db; - $table = self::get_hashed_table_name( $db->get_table_name() ); + // Always use the site table for "local" snippets, even in Network Admin. + $table = self::get_hashed_table_name( $db->get_table_name( false ) ); $snippets = self::load_active_snippets_from_file( $table, $snippet_type, @@ -289,8 +290,14 @@ public static function get_active_snippets_from_flat_files( foreach ( $ms_snippets as $snippet ) { $id = intval( $snippet['id'] ); + $active_value = intval( $snippet['active'] ); - if ( ! $snippet['active'] && ! in_array( $id, $active_shared_ids, true ) ) { + // Never execute trashed snippets (active = -1). + if ( -1 === $active_value ) { + continue; + } + + if ( 1 !== $active_value && ! in_array( $id, $active_shared_ids, true ) ) { continue; } @@ -375,7 +382,14 @@ private static function load_active_snippets_from_file( $filtered_snippets = array_filter( $file_snippets, function ( $snippet ) use ( $scopes, $active_shared_ids ) { - $is_active = $snippet['active']; + $active_value = isset( $snippet['active'] ) ? intval( $snippet['active'] ) : 0; + + // Never treat trashed snippets as active. + if ( -1 === $active_value ) { + return false; + } + + $is_active = 1 === $active_value; if ( null !== $active_shared_ids ) { $is_active = $is_active || in_array( From 29669218d3cebd6bf7046a40708e55ab455e2e3d Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 13:59:45 +0200 Subject: [PATCH 03/30] fix: refactor snippet execution logic for multisite support by centralizing trashed snippet handling --- src/php/class-db.php | 32 +++++++++++++++---- .../classes/class-snippet-files.php | 27 ++++------------ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/php/class-db.php b/src/php/class-db.php index f88bdea6..cbedd0ec 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -327,13 +327,7 @@ public function fetch_active_snippets( array $scopes ): array { $id = intval( $snippet['id'] ); $active_value = intval( $snippet['active'] ); - // Skip if snippet is trashed. - if ( -1 === $active_value ) { - continue; - } - - // Skip if snippet is not active and not in the list of active shared snippets. - if ( 1 !== $active_value && ! in_array( $id, $active_shared_ids, true ) ) { + if ( ! self::is_network_snippet_enabled( $active_value, $id, $active_shared_ids ) ) { continue; } @@ -353,4 +347,28 @@ public function fetch_active_snippets( array $scopes ): array { return $active_snippets; } + + /** + * Determine whether a network snippet should execute on the current site. + * + * Network snippets execute when active=1, or when the snippet is listed as active-shared for the site. + * Trashed snippets (active=-1) never execute. + * + * @param int $active_value Raw active value from the snippet record. + * @param int $snippet_id Snippet ID. + * @param int[] $active_shared_ids Active shared network snippet IDs for the current site. + * + * @return bool + */ + public static function is_network_snippet_enabled( int $active_value, int $snippet_id, array $active_shared_ids ): bool { + if ( -1 === $active_value ) { + return false; + } + + if ( 1 === $active_value ) { + return true; + } + + return in_array( $snippet_id, $active_shared_ids, true ); + } } diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 1700fc67..6ade8cf0 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -292,12 +292,7 @@ public static function get_active_snippets_from_flat_files( $id = intval( $snippet['id'] ); $active_value = intval( $snippet['active'] ); - // Never execute trashed snippets (active = -1). - if ( -1 === $active_value ) { - continue; - } - - if ( 1 !== $active_value && ! in_array( $id, $active_shared_ids, true ) ) { + if ( ! DB::is_network_snippet_enabled( $active_value, $id, $active_shared_ids ) ) { continue; } @@ -378,26 +373,16 @@ private static function load_active_snippets_from_file( } $file_snippets = require $snippets_file_path; + $shared_ids = is_array( $active_shared_ids ) + ? array_map( 'intval', $active_shared_ids ) + : []; $filtered_snippets = array_filter( $file_snippets, - function ( $snippet ) use ( $scopes, $active_shared_ids ) { + function ( $snippet ) use ( $scopes, $shared_ids ) { $active_value = isset( $snippet['active'] ) ? intval( $snippet['active'] ) : 0; - // Never treat trashed snippets as active. - if ( -1 === $active_value ) { - return false; - } - - $is_active = 1 === $active_value; - - if ( null !== $active_shared_ids ) { - $is_active = $is_active || in_array( - intval( $snippet['id'] ), - $active_shared_ids, - true - ); - } + $is_active = DB::is_network_snippet_enabled( $active_value, intval( $snippet['id'] ), $shared_ids ); return ( $is_active || 'condition' === $snippet['scope'] ) && in_array( $snippet['scope'], $scopes, true ); } From 0ce8c45799fd779cfce9c8f5b2bf21e756ed714a Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 14:18:27 +0200 Subject: [PATCH 04/30] phpcs: enhance documentation for Snippet_Files class with detailed PHPDoc comments --- .../classes/class-snippet-files.php | 210 +++++++++++++++++- 1 file changed, 209 insertions(+), 1 deletion(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 6ade8cf0..b41f8df4 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -2,6 +2,12 @@ namespace Code_Snippets; +/** + * Manage file-based snippet execution. + * + * Responsible for writing snippet code to disk, maintaining per-table config indexes, + * and retrieving the active snippet list from those config files. + */ class Snippet_Files { /** @@ -9,12 +15,34 @@ class Snippet_Files { */ private const ENABLED_FLAG_FILE = 'flat-files-enabled.flag'; + /** + * Snippet handler registry. + * + * @var Snippet_Handler_Registry + */ private Snippet_Handler_Registry $handler_registry; + /** + * File system adapter. + * + * @var File_System_Interface + */ private File_System_Interface $fs; + /** + * Config repository. + * + * @var Snippet_Config_Repository_Interface + */ private Snippet_Config_Repository_Interface $config_repo; + /** + * Constructor. + * + * @param Snippet_Handler_Registry $handler_registry Handler registry instance. + * @param File_System_Interface $fs File system adapter. + * @param Snippet_Config_Repository_Interface $config_repo Config repository instance. + */ public function __construct( Snippet_Handler_Registry $handler_registry, File_System_Interface $fs, @@ -36,10 +64,22 @@ public static function is_active(): bool { return file_exists( $flag_file_path ); } + /** + * Get the full path to the flat-file enabled flag. + * + * @return string + */ private static function get_flag_file_path(): string { return self::get_base_dir() . '/' . self::ENABLED_FLAG_FILE; } + /** + * Create or delete the enabled flag file. + * + * @param bool $enabled Whether file-based execution is enabled. + * + * @return void + */ private function handle_enabled_file_flag( bool $enabled ): void { $flag_file_path = self::get_flag_file_path(); @@ -53,6 +93,11 @@ private function handle_enabled_file_flag( bool $enabled ): void { } } + /** + * Register WordPress hooks used by file-based execution. + * + * @return void + */ public function register_hooks(): void { if ( ! $this->fs->is_writable( WP_CONTENT_DIR ) ) { return; @@ -75,6 +120,14 @@ public function register_hooks(): void { add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 2 ); } + /** + * Activate multiple snippets and regenerate their flat files. + * + * @param Snippet[] $valid_snippets Snippets to activate. + * @param string $table Table name. + * + * @return void + */ public function activate_snippets( $valid_snippets, $table ): void { foreach ( $valid_snippets as $snippet ) { $snippet->active = true; @@ -82,6 +135,14 @@ public function activate_snippets( $valid_snippets, $table ): void { } } + /** + * Write a snippet file and update its config index entry. + * + * @param Snippet $snippet Snippet object. + * @param string $table Table name. + * + * @return void + */ public function handle_snippet( Snippet $snippet, string $table ): void { if ( 0 === $snippet->id ) { return; @@ -106,6 +167,14 @@ public function handle_snippet( Snippet $snippet, string $table ): void { $this->config_repo->update( $base_dir, $snippet ); } + /** + * Delete a snippet file and remove it from the config index. + * + * @param Snippet $snippet Snippet object. + * @param bool $network Whether the snippet is network-wide. + * + * @return void + */ public function delete_snippet( Snippet $snippet, bool $network ): void { $handler = $this->handler_registry->get_handler( $snippet->type ); @@ -122,6 +191,13 @@ public function delete_snippet( Snippet $snippet, bool $network ): void { $this->config_repo->update( $base_dir, $snippet, true ); } + /** + * Activate a snippet by writing its code file and updating config. + * + * @param Snippet $snippet Snippet object. + * + * @return void + */ public function activate_snippet( Snippet $snippet ): void { $snippet = get_snippet( $snippet->id, $snippet->network ); $handler = $this->handler_registry->get_handler( $snippet->type ); @@ -144,6 +220,14 @@ public function activate_snippet( Snippet $snippet ): void { $this->config_repo->update( $base_dir, $snippet ); } + /** + * Deactivate a snippet by updating its config entry. + * + * @param int $snippet_id Snippet ID. + * @param bool $network Whether the snippet is network-wide. + * + * @return void + */ public function deactivate_snippet( int $snippet_id, bool $network ): void { $snippet = get_snippet( $snippet_id, $network ); $handler = $this->handler_registry->get_handler( $snippet->type ); @@ -158,6 +242,14 @@ public function deactivate_snippet( int $snippet_id, bool $network ): void { $this->config_repo->update( $base_dir, $snippet ); } + /** + * Get the base directory for flat files. + * + * @param string $table Optional hashed table name. + * @param string $snippet_type Optional snippet type directory. + * + * @return string + */ public static function get_base_dir( string $table = '', string $snippet_type = '' ): string { $base_dir = WP_CONTENT_DIR . '/code-snippets'; @@ -172,6 +264,14 @@ public static function get_base_dir( string $table = '', string $snippet_type = return $base_dir; } + /** + * Get the base URL for flat files. + * + * @param string $table Optional hashed table name. + * @param string $snippet_type Optional snippet type directory. + * + * @return string + */ public static function get_base_url( string $table = '', string $snippet_type = '' ): string { $base_url = WP_CONTENT_URL . '/code-snippets'; @@ -186,6 +286,13 @@ public static function get_base_url( string $table = '', string $snippet_type = return $base_url; } + /** + * Create a directory if it does not exist. + * + * @param string $dir Directory path. + * + * @return void + */ private function maybe_create_directory( string $dir ): void { if ( ! $this->fs->is_dir( $dir ) ) { $result = wp_mkdir_p( $dir ); @@ -196,16 +303,41 @@ private function maybe_create_directory( string $dir ): void { } } + /** + * Build the file path for a snippet's code file. + * + * @param string $base_dir Base directory path. + * @param int $snippet_id Snippet ID. + * @param string $ext File extension. + * + * @return string + */ private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ): string { return trailingslashit( $base_dir ) . $snippet_id . '.' . $ext; } + /** + * Delete a file if it exists. + * + * @param string $file_path File path. + * + * @return void + */ private function delete_file( string $file_path ): void { if ( $this->fs->exists( $file_path ) ) { $this->fs->delete( $file_path ); } } + /** + * Sync the active shared network snippets list to a config file. + * + * @param string $option Option name. + * @param mixed $old_value Previous value. + * @param mixed $value New value. + * + * @return void + */ public function sync_active_shared_network_snippets( $option, $old_value, $value ): void { if ( 'active_shared_network_snippets' !== $option ) { return; @@ -214,6 +346,14 @@ public function sync_active_shared_network_snippets( $option, $old_value, $value $this->create_active_shared_network_snippets_file( $value ); } + /** + * Sync the active shared network snippets list to a config file when first added. + * + * @param string $option Option name. + * @param mixed $value Option value. + * + * @return void + */ public function sync_active_shared_network_snippets_add( $option, $value ): void { if ( 'active_shared_network_snippets' !== $option ) { return; @@ -222,6 +362,13 @@ public function sync_active_shared_network_snippets_add( $option, $value ): void $this->create_active_shared_network_snippets_file( $value ); } + /** + * Create or update the active shared network snippets config file. + * + * @param mixed $value Option value. + * + * @return void + */ private function create_active_shared_network_snippets_file( $value ): void { $table = self::get_hashed_table_name( code_snippets()->db->get_table_name( false ) ); $base_dir = self::get_base_dir( $table ); @@ -229,15 +376,31 @@ private function create_active_shared_network_snippets_file( $value ): void { $this->maybe_create_directory( $base_dir ); $file_path = trailingslashit( $base_dir ) . 'active-shared-network-snippets.php'; + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- var_export is required for writing PHP config files. $file_content = "fs->put_contents( $file_path, $file_content, FS_CHMOD_FILE ); } + /** + * Hash a table name for file system usage. + * + * @param string $table Table name. + * + * @return string + */ public static function get_hashed_table_name( string $table ): string { return wp_hash( $table ); } + /** + * Get a list of active snippets from flat file config. + * + * @param array $scopes Scopes to include. + * @param string $snippet_type Snippet type directory. + * + * @return array> + */ public static function get_active_snippets_from_flat_files( array $scopes = [], $snippet_type = 'php' @@ -314,7 +477,15 @@ public static function get_active_snippets_from_flat_files( return $active_snippets; } - private static function sort_active_snippets( array &$active_snippets, $db ): void { + /** + * Sort active snippet entries for execution order. + * + * @param array> $active_snippets Active snippets list. + * @param DB $db Database instance. + * + * @return void + */ + private static function sort_active_snippets( array &$active_snippets, DB $db ): void { $comparisons = [ function ( array $a, array $b ) { return $a['priority'] <=> $b['priority']; @@ -344,6 +515,16 @@ static function ( $a, $b ) use ( $comparisons ) { ); } + /** + * Load active snippets from a flat file config index. + * + * @param string $table Hashed table directory name. + * @param string $snippet_type Snippet type directory. + * @param string[] $scopes Scopes to include. + * @param int[]|null $active_shared_ids Optional list of active shared network snippet IDs. + * + * @return array> + */ private static function load_active_snippets_from_file( string $table, string $snippet_type, @@ -393,6 +574,13 @@ function ( $snippet ) use ( $scopes, $shared_ids ) { return $filtered_snippets; } + /** + * Add file-based execution settings fields. + * + * @param array $fields Settings fields. + * + * @return array + */ public function add_settings_fields( array $fields ): array { $fields['general']['enable_flat_files'] = [ 'name' => __( 'Enable file-based execution', 'code-snippets' ), @@ -407,7 +595,17 @@ public function add_settings_fields( array $fields ): array { return $fields; } + /** + * Recreate all flat files when file-based execution settings are updated. + * + * @param array $settings Settings data. + * @param array $input Raw input data. + * + * @return void + */ public function create_all_flat_files( array $settings, array $input ): void { + unset( $input ); + if ( ! isset( $settings['general']['enable_flat_files'] ) ) { return; } @@ -422,6 +620,11 @@ public function create_all_flat_files( array $settings, array $input ): void { $this->create_active_shared_network_snippets_config_file(); } + /** + * Create snippet code files and config indexes for all active snippets. + * + * @return void + */ private function create_snippet_flat_files(): void { $db = code_snippets()->db; @@ -456,6 +659,11 @@ private function create_snippet_flat_files(): void { } } + /** + * Create active shared network snippet config files for each site (multisite) or the current site. + * + * @return void + */ private function create_active_shared_network_snippets_config_file(): void { if ( is_multisite() ) { $current_blog_id = get_current_blog_id(); From 5c5c035567306033d8ffd04d36dbece01c49bc95 Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 14:20:35 +0200 Subject: [PATCH 05/30] fix: reorganize class and move private method to the bottom --- src/php/class-db.php | 196 +++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/src/php/class-db.php b/src/php/class-db.php index cbedd0ec..27ed423f 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -197,104 +197,20 @@ public static function create_table( string $table_name ): bool { return $success; } - /** - * Fetch a list of active snippets from a database table. - * - * @param string $table_name Name of table to fetch snippets from. - * @param array $scopes List of scopes to include in query. - * @param boolean $active_only Whether to only fetch active snippets from the table. - * - * @return array>|false List of active snippets, if any could be retrieved. - * - * @phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare - */ - private static function fetch_snippets_from_table( string $table_name, array $scopes, bool $active_only = true ) { - global $wpdb; - - $cache_key = sprintf( 'active_snippets_%s_%s', sanitize_key( join( '_', $scopes ) ), $table_name ); - $cached_snippets = wp_cache_get( $cache_key, CACHE_GROUP ); - - if ( is_array( $cached_snippets ) ) { - return $cached_snippets; - } - - if ( ! self::table_exists( $table_name ) ) { - return false; - } - - $scopes_format = implode( ',', array_fill( 0, count( $scopes ), '%s' ) ); - $extra_where = $active_only ? 'AND active=1' : ''; - - $snippets = $wpdb->get_results( - $wpdb->prepare( - " - SELECT id, code, scope, active, priority - FROM $table_name - WHERE scope IN ($scopes_format) $extra_where - ORDER BY priority, id", - $scopes - ), - 'ARRAY_A' - ); - - // Cache the full list of snippets. - if ( is_array( $snippets ) ) { - wp_cache_set( $cache_key, $snippets, CACHE_GROUP ); - return $snippets; - } - - return false; - } - - /** - * Sort the active snippets by priority, table, and ID. - * - * @param array $active_snippets List of active snippets to sort. - */ - private function sort_active_snippets( array &$active_snippets ): void { - $comparisons = [ - function ( array $a, array $b ) { - return $a['priority'] <=> $b['priority']; - }, - function ( array $a, array $b ) { - $a_table = $a['table'] === $this->ms_table ? 0 : 1; - $b_table = $b['table'] === $this->ms_table ? 0 : 1; - return $a_table <=> $b_table; - }, - function ( array $a, array $b ) { - return $a['id'] <=> $b['id']; - }, - ]; - - usort( - $active_snippets, - static function ( $a, $b ) use ( $comparisons ) { - foreach ( $comparisons as $comparison ) { - $result = $comparison( $a, $b ); - if ( 0 !== $result ) { - return $result; - } - } - - return 0; - } - ); - } - - /** - * Generate the SQL for fetching active snippets from the database. - * - * @param string[] $scopes List of scopes to retrieve in. - * - * @return array{ - * id: int, - * code: string, - * scope: string, - * table: string, - * network: bool, - * priority: int, - * } List of active snippets. - */ + /** + * Generate the SQL for fetching active snippets from the database. + * + * @param string[] $scopes List of scopes to retrieve in. + * + * @return array{ + * id: int, + * code: string, + * scope: string, + * table: string, + * network: bool, + * priority: int, + * } List of active snippets. + */ public function fetch_active_snippets( array $scopes ): array { $active_snippets = []; @@ -371,4 +287,88 @@ public static function is_network_snippet_enabled( int $active_value, int $snipp return in_array( $snippet_id, $active_shared_ids, true ); } + + /** + * Sort the active snippets by priority, table, and ID. + * + * @param array $active_snippets List of active snippets to sort. + */ + private function sort_active_snippets( array &$active_snippets ): void { + $comparisons = [ + function ( array $a, array $b ) { + return $a['priority'] <=> $b['priority']; + }, + function ( array $a, array $b ) { + $a_table = $a['table'] === $this->ms_table ? 0 : 1; + $b_table = $b['table'] === $this->ms_table ? 0 : 1; + return $a_table <=> $b_table; + }, + function ( array $a, array $b ) { + return $a['id'] <=> $b['id']; + }, + ]; + + usort( + $active_snippets, + static function ( $a, $b ) use ( $comparisons ) { + foreach ( $comparisons as $comparison ) { + $result = $comparison( $a, $b ); + if ( 0 !== $result ) { + return $result; + } + } + + return 0; + } + ); + } + + /** + * Fetch a list of active snippets from a database table. + * + * @param string $table_name Name of table to fetch snippets from. + * @param array $scopes List of scopes to include in query. + * @param boolean $active_only Whether to only fetch active snippets from the table. + * + * @return array>|false List of active snippets, if any could be retrieved. + * + * @phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + */ + private static function fetch_snippets_from_table( string $table_name, array $scopes, bool $active_only = true ) { + global $wpdb; + + $cache_key = sprintf( 'active_snippets_%s_%s', sanitize_key( join( '_', $scopes ) ), $table_name ); + $cached_snippets = wp_cache_get( $cache_key, CACHE_GROUP ); + + if ( is_array( $cached_snippets ) ) { + return $cached_snippets; + } + + if ( ! self::table_exists( $table_name ) ) { + return false; + } + + $scopes_format = implode( ',', array_fill( 0, count( $scopes ), '%s' ) ); + $extra_where = $active_only ? 'AND active=1' : ''; + + $snippets = $wpdb->get_results( + $wpdb->prepare( + " + SELECT id, code, scope, active, priority + FROM $table_name + WHERE scope IN ($scopes_format) $extra_where + ORDER BY priority, id", + $scopes + ), + 'ARRAY_A' + ); + + // Cache the full list of snippets. + if ( is_array( $snippets ) ) { + wp_cache_set( $cache_key, $snippets, CACHE_GROUP ); + return $snippets; + } + + return false; + } } From 42235306f6fc53d8e95ca4662db4b19096a75228 Mon Sep 17 00:00:00 2001 From: Imants Date: Mon, 15 Dec 2025 20:04:41 +0200 Subject: [PATCH 06/30] fix: multisite capability check in Plugin class --- src/php/class-plugin.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index c05613e2..84b872c1 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -132,7 +132,7 @@ public function load_plugin() { // Settings component. require_once $includes_path . '/settings/settings-fields.php'; require_once $includes_path . '/settings/editor-preview.php'; - require_once $includes_path . '/settings/class-version-switch.php'; + require_once $includes_path . '/settings/class-version-switch.php'; require_once $includes_path . '/settings/settings.php'; // Cloud List Table shared functions. @@ -357,6 +357,10 @@ public function get_cap(): string { return $this->get_network_cap_name(); } + if ( is_multisite() && ! $this->is_subsite_menu_enabled() ) { + return $this->get_network_cap_name(); + } + return $this->get_cap_name(); } From 49f9e95f59f455dde45eb9bc73d7ae7e8948299e Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 11:06:39 +0200 Subject: [PATCH 07/30] fix: add screenshot assertions maxDiffPixelRatio --- tests/e2e/code-snippets-list.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index 1b01bd06..e61bb6a1 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -3,6 +3,7 @@ import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' import { SELECTORS } from './helpers/constants' const TEST_SNIPPET_NAME = 'E2E List Test Snippet' +const MAX_DIFF_PIXEL_RATIO = 0.1 test.describe('Code Snippets List Page Actions', () => { let helper: SnippetsTestHelper @@ -29,7 +30,7 @@ test.describe('Code Snippets List Page Actions', () => { await expect(toggleSwitch).toHaveAttribute('title', 'Deactivate') // Check that the toggle is rendered to the right (active) - await expect(snippetRow).toHaveScreenshot('snippet-row-active.png') + await expect(snippetRow).toHaveScreenshot('snippet-row-active.png', { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO }) await toggleSwitch.click() await page.waitForLoadState('networkidle') @@ -39,7 +40,7 @@ test.describe('Code Snippets List Page Actions', () => { await expect(updatedToggle).toHaveAttribute('title', 'Activate') // Check that the toggle is rendered to the left (inactive) - await expect(updatedRow).toHaveScreenshot('snippet-row-inactive.png') + await expect(updatedRow).toHaveScreenshot('snippet-row-inactive.png', { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO }) await updatedToggle.click() await page.waitForLoadState('networkidle') From 2fb1b60e5fecbda8b2f94ec416c42ec00fcf194f Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 15:12:25 +0200 Subject: [PATCH 08/30] fix: enhance Playwright test workflow with multisite support and improved WordPress setup --- .github/workflows/playwright-test.yml | 138 ++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 7103c136..5325b2b7 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -11,11 +11,46 @@ on: required: true type: string description: 'Playwright project name to run' + multisite: + required: false + type: boolean + default: false + description: 'If true, convert the site to multisite and create a subsite.' jobs: playwright-test: name: Playwright tests (${{ inputs.test-mode == 'default' && 'Default Mode' || 'File-based Execution' }}) runs-on: ubuntu-22.04 + services: + mysql: + image: mysql:8.0 + env: + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: root + options: >- + --health-cmd="mysqladmin ping -h localhost -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + + wordpress: + image: wordpress:php8.1-apache + env: + WORDPRESS_DB_HOST: mysql:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + WORDPRESS_DEBUG: 1 + WORDPRESS_CONFIG_EXTRA: | + define( 'FS_METHOD', 'direct' ); + define( 'WP_DEBUG_LOG', true ); + define( 'WP_DEBUG_DISPLAY', false ); + define( 'SCRIPT_DEBUG', true ); + define( 'WP_ENVIRONMENT_TYPE', 'local' ); + ports: + - 8888:80 steps: - name: Checkout source code uses: actions/checkout@v4 @@ -59,7 +94,7 @@ jobs: restore-keys: | ${{ runner.os }}-deps- - - name: Install workflow dependencies (wp-env, playwright) + - name: Install workflow dependencies if: steps.deps-cache.outputs.cache-hit != 'true' run: npm run prepare-environment:ci && npm run bundle @@ -72,17 +107,97 @@ jobs: node_modules key: ${{ runner.os }}-deps-${{ steps.deps-hash.outputs.deps_hash }} - - name: Start WordPress environment + - name: Wait for WordPress to be reachable + run: | + for i in $(seq 1 60); do + if curl -fsS http://localhost:8888/wp-login.php >/dev/null; then + echo "WordPress is reachable." + exit 0 + fi + echo "Waiting for WordPress... ($i/60)" + sleep 2 + done + + echo "WordPress did not start in time." + echo "::group::WordPress container logs" + docker logs "${{ job.services.wordpress.id }}" || true + echo "::endgroup::" + exit 1 + + - name: Download WP-CLI + run: | + curl -fsSL -o "${RUNNER_TEMP}/wp-cli.phar" https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + + - name: Install WordPress (and optionally multisite) + env: + WP_CONTAINER: ${{ job.services.wordpress.id }} + WP_URL: http://localhost:8888 + WP_ADMIN_USER: admin + WP_ADMIN_PASSWORD: password + WP_ADMIN_EMAIL: admin@example.org + run: | + set -euo pipefail + + docker cp "${RUNNER_TEMP}/wp-cli.phar" "$WP_CONTAINER:/tmp/wp-cli.phar" + + # Install WordPress if it isn't already installed. + if ! docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core is-installed --allow-root >/dev/null 2>&1; then + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core install \ + --url="$WP_URL" \ + --title="Test Blog" \ + --admin_user="$WP_ADMIN_USER" \ + --admin_password="$WP_ADMIN_PASSWORD" \ + --admin_email="$WP_ADMIN_EMAIL" \ + --skip-email \ + --allow-root + fi + + if [ "${{ inputs.multisite }}" = "true" ]; then + # Convert single site -> multisite (subdirectory). Subdomains don't work with localhost. + if ! docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core is-installed --network --allow-root >/dev/null 2>&1; then + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core multisite-convert \ + --title="Test Network" \ + --base=/ \ + --allow-root + fi + + # Create a subsite for future multisite test coverage. + if ! docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar site list --field=path --allow-root | grep -qx "/subsite/"; then + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar site create \ + --slug=subsite \ + --title="Subsite" \ + --email="$WP_ADMIN_EMAIL" \ + --allow-root + fi + fi + + - name: Install plugin into WordPress container + env: + WP_CONTAINER: ${{ job.services.wordpress.id }} run: | - npx wp-env start + set -euo pipefail + + docker exec -u root -w /var/www/html "$WP_CONTAINER" rm -rf wp-content/plugins/code-snippets + docker cp src "$WP_CONTAINER:/var/www/html/wp-content/plugins/code-snippets" + docker exec -u root -w /var/www/html "$WP_CONTAINER" chown -R www-data:www-data wp-content/plugins/code-snippets - name: Activate code-snippets plugin - run: npx wp-env run cli wp plugin activate code-snippets + env: + WP_CONTAINER: ${{ job.services.wordpress.id }} + run: | + set -euo pipefail + if [ "${{ inputs.multisite }}" = "true" ]; then + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar plugin activate code-snippets --network --allow-root + else + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar plugin activate code-snippets --allow-root + fi - name: WordPress debug information + env: + WP_CONTAINER: ${{ job.services.wordpress.id }} run: | - npx wp-env run cli wp core version - npx wp-env run cli wp --info + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core version --allow-root + docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar --allow-root --info - name: Install playwright/test run: | @@ -90,10 +205,13 @@ jobs: - name: Run Playwright tests run: npm run test:playwright -- --project=${{ inputs.project-name }} - - - name: Stop WordPress environment - if: always() - run: npx wp-env stop + + - name: Print WordPress logs on failure + if: failure() + run: | + echo "::group::WordPress container logs" + docker logs "${{ job.services.wordpress.id }}" || true + echo "::endgroup::" - uses: actions/upload-artifact@v4 if: always() From 47a20a4d5a15dc9a79cde5354e15c2198023740d Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 17:00:29 +0200 Subject: [PATCH 09/30] fix: clarify documentation is_network_snippet_enabled --- src/php/class-db.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/php/class-db.php b/src/php/class-db.php index 27ed423f..4254dc51 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -268,9 +268,9 @@ public function fetch_active_snippets( array $scopes ): array { * Determine whether a network snippet should execute on the current site. * * Network snippets execute when active=1, or when the snippet is listed as active-shared for the site. - * Trashed snippets (active=-1) never execute. + * Trashed snippets (active=-1) should never execute. * - * @param int $active_value Raw active value from the snippet record. + * @param int $active_value Raw active value: 1=active, 0=inactive, -1=trashed (can be stored as a string in the database). * @param int $snippet_id Snippet ID. * @param int[] $active_shared_ids Active shared network snippet IDs for the current site. * From d840f7da2d18676c1ae64a2812488fcc071acdc5 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 17:02:29 +0200 Subject: [PATCH 10/30] fix: update create_all_flat_files method to remove unused parameter and adjust action hook --- src/php/flat-files/classes/class-snippet-files.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index b41f8df4..85a2ae62 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -117,7 +117,7 @@ public function register_hooks(): void { } add_filter( 'code_snippets_settings_fields', [ $this, 'add_settings_fields' ], 10, 1 ); - add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 2 ); + add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 1 ); } /** @@ -599,13 +599,10 @@ public function add_settings_fields( array $fields ): array { * Recreate all flat files when file-based execution settings are updated. * * @param array $settings Settings data. - * @param array $input Raw input data. * * @return void */ - public function create_all_flat_files( array $settings, array $input ): void { - unset( $input ); - + public function create_all_flat_files( array $settings ): void { if ( ! isset( $settings['general']['enable_flat_files'] ) ) { return; } From c49e829e29da938fe284b123f420ca81f5729781 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 22:21:14 +0200 Subject: [PATCH 11/30] fix: enhance Playwright test workflow and add wpCli helper for WP-CLI commands --- .github/workflows/playwright-test.yml | 5 +++ tests/e2e/code-snippets-evaluation.spec.ts | 24 ++++------- tests/e2e/helpers/wpCli.ts | 49 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 tests/e2e/helpers/wpCli.ts diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 5325b2b7..66f48101 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -204,6 +204,11 @@ jobs: npx playwright install chromium - name: Run Playwright tests + env: + WP_E2E_WPCLI_MODE: gh-actions-ci + WP_E2E_WP_CONTAINER: ${{ job.services.wordpress.id }} + WP_E2E_WPCLI_PHAR: /tmp/wp-cli.phar + WP_E2E_WPCLI_URL: http://localhost:8888 run: npm run test:playwright -- --project=${{ inputs.project-name }} - name: Print WordPress logs on failure diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 48f11e93..3bccb588 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -1,8 +1,7 @@ -import util from 'util' -import { exec } from 'child_process' import { expect, test } from '@playwright/test' import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' import { SELECTORS } from './helpers/constants' +import { wpCli } from './helpers/wpCli' import type { Page } from '@playwright/test' const TEST_SNIPPET_NAME = 'E2E Snippet Test' @@ -34,27 +33,22 @@ const verifyShortcodeRendersCorrectly = async ( } const createPageWithShortcode = async (snippetId: string): Promise => { - const execAsync = util.promisify(exec) - const shortcode = `[code_snippet id=${snippetId} format name="${TEST_SNIPPET_NAME}"]` const pageContent = `

Page content before shortcode.

\n\n${shortcode}\n\n

Page content after shortcode.

` try { - const createPageCmd = [ - 'npx wp-env run cli wp post create', + const pageId = (await wpCli([ + 'post', + 'create', '--post_type=page', - '--post_title="Test Page for Snippet Shortcode"', - `--post_content='${pageContent}'`, + '--post_title=Test Page for Snippet Shortcode', + `--post_content=${pageContent}`, '--post_status=publish', '--porcelain' - ].join(' ') - - const { stdout } = await execAsync(createPageCmd) - const pageId = stdout.trim() + ])).trim() - const getUrlCmd = `npx wp-env run cli wp post url ${pageId}` - const { stdout: pageUrl } = await execAsync(getUrlCmd) - return pageUrl.trim() + const pageUrl = (await wpCli(['post', 'url', pageId])).trim() + return pageUrl } catch (error) { console.error('Failed to create page via WP-CLI:', error) throw error diff --git a/tests/e2e/helpers/wpCli.ts b/tests/e2e/helpers/wpCli.ts new file mode 100644 index 00000000..99b3e68b --- /dev/null +++ b/tests/e2e/helpers/wpCli.ts @@ -0,0 +1,49 @@ +import util from 'util' +import { execFile } from 'child_process' + +export interface WpCliOptions { + url?: string +} + +const execFileAsync = util.promisify(execFile) + +const hasArg = (args: string[], prefix: string): boolean => + args.some(arg => arg === prefix || arg.startsWith(`${prefix}=`)) + +export const wpCli = async (args: string[], options: WpCliOptions = {}): Promise => { + const mode = (process.env.WP_E2E_WPCLI_MODE ?? '').toLowerCase() + const dockerContainer = process.env.WP_E2E_WP_CONTAINER + + const url = options.url ?? process.env.WP_E2E_WPCLI_URL + const urlArgs = url && !hasArg(args, '--url') ? [`--url=${url}`] : [] + + if (dockerContainer || 'gh-actions-ci' === mode) { + if (!dockerContainer) { + throw new Error('WP_E2E_WP_CONTAINER must be set when WP_E2E_WPCLI_MODE is gh-actions-ci.') + } + + const pharPath = process.env.WP_E2E_WPCLI_PHAR ?? '/tmp/wp-cli.phar' + const allowRootArgs = hasArg(args, '--allow-root') ? [] : ['--allow-root'] + + const { stdout } = await execFileAsync('docker', [ + 'exec', + '-u', + 'root', + '-w', + '/var/www/html', + dockerContainer, + 'php', + pharPath, + ...urlArgs, + ...allowRootArgs, + ...args + ]) + + return stdout + } + + // Default to wp-env (local dev) for backwards compatibility. + const { stdout } = await execFileAsync('npx', ['wp-env', 'run', 'cli', 'wp', ...urlArgs, ...args]) + + return stdout +} From faf297836594191034a47adbed6686c58f6ce281 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 23:17:23 +0200 Subject: [PATCH 12/30] fix: update Playwright workflow to support `multisite` configuration and improve caching keys --- .github/workflows/playwright-test.yml | 11 +++++++---- .github/workflows/playwright.yml | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 66f48101..921dc0ee 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -55,6 +55,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: WordPress mode + run: echo "multisite=${{ inputs.multisite }}" + - name: Set up PHP uses: codesnippetspro/setup-php@v2 with: @@ -90,9 +93,9 @@ jobs: path: | node_modules src/vendor - key: ${{ runner.os }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} restore-keys: | - ${{ runner.os }}-deps- + ${{ runner.os }}-${{ inputs.test-mode }}-deps- - name: Install workflow dependencies if: steps.deps-cache.outputs.cache-hit != 'true' @@ -105,7 +108,7 @@ jobs: path: | src/vendor node_modules - key: ${{ runner.os }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} - name: Wait for WordPress to be reachable run: | @@ -128,7 +131,7 @@ jobs: run: | curl -fsSL -o "${RUNNER_TEMP}/wp-cli.phar" https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar - - name: Install WordPress (and optionally multisite) + - name: "Install WordPress: ${{ inputs.multisite && 'Multisite' || 'Single Site' }}" env: WP_CONTAINER: ${{ job.services.wordpress.id }} WP_URL: http://localhost:8888 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 4d3274f8..60f88e53 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,6 +30,7 @@ jobs: with: test-mode: 'default' project-name: 'chromium-db-snippets' + multisite: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests') && contains(github.event.pull_request.labels.*.name, 'multisite') }} playwright-file-based-execution: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') @@ -37,6 +38,7 @@ jobs: with: test-mode: 'file-based-execution' project-name: 'chromium-file-based-snippets' + multisite: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests') && contains(github.event.pull_request.labels.*.name, 'multisite') }} test-result: needs: [playwright-default, playwright-file-based-execution] From 81579b548a3bf2a918faae6c8214d3e99ad0ba5d Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 23:19:55 +0200 Subject: [PATCH 13/30] fix: remove unnecessary WordPress mode echo statement from Playwright workflow --- .github/workflows/playwright-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 921dc0ee..53eee5ee 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -55,9 +55,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: WordPress mode - run: echo "multisite=${{ inputs.multisite }}" - - name: Set up PHP uses: codesnippetspro/setup-php@v2 with: From cc2ec6b8983b626f87294fee009aa5cade93a1a8 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 23:53:33 +0200 Subject: [PATCH 14/30] fix: update Playwright workflow conditions to support multisite execution --- .github/workflows/playwright.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 60f88e53..a125c979 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -25,7 +25,7 @@ concurrency: jobs: playwright-default: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + if: github.event_name != 'pull_request' || ((github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-tests')) || (github.event.action == 'labeled' && (github.event.label.name == 'run-tests' || (github.event.label.name == 'multisite' && contains(github.event.pull_request.labels.*.name, 'run-tests'))))) uses: ./.github/workflows/playwright-test.yml with: test-mode: 'default' @@ -33,7 +33,7 @@ jobs: multisite: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests') && contains(github.event.pull_request.labels.*.name, 'multisite') }} playwright-file-based-execution: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests') + if: github.event_name != 'pull_request' || ((github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-tests')) || (github.event.action == 'labeled' && (github.event.label.name == 'run-tests' || (github.event.label.name == 'multisite' && contains(github.event.pull_request.labels.*.name, 'run-tests'))))) uses: ./.github/workflows/playwright-test.yml with: test-mode: 'file-based-execution' From 7ceb12a95ecfef15c3afd96497578e0bf5211e47 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 18 Dec 2025 23:53:57 +0200 Subject: [PATCH 15/30] fix: enhance pull request workflow to handle labeled and unlabeled events, improve build comment management, and cancel in-progress builds --- .github/workflows/pull-request.yml | 188 +++++++++++++++++++++++++---- 1 file changed, 167 insertions(+), 21 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 26675139..f88e465b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -2,27 +2,165 @@ name: "(Pull Request): Build" on: pull_request: - types: [labeled] + types: [labeled, unlabeled] -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +permissions: + contents: read + issues: write + actions: write jobs: + gate: + if: github.event.action == 'labeled' && github.event.label.name == 'build' + runs-on: ubuntu-latest + outputs: + should_build: ${{ steps.gate.outputs.should_build }} + steps: + - name: Check for in-progress build runs + id: gate + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + workflow_file="pull-request.yml" + other_run_ids="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow_file}/runs?per_page=100" --paginate \ + | jq -r --argjson pr "${PR_NUMBER}" --argjson current "${GITHUB_RUN_ID}" ' + .workflow_runs[] + | select(.status != "completed") + | select(.id != $current) + | select(any(.pull_requests[]?; .number == $pr)) + | .id + ')" + + if [ -n "${other_run_ids}" ]; then + echo "::notice::Another build workflow run is already in progress; ignoring this trigger." + echo "::notice::In-progress run ids: ${other_run_ids//$'\n'/, }" + echo "should_build=false" >> "${GITHUB_OUTPUT}" + else + echo "should_build=true" >> "${GITHUB_OUTPUT}" + fi + + cleanup: + if: github.event.action == 'unlabeled' && github.event.label.name == 'build' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + steps: + - name: Cancel running build workflows for this PR + run: | + set -euo pipefail + + workflow_file="pull-request.yml" + run_ids="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow_file}/runs?per_page=100" --paginate \ + | jq -r --argjson pr "${PR_NUMBER}" --argjson current "${GITHUB_RUN_ID}" ' + .workflow_runs[] + | select(.status != "completed") + | select(.id != $current) + | select(any(.pull_requests[]?; .number == $pr)) + | .id + ')" + + if [ -z "${run_ids}" ]; then + echo "::notice::No in-progress build workflow runs to cancel." + exit 0 + fi + + echo "::group::Canceling build workflow runs" + for run_id in ${run_ids}; do + echo "Canceling run ${run_id}" + gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/cancel" || true + done + echo "::endgroup::" + + - name: Delete build comments + run: | + set -euo pipefail + + marker="" + legacy_prefixes_regex='Build started.. a link to the built zip file will appear here soon..|### Download and install' + bot_logins_regex='^(code-snippets-bot|github-actions\\[bot\\])$' + + comment_ids="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \ + | jq -r --arg marker "${marker}" \ + --arg legacy_prefixes_regex "${legacy_prefixes_regex}" \ + --arg bot_logins_regex "${bot_logins_regex}" ' + .[] + | select(.user.login | test($bot_logins_regex)) + | select(.body | contains($marker) or test($legacy_prefixes_regex)) + | .id + ')" + + if [ -z "${comment_ids}" ]; then + echo "::notice::No build comments found to delete." + exit 0 + fi + + echo "::group::Deleting build comments" + for comment_id in ${comment_ids}; do + echo "Deleting comment ${comment_id}" + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" || true + done + echo "::endgroup::" + comment: - if: contains(github.event.pull_request.labels.*.name, 'build') + needs: gate + if: needs.gate.outputs.should_build == 'true' runs-on: ubuntu-latest - outputs: - comment_id: ${{ steps.comment.outputs.comment-id }} + outputs: + comment_id: ${{ steps.comment.outputs.comment_id }} + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} steps: - - name: Comment + - name: Delete previous build comments + run: | + set -euo pipefail + + marker="" + legacy_prefixes_regex='Build started.. a link to the built zip file will appear here soon..|### Download and install' + bot_logins_regex='^(code-snippets-bot|github-actions\\[bot\\])$' + + comment_ids="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \ + | jq -r --arg marker "${marker}" \ + --arg legacy_prefixes_regex "${legacy_prefixes_regex}" \ + --arg bot_logins_regex "${bot_logins_regex}" ' + .[] + | select(.user.login | test($bot_logins_regex)) + | select(.body | contains($marker) or test($legacy_prefixes_regex)) + | .id + ')" + + if [ -z "${comment_ids}" ]; then + echo "::notice::No previous build comments found." + exit 0 + fi + + echo "::group::Deleting previous build comments" + for comment_id in ${comment_ids}; do + echo "Deleting comment ${comment_id}" + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" || true + done + echo "::endgroup::" + + - name: Create build comment id: comment - uses: codesnippetspro/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - 👌 Build started.. a link to the built zip file will appear here soon.. - + run: | + set -euo pipefail + + marker="" + body="$(cat <> "${GITHUB_OUTPUT}" + install: needs: comment uses: ./.github/workflows/build.yml @@ -35,10 +173,18 @@ jobs: steps: - name: Publish comment if: ${{ needs.install.outputs.artifact_name && needs.install.outputs.artifact_url }} - uses: codesnippetspro/create-or-update-comment@v4 - with: - edit-mode: replace - comment-id: ${{ needs.comment.outputs.comment_id }} - body: | - ### Download and install - 📦 [${{ needs.install.outputs.artifact_name }}.${{ needs.install.outputs.version }}.zip](${{ needs.install.outputs.artifact_url }}) + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + COMMENT_ID: ${{ needs.comment.outputs.comment_id }} + run: | + set -euo pipefail + + marker="" + body="$(cat < Date: Thu, 18 Dec 2025 23:54:18 +0200 Subject: [PATCH 16/30] fix: correct artifact name output in build workflow and update zip filename format in release workflow --- .github/workflows/build.yml | 10 +++++++--- .github/workflows/release.yml | 5 ++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57617779..bf7966bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.build.outputs.version }} - artifact_name: ${{ steps.build.outputs.name }} + artifact_name: ${{ steps.build.outputs.artifact_name }} artifact_url: ${{ steps.artifacts.outputs.artifact-url }} artifact_id: ${{ steps.artifacts.outputs.artifact-id }} steps: @@ -51,8 +51,12 @@ jobs: npm install && npm run bundle name=$(jq -r .name package.json) + version=$(jq -r .version package.json) + artifact_name="${name}.${version}" + echo "name=$name" >> $GITHUB_OUTPUT - echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT + echo "version=$version" >> $GITHUB_OUTPUT + echo "artifact_name=$artifact_name" >> $GITHUB_OUTPUT mkdir -p ./upload/$name mv ./bundle/* ./upload/$name/ 2>/dev/null || true @@ -61,5 +65,5 @@ jobs: id: artifacts uses: actions/upload-artifact@v4 with: - name: ${{ steps.build.outputs.name }} + name: ${{ steps.build.outputs.artifact_name }} path: ./upload diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e7ee90b..86a1b19f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,8 +32,8 @@ jobs: - name: Create zip archive id: zip run: | - # zip filename format: ..zip - zip_name="${{ needs.build.outputs.artifact_name }}.${{ github.event.release.tag_name }}.zip" + # zip filename format: .zip + zip_name="${{ needs.build.outputs.artifact_name }}.zip" cd ./bundle/${{ needs.build.outputs.artifact_name }} zip -r "../$zip_name" . @@ -89,4 +89,3 @@ jobs: echo "::notice::Monitor workflow progress at: $run_url" echo "workflow_url=$run_url" >> $GITHUB_OUTPUT fi - From 64c39f9dae1bf84e77424550363757e497e954a9 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 00:31:26 +0200 Subject: [PATCH 17/30] fix: concurrency conditions in Playwright workflow --- .github/workflows/playwright.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a125c979..47d675f5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,12 +20,12 @@ permissions: actions: read concurrency: - group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite'))) && 'tests' || 'noop' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) }} jobs: playwright-default: - if: github.event_name != 'pull_request' || ((github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-tests')) || (github.event.action == 'labeled' && (github.event.label.name == 'run-tests' || (github.event.label.name == 'multisite' && contains(github.event.pull_request.labels.*.name, 'run-tests'))))) + if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) uses: ./.github/workflows/playwright-test.yml with: test-mode: 'default' @@ -33,7 +33,7 @@ jobs: multisite: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests') && contains(github.event.pull_request.labels.*.name, 'multisite') }} playwright-file-based-execution: - if: github.event_name != 'pull_request' || ((github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-tests')) || (github.event.action == 'labeled' && (github.event.label.name == 'run-tests' || (github.event.label.name == 'multisite' && contains(github.event.pull_request.labels.*.name, 'run-tests'))))) + if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) uses: ./.github/workflows/playwright-test.yml with: test-mode: 'file-based-execution' From bac0f6698bb34db4b6e6de433865e298f6325ea8 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 00:31:54 +0200 Subject: [PATCH 18/30] fix: standardize GH_TOKEN usage in pull request workflow --- .github/workflows/pull-request.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f88e465b..18f23574 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -19,7 +19,7 @@ jobs: - name: Check for in-progress build runs id: gate env: - GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail @@ -46,7 +46,7 @@ jobs: if: github.event.action == 'unlabeled' && github.event.label.name == 'build' runs-on: ubuntu-latest env: - GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} steps: - name: Cancel running build workflows for this PR @@ -112,7 +112,7 @@ jobs: outputs: comment_id: ${{ steps.comment.outputs.comment_id }} env: - GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} steps: - name: Delete previous build comments @@ -174,7 +174,7 @@ jobs: - name: Publish comment if: ${{ needs.install.outputs.artifact_name && needs.install.outputs.artifact_url }} env: - GH_TOKEN: ${{ secrets.CHANGELOG_PAT || github.token }} + GH_TOKEN: ${{ github.token }} COMMENT_ID: ${{ needs.comment.outputs.comment_id }} run: | set -euo pipefail From c2a10bb743780cdb4f8fbe722ada6481555bf3e7 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 00:45:21 +0200 Subject: [PATCH 19/30] fix: update GH_TOKEN usage to use secrets in pull request workflow --- .github/workflows/pull-request.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 18f23574..c0e7068a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -19,7 +19,7 @@ jobs: - name: Check for in-progress build runs id: gate env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail @@ -46,7 +46,7 @@ jobs: if: github.event.action == 'unlabeled' && github.event.label.name == 'build' runs-on: ubuntu-latest env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} steps: - name: Cancel running build workflows for this PR @@ -112,7 +112,7 @@ jobs: outputs: comment_id: ${{ steps.comment.outputs.comment_id }} env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} steps: - name: Delete previous build comments @@ -174,7 +174,7 @@ jobs: - name: Publish comment if: ${{ needs.install.outputs.artifact_name && needs.install.outputs.artifact_url }} env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} COMMENT_ID: ${{ needs.comment.outputs.comment_id }} run: | set -euo pipefail From d9e36f73895435842311b8e52d45e93d2812ce67 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 00:53:36 +0200 Subject: [PATCH 20/30] fix: concurrency settings in Playwright workflow --- .github/workflows/playwright.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 47d675f5..825d3d23 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -19,13 +19,12 @@ permissions: pull-requests: write actions: read -concurrency: - group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite'))) && 'tests' || 'noop' }} - cancel-in-progress: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) }} - jobs: playwright-default: if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) + concurrency: + group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ github.job }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} uses: ./.github/workflows/playwright-test.yml with: test-mode: 'default' @@ -34,6 +33,9 @@ jobs: playwright-file-based-execution: if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) + concurrency: + group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ github.job }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} uses: ./.github/workflows/playwright-test.yml with: test-mode: 'file-based-execution' From 8145e5d983f87dff64616806541aeab4e7b3f3ec Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 01:13:52 +0200 Subject: [PATCH 21/30] fix: add concurrency settings for pull request builds --- .github/workflows/pull-request.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c0e7068a..1eb53b22 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -9,6 +9,10 @@ permissions: issues: write actions: write +concurrency: + group: pr-build-${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event.label.name == 'build' && 'build' || github.run_id }} + cancel-in-progress: ${{ github.event.label.name == 'build' && github.event.action == 'unlabeled' }} + jobs: gate: if: github.event.action == 'labeled' && github.event.label.name == 'build' From 9cb25e7920f0e86dd7210ed07cb7d17e478d8357 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 01:14:00 +0200 Subject: [PATCH 22/30] fix: concurrency settings in Playwright workflow --- .github/workflows/playwright.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 825d3d23..51f39bd4 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -19,12 +19,13 @@ permissions: pull-requests: write actions: read +concurrency: + group: playwright-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite'))) && 'tests' || github.run_id }} + cancel-in-progress: ${{ github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) }} + jobs: playwright-default: if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) - concurrency: - group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ github.job }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} uses: ./.github/workflows/playwright-test.yml with: test-mode: 'default' @@ -33,9 +34,6 @@ jobs: playwright-file-based-execution: if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'run-tests') && (github.event.action != 'labeled' || github.event.label.name == 'run-tests' || github.event.label.name == 'multisite')) - concurrency: - group: playwright-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}-${{ github.job }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} uses: ./.github/workflows/playwright-test.yml with: test-mode: 'file-based-execution' From 7aad242ed59e3817470480f997641b7f8f2230fe Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 01:23:58 +0200 Subject: [PATCH 23/30] fix: add emoji to build started message in pull request workflow --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1eb53b22..c214cf59 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -158,7 +158,7 @@ jobs: body="$(cat < Date: Fri, 19 Dec 2025 02:05:05 +0200 Subject: [PATCH 24/30] fix: update flat files setup to support multisite mode --- tests/e2e/flat-files.setup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e/flat-files.setup.ts b/tests/e2e/flat-files.setup.ts index 3a9307c4..82b38069 100644 --- a/tests/e2e/flat-files.setup.ts +++ b/tests/e2e/flat-files.setup.ts @@ -1,7 +1,9 @@ import { expect, test as setup } from '@playwright/test' setup('enable flat files', async ({ page }) => { - await page.goto('/wp-admin/admin.php?page=snippets-settings') + const wpAdminbase = process.env.WP_E2E_MULTISITE_MODE ? '/wp-admin' : '/wp-admin/network' + + await page.goto(`${wpAdminbase}/admin.php?page=snippets-settings`) await page.waitForSelector('#wpbody-content') await page.waitForSelector('form') From da2faf9f339938b23d572176c4622c8dca8b4224 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:05:11 +0200 Subject: [PATCH 25/30] fix: add multisite mode support to Playwright test environment --- .github/workflows/playwright-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 53eee5ee..b502f315 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -206,9 +206,10 @@ jobs: - name: Run Playwright tests env: WP_E2E_WPCLI_MODE: gh-actions-ci - WP_E2E_WP_CONTAINER: ${{ job.services.wordpress.id }} WP_E2E_WPCLI_PHAR: /tmp/wp-cli.phar WP_E2E_WPCLI_URL: http://localhost:8888 + WP_E2E_WP_CONTAINER: ${{ job.services.wordpress.id }} + WP_E2E_MULTISITE_MODE: ${{ inputs.multisite }} run: npm run test:playwright -- --project=${{ inputs.project-name }} - name: Print WordPress logs on failure From e4af12e5907eed9219c59cdd19fb5d194f936502 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:05:45 +0200 Subject: [PATCH 26/30] fix: correct wpAdminbase path for multisite mode in flat files setup --- tests/e2e/flat-files.setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/flat-files.setup.ts b/tests/e2e/flat-files.setup.ts index 82b38069..6859f3ae 100644 --- a/tests/e2e/flat-files.setup.ts +++ b/tests/e2e/flat-files.setup.ts @@ -1,7 +1,7 @@ import { expect, test as setup } from '@playwright/test' setup('enable flat files', async ({ page }) => { - const wpAdminbase = process.env.WP_E2E_MULTISITE_MODE ? '/wp-admin' : '/wp-admin/network' + const wpAdminbase = process.env.WP_E2E_MULTISITE_MODE ? '/wp-admin/network' : '/wp-admin' await page.goto(`${wpAdminbase}/admin.php?page=snippets-settings`) await page.waitForSelector('#wpbody-content') From da84739949ee8509b500e6f2776b0113e454ae34 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:20:56 +0200 Subject: [PATCH 27/30] fix: improve caching strategy for Playwright tests to avoid collisions in concurrent runs --- .github/workflows/playwright-test.yml | 29 ++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index b502f315..59f5b4a9 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -90,22 +90,22 @@ jobs: path: | node_modules src/vendor - key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}-${{ github.run_id }}-${{ github.job }} restore-keys: | - ${{ runner.os }}-${{ inputs.test-mode }}-deps- + ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}- - name: Install workflow dependencies - if: steps.deps-cache.outputs.cache-hit != 'true' + if: steps.deps-cache.outputs.cache-matched-key == '' run: npm run prepare-environment:ci && npm run bundle - name: Save vendor and node_modules cache - if: steps.deps-cache.outputs.cache-hit != 'true' + if: steps.deps-cache.outputs.cache-matched-key == '' uses: actions/cache/save@v4 with: path: | src/vendor node_modules - key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ steps.deps-cache.outputs.cache-primary-key }} - name: Wait for WordPress to be reachable run: | @@ -199,10 +199,29 @@ jobs: docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar core version --allow-root docker exec -u root -w /var/www/html "$WP_CONTAINER" php /tmp/wp-cli.phar --allow-root --info + - name: Restore Playwright browsers cache + id: playwright-browsers-cache + uses: actions/cache/restore@v4 + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}-${{ github.job }} + restore-keys: | + ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}- + - name: Install playwright/test + if: steps.playwright-browsers-cache.outputs.cache-matched-key == '' run: | npx playwright install chromium + - name: Save Playwright browsers cache + if: steps.playwright-browsers-cache.outputs.cache-matched-key == '' + uses: actions/cache/save@v4 + with: + path: | + ~/.cache/ms-playwright + key: ${{ steps.playwright-browsers-cache.outputs.cache-primary-key }} + - name: Run Playwright tests env: WP_E2E_WPCLI_MODE: gh-actions-ci From 919ca63495c3bcd9f52c4c56659f15c206b93660 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:25:07 +0200 Subject: [PATCH 28/30] chore: test workflow setup cache From b8e2474de4826dfb858475c612fc6da339c5d303 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:41:38 +0200 Subject: [PATCH 29/30] chore(ci): make cache keys deterministic and add cache debug logging --- .github/workflows/playwright-test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 59f5b4a9..7334bf31 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -90,9 +90,10 @@ jobs: path: | node_modules src/vendor - key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}-${{ github.run_id }}-${{ github.job }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} restore-keys: | ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}- + ${{ runner.os }}-${{ inputs.test-mode }}-deps- - name: Install workflow dependencies if: steps.deps-cache.outputs.cache-matched-key == '' @@ -105,7 +106,7 @@ jobs: path: | src/vendor node_modules - key: ${{ steps.deps-cache.outputs.cache-primary-key }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} - name: Wait for WordPress to be reachable run: | @@ -205,9 +206,10 @@ jobs: with: path: | ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}-${{ github.job }} + key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}- + ${{ runner.os }}-playwright-browsers- - name: Install playwright/test if: steps.playwright-browsers-cache.outputs.cache-matched-key == '' @@ -220,7 +222,7 @@ jobs: with: path: | ~/.cache/ms-playwright - key: ${{ steps.playwright-browsers-cache.outputs.cache-primary-key }} + key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }} - name: Run Playwright tests env: From 2c598edbd4e691910481f2e29433d9a634e93bfa Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 19 Dec 2025 02:43:50 +0200 Subject: [PATCH 30/30] Revert "chore(ci): make cache keys deterministic and add cache debug logging" This reverts commit b8e2474de4826dfb858475c612fc6da339c5d303. --- .github/workflows/playwright-test.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/playwright-test.yml b/.github/workflows/playwright-test.yml index 7334bf31..59f5b4a9 100644 --- a/.github/workflows/playwright-test.yml +++ b/.github/workflows/playwright-test.yml @@ -90,10 +90,9 @@ jobs: path: | node_modules src/vendor - key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}-${{ github.run_id }}-${{ github.job }} restore-keys: | ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }}- - ${{ runner.os }}-${{ inputs.test-mode }}-deps- - name: Install workflow dependencies if: steps.deps-cache.outputs.cache-matched-key == '' @@ -106,7 +105,7 @@ jobs: path: | src/vendor node_modules - key: ${{ runner.os }}-${{ inputs.test-mode }}-deps-${{ steps.deps-hash.outputs.deps_hash }} + key: ${{ steps.deps-cache.outputs.cache-primary-key }} - name: Wait for WordPress to be reachable run: | @@ -206,10 +205,9 @@ jobs: with: path: | ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}-${{ github.job }} restore-keys: | ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }}- - ${{ runner.os }}-playwright-browsers- - name: Install playwright/test if: steps.playwright-browsers-cache.outputs.cache-matched-key == '' @@ -222,7 +220,7 @@ jobs: with: path: | ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('package-lock.json') }} + key: ${{ steps.playwright-browsers-cache.outputs.cache-primary-key }} - name: Run Playwright tests env: