Files
pleno-vue/tests/e2e/release-manager.spec.js
T

2836 lines
112 KiB
JavaScript

import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
import { isDesktopProject } from "./support/projects";
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(), options = {}) {
await page.addInitScript(() => {
window.localStorage.setItem("locale", "en");
});
await mockApi(page, {
authenticated: true,
permissions: options.permissions || permissions,
sessionData: {
runtime_config: {
release: {
source: "local",
requested_source: "local",
trace_id: "trace-e2e",
channel: { slug: "stable", name: "Stable", default_channel: true },
api_base_url: "/api",
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();
state.assignmentSubjectRequests = state.assignmentSubjectRequests || [];
state.assignmentSubjectRequests.push(query);
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.",
context: {
source: "manual",
app: "frontend",
commit_sha: "c0ffee",
},
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 dataSource = state.serviceSets.find(
(entry) => Number(entry.id) === Number(payload.data_source_service_set_id)
);
const mode = payload.mode || "attach_existing";
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,
data_policy: payload.metadata?.data_policy || (mode === "attach_existing" ? "production_shared" : mode),
data_service_mode:
payload.metadata?.data_service_mode || (mode === "attach_existing" ? "production_shared" : mode),
source_service_set_id: payload.source_service_set_id || null,
data_source_service_set_id: payload.data_source_service_set_id || null,
data_source_channel_slug: payload.metadata?.data_source_channel_slug || dataSource?.channel_slug || null,
status:
mode === "clone_existing"
? "provisioning"
: mode === "fresh_empty"
? "isolated_empty"
: 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(mode)
? { database: null, redis: null, minio: null }
: dataSource?.data_services ||
source?.data_services || {
database: null,
redis: null,
minio: null,
},
metadata: payload.metadata || {},
attached_bundles: [],
};
if (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 }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Release overview dashboard detail assertions are desktop-focused.");
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();
state.operations.unshift({
id: 77,
operation_type: "channel_sync",
channel_id: 1,
channel_slug: "stable",
app: "frontend",
status: "passed",
title: "Automatic Stable frontend update",
summary: "FRONTEND container was deployed and promoted at e1a8af5a7271.",
step_count: 3,
passed_step_count: 3,
failed_step_count: 0,
warning_step_count: 0,
context: {
source: "release_gate",
app: "frontend",
commit_sha: "e1a8af5a727176f0343eb5fe3620ea890e7c6135",
auto_sync_event: {
status: "promoted",
app: "frontend",
commit_sha: "e1a8af5a727176f0343eb5fe3620ea890e7c6135",
source: "release_gate",
},
},
steps: [
{
id: 1,
step_key: "frontend_readiness",
label: "FRONTEND container readiness",
status: "passed",
message: "FRONTEND container readiness matched commit e1a8af5a7271.",
},
],
});
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");
await expect(
page.getByTestId("release-operation-grid-row").filter({ hasText: "Automatic Stable frontend update" })
).toContainText("release_gate / frontend / e1a8af5a7271");
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");
await expect(modal.getByTestId("release-operation-meta")).toContainText("manual");
await expect(modal.getByTestId("release-operation-meta")).toContainText("frontend");
await expect(modal.getByTestId("release-operation-meta")).toContainText("c0ffee");
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("view-only release managers cannot search assignment subjects", async ({ page }) => {
const state = await boot(page, createReleaseState(), {
permissions: ["superuser_release_manager_view"],
});
await page.goto("/superuser/configuration/releases/assignments", { waitUntil: "domcontentloaded" });
await expectReleaseManagerReady(page);
await expect(page.getByTestId("release-section-assignments")).toBeVisible();
await expect(page.getByTestId("release-assignment-form")).toHaveCount(0);
await expect(page.getByTestId("release-assignment-channel-suggestions")).toHaveCount(0);
await expect(page.getByTestId("release-assignment-subject-search")).toHaveCount(0);
expect(state.assignmentSubjectRequests || []).toEqual([]);
});
test("view-only release managers see redacted deployment failure diagnostics", async ({ page }) => {
const state = createReleaseState();
state.deployments[0].error_message =
"Clone failed for https://deploy:ghp_SECRET_TOKEN_123456789@github.internal/acme/api.git DB_PASSWORD=s3cr3t internal-host=10.0.4.12";
state.deployments[0].failure_summary.root_cause = state.deployments[0].error_message;
state.deployments[0].failure_summary.next_action =
"Rotate ghp_SECRET_TOKEN_123456789 and inspect /var/lib/coolify/apps/api/.env before retrying.";
await boot(page, state, {
permissions: ["superuser_release_manager_view"],
});
await page.goto("/superuser/configuration/releases/overview?channel=canary&app=api&branch=master", {
waitUntil: "domcontentloaded",
});
await expectReleaseManagerReady(page);
await expect(page.getByTestId("release-manager-page")).not.toContainText("ghp_SECRET_TOKEN_123456789");
await expect(page.getByTestId("release-manager-page")).not.toContainText("DB_PASSWORD=s3cr3t");
await expect(page.getByTestId("release-manager-page")).not.toContainText("10.0.4.12");
await expect(page.getByTestId("release-manager-page")).not.toContainText("github.internal");
await selectReleaseTab(page, "Deployments");
await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted]");
await expect(page.getByTestId("release-deployment-failure-2")).toContainText("DB_PASSWORD=[redacted]");
await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-private-ip]");
await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-internal-host]");
await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-env-path]");
});
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({ force: true });
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();
await expect
.poll(() => state.targetPayloads.at(-1)?.deploy_context, { timeout: 10_000 })
.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("beta bundle flow locks to production data sharing @beta-production-data", async ({ page }) => {
const state = createReleaseState();
state.nextTargetId = 40;
state.nextServiceSetId = 60;
const stableFrontend = {
id: 20,
channel_id: 1,
channel_slug: "stable",
app: "frontend",
repository: "truckwash/front-end-vue",
branch: DEFAULT_RELEASE_BRANCH,
coolify_instance_id: 3,
coolify_instance_label: "Production Coolify",
coolify_service_uuid: "frontend-stable-service",
health_url: "https://app.example.test/health",
auto_deploy: true,
deploy_context: {
coolify_public_url: "https://app.example.test",
coolify_project_uuid: "project-main",
coolify_environment_name: "production",
},
};
const stableApi = {
id: 21,
channel_id: 1,
channel_slug: "stable",
app: "api",
repository: "truckwash/backend-php",
branch: DEFAULT_RELEASE_BRANCH,
coolify_instance_id: 3,
coolify_instance_label: "Production Coolify",
coolify_service_uuid: "api-stable-service",
health_url: "https://api.example.test/ping",
auto_deploy: true,
deploy_context: {
coolify_public_url: "https://api.example.test",
coolify_project_uuid: "project-main",
coolify_environment_name: "production",
},
};
const betaFrontend = {
id: 30,
channel_id: 3,
channel_slug: "beta",
app: "frontend",
repository: "truckwash/front-end-vue",
branch: "release/beta",
coolify_instance_id: 3,
coolify_instance_label: "Production Coolify",
coolify_service_uuid: "frontend-beta-service",
health_url: "https://beta.example.test/health",
auto_deploy: true,
deploy_context: {
coolify_public_url: "https://beta.example.test",
coolify_project_uuid: "project-main",
coolify_environment_name: "production",
},
};
const betaApi = {
id: 31,
channel_id: 3,
channel_slug: "beta",
app: "api",
repository: "truckwash/backend-php",
branch: "release/beta",
coolify_instance_id: 3,
coolify_instance_label: "Production Coolify",
coolify_service_uuid: "api-beta-service",
health_url: "https://api-beta.example.test/ping",
auto_deploy: true,
deploy_context: {
coolify_public_url: "https://api-beta.example.test",
coolify_project_uuid: "project-main",
coolify_environment_name: "production",
},
};
const stableSet = {
id: 50,
channel_id: 1,
channel_slug: "stable",
name: "Stable production stack",
slug: "stable-production-stack",
mode: "attach_existing",
data_policy: "production_shared",
data_service_mode: "production_shared",
status: "ready",
active: true,
targets: {
frontend: stableFrontend,
api: stableApi,
},
data_services: {
database: {
id: 70,
kind: "database",
label: "Production MariaDB",
resource_name: "mariadb-production",
deployment_status: "running",
replication: { label: "prod-mariadb", host: "db.prod.internal", port: 3306, status: "ok" },
},
redis: {
id: 71,
kind: "redis",
label: "Production Redis",
resource_name: "redis-production",
deployment_status: "running",
replication: { label: "prod-redis", host: "redis.prod.internal", port: 6379, status: "ok" },
},
minio: {
id: 72,
kind: "minio",
label: "Production MinIO",
resource_name: "minio-production",
deployment_status: "running",
replication: { label: "prod-minio", host: "minio.prod.internal", port: 9000, status: "ok" },
},
},
attached_bundles: [],
};
state.targets.push(stableFrontend, stableApi, betaFrontend, betaApi);
state.serviceSets.unshift(stableSet);
await boot(page, state);
await page.goto("/superuser/configuration/releases/overview?channel=beta&app=all&branch=release/beta", {
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("3");
await expect(bundleChannel).toHaveValue("3");
await expect(bundleFlow.getByTestId("release-dataset-mode-attach_existing")).not.toBeDisabled();
await expect(bundleFlow.getByTestId("release-dataset-mode-clone_existing")).toBeDisabled();
await expect(bundleFlow.getByTestId("release-dataset-mode-fresh_empty")).toBeDisabled();
await expect(bundleFlow.getByTestId("release-dataset-mode-isolated_stack")).toBeDisabled();
await expect(bundleFlow).toContainText("Beta is locked to production-shared data");
await expect(bundleFlow).toContainText("Beta uses production data");
await expect(bundleFlow.getByTestId("release-bundle-source-service-set")).toHaveValue("50");
await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText("production_shared");
await expect(bundleFlow.getByTestId("release-service-action-table")).toContainText("api-beta-service");
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.getByPlaceholder("master").nth(0)).toHaveValue("release/beta");
await expect(bundleFlow.getByPlaceholder("master").nth(1)).toHaveValue("release/beta");
await bundleFlow.getByRole("button", { name: "Next" }).click();
await expect(page.getByTestId("release-bundle-review-stack")).toContainText("production_shared");
await page.getByTestId("release-bundle-deploy-submit").click();
await expect(page.getByTestId("release-created-bundle")).toContainText("Bundle deployed");
const betaSet = state.serviceSets.find((set) => set.name === "beta-production-data");
expect(betaSet).toBeTruthy();
expect(betaSet.channel_slug).toBe("beta");
expect(betaSet.source_service_set_id).toBeNull();
expect(betaSet.data_source_service_set_id).toBe(stableSet.id);
expect(betaSet.data_policy).toBe("production_shared");
expect(betaSet.targets.frontend.id).toBe(betaFrontend.id);
expect(betaSet.targets.api.id).toBe(betaApi.id);
expect(betaSet.data_services.database.id).toBe(stableSet.data_services.database.id);
expect(state.bundlePayloads[0].service_set_id).toBe(betaSet.id);
expect(state.bundlePayloads[0].service_set_id).not.toBe(stableSet.id);
});
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 bundleCount = state.bundles.length;
const bundlePayloadCount = state.bundlePayloads.length;
await page.getByTestId("release-bundle-deploy-submit").click();
await expect(page.getByTestId("release-created-bundle")).toContainText("Bundle deployed");
await expect.poll(() => state.bundles.length).toBe(bundleCount + 1);
expect(state.bundlePayloads).toHaveLength(bundlePayloadCount + 1);
const repeatedBundlePayload = state.bundlePayloads[state.bundlePayloads.length - 1];
const repeatedBundleServiceSet = state.serviceSets.find(
(set) => Number(set.id) === Number(repeatedBundlePayload.service_set_id)
);
expect(repeatedBundleServiceSet?.mode).toBe("isolated_stack");
expect(repeatedBundleServiceSet?.source_service_set_id).toBeNull();
for (const service of Object.values(repeatedBundleServiceSet?.data_services || {})) {
expect(service.options.isolated_stack).toBe(true);
expect(service.options.production_data_attached).toBe(false);
}
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("isolated stack mode creates a fresh stack even when an active stack has the requested name", 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",
},
});
const activeStack = {
id: state.nextServiceSetId++,
channel_id: 2,
channel_slug: "canary",
name: "Internal safe stack",
slug: "internal-safe-stack",
mode: "isolated_stack",
status: "isolated_stack",
active: true,
targets: {
frontend: {
id: 201,
channel_id: 2,
channel_slug: "canary",
app: "frontend",
repository: "truckwash/front-end-vue",
branch: DEFAULT_RELEASE_BRANCH,
deploy_context: { isolated_stack: true, production_data_attached: false },
},
api: {
id: 202,
channel_id: 2,
channel_slug: "canary",
app: "api",
repository: "truckwash/backend-php",
branch: "canary",
deploy_context: { isolated_stack: true, production_data_attached: false },
},
},
data_services: { database: null, redis: null, minio: null },
attached_bundles: [],
};
completeIsolatedDataServices(state, activeStack);
state.serviceSets.unshift(activeStack);
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");
await bundleFlow.getByTestId("release-bundle-channel").selectOption("2");
await bundleFlow.getByTestId("release-dataset-mode-isolated_stack").click();
await bundleFlow.getByPlaceholder("canary fresh data").fill("Internal safe stack");
await bundleFlow.getByRole("button", { name: "Next" }).click();
await bundleFlow.getByRole("button", { name: "Next" }).click();
await page.getByTestId("release-bundle-deploy-submit").click();
await expect(page.getByTestId("release-created-bundle")).toContainText("Bundle deployed");
const deployedServiceSetId = state.bundlePayloads[0].service_set_id;
expect(deployedServiceSetId).not.toBe(activeStack.id);
expect(state.serviceSets.find((set) => Number(set.id) === Number(activeStack.id))?.active).toBe(true);
expect(
state.serviceSets.filter((set) => set.mode === "isolated_stack" && set.slug === "internal-safe-stack")
).toHaveLength(2);
expect(
state.targets.filter((target) => target.deploy_context?.isolated_stack && target.id !== 201 && target.id !== 202)
).toHaveLength(2);
});
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);
});