query($sql); } self::ensureColumn('release_channel_versions', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER deployment_id'); self::ensureColumn('release_channel_versions', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id'); self::ensureColumn('release_deployments', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER version_id'); self::ensureColumn('release_deployments', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id'); self::ensureColumn('release_deployments', 'deployment_kind', "VARCHAR(32) NOT NULL DEFAULT 'single_app' AFTER bundle_id"); self::ensureColumn('release_deployments', 'active_channel_app_key', 'VARCHAR(96) NULL AFTER app'); self::ensureColumn('release_timeline_sessions', 'device_type', 'VARCHAR(16) NULL AFTER api_version_id'); self::ensureColumn('release_timeline_sessions', 'browser_name', 'VARCHAR(64) NULL AFTER device_type'); self::ensureColumn('release_timeline_sessions', 'browser_version', 'VARCHAR(64) NULL AFTER browser_name'); self::ensureColumn('release_timeline_sessions', 'os_name', 'VARCHAR(64) NULL AFTER browser_version'); self::ensureColumn('release_timeline_sessions', 'os_version', 'VARCHAR(64) NULL AFTER os_name'); self::ensureColumn('release_timeline_sessions', 'viewport_width', 'INT NULL AFTER os_version'); self::ensureColumn('release_timeline_sessions', 'viewport_height', 'INT NULL AFTER viewport_width'); self::ensureColumn('release_timeline_sessions', 'device_pixel_ratio', 'DECIMAL(6,3) NULL AFTER viewport_height'); self::ensureColumn('release_timeline_sessions', 'frontend_version_label', 'VARCHAR(128) NULL AFTER device_pixel_ratio'); self::ensureColumn('release_timeline_sessions', 'frontend_commit_sha', 'VARCHAR(128) NULL AFTER frontend_version_label'); self::ensureColumn('release_timeline_sessions', 'api_version_label', 'VARCHAR(128) NULL AFTER frontend_commit_sha'); self::ensureColumn('release_timeline_sessions', 'api_commit_sha', 'VARCHAR(128) NULL AFTER api_version_label'); self::ensureColumn('release_timeline_sessions', 'last_route_path', 'VARCHAR(255) NULL AFTER api_commit_sha'); self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_device', 'device_type'); self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_release', 'frontend_version_label, api_version_label'); self::ensureUniqueIndex('release_deployments', 'uniq_release_deployments_active_channel_app', 'active_channel_app_key'); self::ensureModuleConfigDefault('ReleaseManager', 'enabled', 'true', 'bool'); self::ensureModuleConfigDefault('ReleaseManager', 'github_webhook_secret', '', 'string'); self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_token', '', 'string'); self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_required_for_promotion', 'true', 'bool'); self::ensureModuleConfigDefault('ReleaseManager', 'github_token', '', 'string'); self::ensureModuleConfigDefault('ReleaseManager', 'github_api_url', 'https://api.github.com', 'string'); self::ensureModuleConfigDefault('ReleaseManager', 'default_retention_days', '14', 'int'); self::ensureDefaultChannels(); self::$initialized = true; self::$tablesExist = true; } public static function tablesExist(): bool { if (self::$tablesExist !== null) { return self::$tablesExist; } global $db; foreach ([ 'release_channels', 'release_versions', 'release_channel_versions', 'release_assignments', 'release_auto_sync_events', 'release_service_sets', 'release_deployments', 'release_bundles', 'release_operation_runs', 'release_operation_steps', 'release_timeline_sessions', 'release_timeline_events', ] as $table) { $tableSql = $db->escape_string($table); $result = $db->query("SHOW TABLES LIKE '$tableSql'"); if ($result === false || $result->num_rows === 0) { self::$tablesExist = false; return false; } } self::$tablesExist = true; return true; } private static function ensureDefaultChannels(): void { global $db; $channels = [ ['stable', 'Stable', 'Default production channel.', 1, 1, 'metadata'], ['canary', 'Canary', 'Earliest production validation channel.', 1, 0, 'metadata'], ['beta', 'Beta', 'Broader pre-stable rollout channel.', 1, 0, 'metadata'], ['internal', 'Internal', 'Internal staff and superuser validation channel.', 1, 0, 'metadata'], ]; foreach ($channels as [$slug, $name, $description, $enabled, $default, $captureLevel]) { $db->query(sprintf( "INSERT IGNORE INTO release_channels (slug, name, description, enabled, default_channel, capture_level) VALUES ('%s', '%s', '%s', %d, %d, '%s')", $db->escape_string($slug), $db->escape_string($name), $db->escape_string($description), (int)$enabled, (int)$default, $db->escape_string($captureLevel) )); } } private static function ensureModuleConfigDefault(string $module, string $variable, string $value, string $type): void { global $db; $moduleSql = $db->escape_string($module); $variableSql = $db->escape_string($variable); $result = $db->query("SELECT value FROM module_config WHERE module = '$moduleSql' AND variable = '$variableSql' LIMIT 1"); if ($result !== false && $result->num_rows > 0) { return; } $valueSql = $db->escape_string($value); $typeSql = $db->escape_string($type); $db->query("INSERT INTO module_config (module, variable, value, type) VALUES ('$moduleSql', '$variableSql', '$valueSql', '$typeSql')"); } private static function ensureColumn(string $table, string $column, string $definition): void { global $db; $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column); if ($table === '' || $column === '') { return; } $result = $db->query("SHOW COLUMNS FROM `$table` LIKE '$column'"); if ($result !== false && $result->num_rows > 0) { return; } $db->query("ALTER TABLE `$table` ADD COLUMN `$column` $definition"); } private static function ensureIndex(string $table, string $index, string $columns): void { global $db; $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $index = preg_replace('/[^a-zA-Z0-9_]/', '', $index); if ($table === '' || $index === '') { return; } $indexSql = $db->escape_string($index); $result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'"); if ($result !== false && $result->num_rows > 0) { return; } $db->query("ALTER TABLE `$table` ADD INDEX `$index` ($columns)"); } private static function ensureUniqueIndex(string $table, string $index, string $columns): void { global $db; $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $index = preg_replace('/[^a-zA-Z0-9_]/', '', $index); if ($table === '' || $index === '') { return; } $indexSql = $db->escape_string($index); $result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'"); if ($result !== false && $result->num_rows > 0) { return; } $db->query("ALTER TABLE `$table` ADD UNIQUE KEY `$index` ($columns)"); } }