@supacloud/cli 0.77.0 → 0.78.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 +697 -71
- package/dist/shared/tools/app-tools.d.ts +12 -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-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 +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,94 @@ deployment. An unknown mutation outcome remains unknown; reconcile the same
|
|
|
79
79
|
activation identity rather than blindly deploying again. These commands require
|
|
80
80
|
Management API credentials except local plan/build and authoring commands.
|
|
81
81
|
|
|
82
|
+
### Isolated Application Previews
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
supacloud-cli app preview-plan --env test --id orders --environment_id test --release_id RELEASE_SHA256 --branch_ref preview-orders
|
|
86
|
+
supacloud-cli app preview --env test --id orders --environment_id test --release_id RELEASE_SHA256 --configuration_id CONFIG_UUID --ttl_seconds 3600 --wait --timeout_seconds 300
|
|
87
|
+
supacloud-cli app previews --env test --id orders --environment_id test
|
|
88
|
+
supacloud-cli app preview-status --env test --id orders --environment_id test --preview_id PREVIEW_UUID --wait --timeout_seconds 300
|
|
89
|
+
supacloud-cli app preview-cleanup --env test --id orders --environment_id test --preview_id PREVIEW_UUID
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The platform owns branch provisioning, queue/Storage namespaces, test Secrets,
|
|
93
|
+
configuration cloning, activation and runtime readiness. The CLI never performs
|
|
94
|
+
these operations through SSH or sequential client-side resource mutations.
|
|
95
|
+
Creation defaults to `schema_only`; copying source rows requires explicitly
|
|
96
|
+
selecting `--data_mode full_clone`. Creation requires an immutable source
|
|
97
|
+
configuration ID and verifies the branch release identity against the source
|
|
98
|
+
manifest. A `provisioning` receipt means accepted, not ready.
|
|
99
|
+
|
|
100
|
+
New previews use the platform's default 24-hour TTL. `--ttl_seconds` accepts 300
|
|
101
|
+
through 604800 seconds for creation or read-only planning. `expires_at` remains
|
|
102
|
+
in the public receipt; a plan has no deadline unless a TTL is requested. Receipt
|
|
103
|
+
expiry requires the matching platform cleanup contract (#1774), not the older
|
|
104
|
+
CLI-only lifecycle contract.
|
|
105
|
+
|
|
106
|
+
`--wait` on creation observes only the accepted Preview ID; it never repeats
|
|
107
|
+
creation. `preview-status --wait` continues observing that same ID. The observation
|
|
108
|
+
budget is 300 seconds by default and accepts 1 through 3600 seconds with
|
|
109
|
+
`--timeout_seconds`; creation's source verification and initial POST precede that
|
|
110
|
+
budget, while status includes its initial GET. Header and body share the remaining
|
|
111
|
+
request budget. Completed readiness ends waiting; a failed/cleaned receipt, failed
|
|
112
|
+
query or identity drift ends it with nonzero status. A wait timeout returns
|
|
113
|
+
`waiting.status: "timed_out"` with the last validated receipt and the exact
|
|
114
|
+
same-ID continuation in `reconciliation`. It does not mark provisioning failed,
|
|
115
|
+
retry a failed lifecycle GET, delete resources or promote an environment.
|
|
116
|
+
|
|
117
|
+
`preview-plan` is read-only. Preview list/status requests may resume persisted
|
|
118
|
+
provisioning on the server, so they are classified as writes, including in
|
|
119
|
+
read-only and production profiles. The platform may clean eligible expired
|
|
120
|
+
previews, but never interrupts a serving preview or empties nonempty Storage
|
|
121
|
+
automatically. Explicit cleanup remains available. Failed previews return
|
|
122
|
+
nonzero with the validated receipt and cleanup state. Unknown create outcomes
|
|
123
|
+
are never retried: list existing previews and reconcile their receipts first.
|
|
124
|
+
No production deployment, database promotion or data rollback is implied.
|
|
125
|
+
Runtime readiness is not authenticated business-flow acceptance.
|
|
126
|
+
|
|
127
|
+
```gherkin
|
|
128
|
+
Feature: Platform-owned application previews
|
|
129
|
+
Scenario: Read-only planning
|
|
130
|
+
Given an explicit release and proposed branch ref
|
|
131
|
+
When preview-plan runs in read-only mode
|
|
132
|
+
Then only a GET plan request is sent
|
|
133
|
+
And no preview is created
|
|
134
|
+
|
|
135
|
+
Scenario: Accepted is not ready
|
|
136
|
+
Given an immutable source release and configuration
|
|
137
|
+
When preview creates an isolated environment
|
|
138
|
+
Then exactly one create request is sent
|
|
139
|
+
And a provisioning receipt is returned without claiming readiness
|
|
140
|
+
|
|
141
|
+
Scenario: TTL is validated before dispatch
|
|
142
|
+
Given a lifetime outside 300 through 604800 integer seconds
|
|
143
|
+
When preview or preview-plan is requested
|
|
144
|
+
Then no HTTP request is sent
|
|
145
|
+
|
|
146
|
+
Scenario: Wait preserves the selected preview
|
|
147
|
+
Given a valid provisioning receipt and an exhausted observation budget
|
|
148
|
+
When preview --wait returns
|
|
149
|
+
Then its exit status is nonzero and the accepted receipt is preserved
|
|
150
|
+
And reconciliation selects the same Preview ID without another creation
|
|
151
|
+
|
|
152
|
+
Scenario: Ready requires verified platform evidence
|
|
153
|
+
Given a provisioning preview selected by its scope, digest and Preview ID
|
|
154
|
+
When its complete readiness receipt is observed
|
|
155
|
+
Then waiting succeeds with that receipt
|
|
156
|
+
And foreign or malformed receipts never become success
|
|
157
|
+
|
|
158
|
+
Scenario: Safety guards precede effects
|
|
159
|
+
Given a read-only or unconfirmed production profile
|
|
160
|
+
When preview creation, status recovery or cleanup is requested
|
|
161
|
+
Then the CLI rejects the request before HTTP dispatch
|
|
162
|
+
|
|
163
|
+
Scenario: Unknown outcome preserves reconciliation
|
|
164
|
+
Given a create response is lost or belongs to another environment
|
|
165
|
+
When the CLI handles that response
|
|
166
|
+
Then it reports OUTCOME_UNKNOWN without retrying creation
|
|
167
|
+
And it retains the selected scope for receipt reconciliation
|
|
168
|
+
```
|
|
169
|
+
|
|
82
170
|
### Framework source upgrades
|
|
83
171
|
|
|
84
172
|
Breaking source-contract changes use the compiler's migration command, which is
|
package/dist/index.js
CHANGED
|
@@ -179093,8 +179093,18 @@ 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", "logs", "get_preview_plan"],
|
|
179097
|
+
write: [
|
|
179098
|
+
"upload_release",
|
|
179099
|
+
"put_configuration",
|
|
179100
|
+
"activate_release",
|
|
179101
|
+
"reconcile_activation",
|
|
179102
|
+
"retire_activation",
|
|
179103
|
+
"create_preview",
|
|
179104
|
+
"list_previews",
|
|
179105
|
+
"get_preview",
|
|
179106
|
+
"cleanup_preview"
|
|
179107
|
+
]
|
|
179098
179108
|
},
|
|
179099
179109
|
project: {
|
|
179100
179110
|
read: ["get", "endpoints", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
|
|
@@ -179189,8 +179199,8 @@ var ACTION_POLICY = {
|
|
|
179189
179199
|
ai: { local: ["show_skill", "install_skill"] },
|
|
179190
179200
|
app: {
|
|
179191
179201
|
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"]
|
|
179202
|
+
read: ["status", "logs", "preview-plan"],
|
|
179203
|
+
write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire", "preview", "previews", "preview-status", "preview-cleanup"]
|
|
179194
179204
|
},
|
|
179195
179205
|
db: {
|
|
179196
179206
|
local: ["context", "lint", "explain", "diff", "role_sql"],
|
|
@@ -179528,19 +179538,34 @@ class HttpTransport {
|
|
|
179528
179538
|
}
|
|
179529
179539
|
}
|
|
179530
179540
|
async get(path, options = {}) {
|
|
179541
|
+
const timeoutMs = validatedPostTimeout(options);
|
|
179531
179542
|
const maxResponseBytes = validatedGetResponseLimit(options);
|
|
179532
179543
|
const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
|
|
179533
179544
|
const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
|
|
179545
|
+
const totalTimeoutMs = validatedResponseTimeout(options.totalTimeoutMs);
|
|
179534
179546
|
if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
|
|
179535
179547
|
throw new RangeError("HTTP response limit options are mutually exclusive");
|
|
179536
179548
|
}
|
|
179549
|
+
if (totalTimeoutMs !== undefined && (options.retry !== false || maxJsonBytes === undefined)) {
|
|
179550
|
+
throw new RangeError("HTTP total timeout requires bounded JSON and retry: false");
|
|
179551
|
+
}
|
|
179552
|
+
const deadline = totalTimeoutMs === undefined ? undefined : performance.now() + totalTimeoutMs;
|
|
179537
179553
|
try {
|
|
179538
|
-
const
|
|
179554
|
+
const request = options.retry === false ? fetchWithTimeout : fetchWithRetry;
|
|
179555
|
+
const res = await request(`${this.baseUrl}${path}`, {
|
|
179539
179556
|
method: "GET",
|
|
179540
179557
|
headers: this.headers()
|
|
179541
|
-
},
|
|
179558
|
+
}, totalTimeoutMs === undefined ? timeoutMs : Math.min(timeoutMs, totalTimeoutMs), this.insecureTls);
|
|
179542
179559
|
if (maxJsonBytes !== undefined) {
|
|
179543
|
-
const
|
|
179560
|
+
const remaining = deadline === undefined ? undefined : Math.floor(deadline - performance.now());
|
|
179561
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
179562
|
+
res.body?.cancel().catch(() => {
|
|
179563
|
+
return;
|
|
179564
|
+
});
|
|
179565
|
+
return responseReadFailure(res.status);
|
|
179566
|
+
}
|
|
179567
|
+
const bodyTimeout = remaining === undefined ? responseTimeoutMs : Math.min(responseTimeoutMs ?? remaining, remaining);
|
|
179568
|
+
const data = await boundedResponseJson(res, maxJsonBytes, bodyTimeout);
|
|
179544
179569
|
return data === null ? responseReadFailure(res.status) : { ok: res.ok, status: res.status, data };
|
|
179545
179570
|
}
|
|
179546
179571
|
const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
|
|
@@ -187228,6 +187253,332 @@ var ApplicationActivationWriteSchema2 = ApplicationActivationWriteSchema;
|
|
|
187228
187253
|
var ApplicationActivationResultSchema2 = ApplicationActivationResultSchema;
|
|
187229
187254
|
var ApplicationActivationRetirementResultSchema2 = ApplicationActivationRetirementResultSchema;
|
|
187230
187255
|
|
|
187256
|
+
// src/shared/tools/application-preview-tools.ts
|
|
187257
|
+
var APPLICATION_PREVIEW_ACTIONS = [
|
|
187258
|
+
"get_preview_plan",
|
|
187259
|
+
"create_preview",
|
|
187260
|
+
"list_previews",
|
|
187261
|
+
"get_preview",
|
|
187262
|
+
"cleanup_preview"
|
|
187263
|
+
];
|
|
187264
|
+
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}$" });
|
|
187265
|
+
var branchRefSchema = String2({ pattern: "^[a-z0-9-]{1,20}$" });
|
|
187266
|
+
var ttlSecondsSchema = Integer({ minimum: 300, maximum: 604800 });
|
|
187267
|
+
var waitSecondsSchema = Integer({ minimum: 1, maximum: 3600 });
|
|
187268
|
+
var timestampSchema = String2({
|
|
187269
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
187270
|
+
maxLength: 24
|
|
187271
|
+
});
|
|
187272
|
+
var APPLICATION_PREVIEW_FIELDS = {
|
|
187273
|
+
preview_id: optional(previewIdSchema, "[get_preview/cleanup_preview] Preview receipt ID"),
|
|
187274
|
+
branch_ref: optional(branchRefSchema, "[get_preview_plan] Proposed branch ref; create assigns its own"),
|
|
187275
|
+
branch_name: optional(String2({ minLength: 1, maxLength: 80 }), "[create_preview] Branch display name"),
|
|
187276
|
+
data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[get_preview_plan/create_preview] Default schema_only; full_clone copies rows"),
|
|
187277
|
+
ttl_seconds: optional(ttlSecondsSchema, "[get_preview_plan/create_preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL"),
|
|
187278
|
+
wait: optional(Boolean2(), "[create_preview/get_preview] Wait for verified readiness of this preview only"),
|
|
187279
|
+
timeout_seconds: optional(waitSecondsSchema, "[create_preview/get_preview] Observation budget with wait, from 1 to 3600 seconds (default 300)")
|
|
187280
|
+
};
|
|
187281
|
+
var strict = { additionalProperties: false };
|
|
187282
|
+
var phase = stringEnum(["pending", "ready", "failed", "cleaned"]);
|
|
187283
|
+
var check5 = String2({ pattern: "^[a-z][a-z0-9_]{0,63}$" });
|
|
187284
|
+
var checks = _Array_(check5, { maxItems: 64, uniqueItems: true });
|
|
187285
|
+
var revision = Union([ApplicationConfigurationIdSchema2, Null()]);
|
|
187286
|
+
var receiptSchema = _Object_({
|
|
187287
|
+
schema: Literal("supacloud.application-preview.v1"),
|
|
187288
|
+
preview_id: previewIdSchema,
|
|
187289
|
+
project_ref: branchRefSchema,
|
|
187290
|
+
application_id: ApplicationIdSchema2,
|
|
187291
|
+
environment_id: ApplicationIdSchema2,
|
|
187292
|
+
release_id: ApplicationReleaseIdSchema2,
|
|
187293
|
+
expires_at: Union([timestampSchema, Null()]),
|
|
187294
|
+
status: stringEnum(["planned", "provisioning", "ready", "failed", "cleaned"]),
|
|
187295
|
+
resources: _Object_({
|
|
187296
|
+
build_artifact: _Object_({ status: phase, release_id: ApplicationReleaseIdSchema2 }, strict),
|
|
187297
|
+
database_branch: _Object_({ status: phase, branch_ref: branchRefSchema, data_mode: stringEnum(["schema_only", "full_clone"]) }, strict),
|
|
187298
|
+
queue_namespace: _Object_({ status: phase, namespace: String2({ minLength: 1, maxLength: 128 }) }, strict),
|
|
187299
|
+
storage_namespace: _Object_({ status: phase, namespace: branchRefSchema }, strict),
|
|
187300
|
+
test_secret: _Object_({ status: phase, name: String2({ pattern: "^[A-Z0-9_-]{1,128}$" }), value_issued: Literal(false) }, strict),
|
|
187301
|
+
configuration_revision: _Object_({ status: phase, configuration_id: revision }, strict),
|
|
187302
|
+
application_activation: _Object_({ status: phase, activation_id: Union([previewIdSchema, Null()]) }, strict),
|
|
187303
|
+
smoke_test: _Object_({ status: phase, checks, passed: checks, failed: checks }, strict)
|
|
187304
|
+
}, strict),
|
|
187305
|
+
cleanup: _Object_({
|
|
187306
|
+
required: Boolean2(),
|
|
187307
|
+
completed: Boolean2(),
|
|
187308
|
+
error: Union([String2({ pattern: "^[A-Z0-9_]{1,128}$" }), Null()])
|
|
187309
|
+
}, strict),
|
|
187310
|
+
branch_name: Optional(String2({ maxLength: 80 })),
|
|
187311
|
+
queue_name: Optional(String2({ maxLength: 128 })),
|
|
187312
|
+
test_secret_name: Optional(String2({ pattern: "^[A-Z0-9_]{1,128}$" })),
|
|
187313
|
+
source_configuration_id: Optional(revision),
|
|
187314
|
+
created_at: Optional(timestampSchema),
|
|
187315
|
+
updated_at: Optional(timestampSchema)
|
|
187316
|
+
}, strict);
|
|
187317
|
+
function required5(args, field) {
|
|
187318
|
+
const value = args[field];
|
|
187319
|
+
if (typeof value !== "string" || !value.trim())
|
|
187320
|
+
throw new Error(`'${field}' is required`);
|
|
187321
|
+
return value.trim();
|
|
187322
|
+
}
|
|
187323
|
+
var mandatoryChecks = [
|
|
187324
|
+
"release_artifact",
|
|
187325
|
+
"database_branch",
|
|
187326
|
+
"queue_namespace",
|
|
187327
|
+
"storage_namespace",
|
|
187328
|
+
"test_secret",
|
|
187329
|
+
"configuration_revision",
|
|
187330
|
+
"application_activation",
|
|
187331
|
+
"application_readiness",
|
|
187332
|
+
"tenant_runtime"
|
|
187333
|
+
];
|
|
187334
|
+
var actionFields = {
|
|
187335
|
+
get_preview_plan: ["release_id", "branch_ref", "data_mode", "configuration_id", "ttl_seconds"],
|
|
187336
|
+
create_preview: ["release_id", "configuration_id", "data_mode", "branch_name", "ttl_seconds", "wait", "timeout_seconds"],
|
|
187337
|
+
list_previews: [],
|
|
187338
|
+
get_preview: ["preview_id", "wait", "timeout_seconds"],
|
|
187339
|
+
cleanup_preview: ["preview_id"]
|
|
187340
|
+
};
|
|
187341
|
+
function timestamp(value) {
|
|
187342
|
+
return Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
187343
|
+
}
|
|
187344
|
+
function receipt(value, identity) {
|
|
187345
|
+
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) {
|
|
187346
|
+
throw new Error("Invalid preview receipt");
|
|
187347
|
+
}
|
|
187348
|
+
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)) {
|
|
187349
|
+
throw new Error("Invalid preview expiry");
|
|
187350
|
+
}
|
|
187351
|
+
if (value.status === "ready") {
|
|
187352
|
+
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) {
|
|
187353
|
+
throw new Error("Incomplete preview readiness");
|
|
187354
|
+
}
|
|
187355
|
+
}
|
|
187356
|
+
if (value.status === "cleaned" && (!value.cleanup.completed || value.cleanup.required || [
|
|
187357
|
+
value.resources.database_branch,
|
|
187358
|
+
value.resources.queue_namespace,
|
|
187359
|
+
value.resources.storage_namespace,
|
|
187360
|
+
value.resources.test_secret
|
|
187361
|
+
].some((resource) => resource.status !== "cleaned"))) {
|
|
187362
|
+
throw new Error("Incomplete preview cleanup");
|
|
187363
|
+
}
|
|
187364
|
+
const {
|
|
187365
|
+
branch_name: _branchName,
|
|
187366
|
+
queue_name: _queueName,
|
|
187367
|
+
test_secret_name: _secretName,
|
|
187368
|
+
source_configuration_id: _sourceConfiguration,
|
|
187369
|
+
created_at: _createdAt,
|
|
187370
|
+
updated_at: _updatedAt,
|
|
187371
|
+
...publicReceipt
|
|
187372
|
+
} = value;
|
|
187373
|
+
return publicReceipt;
|
|
187374
|
+
}
|
|
187375
|
+
async function sourceManifest(http, path, ref, id, releaseId) {
|
|
187376
|
+
const result = await http.get(`${path}/releases/${releaseId}`, { maxJsonBytes: 196608, responseTimeoutMs: 30000 });
|
|
187377
|
+
if (!result.ok)
|
|
187378
|
+
return { result };
|
|
187379
|
+
try {
|
|
187380
|
+
const data = result.data;
|
|
187381
|
+
if (!data || typeof data !== "object" || !("project_ref" in data) || data.project_ref !== ref || !("application_id" in data) || data.application_id !== id || !("release" in data))
|
|
187382
|
+
throw new Error;
|
|
187383
|
+
const release = parseApplicationReleaseRecord(data.release);
|
|
187384
|
+
if (release.project_ref !== ref || release.application_id !== id || release.release_id !== releaseId)
|
|
187385
|
+
throw new Error;
|
|
187386
|
+
return { result, manifest: release.manifest_sha256 };
|
|
187387
|
+
} catch {
|
|
187388
|
+
return { result };
|
|
187389
|
+
}
|
|
187390
|
+
}
|
|
187391
|
+
async function waitForPreview(http, path, operation, identity, initial, timeoutSeconds, startedAt = performance.now()) {
|
|
187392
|
+
const deadline = startedAt + timeoutSeconds * 1000;
|
|
187393
|
+
let current = initial;
|
|
187394
|
+
const state = () => ({
|
|
187395
|
+
...identity,
|
|
187396
|
+
preview_id: initial.preview_id,
|
|
187397
|
+
preview: current,
|
|
187398
|
+
reconciliation: {
|
|
187399
|
+
action: "get_preview",
|
|
187400
|
+
ref: identity.project_ref,
|
|
187401
|
+
id: identity.application_id,
|
|
187402
|
+
environment_id: identity.environment_id,
|
|
187403
|
+
preview_id: initial.preview_id,
|
|
187404
|
+
wait: true,
|
|
187405
|
+
timeout_seconds: timeoutSeconds
|
|
187406
|
+
}
|
|
187407
|
+
});
|
|
187408
|
+
while (true) {
|
|
187409
|
+
if (current.status === "ready")
|
|
187410
|
+
return releaseControlSuccess(operation, {
|
|
187411
|
+
...identity,
|
|
187412
|
+
preview: current,
|
|
187413
|
+
waiting: { status: "ready", timeout_seconds: timeoutSeconds }
|
|
187414
|
+
});
|
|
187415
|
+
if (current.status !== "provisioning")
|
|
187416
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
|
|
187417
|
+
...state(),
|
|
187418
|
+
waiting: { status: "stopped", timeout_seconds: timeoutSeconds }
|
|
187419
|
+
});
|
|
187420
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
187421
|
+
if (remaining < 2)
|
|
187422
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
|
|
187423
|
+
...state(),
|
|
187424
|
+
waiting: { status: "timed_out", timeout_seconds: timeoutSeconds }
|
|
187425
|
+
});
|
|
187426
|
+
const requestBudget = Math.min(120000, remaining);
|
|
187427
|
+
const result = await http.get(`${path}/previews/${initial.preview_id}`, {
|
|
187428
|
+
retry: false,
|
|
187429
|
+
timeoutMs: requestBudget,
|
|
187430
|
+
responseTimeoutMs: requestBudget,
|
|
187431
|
+
totalTimeoutMs: requestBudget,
|
|
187432
|
+
maxJsonBytes: 1048576
|
|
187433
|
+
});
|
|
187434
|
+
if (!result.ok)
|
|
187435
|
+
return releaseControlFailure(operation, "HTTP_ERROR", result.transportError ? null : result.status, {
|
|
187436
|
+
...state(),
|
|
187437
|
+
waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
|
|
187438
|
+
});
|
|
187439
|
+
try {
|
|
187440
|
+
const next = receipt(result.data, {
|
|
187441
|
+
ref: identity.project_ref,
|
|
187442
|
+
id: identity.application_id,
|
|
187443
|
+
environment: identity.environment_id,
|
|
187444
|
+
previewId: initial.preview_id
|
|
187445
|
+
});
|
|
187446
|
+
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) {
|
|
187447
|
+
throw new Error("Preview identity changed during observation");
|
|
187448
|
+
}
|
|
187449
|
+
current = next;
|
|
187450
|
+
} catch {
|
|
187451
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", result.status, {
|
|
187452
|
+
...state(),
|
|
187453
|
+
waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
|
|
187454
|
+
});
|
|
187455
|
+
}
|
|
187456
|
+
if (current.status === "provisioning") {
|
|
187457
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(1000, deadline - performance.now()))));
|
|
187458
|
+
}
|
|
187459
|
+
}
|
|
187460
|
+
}
|
|
187461
|
+
async function applicationPreviewAction(http, args, project) {
|
|
187462
|
+
const action = required5(args, "action"), ref = required5(args, "ref"), id = required5(args, "id");
|
|
187463
|
+
const environment = required5(args, "environment_id");
|
|
187464
|
+
if (!Check2(ApplicationIdSchema2, environment))
|
|
187465
|
+
throw new Error("Invalid environment ID");
|
|
187466
|
+
const fields = new Set(["action", "ref", "id", "environment_id", ...actionFields[action] ?? []]);
|
|
187467
|
+
if (Object.entries(args).some(([key, value]) => value !== undefined && !fields.has(key))) {
|
|
187468
|
+
throw new Error("Invalid option for preview action");
|
|
187469
|
+
}
|
|
187470
|
+
if (args["ttl_seconds"] !== undefined && !Check2(ttlSecondsSchema, args["ttl_seconds"])) {
|
|
187471
|
+
throw new Error("ttl_seconds must be an integer from 300 to 604800");
|
|
187472
|
+
}
|
|
187473
|
+
if (args["wait"] !== undefined && typeof args["wait"] !== "boolean")
|
|
187474
|
+
throw new Error("wait must be boolean");
|
|
187475
|
+
if (args["timeout_seconds"] !== undefined && (args["wait"] !== true || !Check2(waitSecondsSchema, args["timeout_seconds"]))) {
|
|
187476
|
+
throw new Error("timeout_seconds requires wait and must be an integer from 1 to 3600");
|
|
187477
|
+
}
|
|
187478
|
+
const timeoutSeconds = args["wait"] === true ? args["timeout_seconds"] ?? 300 : undefined;
|
|
187479
|
+
const startedAt = action === "get_preview" && timeoutSeconds !== undefined ? performance.now() : undefined;
|
|
187480
|
+
const operation = `applications.${action}`;
|
|
187481
|
+
const identity = { project_ref: ref, application_id: id, environment_id: environment };
|
|
187482
|
+
const applicationPath = `/v1/projects/${project}/applications/${encodeURIComponent(id)}`;
|
|
187483
|
+
const path = `${applicationPath}/environments/${encodeURIComponent(environment)}`;
|
|
187484
|
+
const previewId = action === "get_preview" || action === "cleanup_preview" ? required5(args, "preview_id") : undefined;
|
|
187485
|
+
const mutation = action === "create_preview" || action === "cleanup_preview";
|
|
187486
|
+
const requestBudget = startedAt === undefined ? 120000 : Math.min(120000, timeoutSeconds * 1000);
|
|
187487
|
+
const options = {
|
|
187488
|
+
timeoutMs: requestBudget,
|
|
187489
|
+
maxJsonBytes: 1048576,
|
|
187490
|
+
responseTimeoutMs: requestBudget,
|
|
187491
|
+
retry: false,
|
|
187492
|
+
...startedAt === undefined ? {} : { totalTimeoutMs: requestBudget }
|
|
187493
|
+
};
|
|
187494
|
+
let sourceHash;
|
|
187495
|
+
let releaseId;
|
|
187496
|
+
let result;
|
|
187497
|
+
if (action === "get_preview_plan" || action === "create_preview") {
|
|
187498
|
+
releaseId = required5(args, "release_id");
|
|
187499
|
+
if (!Check2(ApplicationReleaseIdSchema2, releaseId))
|
|
187500
|
+
throw new Error("Invalid release ID");
|
|
187501
|
+
if (action === "get_preview_plan") {
|
|
187502
|
+
const query = new URLSearchParams({ release_id: releaseId, branch_ref: required5(args, "branch_ref") });
|
|
187503
|
+
if (typeof args["data_mode"] === "string")
|
|
187504
|
+
query.set("data_mode", args["data_mode"]);
|
|
187505
|
+
if (typeof args["configuration_id"] === "string")
|
|
187506
|
+
query.set("configuration_id", args["configuration_id"]);
|
|
187507
|
+
if (args["ttl_seconds"] !== undefined)
|
|
187508
|
+
query.set("ttl_seconds", String(args["ttl_seconds"]));
|
|
187509
|
+
result = await http.get(`${path}/preview-plan?${query}`, options);
|
|
187510
|
+
} else {
|
|
187511
|
+
const configurationId = required5(args, "configuration_id");
|
|
187512
|
+
if (!Check2(ApplicationConfigurationIdSchema2, configurationId))
|
|
187513
|
+
throw new Error("Invalid configuration ID");
|
|
187514
|
+
const source = await sourceManifest(http, applicationPath, ref, id, releaseId);
|
|
187515
|
+
if (!source.manifest)
|
|
187516
|
+
return releaseControlFailure(operation, source.result.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", source.result.status, { ...identity, release_id: releaseId, configuration_id: configurationId });
|
|
187517
|
+
sourceHash = source.manifest;
|
|
187518
|
+
result = await http.post(`${path}/previews`, {
|
|
187519
|
+
release_id: releaseId,
|
|
187520
|
+
configuration_id: configurationId,
|
|
187521
|
+
data_mode: args["data_mode"] ?? "schema_only",
|
|
187522
|
+
...args["branch_name"] === undefined ? {} : { branch_name: args["branch_name"] },
|
|
187523
|
+
...args["ttl_seconds"] === undefined ? {} : { ttl_seconds: args["ttl_seconds"] }
|
|
187524
|
+
}, options);
|
|
187525
|
+
}
|
|
187526
|
+
} else if (action === "list_previews") {
|
|
187527
|
+
result = await http.get(`${path}/previews`, options);
|
|
187528
|
+
} else if (previewId) {
|
|
187529
|
+
if (!Check2(previewIdSchema, previewId))
|
|
187530
|
+
throw new Error("Invalid preview ID");
|
|
187531
|
+
result = action === "cleanup_preview" ? await http.deleteReleaseMutation(`${path}/previews/${previewId}`) : await http.get(`${path}/previews/${previewId}`, options);
|
|
187532
|
+
} else {
|
|
187533
|
+
throw new Error("Unknown preview action");
|
|
187534
|
+
}
|
|
187535
|
+
const state = {
|
|
187536
|
+
...identity,
|
|
187537
|
+
...previewId ? { preview_id: previewId } : {},
|
|
187538
|
+
...releaseId ? { source_release_id: releaseId } : {},
|
|
187539
|
+
reconciliation: previewId ? { action: "get_preview", ref, id, environment_id: environment, preview_id: previewId } : { action: "list_previews", ref, id, environment_id: environment }
|
|
187540
|
+
};
|
|
187541
|
+
if (!result.ok)
|
|
187542
|
+
return mutation ? releaseControlMutationFailure(operation, result, state) : releaseControlFailure(operation, "HTTP_ERROR", result.status, state);
|
|
187543
|
+
try {
|
|
187544
|
+
if (action === "list_previews") {
|
|
187545
|
+
const inventorySchema = _Object_({
|
|
187546
|
+
..._Object_({ project_ref: branchRefSchema, application_id: ApplicationIdSchema2, environment_id: ApplicationIdSchema2 }).properties,
|
|
187547
|
+
previews: _Array_(receiptSchema, { maxItems: 1000 })
|
|
187548
|
+
}, strict);
|
|
187549
|
+
if (!Check2(inventorySchema, result.data) || result.data.project_ref !== ref || result.data.application_id !== id || result.data.environment_id !== environment)
|
|
187550
|
+
throw new Error;
|
|
187551
|
+
const previews = result.data.previews.map((value) => receipt(value, { ref, id, environment }));
|
|
187552
|
+
if (new Set(previews.map((value) => value.preview_id)).size !== previews.length)
|
|
187553
|
+
throw new Error;
|
|
187554
|
+
return releaseControlSuccess(operation, { ...identity, previews });
|
|
187555
|
+
}
|
|
187556
|
+
const preview = receipt(result.data, {
|
|
187557
|
+
ref,
|
|
187558
|
+
id,
|
|
187559
|
+
environment,
|
|
187560
|
+
...previewId === undefined ? {} : { previewId }
|
|
187561
|
+
});
|
|
187562
|
+
if (action === "get_preview_plan") {
|
|
187563
|
+
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))
|
|
187564
|
+
throw new Error;
|
|
187565
|
+
} else {
|
|
187566
|
+
if (preview.status === "planned" || preview.resources.database_branch.branch_ref !== `pv${preview.preview_id.replaceAll("-", "").slice(0, 18)}`)
|
|
187567
|
+
throw new Error;
|
|
187568
|
+
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))
|
|
187569
|
+
throw new Error;
|
|
187570
|
+
}
|
|
187571
|
+
if (preview.status === "failed" || action === "cleanup_preview" && preview.status !== "cleaned") {
|
|
187572
|
+
return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", result.status, { ...identity, preview });
|
|
187573
|
+
}
|
|
187574
|
+
if (timeoutSeconds !== undefined)
|
|
187575
|
+
return await waitForPreview(http, path, operation, identity, preview, timeoutSeconds, startedAt);
|
|
187576
|
+
return releaseControlSuccess(operation, { ...identity, preview });
|
|
187577
|
+
} catch {
|
|
187578
|
+
return releaseControlFailure(operation, mutation ? "OUTCOME_UNKNOWN" : "INVALID_RESPONSE", result.status, state);
|
|
187579
|
+
}
|
|
187580
|
+
}
|
|
187581
|
+
|
|
187231
187582
|
// src/shared/tools/application-tools.ts
|
|
187232
187583
|
var APPLICATION_TOOL_SCHEMA = {
|
|
187233
187584
|
action: withDescription(stringEnum([
|
|
@@ -187241,6 +187592,7 @@ var APPLICATION_TOOL_SCHEMA = {
|
|
|
187241
187592
|
"activate_release",
|
|
187242
187593
|
"reconcile_activation",
|
|
187243
187594
|
"retire_activation",
|
|
187595
|
+
...APPLICATION_PREVIEW_ACTIONS,
|
|
187244
187596
|
"logs"
|
|
187245
187597
|
]), "Action"),
|
|
187246
187598
|
ref: withDescription(String2(), "Project ref"),
|
|
@@ -187258,7 +187610,8 @@ var APPLICATION_TOOL_SCHEMA = {
|
|
|
187258
187610
|
service: optional(String2(), "[logs] Application target/service filter"),
|
|
187259
187611
|
search: optional(String2(), "[logs] Full-text log filter"),
|
|
187260
187612
|
start: optional(String2(), "[logs] ISO start timestamp"),
|
|
187261
|
-
end: optional(String2(), "[logs] ISO end timestamp")
|
|
187613
|
+
end: optional(String2(), "[logs] ISO end timestamp"),
|
|
187614
|
+
...APPLICATION_PREVIEW_FIELDS
|
|
187262
187615
|
};
|
|
187263
187616
|
var responseSchema = _Object_({
|
|
187264
187617
|
project_ref: String2(),
|
|
@@ -187382,6 +187735,9 @@ function registerApplicationTools(server2, http) {
|
|
|
187382
187735
|
throw new Error("Invalid application ID");
|
|
187383
187736
|
const path = `/v1/projects/${project}/applications/${encodeURIComponent(id)}/releases`;
|
|
187384
187737
|
const operation = `applications.${action}`;
|
|
187738
|
+
if (APPLICATION_PREVIEW_ACTIONS.some((value) => value === action)) {
|
|
187739
|
+
return applicationPreviewAction(http, args, project);
|
|
187740
|
+
}
|
|
187385
187741
|
if (action === "activate_release" || action === "reconcile_activation" || action === "retire_activation") {
|
|
187386
187742
|
return activationAction(http, args, project);
|
|
187387
187743
|
}
|
|
@@ -188750,6 +189106,34 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
188750
189106
|
}
|
|
188751
189107
|
|
|
188752
189108
|
// src/shared/tools/branch-tools.ts
|
|
189109
|
+
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
189110
|
+
var PROMOTION_PLAN_MAX_JSON_BYTES = 512 * 1024;
|
|
189111
|
+
var PROMOTION_BLOCK_MESSAGES = {
|
|
189112
|
+
parent_ahead: "Parent history is ahead; recreate or rebase the preview branch.",
|
|
189113
|
+
checksum_mismatch: "Parent and branch migration checksums differ.",
|
|
189114
|
+
stored_checksum_mismatch: "Stored checksum does not match migration SQL.",
|
|
189115
|
+
name_conflict: "Migration name already exists in the parent.",
|
|
189116
|
+
out_of_order_migration: "Migration precedes the latest parent migration.",
|
|
189117
|
+
empty_migration: "Migration has no executable statements.",
|
|
189118
|
+
non_transactional_sql: "Migration requires a separate maintenance path.",
|
|
189119
|
+
unsupported_sql: "Migration exceeds the project-scoped SQL policy."
|
|
189120
|
+
};
|
|
189121
|
+
var PROMOTION_FAILURE_CODES = new Set([
|
|
189122
|
+
"promotion_locked",
|
|
189123
|
+
"promotion_plan_changed",
|
|
189124
|
+
"promotion_blocked",
|
|
189125
|
+
"destructive_confirmation_required",
|
|
189126
|
+
"promotion_apply_failed",
|
|
189127
|
+
"promotion_readback_failed",
|
|
189128
|
+
"promotion_failed",
|
|
189129
|
+
"promotion_plan_failed",
|
|
189130
|
+
"promotion_plan_required",
|
|
189131
|
+
"branch_not_active"
|
|
189132
|
+
]);
|
|
189133
|
+
var PROMOTION_WARNINGS = [
|
|
189134
|
+
"Only recorded migrations are promoted; branch data and untracked schema changes are not copied.",
|
|
189135
|
+
"Migration SQL can modify parent data and must be reviewed."
|
|
189136
|
+
];
|
|
188753
189137
|
function resolveProjectRef(ref, projectRef) {
|
|
188754
189138
|
const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef || "";
|
|
188755
189139
|
if (!resolved)
|
|
@@ -188761,32 +189145,188 @@ function requireString(value, field) {
|
|
|
188761
189145
|
throw new Error(`'${field}' is required`);
|
|
188762
189146
|
return value.trim();
|
|
188763
189147
|
}
|
|
189148
|
+
function isRecord4(value) {
|
|
189149
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
189150
|
+
}
|
|
189151
|
+
function isSha256(value) {
|
|
189152
|
+
return typeof value === "string" && SHA256_PATTERN.test(value);
|
|
189153
|
+
}
|
|
189154
|
+
function parsePromotionEntry(candidate) {
|
|
189155
|
+
if (!isRecord4(candidate))
|
|
189156
|
+
return null;
|
|
189157
|
+
const { version, name, checksum, statement_count: statementCount, statements, destructive } = candidate;
|
|
189158
|
+
if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
|
|
189159
|
+
return null;
|
|
189160
|
+
if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
|
|
189161
|
+
return null;
|
|
189162
|
+
if (!isSha256(checksum))
|
|
189163
|
+
return null;
|
|
189164
|
+
if (typeof statementCount !== "number" || !Number.isSafeInteger(statementCount) || statementCount < 0)
|
|
189165
|
+
return null;
|
|
189166
|
+
if (typeof destructive !== "boolean")
|
|
189167
|
+
return null;
|
|
189168
|
+
if (statements !== undefined && (!Array.isArray(statements) || statements.length !== statementCount || statements.some((statement) => typeof statement !== "string"))) {
|
|
189169
|
+
return null;
|
|
189170
|
+
}
|
|
189171
|
+
return {
|
|
189172
|
+
version,
|
|
189173
|
+
name,
|
|
189174
|
+
checksum,
|
|
189175
|
+
statement_count: statementCount,
|
|
189176
|
+
destructive
|
|
189177
|
+
};
|
|
189178
|
+
}
|
|
189179
|
+
function parsePromotionBlock(candidate) {
|
|
189180
|
+
if (!isRecord4(candidate))
|
|
189181
|
+
return null;
|
|
189182
|
+
const { code, version, name, message } = candidate;
|
|
189183
|
+
if (typeof code !== "string" || !Object.hasOwn(PROMOTION_BLOCK_MESSAGES, code))
|
|
189184
|
+
return null;
|
|
189185
|
+
if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
|
|
189186
|
+
return null;
|
|
189187
|
+
if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
|
|
189188
|
+
return null;
|
|
189189
|
+
if (typeof message !== "string")
|
|
189190
|
+
return null;
|
|
189191
|
+
const blockCode = code;
|
|
189192
|
+
return {
|
|
189193
|
+
code: blockCode,
|
|
189194
|
+
version,
|
|
189195
|
+
name,
|
|
189196
|
+
message: PROMOTION_BLOCK_MESSAGES[blockCode]
|
|
189197
|
+
};
|
|
189198
|
+
}
|
|
189199
|
+
function uniqueVersions(entries) {
|
|
189200
|
+
const versions = entries.map((entry) => entry.version);
|
|
189201
|
+
return new Set(versions).size === versions.length;
|
|
189202
|
+
}
|
|
189203
|
+
function samePromotionEntry(left, right) {
|
|
189204
|
+
return left.version === right.version && left.checksum === right.checksum && left.name === right.name && left.statement_count === right.statement_count && left.destructive === right.destructive;
|
|
189205
|
+
}
|
|
189206
|
+
function parsePromotionPlan(candidate, expectedScope) {
|
|
189207
|
+
if (!isRecord4(candidate))
|
|
189208
|
+
return null;
|
|
189209
|
+
const {
|
|
189210
|
+
mode,
|
|
189211
|
+
parent_ref: parentRef,
|
|
189212
|
+
branch_ref: branchRef,
|
|
189213
|
+
safe_to_apply: safeToApply,
|
|
189214
|
+
plan_checksum: planChecksum,
|
|
189215
|
+
pending,
|
|
189216
|
+
applied,
|
|
189217
|
+
blocked,
|
|
189218
|
+
warnings,
|
|
189219
|
+
requires_destructive_confirmation: requiresDestructiveConfirmation,
|
|
189220
|
+
ignored_branch_data: ignoredBranchData
|
|
189221
|
+
} = candidate;
|
|
189222
|
+
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) {
|
|
189223
|
+
return null;
|
|
189224
|
+
}
|
|
189225
|
+
if (parentRef !== expectedScope.parentRef || branchRef !== expectedScope.branchRef)
|
|
189226
|
+
return null;
|
|
189227
|
+
const pendingEntries = pending.map(parsePromotionEntry);
|
|
189228
|
+
const appliedEntries = applied.map(parsePromotionEntry);
|
|
189229
|
+
const blockingFindings = blocked.map(parsePromotionBlock);
|
|
189230
|
+
if (pendingEntries.some((entry) => entry === null) || appliedEntries.some((entry) => entry === null) || blockingFindings.some((entry) => entry === null) || warnings.some((warning) => typeof warning !== "string")) {
|
|
189231
|
+
return null;
|
|
189232
|
+
}
|
|
189233
|
+
const normalizedPending = pendingEntries.filter((entry) => entry !== null);
|
|
189234
|
+
const normalizedApplied = appliedEntries.filter((entry) => entry !== null);
|
|
189235
|
+
const normalizedBlocked = blockingFindings.filter((entry) => entry !== null);
|
|
189236
|
+
const hasDestructivePending = normalizedPending.some((entry) => entry.destructive);
|
|
189237
|
+
if (!uniqueVersions(normalizedPending) || !uniqueVersions(normalizedApplied) || !uniqueVersions([...normalizedPending, ...normalizedApplied]) || normalizedPending.some((entry) => entry.statement_count === 0) || safeToApply !== (normalizedBlocked.length === 0) || requiresDestructiveConfirmation !== hasDestructivePending) {
|
|
189238
|
+
return null;
|
|
189239
|
+
}
|
|
189240
|
+
return {
|
|
189241
|
+
mode,
|
|
189242
|
+
parent_ref: parentRef,
|
|
189243
|
+
branch_ref: branchRef,
|
|
189244
|
+
safe_to_apply: safeToApply,
|
|
189245
|
+
plan_checksum: planChecksum,
|
|
189246
|
+
pending: normalizedPending,
|
|
189247
|
+
applied: normalizedApplied,
|
|
189248
|
+
blocked: normalizedBlocked,
|
|
189249
|
+
warnings: [...PROMOTION_WARNINGS],
|
|
189250
|
+
requires_destructive_confirmation: requiresDestructiveConfirmation,
|
|
189251
|
+
ignored_branch_data: true
|
|
189252
|
+
};
|
|
189253
|
+
}
|
|
189254
|
+
function parsePromotionResult(candidate, expectedScope, reviewedPending) {
|
|
189255
|
+
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)) {
|
|
189256
|
+
return null;
|
|
189257
|
+
}
|
|
189258
|
+
const applied = candidate.applied.map(parsePromotionEntry);
|
|
189259
|
+
if (applied.some((entry) => entry === null))
|
|
189260
|
+
return null;
|
|
189261
|
+
const normalizedApplied = applied.filter((entry) => entry !== null);
|
|
189262
|
+
if (!uniqueVersions(normalizedApplied) || normalizedApplied.length !== reviewedPending.length)
|
|
189263
|
+
return null;
|
|
189264
|
+
const reviewedByVersion = new Map(reviewedPending.map((entry) => [entry.version, entry]));
|
|
189265
|
+
if (normalizedApplied.some((entry) => {
|
|
189266
|
+
const reviewed = reviewedByVersion.get(entry.version);
|
|
189267
|
+
return !reviewed || !samePromotionEntry(entry, reviewed);
|
|
189268
|
+
}))
|
|
189269
|
+
return null;
|
|
189270
|
+
const plan = parsePromotionPlan(candidate.plan, expectedScope);
|
|
189271
|
+
if (!plan || !plan.safe_to_apply || plan.pending.length > 0)
|
|
189272
|
+
return null;
|
|
189273
|
+
const readbackByVersion = new Map(plan.applied.map((entry) => [entry.version, entry]));
|
|
189274
|
+
if (normalizedApplied.some((entry) => {
|
|
189275
|
+
const readback = readbackByVersion.get(entry.version);
|
|
189276
|
+
return !readback || !samePromotionEntry(entry, readback);
|
|
189277
|
+
}))
|
|
189278
|
+
return null;
|
|
189279
|
+
return {
|
|
189280
|
+
promoted: true,
|
|
189281
|
+
mode: "migrations",
|
|
189282
|
+
project_ref: expectedScope.parentRef,
|
|
189283
|
+
branch_ref: expectedScope.branchRef,
|
|
189284
|
+
applied: normalizedApplied,
|
|
189285
|
+
plan,
|
|
189286
|
+
branch_data_copied: false
|
|
189287
|
+
};
|
|
189288
|
+
}
|
|
189289
|
+
function safePromotionCode(result) {
|
|
189290
|
+
if (!isRecord4(result.data) || typeof result.data.code !== "string")
|
|
189291
|
+
return;
|
|
189292
|
+
return PROMOTION_FAILURE_CODES.has(result.data.code) ? result.data.code : undefined;
|
|
189293
|
+
}
|
|
189294
|
+
function appliedVersions(result) {
|
|
189295
|
+
if (result.transportError || result.responseReadError || !isRecord4(result.data) || !Array.isArray(result.data.applied))
|
|
189296
|
+
return [];
|
|
189297
|
+
const entries = result.data.applied.map(parsePromotionEntry);
|
|
189298
|
+
if (entries.some((entry) => !entry))
|
|
189299
|
+
return [];
|
|
189300
|
+
const validated = entries.filter((entry) => entry !== null);
|
|
189301
|
+
return uniqueVersions(validated) ? validated.map((entry) => entry.version) : [];
|
|
189302
|
+
}
|
|
189303
|
+
function promotionFailure(response, json, unknownOutcome) {
|
|
189304
|
+
if (json)
|
|
189305
|
+
return response;
|
|
189306
|
+
const state = JSON.parse(response.content[0].text);
|
|
189307
|
+
if (!isRecord4(state) || !isRecord4(state.error))
|
|
189308
|
+
return response;
|
|
189309
|
+
const lines = [
|
|
189310
|
+
unknownOutcome ? "Promotion outcome is unknown; do not repeat the request automatically." : "Promotion request was not verified.",
|
|
189311
|
+
`Error: ${state.error.code}`,
|
|
189312
|
+
`Project: ${state.project_ref}; branch: ${state.branch_ref}`,
|
|
189313
|
+
...typeof state.reason === "string" ? [`Reason: ${state.reason}`] : [],
|
|
189314
|
+
...state.mutation_sent === false ? ["No promotion request was sent."] : [],
|
|
189315
|
+
...typeof state.plan_checksum === "string" ? [`Reviewed checksum: ${state.plan_checksum}`] : [],
|
|
189316
|
+
...Array.isArray(state.reported_applied_versions) && state.reported_applied_versions.length > 0 ? [`Server-reported applied versions: ${state.reported_applied_versions.join(", ")}`] : [],
|
|
189317
|
+
"Fetch a fresh promotion_plan and review ledger state before another promote."
|
|
189318
|
+
];
|
|
189319
|
+
return { isError: true, content: [{ type: "text", text: lines.join(`
|
|
189320
|
+
`) }] };
|
|
189321
|
+
}
|
|
188764
189322
|
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
|
-
}
|
|
189323
|
+
if (isRecord4(result.data)) {
|
|
189324
|
+
const message = result.data.error || result.data.message;
|
|
189325
|
+
if (typeof message === "string")
|
|
189326
|
+
return message;
|
|
188781
189327
|
}
|
|
188782
189328
|
return `Request failed with HTTP ${result.status}`;
|
|
188783
189329
|
}
|
|
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
189330
|
function projectPath(ref) {
|
|
188791
189331
|
return `/v1/projects/${encodeURIComponent(ref)}/branches`;
|
|
188792
189332
|
}
|
|
@@ -188828,14 +189368,10 @@ function readOnlyResult() {
|
|
|
188828
189368
|
content: [{ type: "text", text: "⚠️ Branch write blocked in read-only mode." }]
|
|
188829
189369
|
};
|
|
188830
189370
|
}
|
|
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");
|
|
189371
|
+
function formatPromotionResult(response) {
|
|
189372
|
+
const versions = response.applied.map((entry) => entry.version);
|
|
188837
189373
|
return [
|
|
188838
|
-
`Migration promotion completed: ${applied.length} applied.`,
|
|
189374
|
+
`Migration promotion completed: ${response.applied.length} applied.`,
|
|
188839
189375
|
...versions.length > 0 ? [`Versions: ${versions.join(", ")}`] : [],
|
|
188840
189376
|
"Branch data was not automatically copied to the parent project."
|
|
188841
189377
|
].join(`
|
|
@@ -188855,13 +189391,19 @@ function registerBranchTools(server2, http, options = {}) {
|
|
|
188855
189391
|
name: optional(String2(), "[create] Branch name"),
|
|
188856
189392
|
data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[create] Preview data mode (default: schema_only)"),
|
|
188857
189393
|
plan_checksum: optional(String2(), "[promote] Reviewed plan checksum from promotion_plan"),
|
|
188858
|
-
confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations")
|
|
189394
|
+
confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations"),
|
|
189395
|
+
json: optional(Boolean2(), "[promotion_plan/promote] Return machine-readable JSON")
|
|
188859
189396
|
}, async (args) => {
|
|
188860
189397
|
const action = requireString(args.action, "action");
|
|
188861
189398
|
const ref = resolveProjectRef(args.ref, options.projectRef);
|
|
188862
189399
|
const writeAction = action === "create" || action === "delete" || action === "promote";
|
|
188863
|
-
if (writeAction && options.readOnly)
|
|
188864
|
-
return
|
|
189400
|
+
if (writeAction && options.readOnly) {
|
|
189401
|
+
return args.json && action === "promote" ? releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, {
|
|
189402
|
+
project_ref: ref,
|
|
189403
|
+
branch_ref: args.branch_ref,
|
|
189404
|
+
reason: "READ_ONLY"
|
|
189405
|
+
}) : readOnlyResult();
|
|
189406
|
+
}
|
|
188865
189407
|
let result;
|
|
188866
189408
|
if (action === "list") {
|
|
188867
189409
|
result = await http.get(projectPath(ref));
|
|
@@ -188876,29 +189418,91 @@ function registerBranchTools(server2, http, options = {}) {
|
|
|
188876
189418
|
result = await http.delete(branchPath(ref, branchRef));
|
|
188877
189419
|
} else if (action === "promotion_plan") {
|
|
188878
189420
|
const branchRef = requireString(args.branch_ref, "branch_ref");
|
|
188879
|
-
|
|
188880
|
-
|
|
188881
|
-
|
|
189421
|
+
const scope = { project_ref: ref, branch_ref: branchRef };
|
|
189422
|
+
result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
|
|
189423
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189424
|
+
responseTimeoutMs: 5000
|
|
189425
|
+
});
|
|
189426
|
+
if (!result.ok) {
|
|
189427
|
+
return promotionFailure(releaseControlFailure("branch.promotion_plan", "HTTP_ERROR", result.transportError ? null : result.status, scope), args.json === true, false);
|
|
188882
189428
|
}
|
|
189429
|
+
const plan = parsePromotionPlan(result.data, { parentRef: ref, branchRef });
|
|
189430
|
+
if (result.status !== 200 || !plan) {
|
|
189431
|
+
return promotionFailure(releaseControlFailure("branch.promotion_plan", "INVALID_RESPONSE", result.status, scope), args.json === true, false);
|
|
189432
|
+
}
|
|
189433
|
+
return args.json ? releaseControlSuccess("branch.promotion_plan", { ...scope, plan }) : { content: [{ type: "text", text: formatPromotionPlan(plan) }] };
|
|
188883
189434
|
} else if (action === "promote") {
|
|
188884
189435
|
const branchRef = requireString(args.branch_ref, "branch_ref");
|
|
188885
189436
|
const planChecksum = requireString(args.plan_checksum, "plan_checksum");
|
|
189437
|
+
if (!isSha256(planChecksum))
|
|
189438
|
+
throw new Error("'plan_checksum' must be a 64-character lowercase SHA-256 checksum");
|
|
189439
|
+
const state = {
|
|
189440
|
+
project_ref: ref,
|
|
189441
|
+
branch_ref: branchRef,
|
|
189442
|
+
plan_checksum: planChecksum,
|
|
189443
|
+
automatic_retry: false,
|
|
189444
|
+
reconciliation: { action: "promotion_plan", ref, branch_ref: branchRef }
|
|
189445
|
+
};
|
|
189446
|
+
const preflight = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
|
|
189447
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189448
|
+
responseTimeoutMs: 5000
|
|
189449
|
+
});
|
|
189450
|
+
const beforeMutation = { ...state, mutation_sent: false };
|
|
189451
|
+
if (!preflight.ok) {
|
|
189452
|
+
return promotionFailure(releaseControlFailure("branch.promote", "HTTP_ERROR", preflight.transportError ? null : preflight.status, { ...beforeMutation, reason: "PREFLIGHT_FAILED" }), args.json === true, false);
|
|
189453
|
+
}
|
|
189454
|
+
const plan = parsePromotionPlan(preflight.data, { parentRef: ref, branchRef });
|
|
189455
|
+
if (preflight.status !== 200 || !plan) {
|
|
189456
|
+
return promotionFailure(releaseControlFailure("branch.promote", "INVALID_RESPONSE", preflight.status, { ...beforeMutation, reason: "PREFLIGHT_UNVERIFIED" }), args.json === true, false);
|
|
189457
|
+
}
|
|
189458
|
+
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;
|
|
189459
|
+
if (preflightReason) {
|
|
189460
|
+
return promotionFailure(releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, { ...beforeMutation, reason: preflightReason, observed_plan_checksum: plan.plan_checksum }), args.json === true, false);
|
|
189461
|
+
}
|
|
189462
|
+
if (plan.pending.length === 0) {
|
|
189463
|
+
return args.json ? releaseControlSuccess("branch.promote", {
|
|
189464
|
+
...beforeMutation,
|
|
189465
|
+
promoted: false,
|
|
189466
|
+
unchanged: true,
|
|
189467
|
+
mode: "migrations",
|
|
189468
|
+
applied: [],
|
|
189469
|
+
plan,
|
|
189470
|
+
branch_data_copied: false,
|
|
189471
|
+
reviewed_plan_checksum: planChecksum
|
|
189472
|
+
}) : { content: [{ type: "text", text: "Migration promotion unchanged: no pending migrations; no promotion request sent." }] };
|
|
189473
|
+
}
|
|
188886
189474
|
result = await http.post(`${branchPath(ref, branchRef)}/promote`, {
|
|
188887
189475
|
mode: "migrations",
|
|
188888
189476
|
plan_checksum: planChecksum,
|
|
188889
189477
|
confirm_destructive: args.confirm_destructive === true
|
|
189478
|
+
}, {
|
|
189479
|
+
maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
|
|
189480
|
+
responseTimeoutMs: 5000,
|
|
189481
|
+
timeoutMs: 30000
|
|
188890
189482
|
});
|
|
189483
|
+
if (!result.ok) {
|
|
189484
|
+
return promotionFailure(releaseControlMutationFailure("branch.promote", result, {
|
|
189485
|
+
...state,
|
|
189486
|
+
mutation_sent: true,
|
|
189487
|
+
reported_applied_versions: appliedVersions(result),
|
|
189488
|
+
...safePromotionCode(result) ? { server_code: safePromotionCode(result) } : {}
|
|
189489
|
+
}), args.json === true, result.transportError === true || result.responseReadError === true || result.status === 408 || result.status >= 500);
|
|
189490
|
+
}
|
|
189491
|
+
const promoted = parsePromotionResult(result.data, { parentRef: ref, branchRef }, plan.pending);
|
|
189492
|
+
if (result.status !== 200 || !promoted) {
|
|
189493
|
+
return promotionFailure(releaseControlFailure("branch.promote", "OUTCOME_UNKNOWN", result.status, { ...state, mutation_sent: true }), args.json === true, true);
|
|
189494
|
+
}
|
|
189495
|
+
return args.json ? releaseControlSuccess("branch.promote", {
|
|
189496
|
+
...promoted,
|
|
189497
|
+
unchanged: false,
|
|
189498
|
+
mutation_sent: true,
|
|
189499
|
+
reviewed_plan_checksum: planChecksum
|
|
189500
|
+
}) : { content: [{ type: "text", text: formatPromotionResult(promoted) }] };
|
|
188891
189501
|
} else {
|
|
188892
189502
|
throw new Error(`Unknown branch action: ${action}`);
|
|
188893
189503
|
}
|
|
188894
189504
|
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) }] };
|
|
189505
|
+
return { isError: true, content: [{ type: "text", text: responseError(result) }] };
|
|
188902
189506
|
}
|
|
188903
189507
|
return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
|
|
188904
189508
|
});
|
|
@@ -189433,7 +190037,7 @@ import { resolve as resolve11 } from "node:path";
|
|
|
189433
190037
|
// src/shared/tools/lite-cli-command.ts
|
|
189434
190038
|
import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "node:fs";
|
|
189435
190039
|
import { isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve10 } from "node:path";
|
|
189436
|
-
function
|
|
190040
|
+
function isRecord5(value) {
|
|
189437
190041
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
189438
190042
|
}
|
|
189439
190043
|
function resolveLiteCommand(workdir, environment = process.env) {
|
|
@@ -189448,7 +190052,7 @@ function resolveLiteCommand(workdir, environment = process.env) {
|
|
|
189448
190052
|
if (!existsSync8(manifestPath))
|
|
189449
190053
|
return ["supacloud-lite"];
|
|
189450
190054
|
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
189451
|
-
if (!
|
|
190055
|
+
if (!isRecord5(manifest) || manifest.name !== "@supacloud/lite" || !isRecord5(manifest.bin)) {
|
|
189452
190056
|
throw new Error("Invalid installed @supacloud/lite package manifest");
|
|
189453
190057
|
}
|
|
189454
190058
|
const entry = manifest.bin["supacloud-lite"];
|
|
@@ -193380,18 +193984,30 @@ var REMOTE_APP_ACTIONS = {
|
|
|
193380
193984
|
rollback: "activate_release",
|
|
193381
193985
|
reconcile: "reconcile_activation",
|
|
193382
193986
|
retire: "retire_activation",
|
|
193383
|
-
logs: "logs"
|
|
193987
|
+
logs: "logs",
|
|
193988
|
+
"preview-plan": "get_preview_plan",
|
|
193989
|
+
preview: "create_preview",
|
|
193990
|
+
previews: "list_previews",
|
|
193991
|
+
"preview-status": "get_preview",
|
|
193992
|
+
"preview-cleanup": "cleanup_preview"
|
|
193384
193993
|
};
|
|
193385
193994
|
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 immutable configuration revision",
|
|
193995
|
+
ref: "[upload/configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Project ref (defaults to context)",
|
|
193996
|
+
id: "[upload/configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Application ID",
|
|
193997
|
+
environment_id: "[configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Environment ID",
|
|
193998
|
+
configuration_id: "[deploy/rollback/preview] Required immutable configuration revision",
|
|
193390
193999
|
activation_id: "[deploy/rollback/reconcile/retire] Required explicit activation ID",
|
|
193391
194000
|
expected_activation_id: "[deploy/rollback] Required current activation ID, or absent for first activation",
|
|
193392
194001
|
configuration_path: "[configure] Configuration write JSON including revision and expected revision",
|
|
193393
194002
|
manifest_path: "[upload] Local delivery.manifest.json",
|
|
193394
|
-
release_id: "[deploy/rollback/reconcile] Required immutable application release ID"
|
|
194003
|
+
release_id: "[deploy/rollback/reconcile/preview-plan/preview] Required immutable application release ID",
|
|
194004
|
+
preview_id: "[preview-status/preview-cleanup] Preview receipt ID",
|
|
194005
|
+
branch_ref: "[preview-plan] Proposed branch ref; preview assigns its own",
|
|
194006
|
+
branch_name: "[preview] Branch display name",
|
|
194007
|
+
data_mode: "[preview-plan/preview] Default schema_only; full_clone copies rows",
|
|
194008
|
+
ttl_seconds: "[preview-plan/preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL",
|
|
194009
|
+
wait: "[preview/preview-status] Wait for verified readiness of the selected preview",
|
|
194010
|
+
timeout_seconds: "[preview/preview-status] Observation budget with --wait, from 1 to 3600 seconds (default 300)"
|
|
193395
194011
|
};
|
|
193396
194012
|
var { action: _remoteAction, ...remoteFields } = APPLICATION_TOOL_SCHEMA;
|
|
193397
194013
|
var REMOTE_APP_SCHEMA = Partial(_Object_(remoteFields)).properties;
|
|
@@ -194396,7 +195012,12 @@ function registerAppTools(server2, options = {}) {
|
|
|
194396
195012
|
"rollback",
|
|
194397
195013
|
"reconcile",
|
|
194398
195014
|
"retire",
|
|
194399
|
-
"logs"
|
|
195015
|
+
"logs",
|
|
195016
|
+
"preview-plan",
|
|
195017
|
+
"preview",
|
|
195018
|
+
"previews",
|
|
195019
|
+
"preview-status",
|
|
195020
|
+
"preview-cleanup"
|
|
194400
195021
|
]), "App action; upload/configure only prepare, rollback activates an explicit old release without schema downgrade"),
|
|
194401
195022
|
kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource"]), "[generate] Scaffold kind"),
|
|
194402
195023
|
template: optional(stringEnum(["minimal", "http", "command", "edge"]), "[init] Minimal application by default; explicit http/command/edge recipes"),
|
|
@@ -195840,7 +196461,7 @@ var RELEASE_CANARY_DISABLE_RECEIPT_KEYS = new Set(["fixtureId", "state", "idempo
|
|
|
195840
196461
|
var RELEASE_CANARY_DISABLE_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_disable";
|
|
195841
196462
|
var RELEASE_CANARY_PENDING_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_pending";
|
|
195842
196463
|
var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
|
|
195843
|
-
function
|
|
196464
|
+
function isRecord6(value) {
|
|
195844
196465
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
195845
196466
|
}
|
|
195846
196467
|
function canonicalTimestamp4(value) {
|
|
@@ -195856,7 +196477,7 @@ function backupBelongsToProject(backupId, projectRef) {
|
|
|
195856
196477
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef}_`);
|
|
195857
196478
|
}
|
|
195858
196479
|
function verifiedBackup(value, projectRef) {
|
|
195859
|
-
if (!
|
|
196480
|
+
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
196481
|
return null;
|
|
195861
196482
|
return {
|
|
195862
196483
|
backup_id: value.backup_id,
|
|
@@ -195870,7 +196491,7 @@ function verifiedBackup(value, projectRef) {
|
|
|
195870
196491
|
};
|
|
195871
196492
|
}
|
|
195872
196493
|
function backupInventory(value, projectRef) {
|
|
195873
|
-
if (!
|
|
196494
|
+
if (!isRecord6(value) || !Array.isArray(value.backups))
|
|
195874
196495
|
return null;
|
|
195875
196496
|
const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef));
|
|
195876
196497
|
if (backups.some((backup) => backup === null))
|
|
@@ -195949,7 +196570,7 @@ function readInventoryFailure(operation, read) {
|
|
|
195949
196570
|
return null;
|
|
195950
196571
|
}
|
|
195951
196572
|
function postgrestStatus(value) {
|
|
195952
|
-
if (!
|
|
196573
|
+
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
196574
|
return null;
|
|
195954
196575
|
return {
|
|
195955
196576
|
desired: value.desired,
|
|
@@ -195970,7 +196591,7 @@ function readPostgrestFailure(operation, read) {
|
|
|
195970
196591
|
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
195971
196592
|
}
|
|
195972
196593
|
function isRestartReceipt(value) {
|
|
195973
|
-
return
|
|
196594
|
+
return isRecord6(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
195974
196595
|
}
|
|
195975
196596
|
function releaseCanaryStageInput(subject, requestId) {
|
|
195976
196597
|
if (typeof subject !== "string" || !UUID.test(subject))
|
|
@@ -195980,7 +196601,7 @@ function releaseCanaryStageInput(subject, requestId) {
|
|
|
195980
196601
|
return { p_subject: subject, p_request_id: requestId };
|
|
195981
196602
|
}
|
|
195982
196603
|
function releaseCanaryStageReceipt(value) {
|
|
195983
|
-
if (!
|
|
196604
|
+
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
196605
|
return null;
|
|
195985
196606
|
return {
|
|
195986
196607
|
fixtureId: value.fixtureId,
|
|
@@ -196023,7 +196644,7 @@ function releaseCanaryDisableInput(fixtureId, disableRequestId, issuer, subject)
|
|
|
196023
196644
|
};
|
|
196024
196645
|
}
|
|
196025
196646
|
function releaseCanaryDisableReceipt(receiptCandidate, fixtureId) {
|
|
196026
|
-
if (!
|
|
196647
|
+
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
196648
|
return null;
|
|
196028
196649
|
return { fixtureId, state: "disabled", idempotent: receiptCandidate.idempotent };
|
|
196029
196650
|
}
|
|
@@ -196196,7 +196817,7 @@ function handleScopeRebind(args) {
|
|
|
196196
196817
|
} catch {
|
|
196197
196818
|
throw new Error(`Scope file '${filePath}' is not valid JSON`);
|
|
196198
196819
|
}
|
|
196199
|
-
if (!
|
|
196820
|
+
if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
|
|
196200
196821
|
throw new Error(`Scope file '${filePath}' has missing or invalid baseCommit`);
|
|
196201
196822
|
}
|
|
196202
196823
|
const previousBaseCommit = parsed.baseCommit;
|
|
@@ -196260,7 +196881,7 @@ function handleScopeInspect(args) {
|
|
|
196260
196881
|
} catch {
|
|
196261
196882
|
throw new Error(`Scope file '${args.file}' is not valid JSON`);
|
|
196262
196883
|
}
|
|
196263
|
-
if (!
|
|
196884
|
+
if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
|
|
196264
196885
|
throw new Error(`Scope file '${args.file}' has missing or invalid baseCommit`);
|
|
196265
196886
|
}
|
|
196266
196887
|
const scopeSha256 = genericScopedReleaseScopeSha256(parsed);
|
|
@@ -196422,7 +197043,7 @@ function registerReleaseTools(server2, http, options = {}) {
|
|
|
196422
197043
|
if (!mutation.ok || mutation.status !== 200) {
|
|
196423
197044
|
return mutationFailure("release.logical_backup.create", mutation);
|
|
196424
197045
|
}
|
|
196425
|
-
const responseBackup =
|
|
197046
|
+
const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.backup, projectRef) : null;
|
|
196426
197047
|
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
196427
197048
|
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
196428
197049
|
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
@@ -196447,7 +197068,7 @@ function registerReleaseTools(server2, http, options = {}) {
|
|
|
196447
197068
|
if (!mutation.ok || mutation.status !== 200) {
|
|
196448
197069
|
return mutationFailure("release.logical_backup.restore", mutation);
|
|
196449
197070
|
}
|
|
196450
|
-
const responseBackup =
|
|
197071
|
+
const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.restored_backup, projectRef) : null;
|
|
196451
197072
|
const after = await readInventory(http, projectRef);
|
|
196452
197073
|
const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
|
|
196453
197074
|
const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
|
|
@@ -197964,7 +198585,7 @@ function registerRemoteDevTools(server2, options = {}) {
|
|
|
197964
198585
|
// package.json
|
|
197965
198586
|
var package_default = {
|
|
197966
198587
|
name: "@supacloud/cli",
|
|
197967
|
-
version: "0.
|
|
198588
|
+
version: "0.78.0",
|
|
197968
198589
|
description: "Project-scoped CLI for SupaCloud users",
|
|
197969
198590
|
type: "module",
|
|
197970
198591
|
main: "./dist/index.js",
|
|
@@ -198286,6 +198907,11 @@ EXAMPLES
|
|
|
198286
198907
|
${preferredCommand} app configure --ref abc123 --id orders --environment_id test --configuration_path configuration.json
|
|
198287
198908
|
${preferredCommand} app deploy --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --activation_id <uuid> --expected_activation_id absent
|
|
198288
198909
|
${preferredCommand} app status --ref abc123 --id orders --environment_id test
|
|
198910
|
+
${preferredCommand} app preview-plan --ref abc123 --id orders --environment_id test --release_id <sha256> --branch_ref preview-orders
|
|
198911
|
+
${preferredCommand} app preview --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --ttl_seconds 3600 --wait
|
|
198912
|
+
${preferredCommand} app previews --ref abc123 --id orders --environment_id test
|
|
198913
|
+
${preferredCommand} app preview-status --ref abc123 --id orders --environment_id test --preview_id <uuid> --wait --timeout_seconds 300
|
|
198914
|
+
${preferredCommand} app preview-cleanup --ref abc123 --id orders --environment_id test --preview_id <uuid>
|
|
198289
198915
|
${preferredCommand} app logs --ref abc123 --id orders --environment_id test
|
|
198290
198916
|
${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
198917
|
${preferredCommand} app check --root . --strict
|
|
@@ -9,6 +9,11 @@ declare const REMOTE_APP_ACTIONS: {
|
|
|
9
9
|
readonly reconcile: "reconcile_activation";
|
|
10
10
|
readonly retire: "retire_activation";
|
|
11
11
|
readonly logs: "logs";
|
|
12
|
+
readonly "preview-plan": "get_preview_plan";
|
|
13
|
+
readonly preview: "create_preview";
|
|
14
|
+
readonly previews: "list_previews";
|
|
15
|
+
readonly "preview-status": "get_preview";
|
|
16
|
+
readonly "preview-cleanup": "cleanup_preview";
|
|
12
17
|
};
|
|
13
18
|
export interface AppToolOptions {
|
|
14
19
|
getApplications?: () => ((args: Record<string, unknown>) => Promise<ToolResult>) | undefined;
|
|
@@ -29,6 +34,13 @@ export interface AppToolArguments {
|
|
|
29
34
|
configuration_path?: string;
|
|
30
35
|
manifest_path?: string;
|
|
31
36
|
release_id?: string;
|
|
37
|
+
preview_id?: string;
|
|
38
|
+
branch_ref?: string;
|
|
39
|
+
branch_name?: string;
|
|
40
|
+
data_mode?: "schema_only" | "full_clone";
|
|
41
|
+
ttl_seconds?: number;
|
|
42
|
+
wait?: boolean;
|
|
43
|
+
timeout_seconds?: number;
|
|
32
44
|
kind?: "module" | "command" | "query" | "controller" | "job" | "contract" | "resource";
|
|
33
45
|
template?: "minimal" | "http" | "command" | "edge";
|
|
34
46
|
name?: string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import type { HttpTransport } from "../transports/http";
|
|
3
|
+
export declare const APPLICATION_PREVIEW_ACTIONS: readonly ["get_preview_plan", "create_preview", "list_previews", "get_preview", "cleanup_preview"];
|
|
4
|
+
export declare const APPLICATION_PREVIEW_FIELDS: {
|
|
5
|
+
preview_id: Type.TOptional<Type.TString>;
|
|
6
|
+
branch_ref: Type.TOptional<Type.TString>;
|
|
7
|
+
branch_name: Type.TOptional<Type.TString>;
|
|
8
|
+
data_mode: Type.TOptional<Type.TEnum<("schema_only" | "full_clone")[]>>;
|
|
9
|
+
ttl_seconds: Type.TOptional<Type.TInteger>;
|
|
10
|
+
wait: Type.TOptional<Type.TBoolean>;
|
|
11
|
+
timeout_seconds: Type.TOptional<Type.TInteger>;
|
|
12
|
+
};
|
|
13
|
+
declare const receiptSchema: Type.TObject<{
|
|
14
|
+
schema: Type.TLiteral<"supacloud.application-preview.v1">;
|
|
15
|
+
preview_id: Type.TString;
|
|
16
|
+
project_ref: Type.TString;
|
|
17
|
+
application_id: Type.TString;
|
|
18
|
+
environment_id: Type.TString;
|
|
19
|
+
release_id: Type.TString;
|
|
20
|
+
expires_at: Type.TUnion<[Type.TString, Type.TNull]>;
|
|
21
|
+
status: Type.TEnum<("failed" | "ready" | "planned" | "cleaned" | "provisioning")[]>;
|
|
22
|
+
resources: Type.TObject<{
|
|
23
|
+
build_artifact: Type.TObject<{
|
|
24
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
25
|
+
release_id: Type.TString;
|
|
26
|
+
}>;
|
|
27
|
+
database_branch: Type.TObject<{
|
|
28
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
29
|
+
branch_ref: Type.TString;
|
|
30
|
+
data_mode: Type.TEnum<("schema_only" | "full_clone")[]>;
|
|
31
|
+
}>;
|
|
32
|
+
queue_namespace: Type.TObject<{
|
|
33
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
34
|
+
namespace: Type.TString;
|
|
35
|
+
}>;
|
|
36
|
+
storage_namespace: Type.TObject<{
|
|
37
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
38
|
+
namespace: Type.TString;
|
|
39
|
+
}>;
|
|
40
|
+
test_secret: Type.TObject<{
|
|
41
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
42
|
+
name: Type.TString;
|
|
43
|
+
value_issued: Type.TLiteral<false>;
|
|
44
|
+
}>;
|
|
45
|
+
configuration_revision: Type.TObject<{
|
|
46
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
47
|
+
configuration_id: Type.TUnion<[Type.TString, Type.TNull]>;
|
|
48
|
+
}>;
|
|
49
|
+
application_activation: Type.TObject<{
|
|
50
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
51
|
+
activation_id: Type.TUnion<[Type.TString, Type.TNull]>;
|
|
52
|
+
}>;
|
|
53
|
+
smoke_test: Type.TObject<{
|
|
54
|
+
status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
|
|
55
|
+
checks: Type.TArray<Type.TString>;
|
|
56
|
+
passed: Type.TArray<Type.TString>;
|
|
57
|
+
failed: Type.TArray<Type.TString>;
|
|
58
|
+
}>;
|
|
59
|
+
}>;
|
|
60
|
+
cleanup: Type.TObject<{
|
|
61
|
+
required: Type.TBoolean;
|
|
62
|
+
completed: Type.TBoolean;
|
|
63
|
+
error: Type.TUnion<[Type.TString, Type.TNull]>;
|
|
64
|
+
}>;
|
|
65
|
+
branch_name: Type.TOptional<Type.TString>;
|
|
66
|
+
queue_name: Type.TOptional<Type.TString>;
|
|
67
|
+
test_secret_name: Type.TOptional<Type.TString>;
|
|
68
|
+
source_configuration_id: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
|
69
|
+
created_at: Type.TOptional<Type.TString>;
|
|
70
|
+
updated_at: Type.TOptional<Type.TString>;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function applicationPreviewAction(http: HttpTransport, args: Record<string, unknown>, project: string): Promise<import("./release-control-response").ReleaseControlToolResponse>;
|
|
73
|
+
export type ApplicationPreviewReceipt = Static<typeof receiptSchema>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,7 +2,14 @@ import { Type } from "typebox";
|
|
|
2
2
|
import type { HttpTransport } from "../transports/http";
|
|
3
3
|
import { type ToolServer } from "../tool-server";
|
|
4
4
|
export declare const APPLICATION_TOOL_SCHEMA: {
|
|
5
|
-
|
|
5
|
+
preview_id: Type.TOptional<Type.TString>;
|
|
6
|
+
branch_ref: Type.TOptional<Type.TString>;
|
|
7
|
+
branch_name: Type.TOptional<Type.TString>;
|
|
8
|
+
data_mode: Type.TOptional<Type.TEnum<("schema_only" | "full_clone")[]>>;
|
|
9
|
+
ttl_seconds: Type.TOptional<Type.TInteger>;
|
|
10
|
+
wait: Type.TOptional<Type.TBoolean>;
|
|
11
|
+
timeout_seconds: Type.TOptional<Type.TInteger>;
|
|
12
|
+
action: Type.TEnum<("list_releases" | "get_release" | "upload_release" | "activate_release" | "logs" | "get_runtime" | "get_deployment_evidence" | "get_configuration" | "get_preview_plan" | "put_configuration" | "reconcile_activation" | "retire_activation" | "create_preview" | "list_previews" | "get_preview" | "cleanup_preview")[]>;
|
|
6
13
|
ref: Type.TString;
|
|
7
14
|
id: Type.TString;
|
|
8
15
|
environment_id: Type.TOptional<Type.TString>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HttpTransport } from "../transports/http";
|
|
2
2
|
import { type ToolServer } from "../tool-server";
|
|
3
|
-
export
|
|
3
|
+
export type BranchHttpTransport = Pick<HttpTransport, "get" | "post" | "delete">;
|
|
4
|
+
export declare function registerBranchTools(server: ToolServer, http: BranchHttpTransport, options?: {
|
|
4
5
|
projectRef?: string;
|
|
5
6
|
readOnly?: boolean;
|
|
6
7
|
}): void;
|
|
@@ -18,6 +18,10 @@ export interface HttpResult<T = unknown> {
|
|
|
18
18
|
responseReadError?: true;
|
|
19
19
|
}
|
|
20
20
|
export interface HttpGetOptions {
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
retry?: boolean;
|
|
23
|
+
/** Complete header/body budget for a bounded JSON GET with retries disabled. */
|
|
24
|
+
totalTimeoutMs?: number;
|
|
21
25
|
maxResponseBytes?: number;
|
|
22
26
|
maxJsonBytes?: number;
|
|
23
27
|
responseTimeoutMs?: number;
|