@supacloud/cli 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/index.js +789 -87
- package/dist/shared/tools/app-tools.d.ts +14 -1
- package/dist/shared/tools/application-activation-http-status.test.d.ts +1 -0
- package/dist/shared/tools/application-preview-tools.d.ts +74 -0
- package/dist/shared/tools/application-preview-tools.test.d.ts +1 -0
- package/dist/shared/tools/application-schemas.d.ts +16 -0
- package/dist/shared/tools/application-tools.d.ts +8 -1
- package/dist/shared/tools/branch-promotion-http-status.test.d.ts +1 -0
- package/dist/shared/tools/branch-tools.d.ts +2 -1
- package/dist/shared/transports/http.d.ts +4 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -179093,8 +179093,19 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
|
|
|
179093
179093
|
var ACTION_POLICY = {
|
|
179094
179094
|
deploy: { write: ["deploy"] },
|
|
179095
179095
|
applications: {
|
|
179096
|
-
read: ["list_releases", "get_release", "get_runtime", "get_deployment_evidence", "get_configuration", "logs"],
|
|
179097
|
-
write: [
|
|
179096
|
+
read: ["list_releases", "get_release", "get_runtime", "get_deployment_evidence", "get_configuration", "get_rollback_snapshot", "logs", "get_preview_plan"],
|
|
179097
|
+
write: [
|
|
179098
|
+
"upload_release",
|
|
179099
|
+
"put_configuration",
|
|
179100
|
+
"activate_release",
|
|
179101
|
+
"rollback_release",
|
|
179102
|
+
"reconcile_activation",
|
|
179103
|
+
"retire_activation",
|
|
179104
|
+
"create_preview",
|
|
179105
|
+
"list_previews",
|
|
179106
|
+
"get_preview",
|
|
179107
|
+
"cleanup_preview"
|
|
179108
|
+
]
|
|
179098
179109
|
},
|
|
179099
179110
|
project: {
|
|
179100
179111
|
read: ["get", "endpoints", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
|
|
@@ -179189,8 +179200,8 @@ var ACTION_POLICY = {
|
|
|
179189
179200
|
ai: { local: ["show_skill", "install_skill"] },
|
|
179190
179201
|
app: {
|
|
179191
179202
|
local: ["init", "generate", "dev", "watch", "verify-plan", "compile", "check", "graph", "explain", "export-tools", "context", "doctor", "fix", "plan", "build"],
|
|
179192
|
-
read: ["status", "logs"],
|
|
179193
|
-
write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire"]
|
|
179203
|
+
read: ["status", "logs", "rollback-plan", "preview-plan"],
|
|
179204
|
+
write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire", "preview", "previews", "preview-status", "preview-cleanup"]
|
|
179194
179205
|
},
|
|
179195
179206
|
db: {
|
|
179196
179207
|
local: ["context", "lint", "explain", "diff", "role_sql"],
|
|
@@ -179528,19 +179539,34 @@ class HttpTransport {
|
|
|
179528
179539
|
}
|
|
179529
179540
|
}
|
|
179530
179541
|
async get(path, options = {}) {
|
|
179542
|
+
const timeoutMs = validatedPostTimeout(options);
|
|
179531
179543
|
const maxResponseBytes = validatedGetResponseLimit(options);
|
|
179532
179544
|
const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
|
|
179533
179545
|
const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
|
|
179546
|
+
const totalTimeoutMs = validatedResponseTimeout(options.totalTimeoutMs);
|
|
179534
179547
|
if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
|
|
179535
179548
|
throw new RangeError("HTTP response limit options are mutually exclusive");
|
|
179536
179549
|
}
|
|
179550
|
+
if (totalTimeoutMs !== undefined && (options.retry !== false || maxJsonBytes === undefined)) {
|
|
179551
|
+
throw new RangeError("HTTP total timeout requires bounded JSON and retry: false");
|
|
179552
|
+
}
|
|
179553
|
+
const deadline = totalTimeoutMs === undefined ? undefined : performance.now() + totalTimeoutMs;
|
|
179537
179554
|
try {
|
|
179538
|
-
const
|
|
179555
|
+
const request = options.retry === false ? fetchWithTimeout : fetchWithRetry;
|
|
179556
|
+
const res = await request(`${this.baseUrl}${path}`, {
|
|
179539
179557
|
method: "GET",
|
|
179540
179558
|
headers: this.headers()
|
|
179541
|
-
},
|
|
179559
|
+
}, totalTimeoutMs === undefined ? timeoutMs : Math.min(timeoutMs, totalTimeoutMs), this.insecureTls);
|
|
179542
179560
|
if (maxJsonBytes !== undefined) {
|
|
179543
|
-
const
|
|
179561
|
+
const remaining = deadline === undefined ? undefined : Math.floor(deadline - performance.now());
|
|
179562
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
179563
|
+
res.body?.cancel().catch(() => {
|
|
179564
|
+
return;
|
|
179565
|
+
});
|
|
179566
|
+
return responseReadFailure(res.status);
|
|
179567
|
+
}
|
|
179568
|
+
const bodyTimeout = remaining === undefined ? responseTimeoutMs : Math.min(responseTimeoutMs ?? remaining, remaining);
|
|
179569
|
+
const data = await boundedResponseJson(res, maxJsonBytes, bodyTimeout);
|
|
179544
179570
|
return data === null ? responseReadFailure(res.status) : { ok: res.ok, status: res.status, data };
|
|
179545
179571
|
}
|
|
179546
179572
|
const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
|
|
@@ -180255,6 +180281,19 @@ var ApplicationActivationRetirementResultSchema = _Object_({
|
|
|
180255
180281
|
activation_id: ApplicationActivationIdSchema,
|
|
180256
180282
|
retired_at: String2()
|
|
180257
180283
|
}, { additionalProperties: false });
|
|
180284
|
+
var RollbackActivationSchema = _Object_({
|
|
180285
|
+
release_id: ApplicationReleaseIdSchema,
|
|
180286
|
+
configuration_id: ApplicationConfigurationIdSchema,
|
|
180287
|
+
activation_id: ApplicationActivationIdSchema
|
|
180288
|
+
}, { additionalProperties: false });
|
|
180289
|
+
var ApplicationRollbackSnapshotSchema = _Object_({
|
|
180290
|
+
schema: Literal("supacloud.application-rollback-snapshot.v1"),
|
|
180291
|
+
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
180292
|
+
application_id: ApplicationIdSchema,
|
|
180293
|
+
environment_id: ApplicationIdSchema,
|
|
180294
|
+
active: Union([RollbackActivationSchema, Null()]),
|
|
180295
|
+
previous: Union([RollbackActivationSchema, Null()])
|
|
180296
|
+
}, { additionalProperties: false });
|
|
180258
180297
|
// node_modules/@supacloud/delivery/src/deployment-evidence.ts
|
|
180259
180298
|
var closed3 = { additionalProperties: false };
|
|
180260
180299
|
var sha256 = String2({ pattern: "^[a-f0-9]{64}$" });
|
|
@@ -187227,6 +187266,333 @@ var ApplicationActivationIdSchema2 = ApplicationActivationIdSchema;
|
|
|
187227
187266
|
var ApplicationActivationWriteSchema2 = ApplicationActivationWriteSchema;
|
|
187228
187267
|
var ApplicationActivationResultSchema2 = ApplicationActivationResultSchema;
|
|
187229
187268
|
var ApplicationActivationRetirementResultSchema2 = ApplicationActivationRetirementResultSchema;
|
|
187269
|
+
var ApplicationRollbackSnapshotSchema2 = ApplicationRollbackSnapshotSchema;
|
|
187270
|
+
|
|
187271
|
+
// src/shared/tools/application-preview-tools.ts
|
|
187272
|
+
var APPLICATION_PREVIEW_ACTIONS = [
|
|
187273
|
+
"get_preview_plan",
|
|
187274
|
+
"create_preview",
|
|
187275
|
+
"list_previews",
|
|
187276
|
+
"get_preview",
|
|
187277
|
+
"cleanup_preview"
|
|
187278
|
+
];
|
|
187279
|
+
var previewIdSchema = String2({ pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" });
|
|
187280
|
+
var branchRefSchema = String2({ pattern: "^[a-z0-9-]{1,20}$" });
|
|
187281
|
+
var ttlSecondsSchema = Integer({ minimum: 300, maximum: 604800 });
|
|
187282
|
+
var waitSecondsSchema = Integer({ minimum: 1, maximum: 3600 });
|
|
187283
|
+
var timestampSchema = String2({
|
|
187284
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
187285
|
+
maxLength: 24
|
|
187286
|
+
});
|
|
187287
|
+
var APPLICATION_PREVIEW_FIELDS = {
|
|
187288
|
+
preview_id: optional(previewIdSchema, "[get_preview/cleanup_preview] Preview receipt ID"),
|
|
187289
|
+
branch_ref: optional(branchRefSchema, "[get_preview_plan] Proposed branch ref; create assigns its own"),
|
|
187290
|
+
branch_name: optional(String2({ minLength: 1, maxLength: 80 }), "[create_preview] Branch display name"),
|
|
187291
|
+
data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[get_preview_plan/create_preview] Default schema_only; full_clone copies rows"),
|
|
187292
|
+
ttl_seconds: optional(ttlSecondsSchema, "[get_preview_plan/create_preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL"),
|
|
187293
|
+
wait: optional(Boolean2(), "[create_preview/get_preview] Wait for verified readiness of this preview only"),
|
|
187294
|
+
timeout_seconds: optional(waitSecondsSchema, "[create_preview/get_preview] Observation budget with wait, from 1 to 3600 seconds (default 300)")
|
|
187295
|
+
};
|
|
187296
|
+
var strict = { additionalProperties: false };
|
|
187297
|
+
var phase = stringEnum(["pending", "ready", "failed", "cleaned"]);
|
|
187298
|
+
var check5 = String2({ pattern: "^[a-z][a-z0-9_]{0,63}$" });
|
|
187299
|
+
var checks = _Array_(check5, { maxItems: 64, uniqueItems: true });
|
|
187300
|
+
var revision = Union([ApplicationConfigurationIdSchema2, Null()]);
|
|
187301
|
+
var receiptSchema = _Object_({
|
|
187302
|
+
schema: Literal("supacloud.application-preview.v1"),
|
|
187303
|
+
preview_id: previewIdSchema,
|
|
187304
|
+
project_ref: branchRefSchema,
|
|
187305
|
+
application_id: ApplicationIdSchema2,
|
|
187306
|
+
environment_id: ApplicationIdSchema2,
|
|
187307
|
+
release_id: ApplicationReleaseIdSchema2,
|
|
187308
|
+
expires_at: Union([timestampSchema, Null()]),
|
|
187309
|
+
status: stringEnum(["planned", "provisioning", "ready", "failed", "cleaned"]),
|
|
187310
|
+
resources: _Object_({
|
|
187311
|
+
build_artifact: _Object_({ status: phase, release_id: ApplicationReleaseIdSchema2 }, strict),
|
|
187312
|
+
database_branch: _Object_({ status: phase, branch_ref: branchRefSchema, data_mode: stringEnum(["schema_only", "full_clone"]) }, strict),
|
|
187313
|
+
queue_namespace: _Object_({ status: phase, namespace: String2({ minLength: 1, maxLength: 128 }) }, strict),
|
|
187314
|
+
storage_namespace: _Object_({ status: phase, namespace: branchRefSchema }, strict),
|
|
187315
|
+
test_secret: _Object_({ status: phase, name: String2({ pattern: "^[A-Z0-9_-]{1,128}$" }), value_issued: Literal(false) }, strict),
|
|
187316
|
+
configuration_revision: _Object_({ status: phase, configuration_id: revision }, strict),
|
|
187317
|
+
application_activation: _Object_({ status: phase, activation_id: Union([previewIdSchema, Null()]) }, strict),
|
|
187318
|
+
smoke_test: _Object_({ status: phase, checks, passed: checks, failed: checks }, strict)
|
|
187319
|
+
}, strict),
|
|
187320
|
+
cleanup: _Object_({
|
|
187321
|
+
required: Boolean2(),
|
|
187322
|
+
completed: Boolean2(),
|
|
187323
|
+
error: Union([String2({ pattern: "^[A-Z0-9_]{1,128}$" }), Null()])
|
|
187324
|
+
}, strict),
|
|
187325
|
+
branch_name: Optional(String2({ maxLength: 80 })),
|
|
187326
|
+
queue_name: Optional(String2({ maxLength: 128 })),
|
|
187327
|
+
test_secret_name: Optional(String2({ pattern: "^[A-Z0-9_]{1,128}$" })),
|
|
187328
|
+
source_configuration_id: Optional(revision),
|
|
187329
|
+
created_at: Optional(timestampSchema),
|
|
187330
|
+
updated_at: Optional(timestampSchema)
|
|
187331
|
+
}, strict);
|
|
187332
|
+
function required5(args, field) {
|
|
187333
|
+
const value = args[field];
|
|
187334
|
+
if (typeof value !== "string" || !value.trim())
|
|
187335
|
+
throw new Error(`'${field}' is required`);
|
|
187336
|
+
return value.trim();
|
|
187337
|
+
}
|
|
187338
|
+
var mandatoryChecks = [
|
|
187339
|
+
"release_artifact",
|
|
187340
|
+
"database_branch",
|
|
187341
|
+
"queue_namespace",
|
|
187342
|
+
"storage_namespace",
|
|
187343
|
+
"test_secret",
|
|
187344
|
+
"configuration_revision",
|
|
187345
|
+
"application_activation",
|
|
187346
|
+
"application_readiness",
|
|
187347
|
+
"tenant_runtime"
|
|
187348
|
+
];
|
|
187349
|
+
var actionFields = {
|
|
187350
|
+
get_preview_plan: ["release_id", "branch_ref", "data_mode", "configuration_id", "ttl_seconds"],
|
|
187351
|
+
create_preview: ["release_id", "configuration_id", "data_mode", "branch_name", "ttl_seconds", "wait", "timeout_seconds"],
|
|
187352
|
+
list_previews: [],
|
|
187353
|
+
get_preview: ["preview_id", "wait", "timeout_seconds"],
|
|
187354
|
+
cleanup_preview: ["preview_id"]
|
|
187355
|
+
};
|
|
187356
|
+
function timestamp(value) {
|
|
187357
|
+
return Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
187358
|
+
}
|
|
187359
|
+
function receipt(value, identity) {
|
|
187360
|
+
if (!Check2(receiptSchema, value) || value.project_ref !== identity.ref || value.application_id !== identity.id || value.environment_id !== identity.environment || identity.previewId && value.preview_id !== identity.previewId || value.resources.build_artifact.release_id !== value.release_id || value.resources.storage_namespace.namespace !== value.resources.database_branch.branch_ref) {
|
|
187361
|
+
throw new Error("Invalid preview receipt");
|
|
187362
|
+
}
|
|
187363
|
+
if (value.expires_at !== null && !timestamp(value.expires_at) || value.created_at !== undefined && !timestamp(value.created_at) || value.updated_at !== undefined && !timestamp(value.updated_at) || value.expires_at !== null && value.created_at !== undefined && !Check2(ttlSecondsSchema, (Date.parse(value.expires_at) - Date.parse(value.created_at)) / 1000)) {
|
|
187364
|
+
throw new Error("Invalid preview expiry");
|
|
187365
|
+
}
|
|
187366
|
+
if (value.status === "ready") {
|
|
187367
|
+
if (value.resources.smoke_test.status !== "ready" || value.resources.smoke_test.failed.length > 0 || mandatoryChecks.some((check) => !value.resources.smoke_test.checks.includes(check) || !value.resources.smoke_test.passed.includes(check)) || value.resources.smoke_test.passed.some((check) => !value.resources.smoke_test.checks.includes(check)) || Object.values(value.resources).some((resource) => resource.status !== "ready") || !value.resources.configuration_revision.configuration_id || !value.resources.application_activation.activation_id) {
|
|
187368
|
+
throw new Error("Incomplete preview readiness");
|
|
187369
|
+
}
|
|
187370
|
+
}
|
|
187371
|
+
if (value.status === "cleaned" && (!value.cleanup.completed || value.cleanup.required || [
|
|
187372
|
+
value.resources.database_branch,
|
|
187373
|
+
value.resources.queue_namespace,
|
|
187374
|
+
value.resources.storage_namespace,
|
|
187375
|
+
value.resources.test_secret
|
|
187376
|
+
].some((resource) => resource.status !== "cleaned"))) {
|
|
187377
|
+
throw new Error("Incomplete preview cleanup");
|
|
187378
|
+
}
|
|
187379
|
+
const {
|
|
187380
|
+
branch_name: _branchName,
|
|
187381
|
+
queue_name: _queueName,
|
|
187382
|
+
test_secret_name: _secretName,
|
|
187383
|
+
source_configuration_id: _sourceConfiguration,
|
|
187384
|
+
created_at: _createdAt,
|
|
187385
|
+
updated_at: _updatedAt,
|
|
187386
|
+
...publicReceipt
|
|
187387
|
+
} = value;
|
|
187388
|
+
return publicReceipt;
|
|
187389
|
+
}
|
|
187390
|
+
async function sourceManifest(http, path, ref, id, releaseId) {
|
|
187391
|
+
const result = await http.get(`${path}/releases/${releaseId}`, { maxJsonBytes: 196608, responseTimeoutMs: 30000 });
|
|
187392
|
+
if (!result.ok)
|
|
187393
|
+
return { result };
|
|
187394
|
+
try {
|
|
187395
|
+
const data = result.data;
|
|
187396
|
+
if (!data || typeof data !== "object" || !("project_ref" in data) || data.project_ref !== ref || !("application_id" in data) || data.application_id !== id || !("release" in data))
|
|
187397
|
+
throw new Error;
|
|
187398
|
+
const release = parseApplicationReleaseRecord(data.release);
|
|
187399
|
+
if (release.project_ref !== ref || release.application_id !== id || release.release_id !== releaseId)
|
|
187400
|
+
throw new Error;
|
|
187401
|
+
return { result, manifest: release.manifest_sha256 };
|
|
187402
|
+
} catch {
|
|
187403
|
+
return { result };
|
|
187404
|
+
}
|
|
187405
|
+
}
|
|
187406
|
+
async function waitForPreview(http, path, operation, identity, initial, timeoutSeconds, startedAt = performance.now()) {
|
|
187407
|
+
const deadline = startedAt + timeoutSeconds * 1000;
|
|
187408
|
+
let current = initial;
|
|
187409
|
+
const state = () => ({
|
|
187410
|
+
...identity,
|
|
187411
|
+
preview_id: initial.preview_id,
|
|
187412
|
+
preview: current,
|
|
187413
|
+
reconciliation: {
|
|
187414
|
+
action: "get_preview",
|
|
187415
|
+
ref: identity.project_ref,
|
|
187416
|
+
id: identity.application_id,
|
|
187417
|
+
environment_id: identity.environment_id,
|
|
187418
|
+
preview_id: initial.preview_id,
|
|
187419
|
+
wait: true,
|
|
187420
|
+
timeout_seconds: timeoutSeconds
|
|
187421
|
+
}
|
|
187422
|
+
});
|
|
187423
|
+
while (true) {
|
|
187424
|
+
if (current.status === "ready")
|
|
187425
|
+
return releaseControlSuccess(operation, {
|
|
187426
|
+
...identity,
|
|
187427
|
+
preview: current,
|
|
187428
|
+
waiting: { status: "ready", timeout_seconds: timeoutSeconds }
|
|
187429
|
+
});
|
|
187430
|
+
if (current.status !== "provisioning")
|
|
187431
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
|
|
187432
|
+
...state(),
|
|
187433
|
+
waiting: { status: "stopped", timeout_seconds: timeoutSeconds }
|
|
187434
|
+
});
|
|
187435
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
187436
|
+
if (remaining < 2)
|
|
187437
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
|
|
187438
|
+
...state(),
|
|
187439
|
+
waiting: { status: "timed_out", timeout_seconds: timeoutSeconds }
|
|
187440
|
+
});
|
|
187441
|
+
const requestBudget = Math.min(120000, remaining);
|
|
187442
|
+
const result = await http.get(`${path}/previews/${initial.preview_id}`, {
|
|
187443
|
+
retry: false,
|
|
187444
|
+
timeoutMs: requestBudget,
|
|
187445
|
+
responseTimeoutMs: requestBudget,
|
|
187446
|
+
totalTimeoutMs: requestBudget,
|
|
187447
|
+
maxJsonBytes: 1048576
|
|
187448
|
+
});
|
|
187449
|
+
if (!result.ok)
|
|
187450
|
+
return releaseControlFailure(operation, "HTTP_ERROR", result.transportError ? null : result.status, {
|
|
187451
|
+
...state(),
|
|
187452
|
+
waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
|
|
187453
|
+
});
|
|
187454
|
+
try {
|
|
187455
|
+
const next = receipt(result.data, {
|
|
187456
|
+
ref: identity.project_ref,
|
|
187457
|
+
id: identity.application_id,
|
|
187458
|
+
environment: identity.environment_id,
|
|
187459
|
+
previewId: initial.preview_id
|
|
187460
|
+
});
|
|
187461
|
+
if (next.status === "planned" || next.release_id !== initial.release_id || next.expires_at !== initial.expires_at || next.resources.database_branch.branch_ref !== initial.resources.database_branch.branch_ref || next.resources.database_branch.data_mode !== initial.resources.database_branch.data_mode || current.resources.application_activation.activation_id !== null && next.resources.application_activation.activation_id !== current.resources.application_activation.activation_id || current.resources.configuration_revision.configuration_id !== null && next.resources.configuration_revision.configuration_id !== current.resources.configuration_revision.configuration_id) {
|
|
187462
|
+
throw new Error("Preview identity changed during observation");
|
|
187463
|
+
}
|
|
187464
|
+
current = next;
|
|
187465
|
+
} catch {
|
|
187466
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", result.status, {
|
|
187467
|
+
...state(),
|
|
187468
|
+
waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
|
|
187469
|
+
});
|
|
187470
|
+
}
|
|
187471
|
+
if (current.status === "provisioning") {
|
|
187472
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(1000, deadline - performance.now()))));
|
|
187473
|
+
}
|
|
187474
|
+
}
|
|
187475
|
+
}
|
|
187476
|
+
async function applicationPreviewAction(http, args, project) {
|
|
187477
|
+
const action = required5(args, "action"), ref = required5(args, "ref"), id = required5(args, "id");
|
|
187478
|
+
const environment = required5(args, "environment_id");
|
|
187479
|
+
if (!Check2(ApplicationIdSchema2, environment))
|
|
187480
|
+
throw new Error("Invalid environment ID");
|
|
187481
|
+
const fields = new Set(["action", "ref", "id", "environment_id", ...actionFields[action] ?? []]);
|
|
187482
|
+
if (Object.entries(args).some(([key, value]) => value !== undefined && !fields.has(key))) {
|
|
187483
|
+
throw new Error("Invalid option for preview action");
|
|
187484
|
+
}
|
|
187485
|
+
if (args["ttl_seconds"] !== undefined && !Check2(ttlSecondsSchema, args["ttl_seconds"])) {
|
|
187486
|
+
throw new Error("ttl_seconds must be an integer from 300 to 604800");
|
|
187487
|
+
}
|
|
187488
|
+
if (args["wait"] !== undefined && typeof args["wait"] !== "boolean")
|
|
187489
|
+
throw new Error("wait must be boolean");
|
|
187490
|
+
if (args["timeout_seconds"] !== undefined && (args["wait"] !== true || !Check2(waitSecondsSchema, args["timeout_seconds"]))) {
|
|
187491
|
+
throw new Error("timeout_seconds requires wait and must be an integer from 1 to 3600");
|
|
187492
|
+
}
|
|
187493
|
+
const timeoutSeconds = args["wait"] === true ? args["timeout_seconds"] ?? 300 : undefined;
|
|
187494
|
+
const startedAt = action === "get_preview" && timeoutSeconds !== undefined ? performance.now() : undefined;
|
|
187495
|
+
const operation = `applications.${action}`;
|
|
187496
|
+
const identity = { project_ref: ref, application_id: id, environment_id: environment };
|
|
187497
|
+
const applicationPath = `/v1/projects/${project}/applications/${encodeURIComponent(id)}`;
|
|
187498
|
+
const path = `${applicationPath}/environments/${encodeURIComponent(environment)}`;
|
|
187499
|
+
const previewId = action === "get_preview" || action === "cleanup_preview" ? required5(args, "preview_id") : undefined;
|
|
187500
|
+
const mutation = action === "create_preview" || action === "cleanup_preview";
|
|
187501
|
+
const requestBudget = startedAt === undefined ? 120000 : Math.min(120000, timeoutSeconds * 1000);
|
|
187502
|
+
const options = {
|
|
187503
|
+
timeoutMs: requestBudget,
|
|
187504
|
+
maxJsonBytes: 1048576,
|
|
187505
|
+
responseTimeoutMs: requestBudget,
|
|
187506
|
+
retry: false,
|
|
187507
|
+
...startedAt === undefined ? {} : { totalTimeoutMs: requestBudget }
|
|
187508
|
+
};
|
|
187509
|
+
let sourceHash;
|
|
187510
|
+
let releaseId;
|
|
187511
|
+
let result;
|
|
187512
|
+
if (action === "get_preview_plan" || action === "create_preview") {
|
|
187513
|
+
releaseId = required5(args, "release_id");
|
|
187514
|
+
if (!Check2(ApplicationReleaseIdSchema2, releaseId))
|
|
187515
|
+
throw new Error("Invalid release ID");
|
|
187516
|
+
if (action === "get_preview_plan") {
|
|
187517
|
+
const query = new URLSearchParams({ release_id: releaseId, branch_ref: required5(args, "branch_ref") });
|
|
187518
|
+
if (typeof args["data_mode"] === "string")
|
|
187519
|
+
query.set("data_mode", args["data_mode"]);
|
|
187520
|
+
if (typeof args["configuration_id"] === "string")
|
|
187521
|
+
query.set("configuration_id", args["configuration_id"]);
|
|
187522
|
+
if (args["ttl_seconds"] !== undefined)
|
|
187523
|
+
query.set("ttl_seconds", String(args["ttl_seconds"]));
|
|
187524
|
+
result = await http.get(`${path}/preview-plan?${query}`, options);
|
|
187525
|
+
} else {
|
|
187526
|
+
const configurationId = required5(args, "configuration_id");
|
|
187527
|
+
if (!Check2(ApplicationConfigurationIdSchema2, configurationId))
|
|
187528
|
+
throw new Error("Invalid configuration ID");
|
|
187529
|
+
const source = await sourceManifest(http, applicationPath, ref, id, releaseId);
|
|
187530
|
+
if (!source.manifest)
|
|
187531
|
+
return releaseControlFailure(operation, source.result.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", source.result.status, { ...identity, release_id: releaseId, configuration_id: configurationId });
|
|
187532
|
+
sourceHash = source.manifest;
|
|
187533
|
+
result = await http.post(`${path}/previews`, {
|
|
187534
|
+
release_id: releaseId,
|
|
187535
|
+
configuration_id: configurationId,
|
|
187536
|
+
data_mode: args["data_mode"] ?? "schema_only",
|
|
187537
|
+
...args["branch_name"] === undefined ? {} : { branch_name: args["branch_name"] },
|
|
187538
|
+
...args["ttl_seconds"] === undefined ? {} : { ttl_seconds: args["ttl_seconds"] }
|
|
187539
|
+
}, options);
|
|
187540
|
+
}
|
|
187541
|
+
} else if (action === "list_previews") {
|
|
187542
|
+
result = await http.get(`${path}/previews`, options);
|
|
187543
|
+
} else if (previewId) {
|
|
187544
|
+
if (!Check2(previewIdSchema, previewId))
|
|
187545
|
+
throw new Error("Invalid preview ID");
|
|
187546
|
+
result = action === "cleanup_preview" ? await http.deleteReleaseMutation(`${path}/previews/${previewId}`) : await http.get(`${path}/previews/${previewId}`, options);
|
|
187547
|
+
} else {
|
|
187548
|
+
throw new Error("Unknown preview action");
|
|
187549
|
+
}
|
|
187550
|
+
const state = {
|
|
187551
|
+
...identity,
|
|
187552
|
+
...previewId ? { preview_id: previewId } : {},
|
|
187553
|
+
...releaseId ? { source_release_id: releaseId } : {},
|
|
187554
|
+
reconciliation: previewId ? { action: "get_preview", ref, id, environment_id: environment, preview_id: previewId } : { action: "list_previews", ref, id, environment_id: environment }
|
|
187555
|
+
};
|
|
187556
|
+
if (!result.ok)
|
|
187557
|
+
return mutation ? releaseControlMutationFailure(operation, result, state) : releaseControlFailure(operation, "HTTP_ERROR", result.status, state);
|
|
187558
|
+
try {
|
|
187559
|
+
if (action === "list_previews") {
|
|
187560
|
+
const inventorySchema = _Object_({
|
|
187561
|
+
..._Object_({ project_ref: branchRefSchema, application_id: ApplicationIdSchema2, environment_id: ApplicationIdSchema2 }).properties,
|
|
187562
|
+
previews: _Array_(receiptSchema, { maxItems: 1000 })
|
|
187563
|
+
}, strict);
|
|
187564
|
+
if (!Check2(inventorySchema, result.data) || result.data.project_ref !== ref || result.data.application_id !== id || result.data.environment_id !== environment)
|
|
187565
|
+
throw new Error;
|
|
187566
|
+
const previews = result.data.previews.map((value) => receipt(value, { ref, id, environment }));
|
|
187567
|
+
if (new Set(previews.map((value) => value.preview_id)).size !== previews.length)
|
|
187568
|
+
throw new Error;
|
|
187569
|
+
return releaseControlSuccess(operation, { ...identity, previews });
|
|
187570
|
+
}
|
|
187571
|
+
const preview = receipt(result.data, {
|
|
187572
|
+
ref,
|
|
187573
|
+
id,
|
|
187574
|
+
environment,
|
|
187575
|
+
...previewId === undefined ? {} : { previewId }
|
|
187576
|
+
});
|
|
187577
|
+
if (action === "get_preview_plan") {
|
|
187578
|
+
if (preview.status !== "planned" || preview.release_id !== releaseId || preview.resources.database_branch.branch_ref !== args["branch_ref"] || preview.resources.database_branch.data_mode !== (args["data_mode"] ?? "schema_only") || (args["ttl_seconds"] === undefined ? preview.expires_at !== null : preview.expires_at === null))
|
|
187579
|
+
throw new Error;
|
|
187580
|
+
} else {
|
|
187581
|
+
if (preview.status === "planned" || preview.resources.database_branch.branch_ref !== `pv${preview.preview_id.replaceAll("-", "").slice(0, 18)}`)
|
|
187582
|
+
throw new Error;
|
|
187583
|
+
if (action === "create_preview" && (preview.release_id !== applicationReleaseId(preview.resources.database_branch.branch_ref, id, sourceHash) || preview.resources.database_branch.data_mode !== (args["data_mode"] ?? "schema_only") || !Check2(receiptSchema, result.data) || result.data.source_configuration_id !== args["configuration_id"] || preview.expires_at === null || result.data.created_at === undefined || args["ttl_seconds"] !== undefined && Date.parse(preview.expires_at) - Date.parse(result.data.created_at) !== args["ttl_seconds"] * 1000))
|
|
187584
|
+
throw new Error;
|
|
187585
|
+
}
|
|
187586
|
+
if (preview.status === "failed" || action === "cleanup_preview" && preview.status !== "cleaned") {
|
|
187587
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", result.status, { ...identity, preview });
|
|
187588
|
+
}
|
|
187589
|
+
if (timeoutSeconds !== undefined)
|
|
187590
|
+
return await waitForPreview(http, path, operation, identity, preview, timeoutSeconds, startedAt);
|
|
187591
|
+
return releaseControlSuccess(operation, { ...identity, preview });
|
|
187592
|
+
} catch {
|
|
187593
|
+
return releaseControlFailure(operation, mutation ? "OUTCOME_UNKNOWN" : "INVALID_RESPONSE", result.status, state);
|
|
187594
|
+
}
|
|
187595
|
+
}
|
|
187230
187596
|
|
|
187231
187597
|
// src/shared/tools/application-tools.ts
|
|
187232
187598
|
var APPLICATION_TOOL_SCHEMA = {
|
|
@@ -187239,26 +187605,30 @@ var APPLICATION_TOOL_SCHEMA = {
|
|
|
187239
187605
|
"get_configuration",
|
|
187240
187606
|
"put_configuration",
|
|
187241
187607
|
"activate_release",
|
|
187608
|
+
"rollback_release",
|
|
187609
|
+
"get_rollback_snapshot",
|
|
187242
187610
|
"reconcile_activation",
|
|
187243
187611
|
"retire_activation",
|
|
187612
|
+
...APPLICATION_PREVIEW_ACTIONS,
|
|
187244
187613
|
"logs"
|
|
187245
187614
|
]), "Action"),
|
|
187246
187615
|
ref: withDescription(String2(), "Project ref"),
|
|
187247
187616
|
id: withDescription(ApplicationIdSchema2, "Application ID"),
|
|
187248
|
-
environment_id: optional(withDescription(ApplicationIdSchema2, "[get_runtime/get_configuration/put_configuration] Environment ID")),
|
|
187249
|
-
configuration_id: optional(ApplicationConfigurationIdSchema2, "[get_configuration/activate_release] Immutable revision; required for activation"),
|
|
187250
|
-
activation_id: optional(ApplicationActivationIdSchema2, "[activate_release/reconcile_activation/retire_activation] Explicit stable activation ID"),
|
|
187251
|
-
expected_activation_id: optional(Union([ApplicationActivationIdSchema2, Null(), Literal("absent")]), "[activate_release] Current activation ID, or absent for first activation"),
|
|
187617
|
+
environment_id: optional(withDescription(ApplicationIdSchema2, "[get_runtime/get_configuration/put_configuration/get_rollback_snapshot/rollback_release] Environment ID")),
|
|
187618
|
+
configuration_id: optional(ApplicationConfigurationIdSchema2, "[get_configuration/activate_release/rollback_release] Immutable revision; required for explicit activation"),
|
|
187619
|
+
activation_id: optional(ApplicationActivationIdSchema2, "[activate_release/reconcile_activation/retire_activation/rollback_release] Explicit stable activation ID; generated for rollback if omitted"),
|
|
187620
|
+
expected_activation_id: optional(Union([ApplicationActivationIdSchema2, Null(), Literal("absent")]), "[activate_release/rollback_release] Current activation ID, or absent for first activation; selected by platform for default rollback"),
|
|
187252
187621
|
configuration_path: optional(String2(), "[put_configuration] Local configuration write JSON including revision and expected revision"),
|
|
187253
187622
|
manifest_path: optional(String2(), "[upload_release] Local delivery.manifest.json"),
|
|
187254
|
-
release_id: optional(ApplicationReleaseIdSchema2, "[get_release/activate_release/reconcile_activation] Immutable application release ID"),
|
|
187623
|
+
release_id: optional(ApplicationReleaseIdSchema2, "[get_release/activate_release/reconcile_activation/rollback_release] Immutable application release ID; platform selects previous for default rollback"),
|
|
187255
187624
|
cursor: optional(ApplicationReleaseIdSchema2, "[list_releases] Last release ID"),
|
|
187256
187625
|
limit: optional(Integer({ minimum: 1, maximum: 100 }), "[list_releases] Page size, default 50"),
|
|
187257
187626
|
offset: optional(Integer({ minimum: 0, maximum: 1e6 }), "[logs] Result offset"),
|
|
187258
187627
|
service: optional(String2(), "[logs] Application target/service filter"),
|
|
187259
187628
|
search: optional(String2(), "[logs] Full-text log filter"),
|
|
187260
187629
|
start: optional(String2(), "[logs] ISO start timestamp"),
|
|
187261
|
-
end: optional(String2(), "[logs] ISO end timestamp")
|
|
187630
|
+
end: optional(String2(), "[logs] ISO end timestamp"),
|
|
187631
|
+
...APPLICATION_PREVIEW_FIELDS
|
|
187262
187632
|
};
|
|
187263
187633
|
var responseSchema = _Object_({
|
|
187264
187634
|
project_ref: String2(),
|
|
@@ -187329,7 +187699,7 @@ async function configurationAction(http, args, project) {
|
|
|
187329
187699
|
return releaseControlFailure(operation, action === "put_configuration" ? "OUTCOME_UNKNOWN" : "INVALID_RESPONSE", result.status, identity);
|
|
187330
187700
|
}
|
|
187331
187701
|
}
|
|
187332
|
-
async function activationAction(http, args, project) {
|
|
187702
|
+
async function activationAction(http, args, project, rollback) {
|
|
187333
187703
|
const action = text(args, "action"), ref = text(args, "ref"), id = text(args, "id");
|
|
187334
187704
|
const environmentId = text(args, "environment_id"), activationId = text(args, "activation_id");
|
|
187335
187705
|
const releaseId = args["release_id"] === undefined ? undefined : text(args, "release_id");
|
|
@@ -187339,7 +187709,7 @@ async function activationAction(http, args, project) {
|
|
|
187339
187709
|
if (!Check2(ApplicationIdSchema2, environmentId) || !Check2(ApplicationActivationIdSchema2, activationId) || action !== "retire_activation" && !Check2(ApplicationReleaseIdSchema2, releaseId)) {
|
|
187340
187710
|
throw new Error("Invalid application activation identity");
|
|
187341
187711
|
}
|
|
187342
|
-
const operation = `applications.${action}`;
|
|
187712
|
+
const operation = rollback ? "applications.rollback_release" : `applications.${action}`;
|
|
187343
187713
|
const path = `/v1/projects/${project}/applications/${encodeURIComponent(id)}/environments/${encodeURIComponent(environmentId)}/activations`;
|
|
187344
187714
|
const identity = {
|
|
187345
187715
|
project_ref: ref,
|
|
@@ -187348,6 +187718,7 @@ async function activationAction(http, args, project) {
|
|
|
187348
187718
|
activation_id: activationId,
|
|
187349
187719
|
...releaseId ? { release_id: releaseId } : {}
|
|
187350
187720
|
};
|
|
187721
|
+
const safeState = { ...identity, ...rollback ?? {} };
|
|
187351
187722
|
let body = {};
|
|
187352
187723
|
if (action === "activate_release") {
|
|
187353
187724
|
body = {
|
|
@@ -187362,7 +187733,9 @@ async function activationAction(http, args, project) {
|
|
|
187362
187733
|
const endpoint = action === "activate_release" ? path : `${path}/${activationId}/${action === "retire_activation" ? "retire" : "reconcile"}`;
|
|
187363
187734
|
const result = await http.post(endpoint, body, { timeoutMs: 120000, maxJsonBytes: 65536, responseTimeoutMs: 30000 });
|
|
187364
187735
|
if (!result.ok)
|
|
187365
|
-
return releaseControlMutationFailure(operation, result,
|
|
187736
|
+
return releaseControlMutationFailure(operation, result, safeState);
|
|
187737
|
+
if (result.status !== 200)
|
|
187738
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", result.status, safeState);
|
|
187366
187739
|
if (action === "retire_activation") {
|
|
187367
187740
|
if (!Check2(ApplicationActivationRetirementResultSchema2, result.data) || Object.entries(identity).some(([key, value]) => Reflect.get(result.data, key) !== value)) {
|
|
187368
187741
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", result.status, identity);
|
|
@@ -187370,9 +187743,60 @@ async function activationAction(http, args, project) {
|
|
|
187370
187743
|
return releaseControlSuccess(operation, result.data);
|
|
187371
187744
|
}
|
|
187372
187745
|
if (!Check2(ApplicationActivationResultSchema2, result.data) || Object.entries(identity).some(([key, value]) => Reflect.get(result.data, key) !== value) || action === "reconcile_activation" && !result.data.replayed) {
|
|
187373
|
-
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", result.status,
|
|
187746
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", result.status, safeState);
|
|
187374
187747
|
}
|
|
187375
|
-
return releaseControlSuccess(operation, result.data);
|
|
187748
|
+
return releaseControlSuccess(operation, { ...result.data, ...rollback ?? {} });
|
|
187749
|
+
}
|
|
187750
|
+
async function rollbackAction(http, args, project) {
|
|
187751
|
+
const ref = text(args, "ref"), id = text(args, "id"), environmentId = text(args, "environment_id");
|
|
187752
|
+
if (!Check2(ApplicationIdSchema2, environmentId))
|
|
187753
|
+
throw new Error("Invalid environment ID");
|
|
187754
|
+
const operation = `applications.${text(args, "action")}`;
|
|
187755
|
+
const identity = { project_ref: ref, application_id: id, environment_id: environmentId };
|
|
187756
|
+
let activationId = args["activation_id"];
|
|
187757
|
+
if (activationId !== undefined && !Check2(ApplicationActivationIdSchema2, activationId)) {
|
|
187758
|
+
throw new Error("Invalid application activation ID");
|
|
187759
|
+
}
|
|
187760
|
+
let releaseId = args["release_id"], configurationId = args["configuration_id"];
|
|
187761
|
+
let expectedActivationId = args["expected_activation_id"];
|
|
187762
|
+
if (releaseId === undefined || args["action"] === "get_rollback_snapshot") {
|
|
187763
|
+
if (releaseId !== undefined || configurationId !== undefined || expectedActivationId !== undefined) {
|
|
187764
|
+
throw new Error("Default rollback selection cannot mix explicit release/configuration/CAS flags");
|
|
187765
|
+
}
|
|
187766
|
+
const response = await http.get(`/v1/projects/${project}/applications/${encodeURIComponent(id)}/environments/${encodeURIComponent(environmentId)}/rollback-snapshot`, { maxJsonBytes: 65536, responseTimeoutMs: 30000 });
|
|
187767
|
+
if (!response.ok)
|
|
187768
|
+
return releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status, identity);
|
|
187769
|
+
const snapshot = response.data;
|
|
187770
|
+
if (response.status !== 200 || !Check2(ApplicationRollbackSnapshotSchema2, snapshot) || snapshot.project_ref !== ref || snapshot.application_id !== id || snapshot.environment_id !== environmentId || snapshot.previous !== null && (snapshot.active === null || snapshot.previous.activation_id === snapshot.active.activation_id)) {
|
|
187771
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", response.status, identity);
|
|
187772
|
+
}
|
|
187773
|
+
if (args["action"] === "get_rollback_snapshot")
|
|
187774
|
+
return releaseControlSuccess(operation, { ...identity, snapshot });
|
|
187775
|
+
if (!snapshot.active || !snapshot.previous) {
|
|
187776
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", response.status, {
|
|
187777
|
+
...identity,
|
|
187778
|
+
reason: "NO_PREVIOUS_ACTIVATION"
|
|
187779
|
+
});
|
|
187780
|
+
}
|
|
187781
|
+
if (activationId === snapshot.active.activation_id || activationId === snapshot.previous.activation_id) {
|
|
187782
|
+
throw new Error("Rollback requires a new activation ID");
|
|
187783
|
+
}
|
|
187784
|
+
releaseId = snapshot.previous.release_id;
|
|
187785
|
+
configurationId = snapshot.previous.configuration_id;
|
|
187786
|
+
expectedActivationId = snapshot.active.activation_id;
|
|
187787
|
+
}
|
|
187788
|
+
activationId ??= crypto.randomUUID();
|
|
187789
|
+
return activationAction(http, {
|
|
187790
|
+
...args,
|
|
187791
|
+
action: "activate_release",
|
|
187792
|
+
activation_id: activationId,
|
|
187793
|
+
release_id: releaseId,
|
|
187794
|
+
configuration_id: configurationId,
|
|
187795
|
+
expected_activation_id: expectedActivationId
|
|
187796
|
+
}, project, {
|
|
187797
|
+
configuration_id: configurationId,
|
|
187798
|
+
expected_activation_id: expectedActivationId === "absent" ? null : expectedActivationId
|
|
187799
|
+
});
|
|
187376
187800
|
}
|
|
187377
187801
|
function registerApplicationTools(server2, http) {
|
|
187378
187802
|
registerTool(server2, "applications", "Store, inspect and explicitly activate immutable HTTP/Worker application releases. Upload does not activate a release.", APPLICATION_TOOL_SCHEMA, async (args) => {
|
|
@@ -187382,6 +187806,12 @@ function registerApplicationTools(server2, http) {
|
|
|
187382
187806
|
throw new Error("Invalid application ID");
|
|
187383
187807
|
const path = `/v1/projects/${project}/applications/${encodeURIComponent(id)}/releases`;
|
|
187384
187808
|
const operation = `applications.${action}`;
|
|
187809
|
+
if (action === "rollback_release" || action === "get_rollback_snapshot") {
|
|
187810
|
+
return rollbackAction(http, args, project);
|
|
187811
|
+
}
|
|
187812
|
+
if (APPLICATION_PREVIEW_ACTIONS.some((value) => value === action)) {
|
|
187813
|
+
return applicationPreviewAction(http, args, project);
|
|
187814
|
+
}
|
|
187385
187815
|
if (action === "activate_release" || action === "reconcile_activation" || action === "retire_activation") {
|
|
187386
187816
|
return activationAction(http, args, project);
|
|
187387
187817
|
}
|
|
@@ -188750,6 +189180,34 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
188750
189180
|
}
|
|
188751
189181
|
|
|
188752
189182
|
// src/shared/tools/branch-tools.ts
|
|
189183
|
+
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
189184
|
+
var PROMOTION_PLAN_MAX_JSON_BYTES = 512 * 1024;
|
|
189185
|
+
var PROMOTION_BLOCK_MESSAGES = {
|
|
189186
|
+
parent_ahead: "Parent history is ahead; recreate or rebase the preview branch.",
|
|
189187
|
+
checksum_mismatch: "Parent and branch migration checksums differ.",
|
|
189188
|
+
stored_checksum_mismatch: "Stored checksum does not match migration SQL.",
|
|
189189
|
+
name_conflict: "Migration name already exists in the parent.",
|
|
189190
|
+
out_of_order_migration: "Migration precedes the latest parent migration.",
|
|
189191
|
+
empty_migration: "Migration has no executable statements.",
|
|
189192
|
+
non_transactional_sql: "Migration requires a separate maintenance path.",
|
|
189193
|
+
unsupported_sql: "Migration exceeds the project-scoped SQL policy."
|
|
189194
|
+
};
|
|
189195
|
+
var PROMOTION_FAILURE_CODES = new Set([
|
|
189196
|
+
"promotion_locked",
|
|
189197
|
+
"promotion_plan_changed",
|
|
189198
|
+
"promotion_blocked",
|
|
189199
|
+
"destructive_confirmation_required",
|
|
189200
|
+
"promotion_apply_failed",
|
|
189201
|
+
"promotion_readback_failed",
|
|
189202
|
+
"promotion_failed",
|
|
189203
|
+
"promotion_plan_failed",
|
|
189204
|
+
"promotion_plan_required",
|
|
189205
|
+
"branch_not_active"
|
|
189206
|
+
]);
|
|
189207
|
+
var PROMOTION_WARNINGS = [
|
|
189208
|
+
"Only recorded migrations are promoted; branch data and untracked schema changes are not copied.",
|
|
189209
|
+
"Migration SQL can modify parent data and must be reviewed."
|
|
189210
|
+
];
|
|
188753
189211
|
function resolveProjectRef(ref, projectRef) {
|
|
188754
189212
|
const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef || "";
|
|
188755
189213
|
if (!resolved)
|
|
@@ -188761,32 +189219,188 @@ function requireString(value, field) {
|
|
|
188761
189219
|
throw new Error(`'${field}' is required`);
|
|
188762
189220
|
return value.trim();
|
|
188763
189221
|
}
|
|
189222
|
+
function isRecord4(value) {
|
|
189223
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
189224
|
+
}
|
|
189225
|
+
function isSha256(value) {
|
|
189226
|
+
return typeof value === "string" && SHA256_PATTERN.test(value);
|
|
189227
|
+
}
|
|
189228
|
+
function parsePromotionEntry(candidate) {
|
|
189229
|
+
if (!isRecord4(candidate))
|
|
189230
|
+
return null;
|
|
189231
|
+
const { version, name, checksum, statement_count: statementCount, statements, destructive } = candidate;
|
|
189232
|
+
if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
|
|
189233
|
+
return null;
|
|
189234
|
+
if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
|
|
189235
|
+
return null;
|
|
189236
|
+
if (!isSha256(checksum))
|
|
189237
|
+
return null;
|
|
189238
|
+
if (typeof statementCount !== "number" || !Number.isSafeInteger(statementCount) || statementCount < 0)
|
|
189239
|
+
return null;
|
|
189240
|
+
if (typeof destructive !== "boolean")
|
|
189241
|
+
return null;
|
|
189242
|
+
if (statements !== undefined && (!Array.isArray(statements) || statements.length !== statementCount || statements.some((statement) => typeof statement !== "string"))) {
|
|
189243
|
+
return null;
|
|
189244
|
+
}
|
|
189245
|
+
return {
|
|
189246
|
+
version,
|
|
189247
|
+
name,
|
|
189248
|
+
checksum,
|
|
189249
|
+
statement_count: statementCount,
|
|
189250
|
+
destructive
|
|
189251
|
+
};
|
|
189252
|
+
}
|
|
189253
|
+
function parsePromotionBlock(candidate) {
|
|
189254
|
+
if (!isRecord4(candidate))
|
|
189255
|
+
return null;
|
|
189256
|
+
const { code, version, name, message } = candidate;
|
|
189257
|
+
if (typeof code !== "string" || !Object.hasOwn(PROMOTION_BLOCK_MESSAGES, code))
|
|
189258
|
+
return null;
|
|
189259
|
+
if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
|
|
189260
|
+
return null;
|
|
189261
|
+
if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
|
|
189262
|
+
return null;
|
|
189263
|
+
if (typeof message !== "string")
|
|
189264
|
+
return null;
|
|
189265
|
+
const blockCode = code;
|
|
189266
|
+
return {
|
|
189267
|
+
code: blockCode,
|
|
189268
|
+
version,
|
|
189269
|
+
name,
|
|
189270
|
+
message: PROMOTION_BLOCK_MESSAGES[blockCode]
|
|
189271
|
+
};
|
|
189272
|
+
}
|
|
189273
|
+
function uniqueVersions(entries) {
|
|
189274
|
+
const versions = entries.map((entry) => entry.version);
|
|
189275
|
+
return new Set(versions).size === versions.length;
|
|
189276
|
+
}
|
|
189277
|
+
function samePromotionEntry(left, right) {
|
|
189278
|
+
return left.version === right.version && left.checksum === right.checksum && left.name === right.name && left.statement_count === right.statement_count && left.destructive === right.destructive;
|
|
189279
|
+
}
|
|
189280
|
+
function parsePromotionPlan(candidate, expectedScope) {
|
|
189281
|
+
if (!isRecord4(candidate))
|
|
189282
|
+
return null;
|
|
189283
|
+
const {
|
|
189284
|
+
mode,
|
|
189285
|
+
parent_ref: parentRef,
|
|
189286
|
+
branch_ref: branchRef,
|
|
189287
|
+
safe_to_apply: safeToApply,
|
|
189288
|
+
plan_checksum: planChecksum,
|
|
189289
|
+
pending,
|
|
189290
|
+
applied,
|
|
189291
|
+
blocked,
|
|
189292
|
+
warnings,
|
|
189293
|
+
requires_destructive_confirmation: requiresDestructiveConfirmation,
|
|
189294
|
+
ignored_branch_data: ignoredBranchData
|
|
189295
|
+
} = candidate;
|
|
189296
|
+
if (mode !== "migrations" || typeof parentRef !== "string" || typeof branchRef !== "string" || typeof safeToApply !== "boolean" || !isSha256(planChecksum) || !Array.isArray(pending) || !Array.isArray(applied) || !Array.isArray(blocked) || !Array.isArray(warnings) || typeof requiresDestructiveConfirmation !== "boolean" || ignoredBranchData !== true) {
|
|
189297
|
+
return null;
|
|
189298
|
+
}
|
|
189299
|
+
if (parentRef !== expectedScope.parentRef || branchRef !== expectedScope.branchRef)
|
|
189300
|
+
return null;
|
|
189301
|
+
const pendingEntries = pending.map(parsePromotionEntry);
|
|
189302
|
+
const appliedEntries = applied.map(parsePromotionEntry);
|
|
189303
|
+
const blockingFindings = blocked.map(parsePromotionBlock);
|
|
189304
|
+
if (pendingEntries.some((entry) => entry === null) || appliedEntries.some((entry) => entry === null) || blockingFindings.some((entry) => entry === null) || warnings.some((warning) => typeof warning !== "string")) {
|
|
189305
|
+
return null;
|
|
189306
|
+
}
|
|
189307
|
+
const normalizedPending = pendingEntries.filter((entry) => entry !== null);
|
|
189308
|
+
const normalizedApplied = appliedEntries.filter((entry) => entry !== null);
|
|
189309
|
+
const normalizedBlocked = blockingFindings.filter((entry) => entry !== null);
|
|
189310
|
+
const hasDestructivePending = normalizedPending.some((entry) => entry.destructive);
|
|
189311
|
+
if (!uniqueVersions(normalizedPending) || !uniqueVersions(normalizedApplied) || !uniqueVersions([...normalizedPending, ...normalizedApplied]) || normalizedPending.some((entry) => entry.statement_count === 0) || safeToApply !== (normalizedBlocked.length === 0) || requiresDestructiveConfirmation !== hasDestructivePending) {
|
|
189312
|
+
return null;
|
|
189313
|
+
}
|
|
189314
|
+
return {
|
|
189315
|
+
mode,
|
|
189316
|
+
parent_ref: parentRef,
|
|
189317
|
+
branch_ref: branchRef,
|
|
189318
|
+
safe_to_apply: safeToApply,
|
|
189319
|
+
plan_checksum: planChecksum,
|
|
189320
|
+
pending: normalizedPending,
|
|
189321
|
+
applied: normalizedApplied,
|
|
189322
|
+
blocked: normalizedBlocked,
|
|
189323
|
+
warnings: [...PROMOTION_WARNINGS],
|
|
189324
|
+
requires_destructive_confirmation: requiresDestructiveConfirmation,
|
|
189325
|
+
ignored_branch_data: true
|
|
189326
|
+
};
|
|
189327
|
+
}
|
|
189328
|
+
function parsePromotionResult(candidate, expectedScope, reviewedPending) {
|
|
189329
|
+
if (!isRecord4(candidate) || candidate.promoted !== true || candidate.mode !== "migrations" || candidate.project_ref !== expectedScope.parentRef || candidate.branch_ref !== expectedScope.branchRef || candidate.branch_data_copied !== false || !Array.isArray(candidate.applied)) {
|
|
189330
|
+
return null;
|
|
189331
|
+
}
|
|
189332
|
+
const applied = candidate.applied.map(parsePromotionEntry);
|
|
189333
|
+
if (applied.some((entry) => entry === null))
|
|
189334
|
+
return null;
|
|
189335
|
+
const normalizedApplied = applied.filter((entry) => entry !== null);
|
|
189336
|
+
if (!uniqueVersions(normalizedApplied) || normalizedApplied.length !== reviewedPending.length)
|
|
189337
|
+
return null;
|
|
189338
|
+
const reviewedByVersion = new Map(reviewedPending.map((entry) => [entry.version, entry]));
|
|
189339
|
+
if (normalizedApplied.some((entry) => {
|
|
189340
|
+
const reviewed = reviewedByVersion.get(entry.version);
|
|
189341
|
+
return !reviewed || !samePromotionEntry(entry, reviewed);
|
|
189342
|
+
}))
|
|
189343
|
+
return null;
|
|
189344
|
+
const plan = parsePromotionPlan(candidate.plan, expectedScope);
|
|
189345
|
+
if (!plan || !plan.safe_to_apply || plan.pending.length > 0)
|
|
189346
|
+
return null;
|
|
189347
|
+
const readbackByVersion = new Map(plan.applied.map((entry) => [entry.version, entry]));
|
|
189348
|
+
if (normalizedApplied.some((entry) => {
|
|
189349
|
+
const readback = readbackByVersion.get(entry.version);
|
|
189350
|
+
return !readback || !samePromotionEntry(entry, readback);
|
|
189351
|
+
}))
|
|
189352
|
+
return null;
|
|
189353
|
+
return {
|
|
189354
|
+
promoted: true,
|
|
189355
|
+
mode: "migrations",
|
|
189356
|
+
project_ref: expectedScope.parentRef,
|
|
189357
|
+
branch_ref: expectedScope.branchRef,
|
|
189358
|
+
applied: normalizedApplied,
|
|
189359
|
+
plan,
|
|
189360
|
+
branch_data_copied: false
|
|
189361
|
+
};
|
|
189362
|
+
}
|
|
189363
|
+
function safePromotionCode(result) {
|
|
189364
|
+
if (!isRecord4(result.data) || typeof result.data.code !== "string")
|
|
189365
|
+
return;
|
|
189366
|
+
return PROMOTION_FAILURE_CODES.has(result.data.code) ? result.data.code : undefined;
|
|
189367
|
+
}
|
|
189368
|
+
function appliedVersions(result) {
|
|
189369
|
+
if (result.transportError || result.responseReadError || !isRecord4(result.data) || !Array.isArray(result.data.applied))
|
|
189370
|
+
return [];
|
|
189371
|
+
const entries = result.data.applied.map(parsePromotionEntry);
|
|
189372
|
+
if (entries.some((entry) => !entry))
|
|
189373
|
+
return [];
|
|
189374
|
+
const validated = entries.filter((entry) => entry !== null);
|
|
189375
|
+
return uniqueVersions(validated) ? validated.map((entry) => entry.version) : [];
|
|
189376
|
+
}
|
|
189377
|
+
function promotionFailure(response, json, unknownOutcome) {
|
|
189378
|
+
if (json)
|
|
189379
|
+
return response;
|
|
189380
|
+
const state = JSON.parse(response.content[0].text);
|
|
189381
|
+
if (!isRecord4(state) || !isRecord4(state.error))
|
|
189382
|
+
return response;
|
|
189383
|
+
const lines = [
|
|
189384
|
+
unknownOutcome ? "Promotion outcome is unknown; do not repeat the request automatically." : "Promotion request was not verified.",
|
|
189385
|
+
`Error: ${state.error.code}`,
|
|
189386
|
+
`Project: ${state.project_ref}; branch: ${state.branch_ref}`,
|
|
189387
|
+
...typeof state.reason === "string" ? [`Reason: ${state.reason}`] : [],
|
|
189388
|
+
...state.mutation_sent === false ? ["No promotion request was sent."] : [],
|
|
189389
|
+
...typeof state.plan_checksum === "string" ? [`Reviewed checksum: ${state.plan_checksum}`] : [],
|
|
189390
|
+
...Array.isArray(state.reported_applied_versions) && state.reported_applied_versions.length > 0 ? [`Server-reported applied versions: ${state.reported_applied_versions.join(", ")}`] : [],
|
|
189391
|
+
"Fetch a fresh promotion_plan and review ledger state before another promote."
|
|
189392
|
+
];
|
|
189393
|
+
return { isError: true, content: [{ type: "text", text: lines.join(`
|
|
189394
|
+
`) }] };
|
|
189395
|
+
}
|
|
188764
189396
|
function responseError(result) {
|
|
188765
|
-
if (result.data
|
|
188766
|
-
const
|
|
188767
|
-
|
|
188768
|
-
|
|
188769
|
-
const details = [message];
|
|
188770
|
-
if (Array.isArray(data.applied) && data.applied.length > 0) {
|
|
188771
|
-
const versions = data.applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
|
|
188772
|
-
details.push(`Applied before failure: ${versions.join(", ") || data.applied.length}`);
|
|
188773
|
-
details.push("Fetch a fresh promotion_plan before retrying.");
|
|
188774
|
-
}
|
|
188775
|
-
if (data.replacement_committed === true) {
|
|
188776
|
-
details.push(`Database replacement committed; recovery is required${typeof data.backup_database === "string" ? ` using backup ${data.backup_database}` : ""}.`);
|
|
188777
|
-
}
|
|
188778
|
-
return details.join(`
|
|
188779
|
-
`);
|
|
188780
|
-
}
|
|
189397
|
+
if (isRecord4(result.data)) {
|
|
189398
|
+
const message = result.data.error || result.data.message;
|
|
189399
|
+
if (typeof message === "string")
|
|
189400
|
+
return message;
|
|
188781
189401
|
}
|
|
188782
189402
|
return `Request failed with HTTP ${result.status}`;
|
|
188783
189403
|
}
|
|
188784
|
-
function isPromotionPlan(candidate) {
|
|
188785
|
-
if (!candidate || typeof candidate !== "object")
|
|
188786
|
-
return false;
|
|
188787
|
-
const plan = candidate;
|
|
188788
|
-
return plan.mode === "migrations" && typeof plan.safe_to_apply === "boolean" && typeof plan.plan_checksum === "string" && Array.isArray(plan.pending) && Array.isArray(plan.applied) && Array.isArray(plan.blocked) && Array.isArray(plan.warnings) && typeof plan.requires_destructive_confirmation === "boolean";
|
|
188789
|
-
}
|
|
188790
189404
|
function projectPath(ref) {
|
|
188791
189405
|
return `/v1/projects/${encodeURIComponent(ref)}/branches`;
|
|
188792
189406
|
}
|
|
@@ -188828,14 +189442,10 @@ function readOnlyResult() {
|
|
|
188828
189442
|
content: [{ type: "text", text: "⚠️ Branch write blocked in read-only mode." }]
|
|
188829
189443
|
};
|
|
188830
189444
|
}
|
|
188831
|
-
function formatPromotionResult(
|
|
188832
|
-
|
|
188833
|
-
return "Migrations promoted.";
|
|
188834
|
-
const response = responseValue;
|
|
188835
|
-
const applied = Array.isArray(response.applied) ? response.applied : [];
|
|
188836
|
-
const versions = applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
|
|
189445
|
+
function formatPromotionResult(response) {
|
|
189446
|
+
const versions = response.applied.map((entry) => entry.version);
|
|
188837
189447
|
return [
|
|
188838
|
-
`Migration promotion completed: ${applied.length} applied.`,
|
|
189448
|
+
`Migration promotion completed: ${response.applied.length} applied.`,
|
|
188839
189449
|
...versions.length > 0 ? [`Versions: ${versions.join(", ")}`] : [],
|
|
188840
189450
|
"Branch data was not automatically copied to the parent project."
|
|
188841
189451
|
].join(`
|
|
@@ -188855,13 +189465,19 @@ function registerBranchTools(server2, http, options = {}) {
|
|
|
188855
189465
|
name: optional(String2(), "[create] Branch name"),
|
|
188856
189466
|
data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[create] Preview data mode (default: schema_only)"),
|
|
188857
189467
|
plan_checksum: optional(String2(), "[promote] Reviewed plan checksum from promotion_plan"),
|
|
188858
|
-
confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations")
|
|
189468
|
+
confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations"),
|
|
189469
|
+
json: optional(Boolean2(), "[promotion_plan/promote] Return machine-readable JSON")
|
|
188859
189470
|
}, async (args) => {
|
|
188860
189471
|
const action = requireString(args.action, "action");
|
|
188861
189472
|
const ref = resolveProjectRef(args.ref, options.projectRef);
|
|
188862
189473
|
const writeAction = action === "create" || action === "delete" || action === "promote";
|
|
188863
|
-
if (writeAction && options.readOnly)
|
|
188864
|
-
return
|
|
189474
|
+
if (writeAction && options.readOnly) {
|
|
189475
|
+
return args.json && action === "promote" ? releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, {
|
|
189476
|
+
project_ref: ref,
|
|
189477
|
+
branch_ref: args.branch_ref,
|
|
189478
|
+
reason: "READ_ONLY"
|
|
189479
|
+
}) : readOnlyResult();
|
|
189480
|
+
}
|
|
188865
189481
|
let result;
|
|
188866
189482
|
if (action === "list") {
|
|
188867
189483
|
result = await http.get(projectPath(ref));
|
|
@@ -188876,29 +189492,91 @@ function registerBranchTools(server2, http, options = {}) {
|
|
|
188876
189492
|
result = await http.delete(branchPath(ref, branchRef));
|
|
188877
189493
|
} else if (action === "promotion_plan") {
|
|
188878
189494
|
const branchRef = requireString(args.branch_ref, "branch_ref");
|
|
188879
|
-
|
|
188880
|
-
|
|
188881
|
-
|
|
189495
|
+
const scope = { project_ref: ref, branch_ref: branchRef };
|
|
189496
|
+
result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
|
|
189497
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189498
|
+
responseTimeoutMs: 5000
|
|
189499
|
+
});
|
|
189500
|
+
if (!result.ok) {
|
|
189501
|
+
return promotionFailure(releaseControlFailure("branch.promotion_plan", "HTTP_ERROR", result.transportError ? null : result.status, scope), args.json === true, false);
|
|
189502
|
+
}
|
|
189503
|
+
const plan = parsePromotionPlan(result.data, { parentRef: ref, branchRef });
|
|
189504
|
+
if (result.status !== 200 || !plan) {
|
|
189505
|
+
return promotionFailure(releaseControlFailure("branch.promotion_plan", "INVALID_RESPONSE", result.status, scope), args.json === true, false);
|
|
188882
189506
|
}
|
|
189507
|
+
return args.json ? releaseControlSuccess("branch.promotion_plan", { ...scope, plan }) : { content: [{ type: "text", text: formatPromotionPlan(plan) }] };
|
|
188883
189508
|
} else if (action === "promote") {
|
|
188884
189509
|
const branchRef = requireString(args.branch_ref, "branch_ref");
|
|
188885
189510
|
const planChecksum = requireString(args.plan_checksum, "plan_checksum");
|
|
189511
|
+
if (!isSha256(planChecksum))
|
|
189512
|
+
throw new Error("'plan_checksum' must be a 64-character lowercase SHA-256 checksum");
|
|
189513
|
+
const state = {
|
|
189514
|
+
project_ref: ref,
|
|
189515
|
+
branch_ref: branchRef,
|
|
189516
|
+
plan_checksum: planChecksum,
|
|
189517
|
+
automatic_retry: false,
|
|
189518
|
+
reconciliation: { action: "promotion_plan", ref, branch_ref: branchRef }
|
|
189519
|
+
};
|
|
189520
|
+
const preflight = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
|
|
189521
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189522
|
+
responseTimeoutMs: 5000
|
|
189523
|
+
});
|
|
189524
|
+
const beforeMutation = { ...state, mutation_sent: false };
|
|
189525
|
+
if (!preflight.ok) {
|
|
189526
|
+
return promotionFailure(releaseControlFailure("branch.promote", "HTTP_ERROR", preflight.transportError ? null : preflight.status, { ...beforeMutation, reason: "PREFLIGHT_FAILED" }), args.json === true, false);
|
|
189527
|
+
}
|
|
189528
|
+
const plan = parsePromotionPlan(preflight.data, { parentRef: ref, branchRef });
|
|
189529
|
+
if (preflight.status !== 200 || !plan) {
|
|
189530
|
+
return promotionFailure(releaseControlFailure("branch.promote", "INVALID_RESPONSE", preflight.status, { ...beforeMutation, reason: "PREFLIGHT_UNVERIFIED" }), args.json === true, false);
|
|
189531
|
+
}
|
|
189532
|
+
const preflightReason = plan.plan_checksum !== planChecksum ? "PLAN_CHANGED" : !plan.safe_to_apply ? "PLAN_BLOCKED" : plan.requires_destructive_confirmation && args.confirm_destructive !== true ? "DESTRUCTIVE_CONFIRMATION_REQUIRED" : null;
|
|
189533
|
+
if (preflightReason) {
|
|
189534
|
+
return promotionFailure(releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, { ...beforeMutation, reason: preflightReason, observed_plan_checksum: plan.plan_checksum }), args.json === true, false);
|
|
189535
|
+
}
|
|
189536
|
+
if (plan.pending.length === 0) {
|
|
189537
|
+
return args.json ? releaseControlSuccess("branch.promote", {
|
|
189538
|
+
...beforeMutation,
|
|
189539
|
+
promoted: false,
|
|
189540
|
+
unchanged: true,
|
|
189541
|
+
mode: "migrations",
|
|
189542
|
+
applied: [],
|
|
189543
|
+
plan,
|
|
189544
|
+
branch_data_copied: false,
|
|
189545
|
+
reviewed_plan_checksum: planChecksum
|
|
189546
|
+
}) : { content: [{ type: "text", text: "Migration promotion unchanged: no pending migrations; no promotion request sent." }] };
|
|
189547
|
+
}
|
|
188886
189548
|
result = await http.post(`${branchPath(ref, branchRef)}/promote`, {
|
|
188887
189549
|
mode: "migrations",
|
|
188888
189550
|
plan_checksum: planChecksum,
|
|
188889
189551
|
confirm_destructive: args.confirm_destructive === true
|
|
189552
|
+
}, {
|
|
189553
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189554
|
+
responseTimeoutMs: 5000,
|
|
189555
|
+
timeoutMs: 30000
|
|
188890
189556
|
});
|
|
189557
|
+
if (!result.ok) {
|
|
189558
|
+
return promotionFailure(releaseControlMutationFailure("branch.promote", result, {
|
|
189559
|
+
...state,
|
|
189560
|
+
mutation_sent: true,
|
|
189561
|
+
reported_applied_versions: appliedVersions(result),
|
|
189562
|
+
...safePromotionCode(result) ? { server_code: safePromotionCode(result) } : {}
|
|
189563
|
+
}), args.json === true, result.transportError === true || result.responseReadError === true || result.status === 408 || result.status >= 500);
|
|
189564
|
+
}
|
|
189565
|
+
const promoted = parsePromotionResult(result.data, { parentRef: ref, branchRef }, plan.pending);
|
|
189566
|
+
if (result.status !== 200 || !promoted) {
|
|
189567
|
+
return promotionFailure(releaseControlFailure("branch.promote", "OUTCOME_UNKNOWN", result.status, { ...state, mutation_sent: true }), args.json === true, true);
|
|
189568
|
+
}
|
|
189569
|
+
return args.json ? releaseControlSuccess("branch.promote", {
|
|
189570
|
+
...promoted,
|
|
189571
|
+
unchanged: false,
|
|
189572
|
+
mutation_sent: true,
|
|
189573
|
+
reviewed_plan_checksum: planChecksum
|
|
189574
|
+
}) : { content: [{ type: "text", text: formatPromotionResult(promoted) }] };
|
|
188891
189575
|
} else {
|
|
188892
189576
|
throw new Error(`Unknown branch action: ${action}`);
|
|
188893
189577
|
}
|
|
188894
189578
|
if (!result.ok) {
|
|
188895
|
-
return {
|
|
188896
|
-
isError: true,
|
|
188897
|
-
content: [{ type: "text", text: `❌ ${responseError(result)}` }]
|
|
188898
|
-
};
|
|
188899
|
-
}
|
|
188900
|
-
if (action === "promote") {
|
|
188901
|
-
return { content: [{ type: "text", text: formatPromotionResult(result.data) }] };
|
|
189579
|
+
return { isError: true, content: [{ type: "text", text: responseError(result) }] };
|
|
188902
189580
|
}
|
|
188903
189581
|
return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
|
|
188904
189582
|
});
|
|
@@ -189433,7 +190111,7 @@ import { resolve as resolve11 } from "node:path";
|
|
|
189433
190111
|
// src/shared/tools/lite-cli-command.ts
|
|
189434
190112
|
import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "node:fs";
|
|
189435
190113
|
import { isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve10 } from "node:path";
|
|
189436
|
-
function
|
|
190114
|
+
function isRecord5(value) {
|
|
189437
190115
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
189438
190116
|
}
|
|
189439
190117
|
function resolveLiteCommand(workdir, environment = process.env) {
|
|
@@ -189448,7 +190126,7 @@ function resolveLiteCommand(workdir, environment = process.env) {
|
|
|
189448
190126
|
if (!existsSync8(manifestPath))
|
|
189449
190127
|
return ["supacloud-lite"];
|
|
189450
190128
|
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
189451
|
-
if (!
|
|
190129
|
+
if (!isRecord5(manifest) || manifest.name !== "@supacloud/lite" || !isRecord5(manifest.bin)) {
|
|
189452
190130
|
throw new Error("Invalid installed @supacloud/lite package manifest");
|
|
189453
190131
|
}
|
|
189454
190132
|
const entry = manifest.bin["supacloud-lite"];
|
|
@@ -193377,21 +194055,34 @@ var REMOTE_APP_ACTIONS = {
|
|
|
193377
194055
|
configure: "put_configuration",
|
|
193378
194056
|
deploy: "activate_release",
|
|
193379
194057
|
status: "get_runtime",
|
|
193380
|
-
rollback: "
|
|
194058
|
+
rollback: "rollback_release",
|
|
194059
|
+
"rollback-plan": "get_rollback_snapshot",
|
|
193381
194060
|
reconcile: "reconcile_activation",
|
|
193382
194061
|
retire: "retire_activation",
|
|
193383
|
-
logs: "logs"
|
|
194062
|
+
logs: "logs",
|
|
194063
|
+
"preview-plan": "get_preview_plan",
|
|
194064
|
+
preview: "create_preview",
|
|
194065
|
+
previews: "list_previews",
|
|
194066
|
+
"preview-status": "get_preview",
|
|
194067
|
+
"preview-cleanup": "cleanup_preview"
|
|
193384
194068
|
};
|
|
193385
194069
|
var REMOTE_APP_DESCRIPTIONS = {
|
|
193386
|
-
ref: "[upload/configure/deploy/status/rollback/reconcile/retire] Project ref (defaults to context)",
|
|
193387
|
-
id: "[upload/configure/deploy/status/rollback/reconcile/retire] Application ID",
|
|
193388
|
-
environment_id: "[configure/deploy/status/rollback/reconcile/retire] Environment ID",
|
|
193389
|
-
configuration_id: "[deploy/rollback] Required
|
|
193390
|
-
activation_id: "[deploy/rollback/reconcile/retire] Required explicit activation ID",
|
|
193391
|
-
expected_activation_id: "[deploy/rollback] Required
|
|
194070
|
+
ref: "[upload/configure/deploy/status/rollback/rollback-plan/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Project ref (defaults to context)",
|
|
194071
|
+
id: "[upload/configure/deploy/status/rollback/rollback-plan/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Application ID",
|
|
194072
|
+
environment_id: "[configure/deploy/status/rollback/rollback-plan/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Environment ID",
|
|
194073
|
+
configuration_id: "[deploy/rollback/preview] Required for deploy/preview/explicit rollback; platform selects for default rollback",
|
|
194074
|
+
activation_id: "[deploy/rollback/reconcile/retire] Required explicit activation ID; generated for rollback if omitted",
|
|
194075
|
+
expected_activation_id: "[deploy/rollback] Required for deploy/explicit rollback; platform selects current CAS for default rollback",
|
|
193392
194076
|
configuration_path: "[configure] Configuration write JSON including revision and expected revision",
|
|
193393
194077
|
manifest_path: "[upload] Local delivery.manifest.json",
|
|
193394
|
-
release_id: "[deploy/rollback/reconcile] Required
|
|
194078
|
+
release_id: "[deploy/rollback/reconcile/preview-plan/preview] Required for deploy/reconcile/preview; defaults to journal-selected previous for rollback",
|
|
194079
|
+
preview_id: "[preview-status/preview-cleanup] Preview receipt ID",
|
|
194080
|
+
branch_ref: "[preview-plan] Proposed branch ref; preview assigns its own",
|
|
194081
|
+
branch_name: "[preview] Branch display name",
|
|
194082
|
+
data_mode: "[preview-plan/preview] Default schema_only; full_clone copies rows",
|
|
194083
|
+
ttl_seconds: "[preview-plan/preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL",
|
|
194084
|
+
wait: "[preview/preview-status] Wait for verified readiness of the selected preview",
|
|
194085
|
+
timeout_seconds: "[preview/preview-status] Observation budget with --wait, from 1 to 3600 seconds (default 300)"
|
|
193395
194086
|
};
|
|
193396
194087
|
var { action: _remoteAction, ...remoteFields } = APPLICATION_TOOL_SCHEMA;
|
|
193397
194088
|
var REMOTE_APP_SCHEMA = Partial(_Object_(remoteFields)).properties;
|
|
@@ -194394,10 +195085,16 @@ function registerAppTools(server2, options = {}) {
|
|
|
194394
195085
|
"deploy",
|
|
194395
195086
|
"status",
|
|
194396
195087
|
"rollback",
|
|
195088
|
+
"rollback-plan",
|
|
194397
195089
|
"reconcile",
|
|
194398
195090
|
"retire",
|
|
194399
|
-
"logs"
|
|
194400
|
-
|
|
195091
|
+
"logs",
|
|
195092
|
+
"preview-plan",
|
|
195093
|
+
"preview",
|
|
195094
|
+
"previews",
|
|
195095
|
+
"preview-status",
|
|
195096
|
+
"preview-cleanup"
|
|
195097
|
+
]), "App action; upload/configure only prepare, rollback activates the journal-selected previous release without schema downgrade"),
|
|
194401
195098
|
kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource"]), "[generate] Scaffold kind"),
|
|
194402
195099
|
template: optional(stringEnum(["minimal", "http", "command", "edge"]), "[init] Minimal application by default; explicit http/command/edge recipes"),
|
|
194403
195100
|
name: optional(String2(), "[init/generate] Project or object name"),
|
|
@@ -195840,7 +196537,7 @@ var RELEASE_CANARY_DISABLE_RECEIPT_KEYS = new Set(["fixtureId", "state", "idempo
|
|
|
195840
196537
|
var RELEASE_CANARY_DISABLE_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_disable";
|
|
195841
196538
|
var RELEASE_CANARY_PENDING_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_pending";
|
|
195842
196539
|
var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
|
|
195843
|
-
function
|
|
196540
|
+
function isRecord6(value) {
|
|
195844
196541
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
195845
196542
|
}
|
|
195846
196543
|
function canonicalTimestamp4(value) {
|
|
@@ -195856,7 +196553,7 @@ function backupBelongsToProject(backupId, projectRef) {
|
|
|
195856
196553
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef}_`);
|
|
195857
196554
|
}
|
|
195858
196555
|
function verifiedBackup(value, projectRef) {
|
|
195859
|
-
if (!
|
|
196556
|
+
if (!isRecord6(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef) || value.project_ref !== projectRef || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp4(value.created_at) || !canonicalTimestamp4(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
|
|
195860
196557
|
return null;
|
|
195861
196558
|
return {
|
|
195862
196559
|
backup_id: value.backup_id,
|
|
@@ -195870,7 +196567,7 @@ function verifiedBackup(value, projectRef) {
|
|
|
195870
196567
|
};
|
|
195871
196568
|
}
|
|
195872
196569
|
function backupInventory(value, projectRef) {
|
|
195873
|
-
if (!
|
|
196570
|
+
if (!isRecord6(value) || !Array.isArray(value.backups))
|
|
195874
196571
|
return null;
|
|
195875
196572
|
const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef));
|
|
195876
196573
|
if (backups.some((backup) => backup === null))
|
|
@@ -195949,7 +196646,7 @@ function readInventoryFailure(operation, read) {
|
|
|
195949
196646
|
return null;
|
|
195950
196647
|
}
|
|
195951
196648
|
function postgrestStatus(value) {
|
|
195952
|
-
if (!
|
|
196649
|
+
if (!isRecord6(value) || value.component !== "postgrest" || !["running", "stopped"].includes(String(value.desired)) || !["running", "stopped", "starting", "error"].includes(String(value.actual)) || !["healthy", "unhealthy", "unknown"].includes(String(value.health)))
|
|
195953
196650
|
return null;
|
|
195954
196651
|
return {
|
|
195955
196652
|
desired: value.desired,
|
|
@@ -195970,7 +196667,7 @@ function readPostgrestFailure(operation, read) {
|
|
|
195970
196667
|
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
195971
196668
|
}
|
|
195972
196669
|
function isRestartReceipt(value) {
|
|
195973
|
-
return
|
|
196670
|
+
return isRecord6(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
195974
196671
|
}
|
|
195975
196672
|
function releaseCanaryStageInput(subject, requestId) {
|
|
195976
196673
|
if (typeof subject !== "string" || !UUID.test(subject))
|
|
@@ -195980,7 +196677,7 @@ function releaseCanaryStageInput(subject, requestId) {
|
|
|
195980
196677
|
return { p_subject: subject, p_request_id: requestId };
|
|
195981
196678
|
}
|
|
195982
196679
|
function releaseCanaryStageReceipt(value) {
|
|
195983
|
-
if (!
|
|
196680
|
+
if (!isRecord6(value) || Object.keys(value).some((key) => !RELEASE_CANARY_STAGE_RECEIPT_KEYS.has(key)) || Object.keys(value).length !== RELEASE_CANARY_STAGE_RECEIPT_KEYS.size || typeof value.fixtureId !== "string" || !UUID.test(value.fixtureId) || typeof value.tenantKey !== "string" || !RELEASE_CANARY_TENANT_KEY.test(value.tenantKey) || value.state !== "staged" || value.idempotent !== true)
|
|
195984
196681
|
return null;
|
|
195985
196682
|
return {
|
|
195986
196683
|
fixtureId: value.fixtureId,
|
|
@@ -196023,7 +196720,7 @@ function releaseCanaryDisableInput(fixtureId, disableRequestId, issuer, subject)
|
|
|
196023
196720
|
};
|
|
196024
196721
|
}
|
|
196025
196722
|
function releaseCanaryDisableReceipt(receiptCandidate, fixtureId) {
|
|
196026
|
-
if (!
|
|
196723
|
+
if (!isRecord6(receiptCandidate) || Object.keys(receiptCandidate).some((key) => !RELEASE_CANARY_DISABLE_RECEIPT_KEYS.has(key)) || Object.keys(receiptCandidate).length !== RELEASE_CANARY_DISABLE_RECEIPT_KEYS.size || receiptCandidate.fixtureId !== fixtureId || receiptCandidate.state !== "disabled" || typeof receiptCandidate.idempotent !== "boolean")
|
|
196027
196724
|
return null;
|
|
196028
196725
|
return { fixtureId, state: "disabled", idempotent: receiptCandidate.idempotent };
|
|
196029
196726
|
}
|
|
@@ -196196,7 +196893,7 @@ function handleScopeRebind(args) {
|
|
|
196196
196893
|
} catch {
|
|
196197
196894
|
throw new Error(`Scope file '${filePath}' is not valid JSON`);
|
|
196198
196895
|
}
|
|
196199
|
-
if (!
|
|
196896
|
+
if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
|
|
196200
196897
|
throw new Error(`Scope file '${filePath}' has missing or invalid baseCommit`);
|
|
196201
196898
|
}
|
|
196202
196899
|
const previousBaseCommit = parsed.baseCommit;
|
|
@@ -196260,7 +196957,7 @@ function handleScopeInspect(args) {
|
|
|
196260
196957
|
} catch {
|
|
196261
196958
|
throw new Error(`Scope file '${args.file}' is not valid JSON`);
|
|
196262
196959
|
}
|
|
196263
|
-
if (!
|
|
196960
|
+
if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
|
|
196264
196961
|
throw new Error(`Scope file '${args.file}' has missing or invalid baseCommit`);
|
|
196265
196962
|
}
|
|
196266
196963
|
const scopeSha256 = genericScopedReleaseScopeSha256(parsed);
|
|
@@ -196422,7 +197119,7 @@ function registerReleaseTools(server2, http, options = {}) {
|
|
|
196422
197119
|
if (!mutation.ok || mutation.status !== 200) {
|
|
196423
197120
|
return mutationFailure("release.logical_backup.create", mutation);
|
|
196424
197121
|
}
|
|
196425
|
-
const responseBackup =
|
|
197122
|
+
const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.backup, projectRef) : null;
|
|
196426
197123
|
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
196427
197124
|
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
196428
197125
|
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
@@ -196447,7 +197144,7 @@ function registerReleaseTools(server2, http, options = {}) {
|
|
|
196447
197144
|
if (!mutation.ok || mutation.status !== 200) {
|
|
196448
197145
|
return mutationFailure("release.logical_backup.restore", mutation);
|
|
196449
197146
|
}
|
|
196450
|
-
const responseBackup =
|
|
197147
|
+
const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.restored_backup, projectRef) : null;
|
|
196451
197148
|
const after = await readInventory(http, projectRef);
|
|
196452
197149
|
const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
|
|
196453
197150
|
const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
|
|
@@ -197964,7 +198661,7 @@ function registerRemoteDevTools(server2, options = {}) {
|
|
|
197964
198661
|
// package.json
|
|
197965
198662
|
var package_default = {
|
|
197966
198663
|
name: "@supacloud/cli",
|
|
197967
|
-
version: "0.
|
|
198664
|
+
version: "0.79.0",
|
|
197968
198665
|
description: "Project-scoped CLI for SupaCloud users",
|
|
197969
198666
|
type: "module",
|
|
197970
198667
|
main: "./dist/index.js",
|
|
@@ -198016,14 +198713,14 @@ var package_default = {
|
|
|
198016
198713
|
},
|
|
198017
198714
|
dependencies: {
|
|
198018
198715
|
"@supacloud/compiler": "^0.43.0",
|
|
198019
|
-
"@supacloud/delivery": "0.
|
|
198716
|
+
"@supacloud/delivery": "0.9.0",
|
|
198020
198717
|
"@supacloud/db": "0.16.0",
|
|
198021
198718
|
fflate: "^0.8.3",
|
|
198022
198719
|
typebox: "1.3.34"
|
|
198023
198720
|
},
|
|
198024
198721
|
overrides: {
|
|
198025
198722
|
"@supacloud/contracts": "0.8.0",
|
|
198026
|
-
"@supacloud/delivery": "0.
|
|
198723
|
+
"@supacloud/delivery": "0.9.0",
|
|
198027
198724
|
"@graphql-tools/utils": "^12.0.3"
|
|
198028
198725
|
},
|
|
198029
198726
|
devDependencies: {
|
|
@@ -198286,6 +198983,11 @@ EXAMPLES
|
|
|
198286
198983
|
${preferredCommand} app configure --ref abc123 --id orders --environment_id test --configuration_path configuration.json
|
|
198287
198984
|
${preferredCommand} app deploy --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --activation_id <uuid> --expected_activation_id absent
|
|
198288
198985
|
${preferredCommand} app status --ref abc123 --id orders --environment_id test
|
|
198986
|
+
${preferredCommand} app preview-plan --ref abc123 --id orders --environment_id test --release_id <sha256> --branch_ref preview-orders
|
|
198987
|
+
${preferredCommand} app preview --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --ttl_seconds 3600 --wait
|
|
198988
|
+
${preferredCommand} app previews --ref abc123 --id orders --environment_id test
|
|
198989
|
+
${preferredCommand} app preview-status --ref abc123 --id orders --environment_id test --preview_id <uuid> --wait --timeout_seconds 300
|
|
198990
|
+
${preferredCommand} app preview-cleanup --ref abc123 --id orders --environment_id test --preview_id <uuid>
|
|
198289
198991
|
${preferredCommand} app logs --ref abc123 --id orders --environment_id test
|
|
198290
198992
|
${preferredCommand} app rollback --ref abc123 --id orders --environment_id test --release_id <old-sha256> --configuration_id <uuid> --activation_id <new-uuid> --expected_activation_id <current-uuid>
|
|
198291
198993
|
${preferredCommand} app check --root . --strict
|