From f1aec3af95a75c455937d93749000578a2fdb510 Mon Sep 17 00:00:00 2001 From: Loren Johnson Date: Fri, 9 Oct 2026 12:00:09 +0200 Subject: [PATCH] fix(wikibase): migrate legacy pre-WBS 8 configurations (T440701) --- .../WikibaseSuite/config/Settings.php | 6 + .../WikibaseSuite/setup/Configuration.php | 3 +- .../setup/migration/MigrateConfiguration.php | 213 ++++++++++++++++-- .../WikibaseSuite/setup/migration/migrate.sh | 65 +++++- .../upgrade/configuration-recognition.php | 121 ++++++++++ .../upgrade/configuration-recognition.spec.ts | 9 + .../tests/upgrade/docker-compose.override.yml | 3 + .../fixtures/wbs-1-to-6/LocalSettings.php | 184 +++++++++++++++ .../upgrade/fixtures/wbs-1-to-6/README.md | 13 ++ .../pre-wbs-8-configuration-migration.spec.ts | 104 +++++++++ development/tests/upgrade/wbs-7-to-8.spec.ts | 69 ------ docs/operate/migration-guides/wbs-3-to-4.md | 5 + 12 files changed, 694 insertions(+), 101 deletions(-) create mode 100644 development/tests/upgrade/configuration-recognition.php create mode 100644 development/tests/upgrade/configuration-recognition.spec.ts create mode 100644 development/tests/upgrade/fixtures/wbs-1-to-6/LocalSettings.php create mode 100644 development/tests/upgrade/fixtures/wbs-1-to-6/README.md create mode 100644 development/tests/upgrade/pre-wbs-8-configuration-migration.spec.ts delete mode 100644 development/tests/upgrade/wbs-7-to-8.spec.ts diff --git a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/config/Settings.php b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/config/Settings.php index 760cf372b..707253bc4 100644 --- a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/config/Settings.php +++ b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/config/Settings.php @@ -15,6 +15,12 @@ require $wbsInstanceSettings; require __DIR__ . '/DefaultSettings.php'; + +$wbsBeforeExtensions = '/config/BeforeExtensions.php'; +if ( is_file( $wbsBeforeExtensions ) ) { + require $wbsBeforeExtensions; +} + require __DIR__ . '/LoadExtensions.php'; $wbsCustomSettings = '/config/LocalSettings.php'; diff --git a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/Configuration.php b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/Configuration.php index c5fb81be3..aecf6bf7a 100644 --- a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/Configuration.php +++ b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/Configuration.php @@ -62,8 +62,9 @@ function phpAssignments( array $settings, ?string $elasticsearchHost = null ): s return $output; } -function customSettingsPreamble(): string { +function customSettingsPreamble( string $beforeExtensions = '' ): string { return " $value, 'type' => is_array( $token ) ? $token[0] : null, + 'start' => $offset, 'end' => $end, 'depth' => $depth, + ]; + if ( in_array( $text, [ '(', '[', '{' ], true ) ) { + $depth++; + } elseif ( in_array( $text, [ ')', ']', '}' ], true ) ) { + $depth--; + } + } + $offset = $end; + } + return $result; +} + +function legacyStatementMatches( array $tokens, string $statement ): array { + $fingerprint = array_column( legacyTokens( " $tokens[$i]['start'], + 'end' => $tokens[$i + $length - 1]['end'], + 'topLevel' => $tokens[$i]['depth'] === 0 && + ( $i === 0 || in_array( $values[$i - 1], [ ';', '}' ], true ) ), + ]; + } + } + return $matches; +} - if ( substr_count( $prefix, $mediaWikiLoader ) === 1 && - substr_count( $prefix, $extensionLoader ) === 1 +function loadingSignatures(): array { + // Fresh WBS 7 LocalSettings.php, from wikibase@7.1.0. Installations upgraded + // from earlier versions may still use the older section below. + $wbs7MediaWiki = "require_once '/LocalSettings.MediaWiki.php';"; + $wbs7Extensions = "require_once '/LocalSettings.Extensions.php';"; + + // The generated extension section is byte-identical in the WBS 1–6 + // LocalSettings.wbs.php templates (deploy@1.0.0 through deploy@6.0.0). + $wbs1To6Extensions = <<<'PHP' +foreach (glob("LocalSettings.d/*.php") as $filename) +{ + include $filename; +} +PHP; + return [ $wbs7MediaWiki, $wbs7Extensions, $wbs1To6Extensions ]; +} + +function failUnrecognizedLoadingSection(): never { + $message = <<<'MESSAGE' +Your config/LocalSettings.php could not be migrated automatically for use with +this version of the Wikibase image. The file is unchanged. + +The migration expects one of the following settings-loading sections in your +LocalSettings.php, as generated by earlier Wikibase images: + + For WBS 7: + # Managed by the Wikibase image. DO NOT REMOVE OR MOVE THIS LINE. + require_once '/LocalSettings.MediaWiki.php'; + + # Add MediaWiki configuration values here that must be set before bundled + # extensions are loaded. + + # Managed by the Wikibase image. DO NOT REMOVE OR MOVE THIS LINE. + require_once '/LocalSettings.Extensions.php'; + + For WBS 1–6 (including installations later upgraded to WBS 7): + # Load extensions if present, alphabetically ordered by filename + foreach (glob("LocalSettings.d/*.php") as $filename) + { + include $filename; + } + +Compare your file with these examples. If you can restore its original +settings-loading section, retry docker compose up -d. Do not add a second +copy of either section. + +For additional help from the Wikibase Suite team, open a ticket at: +https://phabricator.wikimedia.org/maniphest/task/edit/form/129/ +MESSAGE; + fwrite( STDERR, "$message\n" ); + // Distinguish an untouched recognition failure from later migration errors. + exit( 2 ); +} + +function adaptLegacyPrefix( string $prefix ): array { + $tokens = legacyTokens( $prefix ); + [ $mediaWikiSignature, $extensionsSignature, $inlineSignature ] = loadingSignatures(); + $mediaWiki = legacyStatementMatches( $tokens, $mediaWikiSignature ); + $extensions = legacyStatementMatches( $tokens, $extensionsSignature ); + // The WBS 1–6 layout can survive in a WBS 7 installation after upgrades. + $loop = legacyStatementMatches( $tokens, $inlineSignature ); + $extensionReplacement = "require '" . dirname( dirname( __DIR__ ) ) . "/config/LoadExtensions.php';"; + if ( count( $mediaWiki ) === 1 && count( $extensions ) === 1 && $loop === [] && + $mediaWiki[0]['topLevel'] && $extensions[0]['topLevel'] && + $mediaWiki[0]['start'] < $extensions[0]['start'] ) { - $prefix = str_replace( - $mediaWikiLoader, - "require '" . __DIR__ . "/Suite7Defaults.php';", - $prefix - ); - $prefix = str_replace( - $extensionLoader, - "require '" . dirname( dirname( __DIR__ ) ) . "/config/LoadExtensions.php';", - $prefix - ); - return [ $prefix, 'wbs-7' ]; + $replacements = [ + [ $extensions[0], $extensionReplacement ], + [ $mediaWiki[0], "require '" . __DIR__ . "/Suite7Defaults.php';" ], + ]; + $shape = 'wbs-7'; + } elseif ( count( $loop ) === 1 && $loop[0]['topLevel'] && $mediaWiki === [] && $extensions === [] ) { + $replacements = [ [ $loop[0], $extensionReplacement ] ]; + $shape = 'wbs-1-to-6'; + } else { + failUnrecognizedLoadingSection(); + } + // Replace from the end so the original offsets remain valid. + foreach ( $replacements as [ $match, $replacement ] ) { + $prefix = substr_replace( $prefix, $replacement, $match['start'], $match['end'] - $match['start'] ); } - fail( 'The generated LocalSettings.php is not a supported Wikibase Suite 7 configuration.' ); + return [ $prefix, $shape ]; +} + +function validateMigratedConfiguration( string $instancePath, string $customPath ): void { + foreach ( [ $instancePath, $customPath ] as $path ) { + if ( !is_readable( $path ) ) { + fail( "The prepared configuration is missing $path." ); + } + } + $custom = file_get_contents( $customPath ); + if ( $custom === false ) { + fail( "Could not read $customPath." ); + } + if ( preg_match( '/^# End of generated LocalSettings\.php\r?$/m', $custom ) ) { + fail( 'The prepared LocalSettings.php still has the historical generated-settings marker. ' . + 'Keep the original file for support to review.' ); + } + // Scan the entire result, including user settings below the old marker. + // Matching string tokens ignores harmless mentions in comments. + $obsolete = [ '/LocalSettings.MediaWiki.php', '/LocalSettings.Extensions.php', 'LocalSettings.d/*.php' ]; + $tokens = legacyTokens( $custom ); + foreach ( $tokens as $index => $token ) { + $path = trim( $token['value'], "'" ); + if ( !in_array( $path, $obsolete, true ) || $token['type'] !== T_CONSTANT_ENCAPSED_STRING ) { + continue; + } + for ( $i = $index - 1; $i >= 0; $i-- ) { + if ( in_array( $tokens[$i]['value'], [ ';', '{', '}' ], true ) ) { + break; + } + $isFileLoader = in_array( $tokens[$i]['type'], [ T_REQUIRE, T_REQUIRE_ONCE, T_INCLUDE, T_INCLUDE_ONCE ], true ); + $isLegacyGlob = $path === 'LocalSettings.d/*.php' && $tokens[$i]['value'] === 'glob'; + if ( $isFileLoader || $isLegacyGlob ) { + fail( 'The prepared LocalSettings.php still contains an active WBS 1–7 loader. ' . + 'Keep the original file for support to review.' ); + } + } + } +} + +function inheritedAnonymousPermissions( array $actual, array $reference ): string { + $settings = ''; + // Earlier installations did not load Suite7Defaults.php. Preserve their + // effective anonymous-access policy before loading user-owned Extensions.php. + foreach ( [ 'edit', 'createpage', 'createtalk', 'writeapi', 'createaccount' ] as $right ) { + $value = $actual['wgGroupPermissions']['*'][$right] ?? false; + if ( $value !== ( $reference['wgGroupPermissions']['*'][$right] ?? false ) ) { + $settings .= "\$wgGroupPermissions['*']['$right'] = " . var_export( $value, true ) . ";\n"; + } + } + return $settings === '' ? '' : "# Anonymous-access settings retained from the WBS 1–6 configuration.\n$settings\n"; } function legacyCustomTail( string $source, int $markerPosition ): string { @@ -169,8 +327,11 @@ function stageMigratedConfiguration( $migratedAssignments[] = $assignment['source']; } } - $custom = customSettingsPreamble(); - $custom .= "\n# Settings migrated from the generated Suite 7 LocalSettings.php.\n"; + [ , $shape ] = adaptLegacyPrefix( substr( $source, 0, $markerPosition ) ); + $custom = customSettingsPreamble( + $shape === 'wbs-1-to-6' ? inheritedAnonymousPermissions( $actual, $reference ) : '' + ); + $custom .= "\n# Settings migrated from the generated WBS 1–7 LocalSettings.php.\n"; $custom .= "# Some may be legacy values; review them after the upgrade.\n"; $custom .= "# Review the backup for custom executable PHP above the old generated marker.\n"; if ( $migratedAssignments !== [] ) { @@ -221,6 +382,12 @@ function installStagedFile( string $sourcePath, string $destinationPath ): void } installStagedFile( $argv[2], $argv[3] ); break; + case 'validate': + if ( count( $argv ) !== 4 ) { + fail( 'Usage: MigrateConfiguration.php validate INSTANCE CUSTOM' ); + } + validateMigratedConfiguration( $argv[2], $argv[3] ); + break; default: - fail( 'Expected stage-instance-settings, write-loadable-legacy-config, stage-migrated-configuration, or install.' ); + fail( 'Expected stage-instance-settings, write-loadable-legacy-config, stage-migrated-configuration, validate, or install.' ); } diff --git a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/migration/migrate.sh b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/migration/migrate.sh index 1f7e637e5..60d6e1580 100644 --- a/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/migration/migrate.sh +++ b/development/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/migration/migrate.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Migrate a WBS 7 generated configuration to the current split configuration. +# Migrate a WBS 1–7 generated configuration retained by a WBS 7 installation. # This script owns all migration state so setup.sh can remain a concise image # lifecycle dispatcher. -set -eu +set -Eeuo pipefail migration_directory_path=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) @@ -14,6 +14,38 @@ image_state_directory=/config/.wikibase-image migration_directory=$image_state_directory/config-migration staged_instance_settings=$migration_directory/InstanceSettings.php staged_custom_settings=$migration_directory/LocalSettings.php +original_custom_settings=$migration_directory/OriginalLocalSettings.php +support_ticket_url=https://phabricator.wikimedia.org/maniphest/task/edit/form/129/ + +on_exit() { + status=$1 + if [ "$status" -eq 0 ]; then + return + fi + # An unrecognized layout is rejected before creating migration state. PHP + # has already printed the complete guidance for this case. + if [ "$status" -eq 2 ] && [ ! -d "$migration_directory" ] && [ ! -e "$instance_settings" ]; then + return + fi + # A preparation failure has not touched active configuration. Discard only + # our working files so the operator can correct the original and retry. + if [ ! -d "$migration_directory" ] && [ ! -e "$instance_settings" ]; then + echo "The active configuration is unchanged." >&2 + elif [ -r "$original_custom_settings" ] && [ ! -e "$instance_settings" ] && \ + cmp -s "$custom_settings" "$original_custom_settings"; then + rm -rf "$migration_directory" + remove_image_state_directory_if_empty + echo "The active configuration is unchanged. Its pre-upgrade backup is in /config/backups/." >&2 + else + echo "Migration state was retained for recovery; active configuration may be partly installed." >&2 + fi + echo "For additional help from the Wikibase Suite team, open a ticket at:" >&2 + echo "$support_ticket_url" >&2 + if [ -d "$migration_directory" ] || [ -e /config/backups/LocalSettings.pre-wbs-8.php.backup ]; then + echo "Keep config/backups/ and any retained migration state for diagnosis." >&2 + fi +} +trap 'on_exit "$?"' EXIT ensure_image_state_directory() { mkdir -p "$image_state_directory" @@ -28,6 +60,22 @@ install_staged_configuration() { echo "The staged WBS 8 configuration migration is incomplete." exit 1 fi + if [ -e "$instance_settings" ] && ! cmp -s "$instance_settings" "$staged_instance_settings"; then + echo "InstanceSettings.php changed during migration; preserving it for support to review." >&2 + exit 1 + fi + if ! cmp -s "$custom_settings" "$original_custom_settings" && \ + ! cmp -s "$custom_settings" "$staged_custom_settings"; then + echo "LocalSettings.php changed during migration; preserving it for support to review." >&2 + exit 1 + fi + php "$migration_directory_path/MigrateConfiguration.php" validate \ + "$staged_instance_settings" "$staged_custom_settings" + php -l "$staged_instance_settings" > /dev/null + php -l "$staged_custom_settings" > /dev/null + if [ -f /config/Extensions.php ]; then + php -l /config/Extensions.php > /dev/null + fi php "$migration_directory_path/MigrateConfiguration.php" install \ "$staged_instance_settings" \ "$instance_settings" @@ -37,7 +85,7 @@ install_staged_configuration() { } is_recognized_legacy_configuration() { - grep -q '^# End of generated LocalSettings.php$' "$custom_settings" + grep -Eq $'^# End of generated LocalSettings[.]php\r?$' "$custom_settings" } migrate_legacy_configuration() { @@ -48,16 +96,17 @@ migrate_legacy_configuration() { temporary_instance=$migration_directory/InstanceSettings.php.tmp legacy_prefix=/tmp/LocalSettings.legacy-prefix.php + legacy_shape=$(php "$migration_directory_path/MigrateConfiguration.php" write-loadable-legacy-config \ + "$legacy_settings" \ + "$legacy_prefix") echo "Legacy LocalSettings.php found; preserving and migrating it." + echo "Recognized $legacy_shape generated configuration." ensure_image_state_directory mkdir -p "$backup_directory" "$migration_directory" if [ ! -e "$backup_directory/LocalSettings.pre-wbs-8.php.backup" ]; then cp "$legacy_settings" "$backup_directory/LocalSettings.pre-wbs-8.php.backup" fi - legacy_shape=$(php "$migration_directory_path/MigrateConfiguration.php" write-loadable-legacy-config \ - "$legacy_settings" \ - "$legacy_prefix") - echo "Recognized $legacy_shape generated configuration." + cp "$legacy_settings" "$original_custom_settings" php /var/www/html/maintenance/run.php getConfiguration \ --conf "$legacy_prefix" \ --format=json \ @@ -103,7 +152,7 @@ if [ -d "$migration_directory" ]; then resume_configuration_migration elif [ -e "$custom_settings" ]; then if ! is_recognized_legacy_configuration; then - echo "$custom_settings exists without $instance_settings and is not a recognized Wikibase Suite 7 configuration." + echo "$custom_settings exists without $instance_settings and is not a recognized WBS 1–7 generated configuration." exit 1 fi migrate_legacy_configuration diff --git a/development/tests/upgrade/configuration-recognition.php b/development/tests/upgrade/configuration-recognition.php new file mode 100644 index 000000000..e21e64367 --- /dev/null +++ b/development/tests/upgrade/configuration-recognition.php @@ -0,0 +1,121 @@ + [ 'pipe', 'r' ], 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ], + $pipes + ); + if ( !is_resource( $process ) ) { + throw new RuntimeException( 'Could not start the migration command.' ); + } + fclose( $pipes[0] ); + $output = stream_get_contents( $pipes[1] ); + $error = stream_get_contents( $pipes[2] ); + fclose( $pipes[1] ); + fclose( $pipes[2] ); + return [ proc_close( $process ), $output, $error ]; +} + +// Run standalone with PHP, or against the actual image from the upgrade suite. +$migration = $argv[1] ?? dirname( __DIR__, 2 ) . + '/images/wikibase/runtime/var/www/html/extensions/WikibaseSuite/setup/migration/MigrateConfiguration.php'; +$suite7 = file_get_contents( __DIR__ . '/fixtures/wbs-7/LocalSettings.php' ); +$inherited = file_get_contents( __DIR__ . '/fixtures/wbs-1-to-6/LocalSettings.php' ); +$mediaWiki = "require_once '/LocalSettings.MediaWiki.php';"; +$extensions = "require_once '/LocalSettings.Extensions.php';"; +$loop = <<<'PHP' +foreach (glob("LocalSettings.d/*.php") as $filename) +{ + include $filename; +} +PHP; + +$cases = [ + 'fresh WBS 7' => [ $suite7, 'wbs-7' ], + 'inherited WBS 1–6' => [ $inherited, 'wbs-1-to-6' ], + 'formatted includes and commented duplicates' => [ + str_replace( $mediaWiki, '// ' . $mediaWiki . "\nrequire_once /* loader */ \"/LocalSettings.MediaWiki.php\" ;", $suite7 ), + 'wbs-7', + ], + 'formatted historical loop' => [ + str_replace( $loop, "foreach ( glob( 'LocalSettings.d/*.php' ) as \$filename ) { /* bundled */ include \$filename; }", $inherited ), + 'wbs-1-to-6', + ], + 'CRLF settings' => [ str_replace( "\n", "\r\n", $inherited ), 'wbs-1-to-6' ], + 'marker mentioned in a comment' => [ + str_replace( ' [ str_replace( $mediaWiki, "$mediaWiki\n$mediaWiki", $suite7 ), null ], + 'duplicate loops' => [ str_replace( $loop, "$loop\n$loop", $inherited ), null ], + 'mixed loaders' => [ str_replace( $extensions, "$extensions\n$loop", $suite7 ), null ], + 'conditional loop' => [ str_replace( $loop, "if ( true ) { $loop }", $inherited ), null ], + 'conditional includes' => [ str_replace( $mediaWiki, "if ( true ) $mediaWiki", $suite7 ), null ], + 'reversed includes' => [ strtr( $suite7, [ $mediaWiki => $extensions, $extensions => $mediaWiki ] ), null ], + 'loader below marker' => [ str_replace( $extensions, '', $suite7 ) . "\n$extensions\n", null ], + 'custom loop path' => [ str_replace( 'LocalSettings.d/*.php', 'custom/*.php', $inherited ), null ], + 'missing marker' => [ str_replace( '# End of generated LocalSettings.php', '# Removed marker', $inherited ), null ], +]; + +$directory = sys_get_temp_dir() . '/wbs-recognition-' . bin2hex( random_bytes( 8 ) ); +mkdir( $directory, 0700 ); +try { + foreach ( $cases as $name => [ $source, $expectedShape ] ) { + file_put_contents( "$directory/LocalSettings.php", $source ); + @unlink( "$directory/prefix.php" ); + [ $status, $output, $error ] = runMigration( $migration, [ + 'write-loadable-legacy-config', "$directory/LocalSettings.php", "$directory/prefix.php" + ] ); + if ( file_get_contents( "$directory/LocalSettings.php" ) !== $source ) { + throw new RuntimeException( "$name: source configuration changed." ); + } + if ( $expectedShape === null ) { + if ( $status === 0 || file_exists( "$directory/prefix.php" ) || $error === '' ) { + throw new RuntimeException( "$name: unsupported configuration was accepted." ); + } + } else { + if ( $status !== 0 || trim( $output ) !== $expectedShape ) { + throw new RuntimeException( "$name: expected $expectedShape, got $output$error" ); + } + $prefix = file_get_contents( "$directory/prefix.php" ); + if ( substr_count( $prefix, '/config/LoadExtensions.php' ) !== 1 || + str_contains( $prefix, "wfLoadExtension( 'WikibaseEdtf' );" ) || + ( $expectedShape === 'wbs-1-to-6' && str_contains( $prefix, 'Suite7Defaults.php' ) ) + ) { + throw new RuntimeException( "$name: incorrect loader adaptation or custom tail included." ); + } + } + } + echo "All configuration recognition checks passed.\n"; + + file_put_contents( "$directory/InstanceSettings.php", " [ + " [ " [ " [ " [ " [ $source, $allowed ] ) { + file_put_contents( "$directory/LocalSettings.php", $source ); + [ $status ] = runMigration( $migration, [ + 'validate', "$directory/InstanceSettings.php", "$directory/LocalSettings.php" + ] ); + if ( ( $status === 0 ) !== $allowed ) { + throw new RuntimeException( "$name: unexpected prepared configuration validation result." ); + } + } + echo "Prepared configuration validation checks passed.\n"; +} finally { + foreach ( glob( "$directory/*" ) as $file ) { + unlink( $file ); + } + rmdir( $directory ); +} diff --git a/development/tests/upgrade/configuration-recognition.spec.ts b/development/tests/upgrade/configuration-recognition.spec.ts new file mode 100644 index 000000000..91098f01d --- /dev/null +++ b/development/tests/upgrade/configuration-recognition.spec.ts @@ -0,0 +1,9 @@ +describe( 'Legacy configuration recognition', function () { + it( 'accepts historical loaders and harmless formatting while rejecting ambiguous layouts', async function () { + const output = await testEnv.runDockerComposeCmd( + 'exec -T wikibase php /migration-tests/configuration-recognition.php ' + + '/var/www/html/extensions/WikibaseSuite/setup/migration/MigrateConfiguration.php' + ); + expect( output ).toContain( 'All configuration recognition checks passed.' ); + } ); +} ); diff --git a/development/tests/upgrade/docker-compose.override.yml b/development/tests/upgrade/docker-compose.override.yml index e5fdb84bf..d44586468 100644 --- a/development/tests/upgrade/docker-compose.override.yml +++ b/development/tests/upgrade/docker-compose.override.yml @@ -1,4 +1,7 @@ services: + wikibase: + volumes: + - ./upgrade:/migration-tests:ro mysql: volumes: - ./upgrade/fixtures/wbs-7/database.sql.gz:/docker-entrypoint-initdb.d/10-wbs-7.sql.gz:ro diff --git a/development/tests/upgrade/fixtures/wbs-1-to-6/LocalSettings.php b/development/tests/upgrade/fixtures/wbs-1-to-6/LocalSettings.php new file mode 100644 index 000000000..27ecf0cac --- /dev/null +++ b/development/tests/upgrade/fixtures/wbs-1-to-6/LocalSettings.php @@ -0,0 +1,184 @@ + 'https://static.example/custom-logo.svg', + 'icon' => "$wgResourceBasePath/resources/assets/change-your-logo-icon.svg", +]; + +## UPO means: this is also a user preference option + +$wgEnableEmail = true; +$wgEnableUserEmail = true; # UPO + +$wgEmergencyContact = "owner@example.test"; +$wgPasswordSender = ""; + +$wgEnotifUserTalk = false; # UPO +$wgEnotifWatchlist = false; # UPO +$wgEmailAuthentication = true; + +## Database settings +$wgDBtype = "mysql"; +$wgDBserver = "mysql:3306"; +$wgDBname = "my_wiki"; +$wgDBuser = "sqluser"; +$wgDBpassword = "test-db-password"; + +# MySQL specific settings +$wgDBprefix = ""; +$wgDBssl = false; + +# MySQL table options to use during installation or update +$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary"; + +# Shared database table +# This has no effect unless $wgSharedDB is also set. +$wgSharedTables[] = "actor"; + +## Shared memory settings +$wgMainCacheType = CACHE_ACCEL; +$wgMemCachedServers = []; + +## To enable image uploads, make sure the 'images' directory +## is writable, then set this to true: +$wgEnableUploads = false; +$wgUseImageMagick = true; +$wgImageMagickConvertCommand = "/usr/bin/convert"; + +# InstantCommons allows wiki to use images from https://commons.wikimedia.org +$wgUseInstantCommons = false; + +# Periodically send a pingback to https://www.mediawiki.org/ with basic data +# about this MediaWiki instance. The Wikimedia Foundation shares this data +# with MediaWiki developers to help guide future development efforts. +$wgPingback = false; + +# Site language code, should be one of the list in ./includes/languages/data/Names.php +$wgLanguageCode = "en"; + +# Time zone +$wgLocaltimezone = "UTC"; + +## Set $wgCacheDirectory to a writable directory on the web server +## to make your wiki go slightly faster. The directory should not be +## publicly accessible from the web. +#$wgCacheDirectory = "$IP/cache"; + +$wgSecretKey = "0000000000000000000000000000000000000000000000000000000000000000"; + +# Changing this will log out all existing sessions. +$wgAuthenticationTokenVersion = "1"; + +# Site upgrade key. Must be set to a string (default provided) to turn on the +# web installer while LocalSettings.php is in place +$wgUpgradeKey = "0000000000000000"; + +## For attaching licensing metadata to pages, and displaying an +## appropriate copyright notice / icon. GNU Free Documentation +## License and Creative Commons licenses are supported so far. +$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright +$wgRightsUrl = ""; +$wgRightsText = ""; +$wgRightsIcon = ""; + +# Path to the GNU diff3 utility. Used for conflict resolution. +$wgDiff3 = "/usr/bin/diff3"; + +## Default skin: you can change the default skin. Use the internal symbolic +## names, e.g. 'vector' or 'monobook': +$wgDefaultSkin = "monobook"; + +# Enabled skins. +# The following skins were automatically enabled: +wfLoadSkin( 'MinervaNeue' ); +wfLoadSkin( 'MonoBook' ); +wfLoadSkin( 'Timeless' ); +wfLoadSkin( 'Vector' ); + + +# End of automatically generated settings. +# Add more configuration options below. + + +# Configuration added by Wikibase Suite installer in entrypoint.sh + +$elasticsearchHost = 'elasticsearch'; + + +# JobRunner instance is assumed by default, so jobs on request is disabled +$wgJobRunRate = 0; + +# File Uploads enabled by default +$wgEnableUploads = true; + +# Logs +# TODO: Explore simply logging to stdout/stderr so these appear in Docker logs +$wgDebugLogGroups = array( + 'resourceloader' => '/var/log/mediawiki/mw.resourceloader.log', + 'exception' => '/var/log/mediawiki/mw.exception.log', + 'error' => '/var/log/mediawiki/mw.error.log', + 'fatal' => '/var/log/mediawiki/mw.fatal.log', +); +$wgDebugLogFile = '/var/log/mediawiki/mw.debug.log'; + +$wgArticlePath = "/wiki/$1"; + +# Add configuration values here or above which should be set before extensions are loaded + +$wgJobRunRate = 0.5; + +# Load extensions if present, alphabetically ordered by filename +foreach (glob("LocalSettings.d/*.php") as $filename) +{ + include $filename; +} + +############################################################################## +# End of generated LocalSettings.php +############################################################################## + +# Add configuration values below which should be set after extensions are loaded +$wgAllowExternalImages = true; +wfLoadExtension( 'WikibaseEdtf' ); diff --git a/development/tests/upgrade/fixtures/wbs-1-to-6/README.md b/development/tests/upgrade/fixtures/wbs-1-to-6/README.md new file mode 100644 index 000000000..25b6f522d --- /dev/null +++ b/development/tests/upgrade/fixtures/wbs-1-to-6/README.md @@ -0,0 +1,13 @@ +# Configuration inherited by WBS 7 + +This fixture represents a WBS 7 installation that retained its configuration +from WBS 1, 2, 3, 4, 5, or 6. It uses the WBS-generated block from +`deploy@6.0.0:build/wikibase/LocalSettings.wbs.php`, which is byte-for-byte +identical in the latest audited WBS 1–5 deployment and image tags. The +matching loading section is recorded in `MigrateConfiguration.php`. + +The MediaWiki-generated prefix and test instance values come from the existing +`wbs-7/LocalSettings.php` fixture. A job-rate override before the extension loop +and custom settings after the marker exercise preservation of user changes. +The suite uses the WBS 7 database fixture to test conversion of a historical +configuration retained by a WBS 7 installation. diff --git a/development/tests/upgrade/pre-wbs-8-configuration-migration.spec.ts b/development/tests/upgrade/pre-wbs-8-configuration-migration.spec.ts new file mode 100644 index 000000000..4832085e9 --- /dev/null +++ b/development/tests/upgrade/pre-wbs-8-configuration-migration.spec.ts @@ -0,0 +1,104 @@ +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const configurationDirectory = resolve( process.cwd(), 'upgrade/tmp/config' ); +const localSettingsPath = resolve( configurationDirectory, 'LocalSettings.php' ); +const instanceSettingsPath = resolve( configurationDirectory, 'InstanceSettings.php' ); +const backupPath = resolve( configurationDirectory, 'backups/LocalSettings.pre-wbs-8.php.backup' ); +const migrationStatePath = resolve( configurationDirectory, '.wikibase-image/config-migration' ); + +async function prepareLegacyConfiguration( contents: string ): Promise { + await testEnv.runDockerComposeCmd( 'stop wikibase wikibase-jobrunner' ); + rmSync( configurationDirectory, { recursive: true, force: true } ); + mkdirSync( configurationDirectory, { recursive: true } ); + writeFileSync( localSettingsPath, contents ); +} + +for ( const [ configurationShape, fixtureDirectory ] of [ + [ 'WBS 7 split settings', 'wbs-7' ], + [ 'WBS 1–6 inline settings', 'wbs-1-to-6' ], +] ) { + describe( 'Pre-WBS 8 configuration migration (' + configurationShape + ')', function () { + it( 'migrates the configuration', async function () { + const fixture = readFileSync( + resolve( process.cwd(), 'upgrade/fixtures/' + fixtureDirectory + '/LocalSettings.php' ), + 'utf8' + ); + await prepareLegacyConfiguration( fixture ); + if ( fixtureDirectory === 'wbs-1-to-6' ) { + // Earlier rejected attempts may leave an incomplete staging directory. + mkdirSync( migrationStatePath, { recursive: true } ); + } + await testEnv.runDockerComposeCmd( 'up -d --wait --wait-timeout 180 wikibase wikibase-jobrunner' ); + + const instanceSettings = readFileSync( instanceSettingsPath, 'utf8' ); + const localSettings = readFileSync( localSettingsPath, 'utf8' ); + expect( readFileSync( backupPath, 'utf8' ) ).toBe( fixture ); + expect( existsSync( migrationStatePath ) ).toBe( false ); + expect( instanceSettings ).toContain( "$wgDBpassword = 'test-db-password';" ); + expect( instanceSettings ).not.toContain( 'custom-logo.svg' ); + expect( localSettings ).toContain( "$wgJobRunRate = 0.5;" ); + expect( localSettings ).toContain( "$wgAllowExternalImages = true;" ); + expect( localSettings ).toContain( "wfLoadExtension( 'WikibaseEdtf' );" ); + expect( localSettings ).toContain( "require_once __DIR__ . '/Extensions.php';" ); + for ( const oldLoader of [ + '/LocalSettings.MediaWiki.php', + '/LocalSettings.Extensions.php', + 'LocalSettings.d/*.php' + ] ) { + expect( localSettings ).not.toContain( oldLoader ); + } + + const configurationOutput = await testEnv.runDockerComposeCmd( + 'exec -T wikibase php /var/www/html/maintenance/run.php getConfiguration --format=json --json-partial-output-on-error' + ); + const effective = JSON.parse( configurationOutput.slice( + configurationOutput.indexOf( '{' ), configurationOutput.lastIndexOf( '}' ) + 1 + ) ); + for ( const right of [ 'edit', 'createpage', 'createtalk', 'createaccount' ] ) { + expect( effective.wgGroupPermissions[ '*' ][ right ] ).toBe( fixtureDirectory === 'wbs-1-to-6' ); + } + expect( effective.wgGroupPermissions[ '*' ].writeapi ).toBe( false ); + + await browser.url( '/wiki/Main_Page' ); + await expect( $( 'body' ) ).toHaveElementClass( 'skin-monobook' ); + await expect( $( '#firstHeading' ) ).toHaveText( 'Main Page' ); + } ); + } ); +} + +describe( 'Pre-WBS 8 configuration rejection', function () { + it( 'leaves an unrecognized file untouched and migrates it after correction', async function () { + const fixture = readFileSync( + resolve( process.cwd(), 'upgrade/fixtures/wbs-7/LocalSettings.php' ), 'utf8' + ); + const include = "require_once '/LocalSettings.MediaWiki.php';"; + const unrecognized = fixture.replace( include, include + '\n' + include ); + await prepareLegacyConfiguration( unrecognized ); + const migrationCommand = 'run --rm --no-deps --entrypoint /bin/bash wikibase ' + + '/var/www/html/extensions/WikibaseSuite/setup/migration/migrate.sh'; + let errorMessage = ''; + try { + await testEnv.runDockerComposeCmd( migrationCommand ); + } catch ( error ) { + errorMessage = String( error ); + } + for ( const text of [ + 'The file is unchanged.', + 'For WBS 7:', + 'For WBS 1–6', + 'https://phabricator.wikimedia.org/maniphest/task/edit/form/129/' + ] ) { + expect( errorMessage ).toContain( text ); + } + expect( readFileSync( localSettingsPath, 'utf8' ) ).toBe( unrecognized ); + expect( existsSync( instanceSettingsPath ) ).toBe( false ); + expect( existsSync( backupPath ) ).toBe( false ); + expect( existsSync( migrationStatePath ) ).toBe( false ); + + writeFileSync( localSettingsPath, fixture ); + await testEnv.runDockerComposeCmd( 'up -d --wait --wait-timeout 180 wikibase wikibase-jobrunner' ); + expect( existsSync( instanceSettingsPath ) ).toBe( true ); + expect( readFileSync( localSettingsPath, 'utf8' ) ).not.toContain( include ); + } ); +} ); diff --git a/development/tests/upgrade/wbs-7-to-8.spec.ts b/development/tests/upgrade/wbs-7-to-8.spec.ts deleted file mode 100644 index 02ab32e16..000000000 --- a/development/tests/upgrade/wbs-7-to-8.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { existsSync, readFileSync, statSync } from 'node:fs'; -import { resolve } from 'node:path'; - -const configurationDirectory = resolve( process.cwd(), 'upgrade/tmp/config' ); - -describe( 'Wikibase Suite 7 to 8 upgrade', function () { - it( 'migrates the generated and user-owned configuration on startup', async function () { - const instanceSettingsPath = resolve( - configurationDirectory, - 'InstanceSettings.php' - ); - const localSettingsPath = resolve( configurationDirectory, 'LocalSettings.php' ); - const backupPath = resolve( - configurationDirectory, - 'backups/LocalSettings.pre-wbs-8.php.backup' - ); - const instanceSettings = readFileSync( instanceSettingsPath, 'utf8' ); - const localSettings = readFileSync( localSettingsPath, 'utf8' ); - const backup = readFileSync( backupPath, 'utf8' ); - const fixture = readFileSync( - resolve( process.cwd(), 'upgrade/fixtures/wbs-7/LocalSettings.php' ), - 'utf8' - ); - - expect( instanceSettings ).toContain( "$wgDBpassword = 'test-db-password';" ); - expect( instanceSettings ).toContain( - "$wgSecretKey = '0000000000000000000000000000000000000000000000000000000000000000';" - ); - expect( instanceSettings ).toContain( - "$elasticsearchHost = 'elasticsearch';" - ); - expect( instanceSettings ).not.toContain( 'custom-logo.svg' ); - - expect( localSettings ).toContain( "require_once __DIR__ . '/Extensions.php';" ); - expect( localSettings ).toContain( - "'1x' => 'https://static.example/custom-logo.svg'" - ); - expect( localSettings ).toContain( '$wgEmergencyContact = "owner@example.test";' ); - expect( localSettings ).toContain( '$wgDefaultSkin = "monobook";' ); - expect( localSettings ).toContain( '$wgJobRunRate = 0.5;' ); - expect( localSettings ).toContain( '$wgAllowExternalImages = true;' ); - expect( localSettings ).toContain( "wfLoadExtension( 'WikibaseEdtf' );" ); - expect( localSettings ).not.toContain( '$wgEnableEmail = true;' ); - const migratedInSourceOrder = [ - "'1x' => 'https://static.example/custom-logo.svg'", - '$wgEmergencyContact = "owner@example.test";', - '$wgDefaultSkin = "monobook";', - '$wgJobRunRate = 0.5;', - '$wgAllowExternalImages = true;', - "wfLoadExtension( 'WikibaseEdtf' );" - ].map( ( setting ) => localSettings.indexOf( setting ) ); - expect( migratedInSourceOrder.every( ( position ) => position >= 0 ) ).toBe( - true - ); - expect( migratedInSourceOrder ).toEqual( - [ ...migratedInSourceOrder ].sort( ( left, right ) => left - right ) - ); - expect( backup ).toBe( fixture ); - expect( statSync( instanceSettingsPath ).mode & 0o777 ).toBe( 0o644 ); - expect( statSync( localSettingsPath ).mode & 0o777 ).toBe( 0o644 ); - expect( - existsSync( resolve( configurationDirectory, '.wikibase-image' ) ) - ).toBe( false ); - - await browser.url( '/wiki/Main_Page' ); - await expect( $( 'body' ) ).toHaveElementClass( 'skin-monobook' ); - await expect( $( '#firstHeading' ) ).toHaveText( 'Main Page' ); - } ); -} ); diff --git a/docs/operate/migration-guides/wbs-3-to-4.md b/docs/operate/migration-guides/wbs-3-to-4.md index 9fd9ba031..fd7070666 100644 --- a/docs/operate/migration-guides/wbs-3-to-4.md +++ b/docs/operate/migration-guides/wbs-3-to-4.md @@ -74,3 +74,8 @@ This guide explains how to upgrade Wikibase Suite (WBS) from version 3 to 4 whil docker compose up -d docker compose ps ``` + +7. Recreate the Query Service index. WBS 4 updates the Query Service from + version 1 to version 2, changing the index format. Follow the [Query Service + data reload guidance](../../../development/images/wdqs/README.md#upgrading) + to reindex the service.