import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; test.describe.configure({ mode: "serial", timeout: 180_000 }); const DEFAULT_RELEASE_BRANCH = "master"; const permissions = [ "superuser", "user", "superuser_release_manager_view", "superuser_release_manager_manage", "superuser_release_manager_deploy", "superuser_release_manager_rollback", "superuser_release_manager_replay", ]; function json(body, status = 200) { return { status, contentType: "application/json", body: JSON.stringify(body), }; } function createReleaseState() { return { nextChannelId: 4, nextAssignmentId: 1, nextTargetId: 2, nextDeploymentId: 3, nextServiceSetId: 2, nextBundleId: 1, nextCoolifyTargetId: 20, assignmentPayloads: [], bundlePayloads: [], targetPayloads: [], deploymentPayloads: [], operations: [], channels: [ { id: 1, slug: "stable", name: "Stable", description: "Default production channel", enabled: true, default_channel: true, rollout_percent: 100, frontend_base_url: "https://app.example.test", api_base_url: "https://api.example.test", replay_enabled: false, capture_level: "metadata", retention_days: 14, versions: { frontend: { version_label: "frontend-2026.05.19", commit_sha: "abc123" }, api: { version_label: "api-2026.05.19", commit_sha: "def456" }, }, }, { id: 2, slug: "canary", name: "Canary", description: "Early release channel", enabled: true, default_channel: false, rollout_percent: 5, frontend_base_url: "https://canary.example.test", api_base_url: "https://api-canary.example.test", replay_enabled: true, capture_level: "full_redacted", retention_days: 7, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" }, api: null, }, }, { id: 3, slug: "beta", name: "Beta", enabled: true, default_channel: false, rollout_percent: 0, replay_enabled: false, capture_level: "metadata", retention_days: 14, versions: {}, }, ], assignments: [], targets: [ { id: 1, channel_id: 2, channel_slug: "canary", app: "frontend", repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, coolify_instance_id: 3, coolify_instance_label: "Production Coolify", coolify_service_uuid: "service-canary", health_url: "https://canary.example.test/health", auto_deploy: true, deploy_context: { coolify_public_url: "https://canary.example.test", coolify_domain: "canary.example.test", coolify_port: "443", coolify_project_uuid: "project-main", coolify_environment_name: "canary", coolify_server_uuid: "server-main", }, }, ], serviceSets: [ { id: 1, channel_id: 2, channel_slug: "canary", name: "Canary shared stack", slug: "canary-shared-stack", mode: "attach_existing", status: "ready", targets: { frontend: { id: 1, channel_id: 2, channel_slug: "canary", app: "frontend", repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, coolify_instance_id: 3, coolify_instance_label: "Production Coolify", coolify_service_uuid: "service-canary", health_url: "https://canary.example.test/health", deploy_context: { coolify_public_url: "https://canary.example.test", coolify_domain: "canary.example.test", coolify_port: "443", coolify_project_uuid: "project-main", coolify_environment_name: "canary", coolify_server_uuid: "server-main", }, }, api: null, }, data_services: { database: { id: 10, kind: "database", label: "Canary MariaDB", resource_name: "mariadb-canary", resource_uuid: "mariadb-canary-service", instance_label: "Production Coolify", deployment_status: "running", replication: { id: 100, label: "mariadb-canary", status: "ok", role: "replica", host: "db-canary.example.test", port: 3306, }, }, redis: { id: 11, kind: "redis", label: "Canary Redis", resource_name: "redis-canary", resource_uuid: "redis-canary-service", instance_label: "Production Coolify", deployment_status: "running", replication: { id: 101, label: "redis-canary", status: "ok", role: "replica", host: "redis-canary.example.test", port: 6379, }, }, minio: { id: 12, kind: "minio", label: "Canary MinIO", resource_name: "minio-canary", resource_uuid: "minio-canary-service", instance_label: "Production Coolify", deployment_status: "running", replication: { id: 102, label: "minio-canary", status: "ok", role: "replica", host: "minio-canary.example.test", port: 9000, }, }, }, attached_bundles: [], }, ], bundles: [], deployments: [ { id: 2, target_id: 1, channel_id: 2, channel_slug: "canary", app: "api", repository: "copenhagentruckwash/api", branch: "master", commit_sha: "ab31cd6dbb288606a24fcdbcb7bb7d58d843e4f3", version_label: "internal-api-2026-05-19", status: "failed", error_message: "Composer install failed in the Coolify build.", failure_summary: { category: "configuration", stage: "provider_configuration", root_cause: "Composer install failed in the Coolify build.", next_action: "Run the PHP test suite in the php1 container before retrying the deployment.", promotion_blocked: true, }, promotable: false, promotion_blocked_reason: "Deployment failed and cannot be promoted. Cause: Composer install failed in the Coolify build.", started_at: "2026-05-19T08:05:00.000Z", }, { id: 1, target_id: 1, channel_id: 2, channel_slug: "canary", app: "frontend", repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, commit_sha: "c0ffee", version_label: "frontend-canary", status: "deployed", promotable: true, started_at: "2026-05-19T08:00:00.000Z", }, ], replayTargets: [], timelineSessions: [ { id: 7, trace_id: "trace-canary-1", principal_type: "user", principal_id: "42", customer_number: 424242, user: { type: "user", id: "42", customer_number: 424242, name: "Acme Dispatcher", email: "dispatcher@example.test", label: "Acme Dispatcher", }, channel_id: 2, channel_slug: "canary", release: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" }, api: { version_label: "api-canary", commit_sha: "def456" }, }, device: { type: "desktop", browser_name: "Chrome", browser_version: "125.0.0", os_name: "Windows", os_version: "11", viewport_width: 1440, viewport_height: 900, device_pixel_ratio: 1, user_agent: "Mozilla/5.0 Chrome/125.0.0", }, last_route_path: "/superuser/configuration/releases", event_count: 1, error_count: 1, error_report_count: 1, module_keys: ["requestqueue"], first_event_at: "2026-05-19T08:10:00.000Z", last_event_at: "2026-05-19T08:10:00.000Z", created_at: "2026-05-19T08:09:00.000Z", last_seen_at: "2026-05-19T08:10:00.000Z", }, ], timelineEvents: [ { id: 99, trace_id: "trace-canary-1", event_type: "request_failed", severity: "error", channel_slug: "canary", module_key: "requestqueue", route_path: "/superuser/configuration/releases", component: null, occurred_at: "2026-05-19T08:10:00.000Z", payload: { request: { url: "/superuser/releases", headers: { Authorization: "[redacted]" } }, response: { status: 500 }, }, }, ], }; } function releaseStatusService(label, service_key, overrides = {}) { return { service_key, label, status: "ready", state: "ready", severity: "ok", message: `${label} release service is ready.`, next_action: "", target_tab: "overview", issue_type: null, ...overrides, }; } function releaseStatusIssue(channel, service, overrides = {}) { const issue = { severity: "critical", type: "missing_value", channel_id: channel.id, channel_slug: channel.slug, service_key: service, label: service ? releaseStatusServiceLabel(service) : "Release bundle", message: "", next_action: "", target_tab: "overview", target_id: null, deployment_id: null, coolify_target_id: null, missing_key: null, ...overrides, }; issue.key = releaseStatusIssueKey(issue); issue.impact = issue.impact || (issue.type === "failed_deployment" ? "This channel cannot be promoted until the failed deployment is replaced by a successful one." : "This channel is incomplete and cannot receive traffic safely."); issue.resolution_state = issue.resolution_state || "blocked"; issue.actions = Array.isArray(issue.actions) ? issue.actions : releaseStatusIssueActions(issue); return issue; } function releaseStatusIssueKey(issue) { return [ issue.type || "", issue.channel_id || issue.channel_slug || "", issue.service_key || "", issue.missing_key || "", issue.deployment_id || "", issue.coolify_target_id || "", ].join(":"); } function releaseStatusIssueActions(issue) { if (issue.type === "failed_deployment") { return [ { id: "retry_deployment", label: "Retry deployment", kind: "mutation", requires_confirmation: true, requires_input: false, disabled_reason: "", permission: "superuser_release_manager_deploy", }, ]; } if (issue.type === "missing_value" && ["frontend_version", "api_version"].includes(issue.missing_key)) { return [ { id: "deploy_missing_version", label: "Deploy missing version", kind: "mutation", requires_confirmation: true, requires_input: false, disabled_reason: "", permission: "superuser_release_manager_deploy", }, ]; } return []; } function releaseStatusServiceLabel(service) { return ( { frontend: "Frontend", api: "API", database: "Database", redis: "Redis", minio: "MinIO", }[service] || service ); } function releaseStatusOverviewFixture(state) { const channels = state.channels.map((channel) => { const latestDeployments = state.deployments.filter( (deployment) => Number(deployment.channel_id) === Number(channel.id) ); const latestApi = latestDeployments.find((deployment) => deployment.app === "api"); const failedApi = latestApi?.status === "failed" ? latestApi : null; const missingValues = channel.slug === "beta" ? [ { key: "frontend_version", label: "Frontend version", service_key: "frontend", target_tab: "deployments" }, { key: "frontend_base_url", label: "Frontend URL", service_key: "frontend", target_tab: "integrations" }, { key: "api_version", label: "API version", service_key: "api", target_tab: "deployments" }, { key: "api_base_url", label: "API URL", service_key: "api", target_tab: "integrations" }, ] : []; const services = [ releaseStatusService("Frontend", "frontend", { version_label: channel.versions?.frontend?.version_label || null, deployment_id: latestDeployments.find((deployment) => deployment.app === "frontend")?.id || null, }), failedApi ? releaseStatusService("API", "api", { status: "failed", state: "failed", severity: "critical", issue_type: "failed_deployment", message: failedApi.failure_summary.root_cause, next_action: failedApi.failure_summary.next_action, target_tab: "deployments", deployment_id: failedApi.id, }) : releaseStatusService("API", "api", { version_label: channel.versions?.api?.version_label || null, deployment_id: latestDeployments.find((deployment) => deployment.app === "api")?.id || null, }), releaseStatusService("Database", "database"), releaseStatusService("Redis", "redis"), releaseStatusService("MinIO", "minio"), ]; if (channel.slug === "beta") { services[0] = releaseStatusService("Frontend", "frontend", { status: "missing", state: "missing_value", severity: "critical", issue_type: "missing_value", message: "Frontend is missing Frontend version.", next_action: "Deploy the missing frontend version.", target_tab: "deployments", missing_key: "frontend_version", }); services[1] = releaseStatusService("API", "api", { status: "missing", state: "missing_value", severity: "critical", issue_type: "missing_value", message: "API is missing API version.", next_action: "Deploy the missing API version.", target_tab: "deployments", missing_key: "api_version", }); } const serviceIssues = services .filter((service) => service.severity !== "ok" && service.issue_type) .map((service) => releaseStatusIssue(channel, service.service_key, { severity: service.severity, type: service.issue_type, label: service.label, message: service.message, next_action: service.next_action, target_tab: service.target_tab, deployment_id: service.deployment_id, missing_key: service.missing_key, }) ); const missingIssues = missingValues.map((missing) => releaseStatusIssue(channel, missing.service_key, { type: "missing_value", label: missing.label, message: `${channel.name} is missing ${missing.label}.`, next_action: `Complete ${missing.label}.`, target_tab: missing.target_tab, missing_key: missing.key, }) ); const issues = [...missingIssues, ...serviceIssues]; const critical = issues.some((issue) => issue.severity === "critical"); return { channel_id: channel.id, channel_slug: channel.slug, channel_name: channel.name, default_channel: Boolean(channel.default_channel), enabled: channel.enabled !== false, severity: critical ? "critical" : "ok", readiness: critical ? "blocked" : "ready", message: critical ? `${issues.length} issues need attention before promotion.` : "All release services are ready.", availability: { configured: missingValues.length === 0, missing: missingValues.map((missing) => missing.key), status: missingValues.length === 0 ? "ready" : "unconfigured", }, missing_values: missingValues, services, versions: { ...channel.versions, bundle_id: channel.slug === "stable" ? 1 : channel.versions?.bundle_id, }, replay: { enabled: Boolean(channel.replay_enabled), capture_level: channel.capture_level, }, latest_deployments: latestDeployments, issues, }; }); const issues = channels .flatMap((channel) => channel.issues) .sort((a, b) => a.severity === b.severity ? a.channel_slug.localeCompare(b.channel_slug) : a.severity === "critical" ? -1 : 1 ); const affectedChannels = new Set(issues.map((issue) => issue.channel_slug)); return { generated_at: "2026-05-20T10:00:00.000Z", state: issues.length > 0 ? "blocked" : "ready", totals: { channels: channels.length, ready_channels: channels.filter((channel) => channel.readiness === "ready").length, affected_channels: affectedChannels.size, issues: issues.length, critical: issues.filter((issue) => issue.severity === "critical").length, warning: issues.filter((issue) => issue.severity === "warning").length, services: channels.length * 5, unhealthy_services: channels.reduce( (count, channel) => count + channel.services.filter((service) => service.severity !== "ok").length, 0 ), missing_values: issues.filter((issue) => issue.type === "missing_value").length, }, issues, channels, }; } function summary(state) { return { generated_at: "2026-05-20T10:00:00.000Z", channels: state.channels, assignments: state.assignments, deployment_targets: state.targets, service_sets: state.serviceSets.map((set) => ({ ...set, active: Boolean(set.active), stack: { frontend: set.targets?.frontend || null, api: set.targets?.api || null, database: set.data_services?.database || null, redis: set.data_services?.redis || null, minio: set.data_services?.minio || null, }, attached_bundle_count: state.bundles.filter((bundle) => Number(bundle.service_set_id) === Number(set.id)).length, })), bundles: state.bundles, deployments: state.deployments, operations: state.operations, data_services: Object.fromEntries( state.channels.map((channel) => [ channel.slug, { channel_id: channel.id, channel_slug: channel.slug, mode: "production_shared", policy: { mode: "production_shared", normal_sync_changes_service: false }, services: { database: { kind: "database", mode: "production_shared", state: "online", target: { instance_label: "node-1", deployment_status: "running", replication: { label: `${channel.slug}-mariadb`, host: `${channel.slug}-db.truckwash.internal`, port: 3306, status: "ok", role: "primary", lag_seconds: 0, last_checked_at: "2026-05-20T10:00:00.000Z", uptime_seconds: 86400, }, }, }, redis: { kind: "redis", mode: "production_shared", state: "online", target: { instance_label: "node-1", deployment_status: "running", replication: { label: `${channel.slug}-redis`, host: `${channel.slug}-redis.truckwash.internal`, port: 6379, status: "ok", role: "primary", lag_seconds: 0, last_checked_at: "2026-05-20T10:00:00.000Z", uptime_seconds: 86400, }, }, }, minio: { kind: "minio", mode: "production_shared", state: "online", target: { instance_label: "node-1", deployment_status: "running", replication: { label: `${channel.slug}-minio`, host: `${channel.slug}-minio.truckwash.internal`, port: 9000, status: "ok", role: "primary", lag_seconds: 0, last_checked_at: "2026-05-20T10:00:00.000Z", uptime_seconds: 86400, }, }, }, }, }, ]) ), replication_policy: { default_mode: "production_shared", normal_sync_changes_data_services: false, service_kinds: ["database", "redis", "minio"], }, coolify: { integrated: true, deployment_targets: state.targets.length, instances: 1, }, failover: { integrated: true, replication_hosts: 3, default_data_mode: "production_shared", }, replay_targets: state.replayTargets, module_health: [ { id: 1, module_key: "requestqueue", status: "ok", checked_at: "2026-05-19T08:11:00.000Z", }, ], timeline: { sessions: state.timelineSessions.length, events: state.timelineEvents.length, }, module_keys: ["requestqueue", "frontend", "auth", "coolify"], suggestions: { repositories: ["truckwash/front-end-vue", "truckwash/backend-php"], branches: [DEFAULT_RELEASE_BRANCH, "beta", "canary", "internal", "release/canary", "release/beta"], frontend_base_urls: ["http://localhost:5173", "https://canary.example.test", "https://app.example.test"], api_base_urls: ["http://api.truckwash.io:4433", "https://api-canary.example.test", "https://api.example.test"], health_urls: [ "http://localhost:5173/health", "http://api.truckwash.io:4433/ping", "https://canary.example.test/health", "https://api-canary.example.test/ping", ], load_balancer_domains: ["api-v2.truckwash.io", "release-api.truckwash.io"], coolify_instances: [{ id: 3, label: "Production Coolify", status: "ok" }], coolify_projects: [ { instance_id: 3, instance_label: "Production Coolify", uuid: "project-main", name: "Production Project", description: "Default production project", default: true, }, { instance_id: 3, instance_label: "Production Coolify", uuid: "project-internal", name: "Internal Project", description: "Internal validation project", default: false, }, ], coolify_services: [ { instance_id: 3, instance_label: "Production Coolify", uuid: "api-canary-service", name: "API Canary", status: "running", urls: ["https://api-canary.example.test"], }, ], coolify_github_apps: [ { instance_id: 3, instance_label: "Production Coolify", uuid: "github-app-copenhagentruckwash-github", name: "copenhagentruckwash-github", organization: "truckwash", type: "github", }, ], coolify_service_uuids: ["service-canary", "api-canary-service"], channel_presets: [ { slug: "stable", name: "Stable", rollout_percent: 100, description: "Default production channel" }, { slug: "canary", name: "Canary", rollout_percent: 5, description: "Early release channel" }, { slug: "internal", name: "Internal", rollout_percent: 0, description: "Staff-only channel" }, ], target_presets: [ { label: "Frontend target", app: "frontend", repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, health_url: "https://canary.example.test/health", auto_deploy: true, }, { label: "API target", app: "api", repository: "truckwash/backend-php", branch: "canary", health_url: "https://api-canary.example.test/ping", auto_deploy: true, }, ], }, status_overview: releaseStatusOverviewFixture(state), }; } function channelSlug(state, id) { return state.channels.find((channel) => Number(channel.id) === Number(id))?.slug || "stable"; } function endpointForTargetFixture(target, slug = target.channel_slug || "stable") { const context = target.deploy_context || {}; if (context.endpoint_mode === "manual") { const host = String(context.manual_endpoint_host || "").trim(); const port = Number(context.manual_endpoint_port || 0) || null; return { mode: "manual", status: host ? "resolved" : "pending", host: host || null, port, url: host ? `https://${host}${port && port !== 443 ? `:${port}` : ""}` : null, source: "manual", message: host ? "Manual endpoint override is configured." : "Manual endpoint mode needs a public host.", }; } const publicUrl = String(context.coolify_public_url || target.health_url || "").trim(); if (publicUrl) { const parsed = new URL(publicUrl, "https://fallback.test"); return { mode: "auto", status: "resolved", host: parsed.hostname, port: Number(parsed.port || (parsed.protocol === "http:" ? 80 : 443)), url: publicUrl.replace(/\/(health|ping)$/i, "").replace(/\/$/, ""), source: context.coolify_public_url ? "coolify_public_url" : "health_url", message: "Automatic endpoint resolved from configured URL.", }; } return { mode: "auto", status: "pending", host: "api-v2.truckwash.io", port: 443, url: `https://api-v2.truckwash.io/${slug}/${target.app}`, source: "auto_gateway", message: "Automatic gateway endpoint will be used when Coolify routing is ready.", }; } function addInternalChannel(state) { const channel = { id: state.nextChannelId++, slug: "internal", name: "Internal", description: "Staff-only channel", enabled: true, default_channel: false, rollout_percent: 0, replay_enabled: false, capture_level: "metadata", retention_days: 14, versions: {}, }; state.channels.push(channel); return channel; } function addDeployedBundle(state, overrides = {}) { const serviceSet = state.serviceSets[0]; const channelId = Number(overrides.channel_id || serviceSet?.channel_id || 2); const id = state.nextBundleId++; const frontendCommit = { sha: "c0ffee0000000000000000000000000000000000", short_sha: "c0ffee000000", message: "Frontend release build", author_name: "Release Bot", authored_at: "2026-05-19T08:00:00.000Z", }; const apiCommit = { sha: "feedface00000000000000000000000000000000", short_sha: "feedface0000", message: "Deploy canary build", author_name: "Release Bot", authored_at: "2026-05-19T08:10:00.000Z", }; const bundle = { id, channel_id: channelId, channel_slug: channelSlug(state, channelId), service_set_id: serviceSet?.id || 1, service_set_name: serviceSet?.name || "Service set", version_label: overrides.version_label || "stable-bundle-2026-05-19", status: "deployed", apps: { frontend: { version_id: id * 10 + 1, deployment_id: null, repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, commit_sha: "c0ffee", commit: frontendCommit, version: { metadata: { github_access: { commit: frontendCommit, latest_commit: frontendCommit } } }, }, api: { version_id: id * 10 + 2, deployment_id: null, repository: "truckwash/backend-php", branch: "canary", commit_sha: "feedface", commit: apiCommit, version: { metadata: { github_access: { commit: apiCommit, latest_commit: apiCommit } } }, }, }, }; state.bundles.unshift(bundle); return bundle; } function setActiveBundle(state, bundle) { if (!bundle) return null; for (const entry of state.bundles) { if ( Number(entry.channel_id) === Number(bundle.channel_id) && Number(entry.id) !== Number(bundle.id) && entry.status === "promoted" ) { entry.status = "superseded"; } } for (const set of state.serviceSets) { set.active = Number(set.id) === Number(bundle.service_set_id); } bundle.status = "promoted"; const channel = state.channels.find((entry) => Number(entry.id) === Number(bundle.channel_id)); if (channel) { channel.versions = { frontend: { version_label: bundle.version_label, commit_sha: bundle.apps?.frontend?.commit_sha || "", }, api: { version_label: bundle.version_label, commit_sha: bundle.apps?.api?.commit_sha || "", }, bundle_id: bundle.id, }; } return channel; } function isolatedDataService(state, kind, stackName = "isolated-stack") { const id = state.nextCoolifyTargetId++; return { id, kind, label: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}`, role: "replica", resource_name: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}-${id}`, resource_uuid: `isolated-${kind}-${id}`, deployment_status: "deploying", availability_state: "degraded", options: { isolated_stack: true, skip_replication_provisioning: true, production_data_attached: false, }, replication: { id: id + 1000, label: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}`, status: "unknown", role: "replica", source_host_id: null, }, }; } function completeIsolatedDataServices(state, serviceSet) { serviceSet.data_services = serviceSet.data_services || {}; for (const kind of ["database", "redis", "minio"]) { if (!serviceSet.data_services[kind]) { serviceSet.data_services[kind] = isolatedDataService(state, kind, serviceSet.name || "isolated-stack"); } } serviceSet.status = "isolated_stack"; serviceSet.health = { status: "isolated_stack", stack_complete: true }; return serviceSet; } async function boot(page, state = createReleaseState()) { await page.addInitScript(() => { window.localStorage.setItem("locale", "en"); }); await mockApi(page, { authenticated: true, permissions, sessionData: { runtime_config: { release: { trace_id: "trace-e2e", channel: { slug: "stable", name: "Stable", default_channel: true }, versions: { frontend: { version_label: "frontend-2026.05.19", commit_sha: "abc123" }, api: { version_label: "api-2026.05.19", commit_sha: "def456" }, }, availability: { configured: true, missing: [], status: "ready" }, capture_policy: { enabled: true, capture_level: "metadata" }, }, }, }, }); await installReleaseMocks(page, state); await seedAuthenticatedState(page, "release-manager-token"); return state; } async function installReleaseMocks(page, state) { await page.route(/\/customers\?search=.*/i, async (route) => { const url = new URL(route.request().url()); const query = (url.searchParams.get("search") || "").toLowerCase(); const customers = [ { id: 1, customerNumber: 424242, name: "Acme Transport" }, { id: 2, customerNumber: 123456, name: "Nordic Logistics" }, ].filter((customer) => { return customer.name.toLowerCase().includes(query) || String(customer.customerNumber).includes(query); }); await route.fulfill( json({ data: customers, meta: { pagination: { total: customers.length, count: customers.length, per_page: 10, current_page: 1 } }, }) ); }); await page.route(/\/superuser\/releases(?:\/.*)?(?:\?.*)?$/i, async (route) => { const request = route.request(); const url = new URL(request.url()); const pathname = url.pathname; const method = request.method(); if (pathname.endsWith("/superuser/releases") && method === "GET") { if (typeof state.beforeSummaryResponse === "function") { await state.beforeSummaryResponse(); } await route.fulfill(json({ data: summary(state) })); return; } if (pathname.endsWith("/superuser/releases/assignment-subjects") && method === "GET") { const query = (url.searchParams.get("search") || "").toLowerCase(); const limit = Math.max(1, Math.min(10, Number(url.searchParams.get("limit") || 5))); const subjects = [ { subject_type: "user", subject_id: "42", label: "Acme Dispatcher - Customer #424242 / dispatcher@example.test", title: "Acme Dispatcher", description: "Customer #424242 / dispatcher@example.test", icon: "fas fa-user", source: "users", }, { subject_type: "subuser", subject_id: "77", label: "Driver Alex - @driveralex / driver@example.test", title: "Driver Alex", description: "@driveralex / driver@example.test", icon: "fas fa-id-badge", source: "subusers", }, { subject_type: "customer", subject_id: "424242", label: "Acme Transport - Customer #424242", title: "Acme Transport", description: "Customer #424242", icon: "fas fa-building", source: "customers", }, ].filter((subject) => { if (!query) return true; return [subject.subject_type, subject.subject_id, subject.label, subject.title, subject.description].some( (value) => String(value || "") .toLowerCase() .includes(query) ); }); await route.fulfill(json({ data: subjects.slice(0, limit) })); return; } if (pathname.endsWith("/superuser/releases/config") && method === "GET") { if (typeof state.beforeConfigResponse === "function") { await state.beforeConfigResponse(); } await route.fulfill( json({ data: { github_token_configured: true, github_token_env_configured: false, github_token_module_configured: true, github_token_variable: "ReleaseManager.github_token", github_token_env_variable: "RELEASE_MANAGER_GITHUB_TOKEN", github_api_url: "https://api.github.com", github_api_url_variable: "ReleaseManager.github_api_url", github_webhook_secret_configured: true, github_webhook_secret_variable: "ReleaseManager.github_webhook_secret", }, }) ); return; } if (pathname.endsWith("/superuser/releases/config") && method === "POST") { const payload = request.postDataJSON?.() || {}; await route.fulfill( json({ data: { github_token_configured: true, github_token_env_configured: false, github_token_module_configured: true, github_token_variable: "ReleaseManager.github_token", github_token_env_variable: "RELEASE_MANAGER_GITHUB_TOKEN", github_api_url: payload.github_api_url || "https://api.github.com", github_api_url_variable: "ReleaseManager.github_api_url", github_webhook_secret_configured: true, github_webhook_secret_variable: "ReleaseManager.github_webhook_secret", }, }) ); return; } if (pathname.endsWith("/superuser/releases/github/repositories") && method === "GET") { if (typeof state.beforeGithubRepositoriesResponse === "function") { await state.beforeGithubRepositoriesResponse(); } await route.fulfill( json({ data: { token_configured: true, repositories: [ { full_name: "truckwash/front-end-vue", name: "front-end-vue", private: true, default_branch: DEFAULT_RELEASE_BRANCH, description: "Truckwash dashboard frontend", }, { full_name: "truckwash/backend-php", name: "backend-php", private: true, default_branch: "canary", description: "Truckwash API", }, ], }, }) ); return; } if (pathname.endsWith("/superuser/releases/github/branches") && method === "GET") { const repository = url.searchParams.get("repository") || "truckwash/front-end-vue"; const branches = repository.includes("backend") ? [ { name: "canary", commit_sha: "feedface", protected: false }, { name: "release/canary", commit_sha: "feedface", protected: false }, { name: "hotfix/api", commit_sha: "badc0de", protected: false }, ] : [ { name: DEFAULT_RELEASE_BRANCH, commit_sha: "c0ffee", protected: true }, { name: "feature/frontend-preview", commit_sha: "f00dbabe", protected: false }, ]; await route.fulfill( json({ data: { token_configured: true, repository, branches, }, }) ); return; } if (pathname.endsWith("/superuser/releases/github/commits") && method === "GET") { const repository = url.searchParams.get("repository") || "truckwash/front-end-vue"; const branch = url.searchParams.get("branch") || DEFAULT_RELEASE_BRANCH; const commits = repository.includes("backend") ? [ { sha: "feedface00000000000000000000000000000000", short_sha: "feedface0000", message: "Deploy canary build", author_name: "Release Bot", authored_at: "2026-05-19T08:10:00.000Z", }, { sha: "badc0de000000000000000000000000000000000", short_sha: "badc0de00000", message: "Previous API release", author_name: "Release Bot", authored_at: "2026-05-19T08:00:00.000Z", }, ] : [ { sha: "f00dbabe00000000000000000000000000000000", short_sha: "f00dbabe0000", message: "Frontend preview", author_name: "Release Bot", authored_at: "2026-05-19T08:15:00.000Z", }, { sha: "c0ffee0000000000000000000000000000000000", short_sha: "c0ffee000000", message: "Previous frontend release", author_name: "Release Bot", authored_at: "2026-05-19T08:00:00.000Z", }, ]; await route.fulfill( json({ data: { token_configured: true, repository, branch, commits, latest: commits[0], }, }) ); return; } if (pathname.endsWith("/superuser/releases/github/test") && method === "POST") { const payload = request.postDataJSON?.() || {}; const latestSha = ["canary", "release/canary"].includes(payload.branch) ? "feedface00000000000000000000000000000000" : "c0ffee"; await route.fulfill( json({ data: { ok: true, status: "accessible", token_configured: true, message: "Repository and branch are accessible with the configured GitHub token.", repository: payload.repository, branch: payload.branch || DEFAULT_RELEASE_BRANCH, commit_mode: payload.commit_mode || "latest", commit_sha: payload.commit_mode === "specific" ? payload.commit_sha : latestSha, latest_commit_sha: latestSha, }, }) ); return; } if (pathname.endsWith("/superuser/releases/channels") && method === "POST") { const payload = request.postDataJSON?.() || {}; const channel = { ...payload, id: state.nextChannelId++, enabled: payload.enabled !== false, default_channel: Boolean(payload.default_channel), rollout_percent: Number(payload.rollout_percent || 0), replay_enabled: Boolean(payload.replay_enabled), retention_days: Number(payload.retention_days || 14), versions: {}, }; state.channels.push(channel); await route.fulfill(json({ data: channel })); return; } if (/\/superuser\/releases\/channels\/\d+$/.test(pathname) && method === "PATCH") { const id = Number(pathname.split("/").pop()); const payload = request.postDataJSON?.() || {}; const channel = state.channels.find((entry) => entry.id === id); Object.assign(channel, payload); await route.fulfill(json({ data: channel })); return; } if (/\/superuser\/releases\/channels\/\d+\/rollback$/.test(pathname) && method === "POST") { const channelId = Number(pathname.match(/channels\/(\d+)\/rollback/)?.[1] || 0); const deployment = { id: state.nextDeploymentId++, channel_id: channelId, channel_slug: channelSlug(state, channelId), app: "api", repository: "truckwash/backend-php", branch: DEFAULT_RELEASE_BRANCH, commit_sha: "rollback", version_label: "rollback", status: "rolled_back", started_at: "2026-05-19T08:13:00.000Z", }; state.deployments.unshift(deployment); await route.fulfill(json({ data: deployment })); return; } if (pathname.endsWith("/superuser/releases/operations") && method === "GET") { if (typeof state.beforeOperationsResponse === "function") { await state.beforeOperationsResponse(); } await route.fulfill(json({ data: state.operations })); return; } if (/\/superuser\/releases\/operations\/\d+$/.test(pathname) && method === "GET") { const id = Number(pathname.split("/").pop()); await route.fulfill(json({ data: state.operations.find((operation) => Number(operation.id) === id) || null })); return; } if (pathname.endsWith("/superuser/releases/test-runs") && method === "POST") { const payload = request.postDataJSON?.() || {}; const channel = state.channels.find((entry) => Number(entry.id) === Number(payload.channel_id)) || state.channels[0]; const operation = { id: state.operations.length + 1, operation_type: "release_test", channel_id: channel?.id || null, channel_slug: channel?.slug || null, status: "warning", title: `Release checks for ${channel?.name || "Release Manager"}`, summary: "Release Manager tests completed with warnings.", solution_hint: "Create the missing branch or target, then retry.", step_count: 3, passed_step_count: 1, failed_step_count: 1, warning_step_count: 1, steps: [ { id: 1, step_key: "dashboard", label: "Dashboard data contract", status: "passed", message: "Dashboard data is available.", }, { id: 2, step_key: "branch", label: "Canary API branch", status: "warning", message: "Branch canary could not be verified and will be skipped by sync.", diagnostic: "GitHub branch was not found.", solution_hint: "Create the canary branch from master, then retry.", context: { retry_action: "retry_sync", channel_id: channel?.id || null }, }, { id: 3, step_key: "target", label: "API Coolify target", status: "failed", message: "No Coolify target exists for this channel/app.", diagnostic: "Missing release_deployment_targets row.", solution_hint: "Create the target in Release Manager integrations.", context: { retry_action: "configure_target", channel_id: channel?.id || null }, }, ], }; state.operations.unshift(operation); await route.fulfill(json({ data: operation }, 202)); return; } if (/\/superuser\/releases\/channels\/\d+\/sync$/.test(pathname) && method === "POST") { const channelId = Number(pathname.match(/channels\/(\d+)\/sync/)?.[1] || 0); const channel = state.channels.find((entry) => Number(entry.id) === channelId); const operation = { id: state.operations.length + 1, operation_type: "channel_sync", channel_id: channelId, channel_slug: channel?.slug || null, status: "passed", title: `Sync ${channel?.name || "channel"} release channel`, summary: "Channel sync completed.", step_count: 2, passed_step_count: 2, failed_step_count: 0, warning_step_count: 0, steps: [ { id: 1, step_key: "channel_mapping", label: "Channel route and branch mapping", status: "passed", message: "Route and branch mapping passed.", }, { id: 2, step_key: "frontend_deploy", label: "Frontend deploy and activate", status: "passed", message: "Frontend is active.", }, ], }; state.operations.unshift(operation); await route.fulfill(json({ data: operation }, 202)); return; } if (/\/superuser\/releases\/channels\/\d+\/bundle$/.test(pathname) && method === "POST") { const channelId = Number(pathname.match(/channels\/(\d+)\/bundle/)?.[1] || 0); const payload = request.postDataJSON?.() || {}; const bundle = state.bundles.find( (entry) => Number(entry.id) === Number(payload.bundle_id) && Number(entry.channel_id) === channelId ); if (!bundle || !["deployed", "promoted", "active"].includes(bundle.status)) { await route.fulfill(json({ message: "Only deployed release bundles can be set on a channel." }, 409)); return; } const channel = setActiveBundle(state, bundle); await route.fulfill(json({ data: { channel, versions: channel?.versions || {}, bundle } })); return; } if (pathname.endsWith("/superuser/releases/assignments") && method === "POST") { const payload = request.postDataJSON?.() || {}; state.assignmentPayloads.push(payload); const assignment = { ...payload, id: state.nextAssignmentId++, channel_slug: channelSlug(state, payload.channel_id), }; state.assignments.unshift(assignment); await route.fulfill(json({ data: assignment })); return; } if (pathname.endsWith("/superuser/releases/targets") && method === "POST") { const payload = request.postDataJSON?.() || {}; state.targetPayloads.push(payload); if (!["project-internal", "project-main"].includes(payload.deploy_context?.coolify_project_uuid)) { await route.fulfill(json({ message: "Coolify project was not selected." }, 400)); return; } const target = { ...payload, id: state.nextTargetId++, channel_slug: channelSlug(state, payload.channel_id), coolify_instance_label: payload.coolify_instance_id ? "Production Coolify" : null, deploy_context: payload.deploy_context || {}, }; target.endpoint = endpointForTargetFixture(target, target.channel_slug); state.targets.unshift(target); if (payload.app === "api" && !payload.deploy_context?.isolated_stack && state.serviceSets[0]) { state.serviceSets[0].targets.api = target; } await route.fulfill(json({ data: target })); return; } if (/\/superuser\/releases\/targets\/\d+$/.test(pathname) && method === "DELETE") { const id = Number(pathname.match(/targets\/(\d+)/)?.[1] || 0); state.targets = state.targets.filter((entry) => Number(entry.id) !== id); for (const serviceSet of state.serviceSets) { for (const app of ["frontend", "api"]) { if (Number(serviceSet.targets?.[app]?.id) === id) { serviceSet.targets[app] = null; } } } await route.fulfill(json({ data: { id, removed: true, provider_resources_deleted: false } })); return; } if (pathname.endsWith("/superuser/releases/service-sets") && method === "GET") { await route.fulfill(json({ data: summary(state).service_sets })); return; } if (pathname.endsWith("/superuser/releases/service-sets") && method === "POST") { const payload = request.postDataJSON?.() || {}; const source = state.serviceSets.find((entry) => Number(entry.id) === Number(payload.source_service_set_id)); const serviceSet = { id: state.nextServiceSetId++, channel_id: payload.channel_id || source?.channel_id || 2, channel_slug: channelSlug(state, payload.channel_id || source?.channel_id || 2), name: payload.name || "Fresh release stack", slug: (payload.name || "fresh-release-stack").toLowerCase().replace(/[^a-z0-9]+/g, "-"), mode: payload.mode || "attach_existing", source_service_set_id: payload.source_service_set_id || null, status: payload.mode === "clone_existing" ? "provisioning" : payload.mode === "fresh_empty" ? "isolated_empty" : payload.mode === "isolated_stack" ? "isolated_stack" : "ready", targets: { frontend: state.targets.find((entry) => Number(entry.id) === Number(payload.frontend_target_id)) || source?.targets?.frontend || null, api: state.targets.find((entry) => Number(entry.id) === Number(payload.api_target_id)) || source?.targets?.api || null, }, data_services: ["fresh_empty", "isolated_stack"].includes(payload.mode) ? { database: null, redis: null, minio: null } : source?.data_services || { database: null, redis: null, minio: null, }, attached_bundles: [], }; if (payload.mode === "isolated_stack" && payload.create_data_targets !== false) { completeIsolatedDataServices(state, serviceSet); } state.serviceSets.unshift(serviceSet); await route.fulfill(json({ data: summary(state).service_sets.find((entry) => entry.id === serviceSet.id) })); return; } if (/\/superuser\/releases\/service-sets\/\d+$/.test(pathname) && method === "DELETE") { const id = Number(pathname.match(/service-sets\/(\d+)/)?.[1] || 0); const serviceSet = state.serviceSets.find((entry) => Number(entry.id) === id); if (!serviceSet || serviceSet.mode !== "isolated_stack" || serviceSet.active) { await route.fulfill(json({ message: "The active release service set cannot be removed." }, 400)); return; } state.serviceSets = state.serviceSets.filter((entry) => Number(entry.id) !== id); state.bundles = state.bundles.filter((entry) => Number(entry.service_set_id) !== id); await route.fulfill(json({ data: { id, removed: true, provider_resources_deleted: false } })); return; } if (/\/superuser\/releases\/service-sets\/\d+\/isolated-data-services$/.test(pathname) && method === "POST") { const id = Number(pathname.match(/service-sets\/(\d+)\/isolated-data-services/)?.[1] || 0); const serviceSet = state.serviceSets.find((entry) => Number(entry.id) === id); if (!serviceSet || serviceSet.mode !== "isolated_stack") { await route.fulfill(json({ message: "Only isolated stacks can add data services." }, 400)); return; } completeIsolatedDataServices(state, serviceSet); await route.fulfill(json({ data: summary(state).service_sets.find((entry) => entry.id === serviceSet.id) }, 202)); return; } if (pathname.endsWith("/superuser/releases/bundles") && method === "GET") { await route.fulfill(json({ data: state.bundles })); return; } if (pathname.endsWith("/superuser/releases/bundles") && method === "POST") { const payload = request.postDataJSON?.() || {}; state.bundlePayloads.push(payload); const serviceSet = state.serviceSets.find((entry) => Number(entry.id) === Number(payload.service_set_id)); const frontendCommit = { sha: payload.frontend?.commit_mode === "specific" ? payload.frontend?.commit_sha : "c0ffee0000000000000000000000000000000000", short_sha: String( payload.frontend?.commit_mode === "specific" ? payload.frontend?.commit_sha : "c0ffee000000" ).slice(0, 12), message: "Frontend release build", author_name: "Release Bot", authored_at: "2026-05-19T08:00:00.000Z", }; const apiCommit = { sha: payload.api?.commit_mode === "specific" ? payload.api?.commit_sha : "feedface00000000000000000000000000000000", short_sha: String(payload.api?.commit_mode === "specific" ? payload.api?.commit_sha : "feedface0000").slice( 0, 12 ), message: "Deploy canary build", author_name: "Release Bot", authored_at: "2026-05-19T08:10:00.000Z", }; const bundle = { id: state.nextBundleId++, channel_id: payload.channel_id || serviceSet?.channel_id || 2, channel_slug: channelSlug(state, payload.channel_id || serviceSet?.channel_id || 2), service_set_id: payload.service_set_id, service_set_name: serviceSet?.name || "Service set", version_label: payload.version_label || `bundle-${state.nextBundleId}`, status: "draft", apps: { frontend: { version_id: state.nextBundleId * 10 + 1, deployment_id: null, repository: payload.frontend?.repository || "truckwash/front-end-vue", branch: payload.frontend?.branch || DEFAULT_RELEASE_BRANCH, commit_sha: frontendCommit.sha, commit: frontendCommit, version: { metadata: { github_access: { commit: frontendCommit, latest_commit: frontendCommit } } }, }, api: { version_id: state.nextBundleId * 10 + 2, deployment_id: null, repository: payload.api?.repository || "truckwash/backend-php", branch: payload.api?.branch || "release/canary", commit_sha: apiCommit.sha, commit: apiCommit, version: { metadata: { github_access: { commit: apiCommit, latest_commit: apiCommit } } }, }, }, }; state.bundles.unshift(bundle); await route.fulfill(json({ data: bundle }, 201)); return; } if (/\/superuser\/releases\/bundles\/\d+\/deploy$/.test(pathname) && method === "POST") { const id = Number(pathname.match(/bundles\/(\d+)\/deploy/)?.[1] || 0); const bundle = state.bundles.find((entry) => entry.id === id); if (bundle) { bundle.status = "deployed"; for (const app of ["frontend", "api"]) { const deployment = { id: state.nextDeploymentId++, target_id: null, channel_id: bundle.channel_id, channel_slug: bundle.channel_slug, app, repository: bundle.apps[app].repository, branch: bundle.apps[app].branch, commit_sha: bundle.apps[app].commit_sha, version_label: bundle.version_label, status: "deployed", bundle_id: bundle.id, service_set_id: bundle.service_set_id, deployment_kind: "bundle_member", promotable: true, started_at: "2026-05-19T08:20:00.000Z", }; bundle.apps[app].deployment_id = deployment.id; state.deployments.unshift(deployment); } } await route.fulfill(json({ data: bundle || null }, 202)); return; } if (/\/superuser\/releases\/bundles\/\d+\/promote$/.test(pathname) && method === "POST") { const id = Number(pathname.match(/bundles\/(\d+)\/promote/)?.[1] || 0); const bundle = state.bundles.find((entry) => entry.id === id); const channel = setActiveBundle(state, bundle); await route.fulfill(json({ data: { channel, versions: channel?.versions || {}, bundle } })); return; } if (pathname.endsWith("/superuser/releases/issues/actions") && method === "POST") { const payload = request.postDataJSON?.() || {}; const currentSummary = summary(state); const issue = currentSummary.status_overview.issues.find((entry) => entry.key === payload.issue_key); if (!issue) { await route.fulfill( json({ data: { status: "failed", message: "This release issue is no longer active.", result: null, summary: currentSummary, }, }) ); return; } if (payload.action_id === "retry_deployment") { const original = state.deployments.find((entry) => Number(entry.id) === Number(issue.deployment_id)); const deployment = { ...(original || {}), id: state.nextDeploymentId++, status: "deploying", error_message: null, failure_summary: null, promotable: false, started_at: "2026-05-19T08:30:00.000Z", }; state.deployments.unshift(deployment); await route.fulfill( json({ data: { status: "queued", message: "Deployment retry started.", issue, action: issue.actions.find((action) => action.id === payload.action_id), result: { deployment }, summary: summary(state), }, }) ); return; } await route.fulfill( json({ data: { status: "needs_input", message: "Select the missing deployment inputs.", issue, action: issue.actions.find((action) => action.id === payload.action_id), result: { required_inputs: ["target_id"] }, summary: currentSummary, }, }) ); return; } if (pathname.endsWith("/superuser/releases/deployments") && method === "POST") { const payload = request.postDataJSON?.() || {}; state.deploymentPayloads.push(payload); const target = state.targets.find((entry) => Number(entry.id) === Number(payload.target_id)); const channelId = Number(payload.channel_id || target?.channel_id || 1); const deployment = { id: state.nextDeploymentId++, target_id: payload.target_id || null, channel_id: channelId, channel_slug: channelSlug(state, channelId), app: payload.app || target?.app || "frontend", repository: payload.repository || target?.repository || "truckwash/front-end-vue", branch: payload.branch || target?.branch || DEFAULT_RELEASE_BRANCH, commit_sha: payload.commit_sha || "latestcommit", version_label: payload.version_label || "manual-release", status: "deploying", started_at: "2026-05-19T08:12:00.000Z", }; state.deployments.unshift(deployment); await route.fulfill(json({ data: deployment })); return; } if (/\/superuser\/releases\/deployments\/\d+\/promote$/.test(pathname) && method === "POST") { const id = Number(pathname.match(/deployments\/(\d+)\/promote/)?.[1] || 0); const deployment = state.deployments.find((entry) => entry.id === id); if (deployment) { deployment.status = "promoted"; } await route.fulfill(json({ data: deployment || null })); return; } if (pathname.endsWith("/superuser/releases/replay-targets") && method === "POST") { const payload = request.postDataJSON?.() || {}; const replayTarget = { ...payload, id: state.replayTargets.length + 1, enabled: payload.enabled !== false, }; state.replayTargets.unshift(replayTarget); await route.fulfill(json({ data: replayTarget })); return; } if (pathname.endsWith("/superuser/releases/timeline/sessions") && method === "GET") { if (typeof state.beforeTimelineSessionsResponse === "function") { await state.beforeTimelineSessionsResponse(); } await route.fulfill(json({ data: state.timelineSessions })); return; } if (/\/superuser\/releases\/timeline\/sessions\/[^/]+$/.test(pathname) && method === "GET") { const traceId = decodeURIComponent(pathname.split("/").pop() || ""); const session = state.timelineSessions.find((entry) => entry.trace_id === traceId); await route.fulfill( json({ data: { session, events: state.timelineEvents.filter((event) => event.trace_id === traceId), error_reports: [ { id: 17, status: "open", reporter: { type: "user", user_id: 42, name: "Acme Dispatcher" }, route_path: "/superuser/configuration/releases", release_trace_id: traceId, frontend_version: "frontend-canary", api_version: "api-canary", request_error_count: 1, vue_error_count: 0, created_at: "2026-05-19T08:11:00.000Z", }, ], release: { channel: state.channels.find((channel) => channel.slug === session?.channel_slug) || null, frontend: { ...(session?.release?.frontend || {}), version: { id: 12, app: "frontend", status: "deployed", version_label: "frontend-canary" }, deployment: { id: 21, status: "deployed", bundle_id: 31 }, }, api: { ...(session?.release?.api || {}), version: { id: 13, app: "api", status: "deployed", version_label: "api-canary" }, deployment: { id: 22, status: "deployed", bundle_id: 31 }, }, bundle: { id: 31, version_label: "canary-bundle", status: "deployed" }, }, }, }) ); return; } if (pathname.endsWith("/superuser/releases/timeline") && method === "GET") { if (typeof state.beforeTimelineResponse === "function") { await state.beforeTimelineResponse(); } await route.fulfill(json({ data: state.timelineEvents })); return; } await route.fulfill(json({ data: [] })); }); await page.route(/\/release\/timeline\/events$/i, async (route) => { await route.fulfill(json({ data: { accepted: 1 } })); }); } async function expectReleaseManagerReady(page) { await expect(page.getByTestId("release-manager-page")).toBeVisible({ timeout: 90_000 }); await expect(page.getByTestId("release-management-interface")).toBeVisible({ timeout: 90_000 }); } async function expandActiveReleaseCategory(page) { await expectReleaseManagerReady(page); } const releaseSectionNames = { Overview: "overview", Channels: "channels", Assignments: "assignments", Deployments: "deployments", Replay: "operations", Operations: "operations", Integrations: "integrations", "Data services": "data-services", Settings: "settings", }; async function selectReleaseTab(page, name) { await expectReleaseManagerReady(page); const key = releaseSectionNames[name] || String(name || "").toLowerCase(); const link = page.getByTestId("release-management-interface").getByTestId(`release-section-link-${key}`); if ((await link.count()) > 0) { await link.click(); return; } await page.goto(`/superuser/configuration/releases/${key}${new URL(page.url()).search}`, { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); } function createResponseGate() { const pending = []; return { wait: () => new Promise((resolve) => { pending.push(resolve); }), async releaseNext() { await expect.poll(() => pending.length).toBeGreaterThan(0); pending.shift()?.(); }, }; } test("loading states cover initial release data, refreshes, settings, and GitHub repositories", async ({ page }) => { const state = createReleaseState(); const summaryGate = createResponseGate(); const configGate = createResponseGate(); const repositoriesGate = createResponseGate(); state.beforeSummaryResponse = () => summaryGate.wait(); state.beforeConfigResponse = () => configGate.wait(); state.beforeGithubRepositoriesResponse = () => repositoriesGate.wait(); await boot(page, state); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=all&branch=master", { waitUntil: "domcontentloaded", }); await expect(page.getByTestId("release-manager-page")).toBeVisible({ timeout: 90_000 }); await expect(page.getByTestId("release-status-dashboard-loading")).toBeVisible(); await expect(page.getByTestId("release-context-loading")).toBeVisible(); await expect(page.getByTestId("release-context-channel")).toBeDisabled(); await expect(page.getByTestId("release-status-dashboard")).toBeHidden(); await summaryGate.releaseNext(); await expect(page.getByTestId("release-status-dashboard")).toBeVisible(); await expect(page.getByTestId("release-context-channel")).toBeEnabled(); await expect(page.getByTestId("release-manager-refreshing")).toBeHidden(); state.beforeSummaryResponse = () => summaryGate.wait(); await page.getByTestId("release-context-refresh").click(); await expect(page.getByTestId("release-manager-refreshing")).toBeVisible(); await expect(page.getByTestId("release-status-dashboard")).toBeVisible(); await summaryGate.releaseNext(); await expect(page.getByTestId("release-manager-refreshing")).toBeHidden(); await selectReleaseTab(page, "Settings"); await expect(page.getByTestId("release-settings-config-loading")).toBeVisible(); await expect(page.getByTestId("release-settings-summary")).toHaveCount(0); await configGate.releaseNext(); await expect(page.getByTestId("release-settings-summary")).toBeVisible(); await selectReleaseTab(page, "Integrations"); await expect(page.getByTestId("release-github-repositories-loading")).toBeVisible(); await repositoriesGate.releaseNext(); await expect(page.getByTestId("release-github-repositories-loading")).toHaveCount(0); await expect(page.getByTestId("release-target-form")).toBeVisible(); }); test("loading states cover operation refresh and replay timeline searches", async ({ page }) => { const state = createReleaseState(); const operationsGate = createResponseGate(); const timelineSessionsGate = createResponseGate(); const timelineEventsGate = createResponseGate(); state.beforeOperationsResponse = () => operationsGate.wait(); state.beforeTimelineSessionsResponse = () => timelineSessionsGate.wait(); state.beforeTimelineResponse = () => timelineEventsGate.wait(); await boot(page, state); await page.goto("/superuser/configuration/releases/operations?channel=stable&app=api&branch=master", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); await expect(page.getByTestId("release-section-operations")).toBeVisible(); await page.getByRole("button", { name: /Refresh operations/i }).click(); await expect(page.getByTestId("release-data-grid-loading")).toBeVisible(); await operationsGate.releaseNext(); await expect(page.getByTestId("release-data-grid-loading")).toHaveCount(0); await expect(page.getByTestId("release-timeline-sessions-loading")).toBeVisible(); await expect(page.getByTestId("release-timeline-events-loading")).toBeVisible(); await timelineSessionsGate.releaseNext(); await timelineEventsGate.releaseNext(); await expect(page.getByTestId("release-timeline-sessions-loading")).toHaveCount(0); await expect(page.getByTestId("release-timeline-events-loading")).toHaveCount(0); }); test("overview status dashboard prioritizes release issues and opens channel details", async ({ page }) => { test.setTimeout(180_000); const state = createReleaseState(); await boot(page, state); if (test.info().project.name.includes("mobile")) { await page.waitForTimeout(5_000); } await page.goto("/superuser/configuration/releases/overview?channel=canary&app=all&branch=master", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); await expect(page.getByTestId("release-section-link-integrations")).toBeVisible(); await expect(page.getByTestId("release-section-link-deployments")).toHaveCount(0); await expect(page.getByTestId("release-section-link-channels")).toHaveCount(0); await expect(page.getByTestId("release-manager-page")).not.toContainText("endpoint not configured"); await expect(page.getByTestId("release-context-source")).toContainText("Local source"); await expect(page.getByTestId("release-context-endpoints")).toContainText("/api"); await expect(page.getByTestId("release-context-endpoints")).not.toContainText("/master/api"); await expect(page.getByTestId("release-runtime-source")).toContainText("Local source"); const dashboard = page.getByTestId("release-status-dashboard"); await expect(dashboard).toBeVisible({ timeout: 90_000 }); await expect(dashboard).toContainText("Release promotion is blocked"); await expect(dashboard.getByTestId("release-status-summary")).toContainText("Critical"); await expect(page.getByTestId("release-cockpit-channel-list")).toContainText("Canary"); await expect(page.getByTestId("release-cockpit-create-release")).toBeVisible(); await expect(page.getByTestId("release-control-api")).toHaveCount(0); await selectReleaseTab(page, "Channels"); await expect(page.getByTestId("release-unified-dashboard")).toContainText( "Channels, deployments, data, Coolify, and failover" ); await expect(page.getByTestId("release-channel-lane")).toHaveCount(1); await expect(page.getByTestId("release-channel-lane").filter({ hasText: "Canary" })).toContainText( "canary -> canary" ); await expect(page.getByTestId("release-channel-lane")).not.toContainText("Stable"); await selectReleaseTab(page, "Overview"); const issuePanel = dashboard.getByTestId("release-status-issues"); const failedDeploymentIssue = issuePanel.getByRole("button").filter({ hasText: "Composer install failed" }).first(); await expect(failedDeploymentIssue).toBeVisible(); await failedDeploymentIssue.evaluate((element) => element.scrollIntoView({ block: "center", inline: "nearest" })); const viewport = page.viewportSize(); const issueBox = await failedDeploymentIssue.boundingBox(); expect(issueBox).toBeTruthy(); if ((viewport?.width || 0) > 768) { await expect(failedDeploymentIssue).toBeVisible(); } else { await failedDeploymentIssue.scrollIntoViewIfNeeded(); await expect(failedDeploymentIssue).toBeVisible(); } await page.getByTestId("release-cockpit-primary-action").click(); await expect(page.getByTestId("release-status-drawer")).toBeVisible(); await expect( page.getByTestId("release-status-drawer").getByRole("button", { name: /Retry deployment/i }) ).toBeVisible(); page.once("dialog", (dialog) => dialog.accept()); await page .getByTestId("release-status-drawer") .getByRole("button", { name: /Retry deployment/i }) .click(); await expect(page.getByTestId("release-issue-action-result")).toContainText("Deployment retry started"); await expect(issuePanel).not.toContainText("Composer install failed"); await page.locator(".release-status-drawer .delete").click(); await selectReleaseTab(page, "Overview"); const canaryCard = page .getByTestId("release-cockpit-channel-list") .locator(".release-cockpit-channel") .filter({ hasText: "Canary" }) .first(); await expect(canaryCard).toBeVisible(); await canaryCard.click(); const drawer = page.getByTestId("release-status-drawer"); await expect(drawer).toBeVisible(); await expect(drawer).toContainText("Canary"); await expect(drawer).toContainText("All release services are ready"); await expect(drawer).not.toContainText("Run the PHP test suite in the php1 container"); await expect(drawer.getByTestId("release-status-service")).toHaveCount(5); await expect(drawer).toContainText("Frontend"); await expect(drawer).toContainText("API"); await expect(drawer).toContainText("Database"); await expect(drawer).toContainText("Redis"); await expect(drawer).toContainText("MinIO"); const drawerBox = await drawer.boundingBox(); expect(drawerBox).toBeTruthy(); expect(drawerBox.x).toBeGreaterThanOrEqual(0); expect(drawerBox.width).toBeLessThanOrEqual((viewport?.width || 1440) + 1); await page.locator(".release-status-drawer .delete").click(); await page.getByTestId("release-context-channel").selectOption("beta"); const betaCard = page .getByTestId("release-cockpit-channel-list") .locator(".release-cockpit-channel") .filter({ hasText: "Beta" }) .first(); await betaCard.click(); await expect(drawer).toBeVisible(); await expect(drawer.getByTestId("release-status-drawer-missing-values")).not.toContainText("Release bundle"); await expect(drawer.getByTestId("release-status-drawer-missing-values")).toContainText("Frontend version"); await expect(drawer.getByTestId("release-status-drawer-missing-values")).toContainText("Frontend URL"); await expect(drawer.getByTestId("release-status-drawer-missing-values")).toContainText("API version"); await expect(drawer.getByTestId("release-status-drawer-missing-values")).toContainText("API URL"); await drawer .getByRole("button", { name: /Operations/ }) .first() .click(); await expect(page).toHaveURL(/\/superuser\/configuration\/releases\/operations/); }); test("unified dashboard exposes channel lanes, operation diagnostics, and old-route redirects", async ({ page }) => { const state = createReleaseState(); await boot(page, state); await page.goto("/superuser/configuration/coolify", { waitUntil: "domcontentloaded" }); await expectReleaseManagerReady(page); await expect(page).toHaveURL(/\/superuser\/configuration\/releases\/integrations\?panel=coolify/); await expect(page.getByTestId("release-section-link-deployments")).toHaveCount(0); await expect(page.getByTestId("release-section-link-channels")).toHaveCount(0); await selectReleaseTab(page, "Channels"); await expect(page.getByTestId("release-coolify-panel")).toContainText("Coolify"); await expect(page.getByTestId("release-failover-panel")).toContainText("Failover"); await expect(page.getByTestId("release-channel-lane")).toHaveCount(1); await expect(page.getByTestId("release-channel-lane").filter({ hasText: "Stable" })).toContainText( "stable -> master" ); await expect(page.getByTestId("release-channel-lane")).not.toContainText("Canary"); await selectReleaseTab(page, "Overview"); await expect(page.getByTestId("release-context-test")).toBeVisible(); await page.getByTestId("release-context-test").click(); const modal = page.getByTestId("release-operation-modal"); await expect(modal).toBeVisible(); await expect(modal).toContainText("Dashboard data contract"); await expect(modal).toContainText("Diagnostic"); await expect(modal).toContainText("Solution"); await modal.getByRole("button", { name: "Retry" }).first().click(); await expect(modal).toContainText("Channel sync completed"); await page.keyboard.press("Escape"); await expect(modal).toBeHidden(); await page.goto("/superuser/configuration/failover", { waitUntil: "domcontentloaded" }); await expectReleaseManagerReady(page); await expect(page).toHaveURL(/\/superuser\/configuration\/releases\/data-services\?panel=failover/); await expect(page.getByTestId("release-section-data-services")).toContainText("Data services and failover"); }); test("route-backed workspace keeps context and exposes operational data-service tooltips", async ({ page }) => { const state = createReleaseState(); await boot(page, state); await page.goto("/superuser/configuration/releases/data-services?channel=stable&app=api&branch=master", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); await expect(page.getByTestId("release-context-channel")).toHaveValue("stable"); await expect(page.getByTestId("release-context-app")).toHaveValue("api"); await expect(page.getByTestId("release-context-branch")).toHaveValue("master"); await expect(page.getByTestId("release-section-data-services")).toBeVisible(); const databaseLink = page .getByTestId("release-data-service-row") .filter({ hasText: "database" }) .first() .locator(".release-entity-link") .first(); await expect(databaseLink).toHaveAttribute("title", /Node: node-1/); await expect(databaseLink).toHaveAttribute("title", /Hostname: stable-db\.truckwash\.internal/); await expect(databaseLink).toHaveAttribute("title", /Port: 3306/); await expect(databaseLink).toHaveAttribute("title", /Uptime: 1d/); await expect(databaseLink).toHaveAttribute("title", /Last check: 2026-05-20T10:00:00\.000Z/); await selectReleaseTab(page, "Operations"); await expect(page).toHaveURL( /\/superuser\/configuration\/releases\/operations\?channel=stable&app=api&branch=master/ ); await expect(page.getByTestId("release-context-channel")).toHaveValue("stable"); await expect(page.getByTestId("release-context-app")).toHaveValue("api"); if ((page.viewportSize()?.width || 0) > 768) { const documentScroll = await page.evaluate(() => ({ clientHeight: document.documentElement.clientHeight, scrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight), })); expect(documentScroll.scrollHeight).toBeLessThanOrEqual(documentScroll.clientHeight + 2); } }); test("primary release workflow hides manual bundle controls and syncs the selected channel", async ({ page }) => { const state = createReleaseState(); addDeployedBundle(state, { channel_id: 2 }); await boot(page, state); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=frontend&branch=canary", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); await expect(page.getByTestId("release-section-link-deployments")).toHaveCount(0); await expect(page.getByTestId("release-section-link-channels")).toHaveCount(0); await expect(page.getByTestId("release-bundle-flow")).toBeHidden(); await expect(page.getByTestId("release-channel-bundle-picker")).toHaveCount(0); await expect(page.getByTestId("release-channels-overview")).toHaveCount(0); await page.getByTestId("release-context-sync").click(); const modal = page.getByTestId("release-operation-modal"); await expect(modal).toBeVisible(); await expect(modal).toContainText("Sync Canary release channel"); await expect(modal).toContainText("Channel route and branch mapping"); await expect(modal).toContainText("Frontend deploy and activate"); expect(state.operations[0]).toMatchObject({ operation_type: "channel_sync", channel_id: 2 }); }); test("release context maps branches and endpoints when the selected channel changes", async ({ page }) => { const state = createReleaseState(); addInternalChannel(state); await boot(page, state); await page.goto("/superuser/configuration/releases/overview?channel=stable&app=api&branch=master", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); await expect(page.getByTestId("release-context-branch")).toHaveValue("master"); await expect(page.getByTestId("release-context-endpoints")).toContainText("/api"); await expect(page.getByTestId("release-context-endpoints")).not.toContainText("/master/api"); await page.getByTestId("release-context-channel").selectOption("beta"); await expect(page.getByTestId("release-context-branch")).toHaveValue("beta"); await expect(page.getByTestId("release-context-endpoints")).toContainText("/api"); await expect(page.getByTestId("release-context-endpoints")).not.toContainText("/beta/api"); await page.getByTestId("release-context-channel").selectOption("internal"); await expect(page.getByTestId("release-context-branch")).toHaveValue("internal"); await expect(page.getByTestId("release-context-endpoints")).toContainText("/api"); await expect(page.getByTestId("release-context-endpoints")).not.toContainText("/internal/api"); }); test("overview filters channel health and issues by selected channel and app", async ({ page }) => { const state = createReleaseState(); await boot(page, state); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=api&branch=canary", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); const cockpitChannels = page.getByTestId("release-cockpit-channel-list"); await expect(cockpitChannels).toContainText("Canary"); await expect(cockpitChannels).not.toContainText("Stable"); await expect(cockpitChannels).not.toContainText("Beta"); const issuePanel = page.getByTestId("release-status-issues"); await expect(issuePanel).toContainText("Composer install failed"); await expect(issuePanel).not.toContainText("Frontend version"); await page.getByTestId("release-context-app").selectOption("frontend"); await expect(page.getByTestId("release-context-app")).toHaveValue("frontend"); await expect(issuePanel).not.toContainText("Composer install failed"); }); test("superusers manage release settings, assignments, integrations, and sync operations", async ({ page }) => { const state = await boot(page); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=api&branch=canary", { waitUntil: "domcontentloaded", }); await expectReleaseManagerReady(page); if ((page.viewportSize()?.width || 0) > 768) { await expect(page.getByRole("link", { name: /Release Manager/i }).first()).toBeVisible(); } await expect(page.locator(".tabs").getByText("Release Manager", { exact: true })).toBeVisible(); await expect(page.getByTestId("release-control-api")).toHaveCount(0); await expect(page.getByTestId("release-section-link-deployments")).toHaveCount(0); await expect(page.getByTestId("release-section-link-channels")).toHaveCount(0); await expect(page.getByTestId("release-status-dashboard")).toBeVisible(); await expect(page.getByTestId("release-channels-overview")).toHaveCount(0); await expect(page.getByTestId("release-guided-setup")).toHaveCount(0); await selectReleaseTab(page, "Settings"); await expect(page.getByTestId("release-settings-summary")).toContainText("ReleaseManager.github_token"); await expect(page.getByTestId("release-settings-guide")).toContainText("GitHub token"); const settingsForm = page.getByTestId("release-settings-form"); await settingsForm.getByPlaceholder("Leave blank to keep the existing token").fill("github_pat_release_manager_test"); await page.getByTestId("release-settings-next").click(); await expect(page.getByTestId("release-settings-step-api-url")).toContainText("ReleaseManager.github_api_url"); await settingsForm.getByPlaceholder("https://api.github.com").fill("https://api.github.com"); await page.getByTestId("release-settings-next").click(); await expect(page.getByTestId("release-settings-step-webhook")).toContainText("/release/github/webhook"); await settingsForm.getByPlaceholder("Webhook HMAC secret").fill("release-manager-webhook-secret"); await page.getByTestId("release-settings-next").click(); await expect(page.getByTestId("release-settings-step-review")).toContainText("GitHub webhook secret"); await page.getByTestId("release-settings-save").click(); await expect(page.getByTestId("release-settings-summary")).toContainText("https://api.github.com"); await selectReleaseTab(page, "Assignments"); await expandActiveReleaseCategory(page); await page.getByTestId("release-assignment-channel-suggestions").getByText("canary").click(); const assignmentSubjectInput = page.getByTestId("release-assignment-subject-search"); await assignmentSubjectInput.fill("Acme"); await expect(page.getByTestId("release-assignment-subject-group-user")).toContainText("Users"); await expect(page.getByTestId("release-assignment-subject-option-user-42")).toContainText("Acme Dispatcher"); await expect(page.getByTestId("release-assignment-subject-group-customer")).toContainText("Customers"); await expect(page.getByTestId("release-assignment-subject-option-customer-424242")).toContainText("Acme Transport"); await assignmentSubjectInput.fill("Driver"); await expect(page.getByTestId("release-assignment-subject-group-subuser")).toContainText("Subusers"); await expect(page.getByTestId("release-assignment-subject-option-subuser-77")).toContainText("Driver Alex"); await assignmentSubjectInput.fill("Acme"); await page.getByTestId("release-assignment-subject-option-customer-424242").click(); await expect(assignmentSubjectInput).toHaveValue("Acme Transport - Customer #424242"); await page.getByTestId("release-assignment-form").getByRole("button", { name: "Assign" }).click(); expect(state.assignmentPayloads[state.assignmentPayloads.length - 1]).toMatchObject({ subject_type: "customer", subject_id: "424242", }); await expect(page.getByTestId("release-assignments-table")).toContainText("customer:424242"); await expect( page.getByTestId("release-assignments-table").locator('[data-testid^="release-assignment-actions-"]').first() ).toBeVisible(); await assignmentSubjectInput.fill("909"); await expect(page.getByTestId("release-assignment-subject-group-manual")).toContainText("Manual"); await expect(page.getByTestId("release-assignment-subject-manual-user-909")).toContainText("User #909"); await expect(page.getByTestId("release-assignment-subject-manual-subuser-909")).toContainText("Subuser #909"); await expect(page.getByTestId("release-assignment-subject-manual-customer-909")).toContainText("Customer #909"); await page.getByTestId("release-assignment-subject-manual-subuser-909").click(); await page.getByTestId("release-assignment-form").getByRole("button", { name: "Assign" }).click(); expect(state.assignmentPayloads[state.assignmentPayloads.length - 1]).toMatchObject({ subject_type: "subuser", subject_id: "909", }); await expect(page.getByTestId("release-assignments-table")).toContainText("subuser:909"); await selectReleaseTab(page, "Integrations"); await expandActiveReleaseCategory(page); await page.getByTestId("release-target-presets").getByText("API target").click(); await page.getByTestId("release-target-form").locator("select").first().selectOption("2"); await page.getByTestId("release-target-form").getByPlaceholder("branch").fill("canary"); await expect(page.getByTestId("release-target-form").getByPlaceholder("owner/repo")).toHaveValue( "truckwash/backend-php" ); await page.getByTestId("release-target-form").getByPlaceholder("Coolify service UUID").fill("API"); await page.getByText("API Canary").click(); await expect(page.getByTestId("release-target-form").getByPlaceholder("Coolify service UUID")).toHaveValue( "api-canary-service" ); const loadBalancerDomainInput = page.getByTestId("release-target-form").getByPlaceholder("Load balancer domain"); await loadBalancerDomainInput.click(); const domainOptions = page .locator(".autocomplete .dropdown-content") .filter({ hasText: "api-v2.truckwash.io" }) .last(); await expect(domainOptions.getByRole("button", { name: /api-v2\.truckwash\.io/ })).toBeVisible(); await expect(domainOptions.getByText("http://localhost:5173")).toHaveCount(0); await expect(domainOptions.getByText("http://api.truckwash.io:4433")).toHaveCount(0); await loadBalancerDomainInput.fill("api-v2.truckwash.io"); await page.getByTestId("release-target-public-url").fill("https://api-v2.truckwash.io/canary/api"); await page .locator('[data-testid="release-target-endpoint-mode"] select, select[data-testid="release-target-endpoint-mode"]') .selectOption("manual"); await page .locator( '[data-testid="release-target-manual-endpoint-host"] input, input[data-testid="release-target-manual-endpoint-host"]' ) .fill("manual-api.truckwash.io"); await page .locator( '[data-testid="release-target-manual-endpoint-port"] input, input[data-testid="release-target-manual-endpoint-port"]' ) .fill("8443"); await page .locator('[data-testid="release-target-app-port"] input, input[data-testid="release-target-app-port"]') .fill("8080"); await expect(page.getByTestId("release-target-form").getByLabel("Coolify SSL")).toBeChecked(); await page.getByTestId("release-target-form").locator("select").nth(3).selectOption("project-internal"); await page.getByTestId("release-target-form").getByRole("button", { name: "Test access" }).click(); await expect(page.getByTestId("release-target-form")).toContainText("accessible"); await page.getByTestId("release-target-form").getByRole("button", { name: "Save target" }).click(); expect(state.targetPayloads[state.targetPayloads.length - 1].deploy_context).toMatchObject({ endpoint_mode: "manual", manual_endpoint_host: "manual-api.truckwash.io", manual_endpoint_port: "8443", coolify_ports_exposes: "8080", }); await expect(page.getByTestId("release-targets-table")).toContainText("truckwash/backend-php#canary"); await expect( page.getByTestId("release-targets-table").locator('[data-testid^="release-target-actions-"]').first() ).toBeVisible(); await page.getByTestId("release-context-sync").click(); await expect(page.getByTestId("release-operation-modal")).toContainText("Channel sync completed"); await page.keyboard.press("Escape"); await selectReleaseTab(page, "Operations"); await expect(page.getByTestId("release-operation-grid-row").first()).toContainText("Sync Canary release channel"); await expect(page.getByTestId("release-operation-grid-row").first()).toContainText("canary"); }); test("isolated stack mode creates fresh Coolify app and data targets without attaching production data", async ({ page, }) => { const state = await boot(page); state.targets[0].deploy_context = { coolify_project_uuid: "project-internal", }; state.targets.push({ id: state.nextTargetId++, channel_id: 2, channel_slug: "canary", app: "api", repository: "truckwash/backend-php", branch: "canary", coolify_instance_id: 3, coolify_instance_label: "Production Coolify", coolify_service_uuid: "api-canary-service", health_url: "https://api-canary.example.test/ping", auto_deploy: true, deploy_context: { coolify_project_uuid: "project-internal", coolify_build_pack: "dockerfile", }, }); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=all&branch=canary", { waitUntil: "domcontentloaded", }); await selectReleaseTab(page, "Deployments"); await expandActiveReleaseCategory(page); const bundleFlow = page.getByTestId("release-bundle-flow"); const bundleChannel = bundleFlow.getByTestId("release-bundle-channel"); await bundleChannel.selectOption("2"); await expect(bundleChannel).toHaveValue("2"); await bundleFlow.getByTestId("release-dataset-mode-isolated_stack").click(); await bundleChannel.selectOption("2"); await expect(bundleChannel).toHaveValue("2"); await expect(bundleFlow).toContainText("without attaching production data"); await expect(bundleFlow.getByTestId("release-bundle-source-service-set")).toBeDisabled(); await bundleFlow.getByPlaceholder("canary fresh data").fill("Internal safe stack"); await expect(bundleFlow.getByTestId("release-apply-preview")).toContainText("Create isolated stack"); await expect(bundleFlow.getByTestId("release-apply-steps")).toContainText("new Coolify frontend/API targets"); await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText( "release-internal-safe-stack-frontend" ); await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText("release-internal-safe-stack-api"); await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText( "release-internal-safe-stack-database" ); await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText("api-v2.truckwash.io:443"); await expect(bundleFlow.getByTestId("release-service-action-table")).not.toContainText("endpoint not configured"); await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText( "created by Coolify; host/port assigned after deployment" ); await bundleFlow.getByRole("button", { name: "Next" }).click(); await expect(bundleFlow.getByPlaceholder("owner/frontend", { exact: true })).toHaveValue("truckwash/front-end-vue"); await expect(bundleFlow.getByPlaceholder("owner/backend-php", { exact: true })).toHaveValue("truckwash/backend-php"); await expect(bundleFlow.getByText("Container image")).toHaveCount(0); await bundleFlow.getByRole("button", { name: "Next" }).click(); await expect(page.getByTestId("release-bundle-review-stack")).toContainText("new isolated service"); await expect(page.getByTestId("release-bundle-review-stack")).toContainText("production data not attached"); await page.getByTestId("release-bundle-deploy-submit").click(); await expect(page.getByTestId("release-created-bundle")).toContainText("Bundle deployed"); const isolatedTargets = state.targets.filter((target) => target.deploy_context?.isolated_stack); expect(isolatedTargets).toHaveLength(2); expect(isolatedTargets.map((target) => target.app).sort()).toEqual(["api", "frontend"]); for (const target of isolatedTargets) { expect(target.coolify_service_uuid || "").toBe(""); expect(target.deploy_context.coolify_auto_create).toBe(true); expect(target.deploy_context.coolify_enable_ssl).toBe(false); expect(target.deploy_context.endpoint_mode).toBe("auto"); expect(target.deploy_context.production_data_attached).toBe(false); expect(target.deploy_context.coolify_github_app_uuid).toBe("github-app-copenhagentruckwash-github"); expect(target.deploy_context.coolify_build_pack).toBe(target.app === "api" ? "dockerfile" : "static"); if (target.app === "frontend") { expect(target.deploy_context.coolify_install_command).toBe("npm ci"); expect(target.deploy_context.coolify_build_command).toBe("npm run build"); expect(target.deploy_context.coolify_publish_directory).toBe("dist"); expect(target.deploy_context.coolify_is_static).toBe(true); expect(target.deploy_context.coolify_is_spa).toBe(true); expect(target.deploy_context.coolify_ports_exposes).toBe("80"); } expect(target.deploy_context.image || "").toBe(""); expect(target.deploy_context.coolify_environment_name).toBe("canary"); expect(target.deploy_context.coolify_service_name).toMatch(/^release-internal-safe-stack-(api|frontend)$/); } const isolatedSet = state.serviceSets.find((set) => set.mode === "isolated_stack"); expect(isolatedSet.source_service_set_id).toBeNull(); expect(Object.keys(isolatedSet.data_services).sort()).toEqual(["database", "minio", "redis"]); for (const service of Object.values(isolatedSet.data_services)) { expect(service.options.isolated_stack).toBe(true); expect(service.options.production_data_attached).toBe(false); expect(service.options.skip_replication_provisioning).toBe(true); expect(service.replication.source_host_id).toBeNull(); } const isolatedServiceSetCount = state.serviceSets.filter((set) => set.mode === "isolated_stack").length; const isolatedTargetCount = state.targets.filter((target) => target.deploy_context?.isolated_stack).length; await page.getByTestId("release-bundle-deploy-submit").click(); await expect(page.getByTestId("release-created-bundle")).toContainText("Bundle deployed"); expect(state.serviceSets.filter((set) => set.mode === "isolated_stack")).toHaveLength(isolatedServiceSetCount); expect(state.targets.filter((target) => target.deploy_context?.isolated_stack)).toHaveLength(isolatedTargetCount); page.once("dialog", (dialog) => { expect(dialog.message()).toContain("soft-deleted"); expect(dialog.message()).toContain("Coolify provider resources are not deleted"); dialog.accept(); }); await page.getByTestId(`release-service-set-delete-${isolatedSet.id}`).click(); await expect(page.getByTestId(`release-service-set-delete-${isolatedSet.id}`)).toHaveCount(0); expect(state.serviceSets.find((set) => Number(set.id) === Number(isolatedSet.id))).toBeUndefined(); }); test("existing isolated stacks can add missing data services safely", async ({ page }) => { const state = createReleaseState(); state.serviceSets.unshift({ id: state.nextServiceSetId++, channel_id: 2, channel_slug: "canary", name: "Internal isolated stack", slug: "internal-isolated-stack", mode: "isolated_stack", status: "needs_isolated_targets", targets: { frontend: { id: 101, channel_id: 2, channel_slug: "canary", app: "frontend", repository: "truckwash/front-end-vue", branch: DEFAULT_RELEASE_BRANCH, deploy_context: { isolated_stack: true, coolify_auto_create: true, production_data_attached: false }, }, api: { id: 102, channel_id: 2, channel_slug: "canary", app: "api", repository: "truckwash/backend-php", branch: "canary", deploy_context: { isolated_stack: true, coolify_auto_create: true, production_data_attached: false }, }, }, data_services: { database: null, redis: null, minio: null }, attached_bundles: [], }); await boot(page, state); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=all&branch=canary", { waitUntil: "domcontentloaded", }); await selectReleaseTab(page, "Deployments"); await expandActiveReleaseCategory(page); await expect(page.getByTestId("release-service-set-cards")).toContainText("Internal isolated stack"); await page.getByTestId("release-service-set-complete-isolated-data-2").click(); await expect(page.getByTestId("release-service-set-complete-isolated-data-2")).toHaveCount(0); const serviceSet = state.serviceSets.find((set) => set.slug === "internal-isolated-stack"); expect(serviceSet.data_services.database.options.production_data_attached).toBe(false); expect(serviceSet.data_services.redis.options.skip_replication_provisioning).toBe(true); expect(serviceSet.data_services.minio.options.isolated_stack).toBe(true); });