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::ensureModuleConfigDefault('ReleaseManager', 'enabled', 'true', 'bool'); self::ensureModuleConfigDefault('ReleaseManager', 'github_webhook_secret', '', 'string'); 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_service_sets', 'release_deployments', 'release_bundles', '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"); } }