@sandblocks/cli 0.5.0-b.27 → 0.5.0-b.29
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/dist/cli.js +737 -92
- package/dist/cli.js.map +8 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,24 @@ function compileProjectGraph(manifest) {
|
|
|
60
60
|
timeoutSeconds: service.health.timeout,
|
|
61
61
|
failureThreshold: service.health.failures
|
|
62
62
|
}
|
|
63
|
+
} : {},
|
|
64
|
+
...service.development ? {
|
|
65
|
+
development: {
|
|
66
|
+
command: normalizeCommand(service.development.command),
|
|
67
|
+
...service.development.health ? {
|
|
68
|
+
health: {
|
|
69
|
+
path: service.development.health.path,
|
|
70
|
+
intervalSeconds: service.development.health.interval,
|
|
71
|
+
timeoutSeconds: service.development.health.timeout,
|
|
72
|
+
failureThreshold: service.development.health.failures
|
|
73
|
+
}
|
|
74
|
+
} : {},
|
|
75
|
+
restart: service.development.restart,
|
|
76
|
+
...service.development.resources ? {
|
|
77
|
+
memory: service.development.resources.memory,
|
|
78
|
+
cpus: service.development.resources.cpus
|
|
79
|
+
} : {}
|
|
80
|
+
}
|
|
63
81
|
} : {}
|
|
64
82
|
};
|
|
65
83
|
}),
|
|
@@ -69,11 +87,19 @@ function compileProjectGraph(manifest) {
|
|
|
69
87
|
context: check.context,
|
|
70
88
|
...check.command ? { command: check.command } : {},
|
|
71
89
|
timeoutSeconds: check.timeout,
|
|
90
|
+
...check.application ? { application: check.application } : {},
|
|
91
|
+
profile: check.profile,
|
|
92
|
+
inputs: check.inputs,
|
|
93
|
+
required: check.required,
|
|
72
94
|
memory: check.resources.memory,
|
|
73
95
|
cpus: check.resources.cpus
|
|
74
96
|
})),
|
|
75
97
|
environments: manifest.environments.map((environment) => ({
|
|
76
98
|
id: environment.id,
|
|
99
|
+
mode: environment.mode,
|
|
100
|
+
managedEnvironment: environment.managedEnvironment ?? environment.id,
|
|
101
|
+
branches: environment.branches,
|
|
102
|
+
checkProfile: environment.checkProfile,
|
|
77
103
|
pipeline: environment.pipeline,
|
|
78
104
|
services: environment.services,
|
|
79
105
|
checks: environment.checks,
|
|
@@ -4076,9 +4102,41 @@ function validateReferences(manifest, ctx) {
|
|
|
4076
4102
|
}
|
|
4077
4103
|
validateAcyclicServices(manifest, ctx);
|
|
4078
4104
|
for (const [index, environment] of manifest.environments.entries()) {
|
|
4105
|
+
if (environment.mode === "development" && environment.checkProfile === "none") {
|
|
4106
|
+
ctx.addIssue({
|
|
4107
|
+
code: exports_external.ZodIssueCode.custom,
|
|
4108
|
+
path: ["environments", index, "checkProfile"],
|
|
4109
|
+
message: "development environments must keep a smoke or full quality gate"
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
if (environment.id === "prod" && environment.mode !== "immutable") {
|
|
4113
|
+
ctx.addIssue({
|
|
4114
|
+
code: exports_external.ZodIssueCode.custom,
|
|
4115
|
+
path: ["environments", index, "mode"],
|
|
4116
|
+
message: "the prod environment must be immutable"
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4079
4119
|
validateReferencesTo(ctx, environment.pipeline, taskIds, ["environments", index, "pipeline"], "task");
|
|
4080
4120
|
validateReferencesTo(ctx, environment.services, serviceIds, ["environments", index, "services"], "service");
|
|
4081
4121
|
validateReferencesTo(ctx, environment.checks, checkIds, ["environments", index, "checks"], "check");
|
|
4122
|
+
if (environment.mode === "development") {
|
|
4123
|
+
for (const serviceId of environment.services) {
|
|
4124
|
+
const serviceIndex = manifest.services.findIndex((service2) => service2.id === serviceId);
|
|
4125
|
+
const service = manifest.services[serviceIndex];
|
|
4126
|
+
if (service && !service.development) {
|
|
4127
|
+
ctx.addIssue({
|
|
4128
|
+
code: exports_external.ZodIssueCode.custom,
|
|
4129
|
+
path: ["services", serviceIndex, "development"],
|
|
4130
|
+
message: `service '${serviceId}' requires a development command`
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
for (const [index, check] of manifest.checks.entries()) {
|
|
4137
|
+
if (check.application && !serviceIds.has(check.application)) {
|
|
4138
|
+
addReferenceIssue(ctx, ["checks", index, "application"], "service", check.application);
|
|
4139
|
+
}
|
|
4082
4140
|
}
|
|
4083
4141
|
}
|
|
4084
4142
|
function validateAcyclicServices(manifest, ctx) {
|
|
@@ -4145,7 +4203,7 @@ function isSafeRepositoryPath(value) {
|
|
|
4145
4203
|
return false;
|
|
4146
4204
|
return !value.split("/").some((segment) => segment === "..");
|
|
4147
4205
|
}
|
|
4148
|
-
var id, repositoryPath, localImage, imageReference, ProjectCommandSchema, duration, resources, WorkspaceValueSchema, WorkspaceSchema, TaskValueSchema, TaskSchema, TasksV2Schema, HealthValueSchema, HealthV2Schema, ServiceValueSchema, ServicesV2Schema, CheckValueSchema, ChecksV2Schema, EnvironmentValueSchema, EnvironmentsV2Schema, HooksV2Schema, ManifestV2Fields, SandblocksManifestV2DocumentSchema, CompleteSandblocksManifestV2Schema, SandblocksManifestV2Schema;
|
|
4206
|
+
var id, repositoryPath, localImage, imageReference, ProjectCommandSchema, duration, resources, WorkspaceValueSchema, WorkspaceSchema, TaskValueSchema, TaskSchema, TasksV2Schema, HealthValueSchema, HealthV2Schema, DevelopmentServiceSchema, ServiceValueSchema, ServicesV2Schema, CheckValueSchema, ChecksV2Schema, EnvironmentValueSchema, EnvironmentsV2Schema, HooksV2Schema, ManifestV2Fields, SandblocksManifestV2DocumentSchema, CompleteSandblocksManifestV2Schema, SandblocksManifestV2Schema;
|
|
4149
4207
|
var init_schema = __esm(() => {
|
|
4150
4208
|
init_zod();
|
|
4151
4209
|
id = exports_external.string().min(1).max(63).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be a lowercase kebab-case identifier");
|
|
@@ -4207,12 +4265,19 @@ var init_schema = __esm(() => {
|
|
|
4207
4265
|
exports_external.string().startsWith("/").transform((path) => ({ path, interval: 10, timeout: 5, failures: 3 })),
|
|
4208
4266
|
HealthValueSchema
|
|
4209
4267
|
]);
|
|
4268
|
+
DevelopmentServiceSchema = exports_external.object({
|
|
4269
|
+
command: ProjectCommandSchema,
|
|
4270
|
+
health: HealthV2Schema.optional(),
|
|
4271
|
+
restart: exports_external.enum(["no", "on-failure", "unless-stopped"]).default("on-failure"),
|
|
4272
|
+
resources: resources.optional()
|
|
4273
|
+
}).strict();
|
|
4210
4274
|
ServiceValueSchema = exports_external.object({
|
|
4211
4275
|
root: repositoryPath.default("."),
|
|
4212
4276
|
command: ProjectCommandSchema.optional(),
|
|
4213
4277
|
static: repositoryPath.optional(),
|
|
4214
4278
|
port: exports_external.number().int().positive().max(65535).optional(),
|
|
4215
4279
|
health: HealthV2Schema.optional(),
|
|
4280
|
+
development: DevelopmentServiceSchema.optional(),
|
|
4216
4281
|
dependsOn: exports_external.array(id).default([]),
|
|
4217
4282
|
resources: resources.default({})
|
|
4218
4283
|
}).strict().superRefine((service, ctx) => {
|
|
@@ -4237,6 +4302,13 @@ var init_schema = __esm(() => {
|
|
|
4237
4302
|
message: "static services use Sandblocks's managed health check"
|
|
4238
4303
|
});
|
|
4239
4304
|
}
|
|
4305
|
+
if (service.static && service.development) {
|
|
4306
|
+
ctx.addIssue({
|
|
4307
|
+
code: exports_external.ZodIssueCode.custom,
|
|
4308
|
+
path: ["development"],
|
|
4309
|
+
message: "static services cannot declare a development process"
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4240
4312
|
});
|
|
4241
4313
|
ServicesV2Schema = keyedCollection(ServiceValueSchema);
|
|
4242
4314
|
CheckValueSchema = exports_external.object({
|
|
@@ -4244,13 +4316,21 @@ var init_schema = __esm(() => {
|
|
|
4244
4316
|
context: repositoryPath.default("."),
|
|
4245
4317
|
command: exports_external.array(exports_external.string().min(1).max(2048)).max(64).optional(),
|
|
4246
4318
|
timeout: duration.default(300),
|
|
4319
|
+
application: id.optional(),
|
|
4320
|
+
profile: exports_external.enum(["smoke", "full"]).default("full"),
|
|
4321
|
+
inputs: exports_external.array(repositoryPath).default([]),
|
|
4322
|
+
required: exports_external.boolean().default(true),
|
|
4247
4323
|
resources: resources.default({ memory: "1g", cpus: "2" })
|
|
4248
4324
|
}).strict();
|
|
4249
4325
|
ChecksV2Schema = keyedCollection(CheckValueSchema);
|
|
4250
4326
|
EnvironmentValueSchema = exports_external.object({
|
|
4327
|
+
mode: exports_external.enum(["immutable", "development"]).default("immutable"),
|
|
4328
|
+
managedEnvironment: id.optional(),
|
|
4329
|
+
branches: exports_external.array(exports_external.string().min(1).max(255)).max(32).default([]),
|
|
4251
4330
|
pipeline: exports_external.array(id).default([]),
|
|
4252
4331
|
services: exports_external.array(id).min(1),
|
|
4253
4332
|
checks: exports_external.array(id).default([]),
|
|
4333
|
+
checkProfile: exports_external.enum(["none", "smoke", "full"]).default("full"),
|
|
4254
4334
|
targetHost: exports_external.string().min(1).max(255).optional(),
|
|
4255
4335
|
routing: exports_external.object({ sso: exports_external.boolean().optional() }).strict().default({})
|
|
4256
4336
|
}).strict();
|
|
@@ -4295,6 +4375,8 @@ function resolveServiceSteps(manifest, environmentId) {
|
|
|
4295
4375
|
command: graph.workspace.setup,
|
|
4296
4376
|
workingDirectory: graph.workspace.root,
|
|
4297
4377
|
timeoutSeconds: 1800,
|
|
4378
|
+
inputs: ["package.json", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"],
|
|
4379
|
+
outputs: [],
|
|
4298
4380
|
required: true
|
|
4299
4381
|
});
|
|
4300
4382
|
}
|
|
@@ -4307,6 +4389,8 @@ function resolveServiceSteps(manifest, environmentId) {
|
|
|
4307
4389
|
command: task.command,
|
|
4308
4390
|
workingDirectory: task.root,
|
|
4309
4391
|
timeoutSeconds: task.timeoutSeconds,
|
|
4392
|
+
inputs: task.inputs,
|
|
4393
|
+
outputs: task.outputs,
|
|
4310
4394
|
required: true
|
|
4311
4395
|
});
|
|
4312
4396
|
}
|
|
@@ -4319,6 +4403,10 @@ function resolveEnvironment(manifest, id2) {
|
|
|
4319
4403
|
const checks = new Map(graph.checks.map((check) => [check.id, check]));
|
|
4320
4404
|
return {
|
|
4321
4405
|
id: environment.id,
|
|
4406
|
+
mode: environment.mode,
|
|
4407
|
+
managedEnvironment: environment.managedEnvironment,
|
|
4408
|
+
branches: environment.branches,
|
|
4409
|
+
checkProfile: environment.checkProfile,
|
|
4322
4410
|
goal: `${environment.id} sandbox`,
|
|
4323
4411
|
deploymentType: environment.id,
|
|
4324
4412
|
baseDomain: environment.baseDomain,
|
|
@@ -4334,7 +4422,7 @@ function resolveEnvironment(manifest, id2) {
|
|
|
4334
4422
|
const service = services.get(serviceId);
|
|
4335
4423
|
if (!service)
|
|
4336
4424
|
throw new Error(`environment '${id2}' references unknown service '${serviceId}'`);
|
|
4337
|
-
return executionService(service);
|
|
4425
|
+
return executionService(service, environment.mode);
|
|
4338
4426
|
}),
|
|
4339
4427
|
steps: environment.checks.map((checkId) => {
|
|
4340
4428
|
const check = checks.get(checkId);
|
|
@@ -4346,6 +4434,10 @@ function resolveEnvironment(manifest, id2) {
|
|
|
4346
4434
|
context: check.context,
|
|
4347
4435
|
...check.command ? { command: check.command } : {},
|
|
4348
4436
|
timeoutSeconds: check.timeoutSeconds,
|
|
4437
|
+
...check.application ? { application: check.application } : {},
|
|
4438
|
+
profile: check.profile,
|
|
4439
|
+
inputs: check.inputs,
|
|
4440
|
+
required: check.required,
|
|
4349
4441
|
memory: check.memory,
|
|
4350
4442
|
cpus: check.cpus
|
|
4351
4443
|
};
|
|
@@ -4358,7 +4450,7 @@ function selectEnvironment(graph, id2) {
|
|
|
4358
4450
|
throw new Error(`sandblocks.yml does not define environment '${id2}'`);
|
|
4359
4451
|
return environment;
|
|
4360
4452
|
}
|
|
4361
|
-
function executionService(service) {
|
|
4453
|
+
function executionService(service, mode) {
|
|
4362
4454
|
if (service.kind === "static") {
|
|
4363
4455
|
return {
|
|
4364
4456
|
id: service.id,
|
|
@@ -4368,15 +4460,19 @@ function executionService(service) {
|
|
|
4368
4460
|
cpus: service.cpus
|
|
4369
4461
|
};
|
|
4370
4462
|
}
|
|
4463
|
+
const development = mode === "development" ? service.development : undefined;
|
|
4464
|
+
if (mode === "development" && !development) {
|
|
4465
|
+
throw new Error(`service '${service.id}' has no development command`);
|
|
4466
|
+
}
|
|
4371
4467
|
return {
|
|
4372
4468
|
id: service.id,
|
|
4373
4469
|
kind: "process",
|
|
4374
4470
|
root: service.root,
|
|
4375
|
-
command: service.command,
|
|
4471
|
+
command: development?.command ?? service.command,
|
|
4376
4472
|
...service.port ? { port: service.port } : {},
|
|
4377
|
-
...service.health ? { healthPath: service.health.path } : {},
|
|
4378
|
-
memory: service.memory,
|
|
4379
|
-
cpus: service.cpus
|
|
4473
|
+
...development?.health ? { healthPath: development.health.path } : service.health ? { healthPath: service.health.path } : {},
|
|
4474
|
+
memory: development?.memory ?? service.memory,
|
|
4475
|
+
cpus: development?.cpus ?? service.cpus
|
|
4380
4476
|
};
|
|
4381
4477
|
}
|
|
4382
4478
|
var init_resolve = () => {};
|
|
@@ -11719,7 +11815,7 @@ var init_src = __esm(() => {
|
|
|
11719
11815
|
|
|
11720
11816
|
// packages/libs/cli/src/cli.ts
|
|
11721
11817
|
init_src();
|
|
11722
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
11818
|
+
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
11723
11819
|
import { lstat, readFile as readFile8, readdir as readdir2, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
11724
11820
|
import path6 from "path";
|
|
11725
11821
|
|
|
@@ -12508,6 +12604,42 @@ class SandblocksClient {
|
|
|
12508
12604
|
cancelAgentSession(projectId, sandboxId, sessionId) {
|
|
12509
12605
|
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/agent-sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" });
|
|
12510
12606
|
}
|
|
12607
|
+
listWorkers(organizationId) {
|
|
12608
|
+
return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers`);
|
|
12609
|
+
}
|
|
12610
|
+
createWorker(input) {
|
|
12611
|
+
return this.request(`/v1/organizations/${encodeURIComponent(input.organizationId)}/workers`, {
|
|
12612
|
+
method: "POST",
|
|
12613
|
+
body: JSON.stringify(input)
|
|
12614
|
+
});
|
|
12615
|
+
}
|
|
12616
|
+
moveWorkerDeployments(organizationId, workerId, targetWorkerIds) {
|
|
12617
|
+
return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/move`, { method: "POST", body: JSON.stringify({ targetWorkerIds }) });
|
|
12618
|
+
}
|
|
12619
|
+
rotateWorkerSecret(organizationId, workerId) {
|
|
12620
|
+
return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/rotate-credential`, { method: "POST" });
|
|
12621
|
+
}
|
|
12622
|
+
approveWorkerUpdate(organizationId, workerId, version) {
|
|
12623
|
+
return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/approve-update`, {
|
|
12624
|
+
method: "POST",
|
|
12625
|
+
body: JSON.stringify({ version })
|
|
12626
|
+
});
|
|
12627
|
+
}
|
|
12628
|
+
listRegistries(organizationId) {
|
|
12629
|
+
return this.request(`/v1/organizations/${encodeURIComponent(organizationId)}/registries`);
|
|
12630
|
+
}
|
|
12631
|
+
createRegistry(input) {
|
|
12632
|
+
return this.request(`/v1/organizations/${encodeURIComponent(input.organizationId)}/registries`, {
|
|
12633
|
+
method: "POST",
|
|
12634
|
+
body: JSON.stringify(input)
|
|
12635
|
+
});
|
|
12636
|
+
}
|
|
12637
|
+
assignProjectRegistry(projectId, input) {
|
|
12638
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/registries`, {
|
|
12639
|
+
method: "PUT",
|
|
12640
|
+
body: JSON.stringify(input)
|
|
12641
|
+
});
|
|
12642
|
+
}
|
|
12511
12643
|
listHosts(projectId) {
|
|
12512
12644
|
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/hosts`);
|
|
12513
12645
|
}
|
|
@@ -12635,7 +12767,12 @@ class SandblocksClient {
|
|
|
12635
12767
|
const image = String(operation.result?.image ?? "");
|
|
12636
12768
|
if (!/@sha256:[a-f0-9]{64}$/.test(image))
|
|
12637
12769
|
throw new Error("Published artifact digest is invalid");
|
|
12638
|
-
return {
|
|
12770
|
+
return {
|
|
12771
|
+
image,
|
|
12772
|
+
workspaceId: input.workspaceId,
|
|
12773
|
+
deploymentId: input.deploymentId,
|
|
12774
|
+
bytes: input.bundle.byteLength
|
|
12775
|
+
};
|
|
12639
12776
|
}
|
|
12640
12777
|
async deployPrebuiltArtifact(input) {
|
|
12641
12778
|
const { operation } = await this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/operations`, {
|
|
@@ -12962,10 +13099,14 @@ function option(options, key) {
|
|
|
12962
13099
|
// packages/libs/cli/src/sandbox.ts
|
|
12963
13100
|
init_src();
|
|
12964
13101
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
13102
|
+
import { watch as watchFiles } from "fs";
|
|
12965
13103
|
import { chmod as chmod3, mkdir as mkdir6, readFile as readFile5, rename, rm as rm4 } from "fs/promises";
|
|
12966
13104
|
import path5 from "path";
|
|
12967
13105
|
var COMMANDS = new Set([
|
|
12968
13106
|
"up",
|
|
13107
|
+
"dev",
|
|
13108
|
+
"sync",
|
|
13109
|
+
"check",
|
|
12969
13110
|
"create",
|
|
12970
13111
|
"status",
|
|
12971
13112
|
"deploy",
|
|
@@ -12982,6 +13123,12 @@ function isStatefulSandboxCommand(command) {
|
|
|
12982
13123
|
async function runStatefulSandboxCommand(command, args, dependencies) {
|
|
12983
13124
|
if (command === "up")
|
|
12984
13125
|
return up(args, dependencies);
|
|
13126
|
+
if (command === "dev")
|
|
13127
|
+
return dev(args, dependencies);
|
|
13128
|
+
if (command === "sync")
|
|
13129
|
+
return sync(args, dependencies);
|
|
13130
|
+
if (command === "check")
|
|
13131
|
+
return check(args, dependencies);
|
|
12985
13132
|
if (command === "create")
|
|
12986
13133
|
return create(args, dependencies);
|
|
12987
13134
|
if (command === "status")
|
|
@@ -13013,6 +13160,98 @@ async function up(args, dependencies) {
|
|
|
13013
13160
|
}
|
|
13014
13161
|
});
|
|
13015
13162
|
}
|
|
13163
|
+
async function dev(args, dependencies) {
|
|
13164
|
+
await up(withDefaultOption(withDefaultEnvironment(args, "develop"), "pool", "development-fleet"), dependencies);
|
|
13165
|
+
}
|
|
13166
|
+
async function sync(args, dependencies) {
|
|
13167
|
+
const effectiveArgs = withDefaultEnvironment(args, "develop");
|
|
13168
|
+
const context = await getContext(effectiveArgs, dependencies);
|
|
13169
|
+
if (context.state.runtimeMode !== "development") {
|
|
13170
|
+
throw new Error("sandbox sync requires a development environment");
|
|
13171
|
+
}
|
|
13172
|
+
let previousDigest = "";
|
|
13173
|
+
const apply = async () => {
|
|
13174
|
+
const source = await dependencies.source(context.root, context.state.sourceManifest);
|
|
13175
|
+
const digest = createHash2("sha256").update(source.bundle).digest("hex");
|
|
13176
|
+
if (digest === previousDigest)
|
|
13177
|
+
return;
|
|
13178
|
+
previousDigest = digest;
|
|
13179
|
+
context.state.sequence += 1;
|
|
13180
|
+
const response = await fetchWithRetry(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.state.projectId)}/workspaces/${encodeURIComponent(context.state.workspaceId)}/sync`, {
|
|
13181
|
+
method: "POST",
|
|
13182
|
+
headers: {
|
|
13183
|
+
"content-type": "application/x-tar",
|
|
13184
|
+
"content-length": String(source.bundle.byteLength),
|
|
13185
|
+
"x-sandblocks-api-key": context.apiKey,
|
|
13186
|
+
"x-sandblocks-sandbox-id": context.state.sandboxId,
|
|
13187
|
+
...context.state.sourceRevision ? { "x-sandblocks-base-revision": context.state.sourceRevision } : {},
|
|
13188
|
+
"idempotency-key": `sync:${context.state.sandboxId}:${context.state.sequence}:${digest}`
|
|
13189
|
+
},
|
|
13190
|
+
body: new Blob([Uint8Array.from(source.bundle)])
|
|
13191
|
+
});
|
|
13192
|
+
const submitted = await response.json().catch(() => ({}));
|
|
13193
|
+
if (!response.ok)
|
|
13194
|
+
throw new Error(String(submitted.error ?? `source sync failed (${response.status})`));
|
|
13195
|
+
const completed = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 5 * 60000);
|
|
13196
|
+
context.state.sourceRevision = String(completed.result?.revision ?? digest);
|
|
13197
|
+
context.state.sourceManifest = source.manifest;
|
|
13198
|
+
await writeState(context.file, context.state);
|
|
13199
|
+
console.log(`sync revision=${context.state.sourceRevision} changed=${completed.result?.changed ?? 0} deleted=${completed.result?.deleted ?? 0}`);
|
|
13200
|
+
if (context.options.checks !== "none") {
|
|
13201
|
+
const changedPaths = [
|
|
13202
|
+
...Array.isArray(completed.result?.changedPaths) ? completed.result.changedPaths : [],
|
|
13203
|
+
...Array.isArray(completed.result?.deletedPaths) ? completed.result.deletedPaths : []
|
|
13204
|
+
].filter((value) => typeof value === "string");
|
|
13205
|
+
await runDevelopmentChecks(context, dependencies, undefined, changedPaths);
|
|
13206
|
+
}
|
|
13207
|
+
};
|
|
13208
|
+
await apply();
|
|
13209
|
+
if (context.options.watch !== true)
|
|
13210
|
+
return;
|
|
13211
|
+
console.log("watch waiting for source changes (Ctrl-C keeps the sandbox running)");
|
|
13212
|
+
while (true) {
|
|
13213
|
+
await waitForSourceChange(context.root);
|
|
13214
|
+
await apply();
|
|
13215
|
+
}
|
|
13216
|
+
}
|
|
13217
|
+
async function check(args, dependencies) {
|
|
13218
|
+
const context = await getContext(withDefaultEnvironment(args, "develop"), dependencies);
|
|
13219
|
+
await runDevelopmentChecks(context, dependencies, dependencies.option(context.options, "profile"));
|
|
13220
|
+
}
|
|
13221
|
+
async function waitForSourceChange(root) {
|
|
13222
|
+
await new Promise((resolveChange) => {
|
|
13223
|
+
let settled = false;
|
|
13224
|
+
let debounce;
|
|
13225
|
+
const watcher = watchFiles(root, { recursive: true }, (_event, filename) => {
|
|
13226
|
+
const path6 = String(filename ?? "").replaceAll("\\", "/");
|
|
13227
|
+
if (!path6 || path6 === ".git" || path6.startsWith(".git/") || path6 === ".sandblocks" || path6.startsWith(".sandblocks/") || path6 === "node_modules" || path6.includes("/node_modules/")) {
|
|
13228
|
+
return;
|
|
13229
|
+
}
|
|
13230
|
+
if (debounce)
|
|
13231
|
+
clearTimeout(debounce);
|
|
13232
|
+
debounce = setTimeout(() => {
|
|
13233
|
+
if (settled)
|
|
13234
|
+
return;
|
|
13235
|
+
settled = true;
|
|
13236
|
+
watcher.close();
|
|
13237
|
+
resolveChange();
|
|
13238
|
+
}, 150);
|
|
13239
|
+
});
|
|
13240
|
+
watcher.on("error", () => {
|
|
13241
|
+
if (settled)
|
|
13242
|
+
return;
|
|
13243
|
+
settled = true;
|
|
13244
|
+
watcher.close();
|
|
13245
|
+
setTimeout(resolveChange, 1000);
|
|
13246
|
+
});
|
|
13247
|
+
});
|
|
13248
|
+
}
|
|
13249
|
+
function withDefaultEnvironment(args, environment) {
|
|
13250
|
+
return withDefaultOption(args, "environment", environment);
|
|
13251
|
+
}
|
|
13252
|
+
function withDefaultOption(args, option2, value) {
|
|
13253
|
+
return args.some((candidate) => candidate === `--${option2}` || candidate.startsWith(`--${option2}=`)) ? args : [...args, `--${option2}`, value];
|
|
13254
|
+
}
|
|
13016
13255
|
async function create(args, dependencies) {
|
|
13017
13256
|
const options = dependencies.parse(args);
|
|
13018
13257
|
const root = path5.resolve(options.positionals[0] ?? process.cwd());
|
|
@@ -13053,9 +13292,10 @@ async function create(args, dependencies) {
|
|
|
13053
13292
|
source: dependencies.source,
|
|
13054
13293
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13055
13294
|
});
|
|
13056
|
-
const hostId =
|
|
13057
|
-
|
|
13058
|
-
|
|
13295
|
+
const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13296
|
+
const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13297
|
+
if (!workerId)
|
|
13298
|
+
throw new Error("workspace import did not bind a worker");
|
|
13059
13299
|
const state = {
|
|
13060
13300
|
version: 1,
|
|
13061
13301
|
environment,
|
|
@@ -13065,14 +13305,17 @@ async function create(args, dependencies) {
|
|
|
13065
13305
|
repository,
|
|
13066
13306
|
leaseId: lease.lease.id,
|
|
13067
13307
|
leaseToken: lease.token,
|
|
13068
|
-
hostId,
|
|
13308
|
+
...hostId ? { hostId } : {},
|
|
13309
|
+
workerId,
|
|
13310
|
+
sourceRevision: typeof imported.operation.result?.sha === "string" ? imported.operation.result.sha : undefined,
|
|
13311
|
+
sourceManifest: imported.manifest,
|
|
13069
13312
|
sequence: 0,
|
|
13070
13313
|
createdAt: new Date().toISOString()
|
|
13071
13314
|
};
|
|
13072
13315
|
await writeState(file, state);
|
|
13073
13316
|
console.log(`sandbox ${sandboxId}`);
|
|
13074
13317
|
console.log(`workspace ${workspaceId}`);
|
|
13075
|
-
console.log(`
|
|
13318
|
+
console.log(`worker ${workerId}`);
|
|
13076
13319
|
} catch (error) {
|
|
13077
13320
|
await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
|
|
13078
13321
|
method: "DELETE"
|
|
@@ -13095,8 +13338,14 @@ async function status2(args, dependencies) {
|
|
|
13095
13338
|
console.log(`state ${path5.relative(context.root, context.file)}`);
|
|
13096
13339
|
console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
|
|
13097
13340
|
console.log(`workspace ${context.state.workspaceId}`);
|
|
13098
|
-
console.log(`
|
|
13341
|
+
console.log(`worker ${context.state.workerId}`);
|
|
13099
13342
|
console.log(`lease expires ${session.session.expiresAt}`);
|
|
13343
|
+
if (context.state.runtimeMode)
|
|
13344
|
+
console.log(`mode ${context.state.runtimeMode}`);
|
|
13345
|
+
if (context.state.sourceRevision)
|
|
13346
|
+
console.log(`revision ${context.state.sourceRevision}`);
|
|
13347
|
+
if (context.state.quality)
|
|
13348
|
+
console.log(`quality ${context.state.quality.status} (${context.state.quality.sourceRevision})`);
|
|
13100
13349
|
for (const route of context.state.previewUrls ?? [])
|
|
13101
13350
|
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13102
13351
|
}
|
|
@@ -13125,15 +13374,22 @@ async function withDeploymentLock(args, dependencies, action) {
|
|
|
13125
13374
|
}
|
|
13126
13375
|
async function deployUnlocked(args, dependencies) {
|
|
13127
13376
|
const context = await getContext(args, dependencies);
|
|
13128
|
-
|
|
13377
|
+
const resume = context.options.resume === true;
|
|
13378
|
+
if (context.state.deploymentId && !(resume && context.state.candidate?.deploymentId === context.state.deploymentId))
|
|
13129
13379
|
throw new Error("preview is already deployed; use sandbox redeploy");
|
|
13130
13380
|
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
13131
13381
|
const stack = loaded.manifest.name ?? context.state.repository;
|
|
13132
13382
|
const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
|
|
13383
|
+
await assertEnvironmentBranch(context.root, preview);
|
|
13384
|
+
context.state.managedEnvironment = preview.managedEnvironment;
|
|
13385
|
+
if (preview.mode === "development") {
|
|
13386
|
+
await deployDevelopmentUnlocked(context, loaded, preview);
|
|
13387
|
+
return;
|
|
13388
|
+
}
|
|
13133
13389
|
const deploymentTarget = await resolveDeploymentTarget(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
|
|
13134
13390
|
const targetHost = deploymentTarget.targetHost;
|
|
13135
13391
|
context.state.deploymentHostId = deploymentTarget.hostId;
|
|
13136
|
-
|
|
13392
|
+
context.state.deploymentWorkerId = deploymentTarget.workerId;
|
|
13137
13393
|
const previousCandidate = resume ? context.state.candidate : undefined;
|
|
13138
13394
|
if (resume && !previousCandidate)
|
|
13139
13395
|
throw new Error("no resumable candidate exists for this sandbox");
|
|
@@ -13143,13 +13399,14 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13143
13399
|
throw new Error("deployment ID must be a UUID");
|
|
13144
13400
|
if (previousCandidate && requestedDeploymentId && requestedDeploymentId !== previousCandidate.deploymentId)
|
|
13145
13401
|
throw new Error("--deployment-id does not match the resumable candidate");
|
|
13146
|
-
const environmentSnapshotId = previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
|
|
13147
|
-
context.state.candidate = previousCandidate
|
|
13402
|
+
const environmentSnapshotId = resume ? await createEnvironmentSnapshot(context) : previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
|
|
13403
|
+
context.state.candidate = previousCandidate ? { ...previousCandidate, environmentSnapshotId } : { deploymentId, environmentSnapshotId };
|
|
13148
13404
|
await writeState(context.file, context.state);
|
|
13149
13405
|
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
13150
13406
|
const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
|
|
13151
13407
|
let buildWorkspaceId = previousCandidate?.buildWorkspaceId;
|
|
13152
13408
|
let buildHostId = previousCandidate?.buildHostId;
|
|
13409
|
+
let buildWorkerId = previousCandidate?.buildWorkerId;
|
|
13153
13410
|
const suppliedArtifact = dependencies.option(context.options, "artifact");
|
|
13154
13411
|
if (suppliedArtifact && !/@sha256:[a-f0-9]{64}$/.test(suppliedArtifact))
|
|
13155
13412
|
throw new Error("--artifact must be an immutable OCI image digest");
|
|
@@ -13173,10 +13430,17 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13173
13430
|
source: dependencies.source,
|
|
13174
13431
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13175
13432
|
});
|
|
13176
|
-
buildHostId =
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13433
|
+
buildHostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13434
|
+
buildWorkerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13435
|
+
if (!buildWorkerId)
|
|
13436
|
+
throw new Error("build workspace did not bind a build worker");
|
|
13437
|
+
context.state.candidate = {
|
|
13438
|
+
deploymentId,
|
|
13439
|
+
environmentSnapshotId,
|
|
13440
|
+
buildWorkspaceId,
|
|
13441
|
+
buildHostId,
|
|
13442
|
+
buildWorkerId
|
|
13443
|
+
};
|
|
13180
13444
|
await writeState(context.file, context.state);
|
|
13181
13445
|
const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13182
13446
|
method: "POST",
|
|
@@ -13184,7 +13448,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13184
13448
|
body: JSON.stringify({
|
|
13185
13449
|
kind: "workspace.step.run",
|
|
13186
13450
|
stack: context.state.repository,
|
|
13187
|
-
environmentId: context
|
|
13451
|
+
environmentId: managedEnvironmentId(context),
|
|
13188
13452
|
environmentSnapshotId,
|
|
13189
13453
|
payload: {
|
|
13190
13454
|
workspaceId: buildWorkspaceId,
|
|
@@ -13194,7 +13458,11 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13194
13458
|
serviceUrls,
|
|
13195
13459
|
steps: serviceSteps,
|
|
13196
13460
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
|
|
13197
|
-
placement: {
|
|
13461
|
+
placement: {
|
|
13462
|
+
workerId: buildWorkerId,
|
|
13463
|
+
...buildHostId ? { hostId: buildHostId } : {},
|
|
13464
|
+
memoryReservationBytes: 2 * 1024 * 1024 * 1024
|
|
13465
|
+
}
|
|
13198
13466
|
},
|
|
13199
13467
|
maxAttempts: 1
|
|
13200
13468
|
})
|
|
@@ -13205,22 +13473,29 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13205
13473
|
environmentSnapshotId,
|
|
13206
13474
|
buildWorkspaceId,
|
|
13207
13475
|
buildHostId,
|
|
13476
|
+
buildWorkerId,
|
|
13208
13477
|
buildCompleted: true
|
|
13209
13478
|
};
|
|
13210
13479
|
await writeState(context.file, context.state);
|
|
13211
13480
|
}
|
|
13212
|
-
if (!
|
|
13213
|
-
throw new Error("resumable build
|
|
13481
|
+
if (!buildWorkerId)
|
|
13482
|
+
throw new Error("resumable build worker is unavailable");
|
|
13214
13483
|
const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13215
13484
|
method: "POST",
|
|
13216
|
-
headers: {
|
|
13485
|
+
headers: {
|
|
13486
|
+
"idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
|
|
13487
|
+
},
|
|
13217
13488
|
body: JSON.stringify({
|
|
13218
13489
|
kind: "workspace.artifact.publish",
|
|
13219
13490
|
payload: {
|
|
13220
13491
|
workspaceId: buildWorkspaceId,
|
|
13221
13492
|
deploymentId,
|
|
13222
13493
|
stack,
|
|
13223
|
-
placement: {
|
|
13494
|
+
placement: {
|
|
13495
|
+
workerId: buildWorkerId,
|
|
13496
|
+
...buildHostId ? { hostId: buildHostId } : {},
|
|
13497
|
+
memoryReservationBytes: 1024 * 1024 * 1024
|
|
13498
|
+
}
|
|
13224
13499
|
},
|
|
13225
13500
|
maxAttempts: 1
|
|
13226
13501
|
})
|
|
@@ -13234,6 +13509,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13234
13509
|
environmentSnapshotId,
|
|
13235
13510
|
buildWorkspaceId,
|
|
13236
13511
|
buildHostId,
|
|
13512
|
+
buildWorkerId,
|
|
13237
13513
|
buildCompleted: true,
|
|
13238
13514
|
artifactImage
|
|
13239
13515
|
};
|
|
@@ -13257,7 +13533,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13257
13533
|
if (!artifactImage) {
|
|
13258
13534
|
const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13259
13535
|
method: "POST",
|
|
13260
|
-
headers: {
|
|
13536
|
+
headers: {
|
|
13537
|
+
"idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
|
|
13538
|
+
},
|
|
13261
13539
|
body: JSON.stringify({
|
|
13262
13540
|
kind: "workspace.artifact.publish",
|
|
13263
13541
|
payload: {
|
|
@@ -13265,7 +13543,8 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13265
13543
|
deploymentId,
|
|
13266
13544
|
stack,
|
|
13267
13545
|
placement: {
|
|
13268
|
-
|
|
13546
|
+
workerId: context.state.workerId,
|
|
13547
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13269
13548
|
memoryReservationBytes: 1024 * 1024 * 1024
|
|
13270
13549
|
}
|
|
13271
13550
|
},
|
|
@@ -13282,12 +13561,12 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13282
13561
|
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13283
13562
|
method: "POST",
|
|
13284
13563
|
headers: {
|
|
13285
|
-
"idempotency-key": `preview:${deploymentId}:deploy`
|
|
13564
|
+
"idempotency-key": resume ? `preview:${deploymentId}:deploy:resume:${randomUUID2()}` : `preview:${deploymentId}:deploy`
|
|
13286
13565
|
},
|
|
13287
13566
|
body: JSON.stringify({
|
|
13288
13567
|
kind: "workspace.service.deploy",
|
|
13289
13568
|
stack: context.state.repository,
|
|
13290
|
-
environmentId: context
|
|
13569
|
+
environmentId: managedEnvironmentId(context),
|
|
13291
13570
|
environmentSnapshotId,
|
|
13292
13571
|
payload: {
|
|
13293
13572
|
workspaceId: context.state.workspaceId,
|
|
@@ -13301,7 +13580,8 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13301
13580
|
serviceUrls,
|
|
13302
13581
|
services: preview.services,
|
|
13303
13582
|
placement: {
|
|
13304
|
-
|
|
13583
|
+
workerId: deploymentTarget.workerId,
|
|
13584
|
+
...deploymentTarget.hostId ? { hostId: deploymentTarget.hostId } : {},
|
|
13305
13585
|
requiredWorkerRole: "runtime",
|
|
13306
13586
|
pool: deploymentTarget.pool,
|
|
13307
13587
|
memoryReservationBytes: serviceMemoryReservation(preview.services)
|
|
@@ -13313,7 +13593,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13313
13593
|
operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
|
|
13314
13594
|
} catch (error) {
|
|
13315
13595
|
const message = error instanceof Error ? error.message : String(error);
|
|
13316
|
-
throw new Error(`${message}; retry with
|
|
13596
|
+
throw new Error(`${message}; retry with \`sandblocks sandbox deploy . --resume\``);
|
|
13317
13597
|
}
|
|
13318
13598
|
context.state.deploymentId = deploymentId;
|
|
13319
13599
|
await writeState(context.file, context.state);
|
|
@@ -13334,15 +13614,15 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13334
13614
|
route.url
|
|
13335
13615
|
]
|
|
13336
13616
|
] : []));
|
|
13337
|
-
const
|
|
13617
|
+
const check2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13338
13618
|
method: "POST",
|
|
13339
13619
|
headers: {
|
|
13340
|
-
"idempotency-key": `preview:${deploymentId}:checks`
|
|
13620
|
+
"idempotency-key": resume ? `preview:${deploymentId}:checks:resume:${randomUUID2()}` : `preview:${deploymentId}:checks`
|
|
13341
13621
|
},
|
|
13342
13622
|
body: JSON.stringify({
|
|
13343
13623
|
kind: "workspace.check.run",
|
|
13344
13624
|
stack: context.state.repository,
|
|
13345
|
-
environmentId: context
|
|
13625
|
+
environmentId: managedEnvironmentId(context),
|
|
13346
13626
|
environmentSnapshotId,
|
|
13347
13627
|
payload: {
|
|
13348
13628
|
workspaceId: buildWorkspaceId ?? context.state.workspaceId,
|
|
@@ -13351,12 +13631,15 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13351
13631
|
runtimeType: preview.deploymentType,
|
|
13352
13632
|
checks: preview.steps,
|
|
13353
13633
|
serviceUrls: serviceUrls2,
|
|
13354
|
-
placement: {
|
|
13634
|
+
placement: {
|
|
13635
|
+
workerId: buildWorkerId ?? context.state.workerId,
|
|
13636
|
+
...buildHostId ?? context.state.hostId ? { hostId: buildHostId ?? context.state.hostId } : {}
|
|
13637
|
+
}
|
|
13355
13638
|
},
|
|
13356
13639
|
maxAttempts: 1
|
|
13357
13640
|
})
|
|
13358
13641
|
});
|
|
13359
|
-
const checked2 = await wait(context.apiUrl, context.apiKey,
|
|
13642
|
+
const checked2 = await wait(context.apiUrl, context.apiKey, check2.operation.id, 60 * 60000);
|
|
13360
13643
|
checkEvidence = checked2.result?.checks ?? [];
|
|
13361
13644
|
if (checked2.result?.passed !== true) {
|
|
13362
13645
|
throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
|
|
@@ -13367,13 +13650,6 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13367
13650
|
await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
|
|
13368
13651
|
return;
|
|
13369
13652
|
});
|
|
13370
|
-
if (buildWorkspaceId)
|
|
13371
|
-
await destroyDetachedWorkspace(context, buildWorkspaceId, `preview:${deploymentId}:build-cleanup`).catch(() => {
|
|
13372
|
-
return;
|
|
13373
|
-
});
|
|
13374
|
-
await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
|
|
13375
|
-
return;
|
|
13376
|
-
});
|
|
13377
13653
|
throw error;
|
|
13378
13654
|
}
|
|
13379
13655
|
if (buildWorkspaceId)
|
|
@@ -13388,6 +13664,219 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13388
13664
|
for (const route of previewUrls)
|
|
13389
13665
|
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13390
13666
|
}
|
|
13667
|
+
async function deployDevelopmentUnlocked(context, loaded, preview) {
|
|
13668
|
+
if (context.state.deploymentId)
|
|
13669
|
+
throw new Error("development sandbox is already running; use sandbox sync");
|
|
13670
|
+
const stack = loaded.manifest.name ?? context.state.repository;
|
|
13671
|
+
const target = await resolveDevelopmentTarget(context, preview.targetHost);
|
|
13672
|
+
const deploymentId = randomUUID2();
|
|
13673
|
+
const environmentSnapshotId = await createEnvironmentSnapshot(context);
|
|
13674
|
+
context.state.deploymentId = deploymentId;
|
|
13675
|
+
context.state.deploymentHostId = target.hostId;
|
|
13676
|
+
context.state.deploymentWorkerId = target.workerId;
|
|
13677
|
+
context.state.runtimeMode = "development";
|
|
13678
|
+
await writeState(context.file, context.state);
|
|
13679
|
+
try {
|
|
13680
|
+
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
13681
|
+
const steps = resolveServiceSteps(loaded.manifest, context.state.environment);
|
|
13682
|
+
if (steps.length) {
|
|
13683
|
+
const submitted2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13684
|
+
method: "POST",
|
|
13685
|
+
headers: { "idempotency-key": `development:${deploymentId}:setup` },
|
|
13686
|
+
body: JSON.stringify({
|
|
13687
|
+
kind: "workspace.step.run",
|
|
13688
|
+
stack,
|
|
13689
|
+
environmentId: managedEnvironmentId(context),
|
|
13690
|
+
environmentSnapshotId,
|
|
13691
|
+
payload: {
|
|
13692
|
+
workspaceId: context.state.workspaceId,
|
|
13693
|
+
sandboxId: context.state.sandboxId,
|
|
13694
|
+
revisionId: context.state.sourceRevision ?? deploymentId,
|
|
13695
|
+
runtimeType: preview.deploymentType,
|
|
13696
|
+
serviceUrls,
|
|
13697
|
+
steps,
|
|
13698
|
+
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
|
|
13699
|
+
placement: {
|
|
13700
|
+
workerId: context.state.workerId,
|
|
13701
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13702
|
+
requiredWorkerRole: "development"
|
|
13703
|
+
}
|
|
13704
|
+
},
|
|
13705
|
+
maxAttempts: 1
|
|
13706
|
+
})
|
|
13707
|
+
});
|
|
13708
|
+
await wait(context.apiUrl, context.apiKey, submitted2.operation.id, 30 * 60000);
|
|
13709
|
+
}
|
|
13710
|
+
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13711
|
+
method: "POST",
|
|
13712
|
+
headers: { "idempotency-key": `development:${deploymentId}:services` },
|
|
13713
|
+
body: JSON.stringify({
|
|
13714
|
+
kind: "workspace.service.deploy",
|
|
13715
|
+
stack,
|
|
13716
|
+
environmentId: managedEnvironmentId(context),
|
|
13717
|
+
environmentSnapshotId,
|
|
13718
|
+
payload: {
|
|
13719
|
+
workspaceId: context.state.workspaceId,
|
|
13720
|
+
deploymentId,
|
|
13721
|
+
sandboxId: context.state.sandboxId,
|
|
13722
|
+
runtimeType: preview.deploymentType,
|
|
13723
|
+
stack,
|
|
13724
|
+
workspace: preview.workspace,
|
|
13725
|
+
mutableDevelopment: true,
|
|
13726
|
+
sourceRevision: context.state.sourceRevision ?? deploymentId,
|
|
13727
|
+
targetHost: target.targetHost,
|
|
13728
|
+
serviceUrls,
|
|
13729
|
+
services: preview.services,
|
|
13730
|
+
placement: {
|
|
13731
|
+
workerId: context.state.workerId,
|
|
13732
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13733
|
+
requiredWorkerRole: "development",
|
|
13734
|
+
memoryReservationBytes: serviceMemoryReservation(preview.services)
|
|
13735
|
+
}
|
|
13736
|
+
},
|
|
13737
|
+
maxAttempts: 1
|
|
13738
|
+
})
|
|
13739
|
+
});
|
|
13740
|
+
const operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
|
|
13741
|
+
if (typeof preview.ssoEnabled === "boolean") {
|
|
13742
|
+
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.deploymentType)}`, { method: "PUT", body: JSON.stringify({ ssoEnabled: preview.ssoEnabled }) });
|
|
13743
|
+
}
|
|
13744
|
+
const sandbox = await recordSandbox(context, loaded, preview, operation.result?.serviceRoutes ?? [], operation.result?.healthEvidence ?? [], operation.result?.images ?? [], "available");
|
|
13745
|
+
const previewUrls = sandbox.previewUrls ?? [];
|
|
13746
|
+
context.state.previewUrls = previewUrls;
|
|
13747
|
+
await writeState(context.file, context.state);
|
|
13748
|
+
console.log(`development ${deploymentId} available`);
|
|
13749
|
+
for (const route of previewUrls)
|
|
13750
|
+
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13751
|
+
await runDevelopmentChecks(context, {
|
|
13752
|
+
parse: () => context.options,
|
|
13753
|
+
option: (options, key) => typeof options[key] === "string" ? options[key] : undefined,
|
|
13754
|
+
load: async () => loaded,
|
|
13755
|
+
source: async () => ({ bundle: new Uint8Array, files: 0 })
|
|
13756
|
+
});
|
|
13757
|
+
} catch (error) {
|
|
13758
|
+
context.state.runtimeMode = "development";
|
|
13759
|
+
await writeState(context.file, context.state);
|
|
13760
|
+
throw error;
|
|
13761
|
+
} finally {
|
|
13762
|
+
await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
|
|
13763
|
+
return;
|
|
13764
|
+
});
|
|
13765
|
+
}
|
|
13766
|
+
}
|
|
13767
|
+
async function runDevelopmentChecks(context, dependencies, requestedProfile, changedPaths = []) {
|
|
13768
|
+
if (!context.state.deploymentId || !context.state.previewUrls?.length) {
|
|
13769
|
+
throw new Error("development services must be available before checks run");
|
|
13770
|
+
}
|
|
13771
|
+
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
13772
|
+
const environment = resolveEnvironment(loaded.manifest, context.state.environment);
|
|
13773
|
+
if (environment.mode !== "development")
|
|
13774
|
+
throw new Error("continuous checks require a development environment");
|
|
13775
|
+
const profile = requestedProfile ?? environment.checkProfile;
|
|
13776
|
+
if (!["none", "smoke", "full"].includes(profile))
|
|
13777
|
+
throw new Error("check profile must be none, smoke, or full");
|
|
13778
|
+
const sourceRevision = context.state.sourceRevision ?? context.state.deploymentId;
|
|
13779
|
+
if (profile === "none") {
|
|
13780
|
+
context.state.quality = { sourceRevision, status: "unchecked", checks: [] };
|
|
13781
|
+
await writeState(context.file, context.state);
|
|
13782
|
+
console.log(`quality unchecked revision=${sourceRevision}`);
|
|
13783
|
+
return;
|
|
13784
|
+
}
|
|
13785
|
+
const profileChecks = environment.steps.filter((candidate) => profile === "full" || candidate.profile === "smoke");
|
|
13786
|
+
const checks = profile === "full" || !changedPaths.length ? profileChecks : profileChecks.filter((candidate) => !candidate.inputs.length || candidate.inputs.some((input) => changedPaths.some((path6) => repositoryPatternMatches(input, path6))));
|
|
13787
|
+
if (!profileChecks.length)
|
|
13788
|
+
throw new Error(`environment '${environment.id}' has no ${profile} checks`);
|
|
13789
|
+
if (!checks.length) {
|
|
13790
|
+
context.state.quality = {
|
|
13791
|
+
sourceRevision,
|
|
13792
|
+
status: context.state.quality?.status === "failed" ? "failed" : "passed",
|
|
13793
|
+
checks: context.state.quality?.checks ?? []
|
|
13794
|
+
};
|
|
13795
|
+
await writeState(context.file, context.state);
|
|
13796
|
+
console.log(`quality reused revision=${sourceRevision} (no affected application suites)`);
|
|
13797
|
+
return;
|
|
13798
|
+
}
|
|
13799
|
+
await waitForPublishedRoutes(context.state.previewUrls);
|
|
13800
|
+
const serviceUrls = Object.fromEntries(context.state.previewUrls.flatMap((route) => route.service ? [[`SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`, route.url]] : []));
|
|
13801
|
+
context.state.sequence += 1;
|
|
13802
|
+
const snapshotId = await createEnvironmentSnapshot(context);
|
|
13803
|
+
try {
|
|
13804
|
+
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13805
|
+
method: "POST",
|
|
13806
|
+
headers: {
|
|
13807
|
+
"idempotency-key": `development:${context.state.deploymentId}:checks:${sourceRevision}:${profile}:${context.state.sequence}`
|
|
13808
|
+
},
|
|
13809
|
+
body: JSON.stringify({
|
|
13810
|
+
kind: "workspace.check.run",
|
|
13811
|
+
stack: loaded.manifest.name ?? context.state.repository,
|
|
13812
|
+
environmentId: managedEnvironmentId(context),
|
|
13813
|
+
environmentSnapshotId: snapshotId,
|
|
13814
|
+
payload: {
|
|
13815
|
+
workspaceId: context.state.workspaceId,
|
|
13816
|
+
deploymentId: context.state.deploymentId,
|
|
13817
|
+
sandboxId: context.state.sandboxId,
|
|
13818
|
+
runtimeType: environment.deploymentType,
|
|
13819
|
+
sourceRevision,
|
|
13820
|
+
checks,
|
|
13821
|
+
serviceUrls,
|
|
13822
|
+
placement: {
|
|
13823
|
+
workerId: context.state.workerId,
|
|
13824
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13825
|
+
requiredWorkerRole: "development"
|
|
13826
|
+
}
|
|
13827
|
+
},
|
|
13828
|
+
maxAttempts: 1
|
|
13829
|
+
})
|
|
13830
|
+
});
|
|
13831
|
+
const completed = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 60 * 60000);
|
|
13832
|
+
const checksResult = Array.isArray(completed.result?.checks) ? completed.result.checks : [];
|
|
13833
|
+
const selectedIds = new Set(checks.map((item) => item.id));
|
|
13834
|
+
const reused = (context.state.quality?.checks ?? []).filter((item) => item && !selectedIds.has(String(item.check ?? ""))).map((item) => ({
|
|
13835
|
+
...item,
|
|
13836
|
+
sourceRevision,
|
|
13837
|
+
status: item.status === "passed" || item.status === "reused" ? "reused" : item.status,
|
|
13838
|
+
reusedFromRevision: item.sourceRevision
|
|
13839
|
+
}));
|
|
13840
|
+
const combined = [...checksResult, ...reused];
|
|
13841
|
+
const status3 = completed.result?.passed === true && combined.every((item) => item.required === false || ["passed", "reused"].includes(item.status)) ? "passed" : "failed";
|
|
13842
|
+
context.state.quality = { sourceRevision, status: status3, checks: combined };
|
|
13843
|
+
await writeState(context.file, context.state);
|
|
13844
|
+
console.log(`quality ${status3} revision=${sourceRevision}`);
|
|
13845
|
+
for (const result of checksResult) {
|
|
13846
|
+
const item = result;
|
|
13847
|
+
console.log(` ${String(item.application ?? item.check ?? "check").padEnd(12)} ${item.status} ${item.durationMs ?? 0}ms`);
|
|
13848
|
+
}
|
|
13849
|
+
} finally {
|
|
13850
|
+
await releaseEnvironmentSnapshot(context, snapshotId).catch(() => {
|
|
13851
|
+
return;
|
|
13852
|
+
});
|
|
13853
|
+
}
|
|
13854
|
+
}
|
|
13855
|
+
async function assertEnvironmentBranch(root, environment) {
|
|
13856
|
+
if (!environment.branches.length)
|
|
13857
|
+
return;
|
|
13858
|
+
const result = Bun.spawnSync(["git", "branch", "--show-current"], { cwd: root });
|
|
13859
|
+
const branch = result.exitCode === 0 ? result.stdout.toString().trim() : "";
|
|
13860
|
+
if (!branch)
|
|
13861
|
+
throw new Error(`environment '${environment.id}' requires a named Git branch`);
|
|
13862
|
+
const allowed = environment.branches.some((pattern) => {
|
|
13863
|
+
const expression = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
|
|
13864
|
+
return expression.test(branch);
|
|
13865
|
+
});
|
|
13866
|
+
if (!allowed) {
|
|
13867
|
+
throw new Error(`branch '${branch}' cannot deploy to environment '${environment.id}' (allowed: ${environment.branches.join(", ")})`);
|
|
13868
|
+
}
|
|
13869
|
+
}
|
|
13870
|
+
function escapeRegExp(value) {
|
|
13871
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
13872
|
+
}
|
|
13873
|
+
function repositoryPatternMatches(pattern, path6) {
|
|
13874
|
+
const normalized = pattern.replace(/^\.\//, "").replace(/\/\*\*$/, "").replace(/\/$/, "");
|
|
13875
|
+
if (!normalized.includes("*"))
|
|
13876
|
+
return path6 === normalized || path6.startsWith(`${normalized}/`);
|
|
13877
|
+
const expression = new RegExp(`^${normalized.split("**").map((part) => part.split("*").map(escapeRegExp).join("[^/]*")).join(".*")}(?:/.*)?$`);
|
|
13878
|
+
return expression.test(path6);
|
|
13879
|
+
}
|
|
13391
13880
|
async function redeploy(args, dependencies) {
|
|
13392
13881
|
const context = await getContext(args, dependencies);
|
|
13393
13882
|
const routing = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-aliases`);
|
|
@@ -13415,10 +13904,12 @@ async function redeploy(args, dependencies) {
|
|
|
13415
13904
|
source: dependencies.source,
|
|
13416
13905
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13417
13906
|
});
|
|
13418
|
-
const hostId =
|
|
13419
|
-
|
|
13420
|
-
|
|
13907
|
+
const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13908
|
+
const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13909
|
+
if (!workerId)
|
|
13910
|
+
throw new Error("workspace re-import did not bind a worker");
|
|
13421
13911
|
context.state.hostId = hostId;
|
|
13912
|
+
context.state.workerId = workerId;
|
|
13422
13913
|
await writeState(context.file, context.state);
|
|
13423
13914
|
await deploy(args, dependencies);
|
|
13424
13915
|
if (previousDeploymentId)
|
|
@@ -13494,25 +13985,47 @@ async function down(args, dependencies) {
|
|
|
13494
13985
|
await rm4(context.file, { force: true });
|
|
13495
13986
|
console.log(`sandbox ${context.state.sandboxId} destroyed`);
|
|
13496
13987
|
}
|
|
13988
|
+
async function resolveDevelopmentTarget(context, fallback) {
|
|
13989
|
+
const project = encodeURIComponent(context.state.projectId);
|
|
13990
|
+
const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
|
|
13991
|
+
const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
|
|
13992
|
+
const worker = workers.find((candidate) => candidate?.id === context.state.workerId);
|
|
13993
|
+
if (!worker || worker.status !== "online")
|
|
13994
|
+
throw new Error("development workspace worker is offline");
|
|
13995
|
+
const configuredTarget = worker?.configuration?.targetAddress;
|
|
13996
|
+
const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
|
|
13997
|
+
if (!targetHost)
|
|
13998
|
+
throw new Error("development worker targetAddress or environment targetHost is required");
|
|
13999
|
+
return {
|
|
14000
|
+
workerId: String(worker.id),
|
|
14001
|
+
...worker.hostId ? { hostId: String(worker.hostId) } : {},
|
|
14002
|
+
targetHost
|
|
14003
|
+
};
|
|
14004
|
+
}
|
|
13497
14005
|
async function resolveDeploymentTarget(context, fallback) {
|
|
13498
14006
|
const project = encodeURIComponent(context.state.projectId);
|
|
13499
|
-
const
|
|
13500
|
-
request(context.apiUrl, context.apiKey, `/v1/projects/${project}/hosts`),
|
|
13501
|
-
request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`)
|
|
13502
|
-
]);
|
|
14007
|
+
const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
|
|
13503
14008
|
const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
|
|
13504
14009
|
const requestedPool = process.env.SANDBLOCKS_DEPLOYMENT_WORKER_POOL?.trim() || process.env.SANDBLOCKS_RUNTIME_WORKER_POOL?.trim();
|
|
13505
14010
|
const eligible = workers.filter((worker2) => worker2?.status === "online" && Array.isArray(worker2.roles) && worker2.roles.includes("runtime"));
|
|
13506
|
-
const worker = eligible.find((candidate) => requestedPool && candidate.pool === requestedPool) ?? eligible[0];
|
|
13507
|
-
if (!worker?.
|
|
14011
|
+
const worker = eligible.find((candidate) => typeof fallback === "string" && candidate?.configuration?.targetAddress === fallback.trim()) ?? eligible.find((candidate) => requestedPool && candidate.pool === requestedPool) ?? eligible[0];
|
|
14012
|
+
if (!worker?.id)
|
|
13508
14013
|
throw new Error("no online deployment worker is available");
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
14014
|
+
let configuredTarget = worker?.configuration?.targetAddress;
|
|
14015
|
+
if (!configuredTarget && worker.hostId) {
|
|
14016
|
+
const legacyHosts = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/hosts`);
|
|
14017
|
+
const host = Array.isArray(legacyHosts.hosts) ? legacyHosts.hosts.find((candidate) => candidate?.id === worker.hostId) : undefined;
|
|
14018
|
+
configuredTarget = host?.labels?.targetHost;
|
|
14019
|
+
}
|
|
13512
14020
|
const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
|
|
13513
14021
|
if (!targetHost)
|
|
13514
|
-
throw new Error("deployment
|
|
13515
|
-
return {
|
|
14022
|
+
throw new Error("deployment worker targetAddress or preview targetHost is required");
|
|
14023
|
+
return {
|
|
14024
|
+
workerId: String(worker.id),
|
|
14025
|
+
...worker.hostId ? { hostId: String(worker.hostId) } : {},
|
|
14026
|
+
targetHost,
|
|
14027
|
+
pool: String(worker.pool ?? "unpooled")
|
|
14028
|
+
};
|
|
13516
14029
|
}
|
|
13517
14030
|
async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
|
|
13518
14031
|
const deploymentId = context.state.deploymentId;
|
|
@@ -13553,7 +14066,7 @@ async function createEnvironmentSnapshot(context) {
|
|
|
13553
14066
|
body: JSON.stringify({
|
|
13554
14067
|
stack: context.state.repository,
|
|
13555
14068
|
app: "*",
|
|
13556
|
-
environmentId: context
|
|
14069
|
+
environmentId: managedEnvironmentId(context)
|
|
13557
14070
|
})
|
|
13558
14071
|
});
|
|
13559
14072
|
const id2 = body.snapshot?.id;
|
|
@@ -13562,6 +14075,9 @@ async function createEnvironmentSnapshot(context) {
|
|
|
13562
14075
|
}
|
|
13563
14076
|
return id2;
|
|
13564
14077
|
}
|
|
14078
|
+
function managedEnvironmentId(context) {
|
|
14079
|
+
return context.state.managedEnvironment ?? context.state.environment;
|
|
14080
|
+
}
|
|
13565
14081
|
async function releaseEnvironmentSnapshot(context, snapshotId) {
|
|
13566
14082
|
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
|
|
13567
14083
|
}
|
|
@@ -13581,7 +14097,13 @@ async function destroyDeploymentById(context, deploymentId) {
|
|
|
13581
14097
|
kind: "workspace.service.destroy",
|
|
13582
14098
|
payload: {
|
|
13583
14099
|
deploymentId,
|
|
13584
|
-
placement:
|
|
14100
|
+
placement: context.state.deploymentWorkerId ? {
|
|
14101
|
+
workerId: context.state.deploymentWorkerId,
|
|
14102
|
+
requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
|
|
14103
|
+
} : {
|
|
14104
|
+
hostId: context.state.deploymentHostId ?? context.state.hostId,
|
|
14105
|
+
requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
|
|
14106
|
+
}
|
|
13585
14107
|
},
|
|
13586
14108
|
maxAttempts: 1
|
|
13587
14109
|
})
|
|
@@ -13629,6 +14151,7 @@ async function importSource(input) {
|
|
|
13629
14151
|
return {
|
|
13630
14152
|
files: source.files,
|
|
13631
14153
|
bytes: source.bundle.byteLength,
|
|
14154
|
+
manifest: source.manifest,
|
|
13632
14155
|
operation: await wait(input.apiUrl, input.apiKey, submitted.operation.id)
|
|
13633
14156
|
};
|
|
13634
14157
|
}
|
|
@@ -13651,6 +14174,10 @@ async function getContext(args, dependencies) {
|
|
|
13651
14174
|
let state;
|
|
13652
14175
|
try {
|
|
13653
14176
|
state = JSON.parse(await readFile5(file, "utf8"));
|
|
14177
|
+
if (!state.workerId && state.hostId)
|
|
14178
|
+
state.workerId = state.hostId;
|
|
14179
|
+
if (!state.workerId)
|
|
14180
|
+
throw new Error("sandbox state has no bound worker");
|
|
13654
14181
|
} catch {
|
|
13655
14182
|
throw new Error(`sandbox '${environment}' does not exist; use sandbox create or up`);
|
|
13656
14183
|
}
|
|
@@ -13674,8 +14201,10 @@ async function request(apiUrl, apiKey, pathname, init = {}) {
|
|
|
13674
14201
|
headers.set("content-type", "application/json");
|
|
13675
14202
|
const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
|
|
13676
14203
|
const body = await response.json().catch(() => ({}));
|
|
13677
|
-
if (!response.ok)
|
|
13678
|
-
|
|
14204
|
+
if (!response.ok) {
|
|
14205
|
+
const placement = body.placement ? `: ${JSON.stringify(body.placement)}` : "";
|
|
14206
|
+
throw new Error(`${String(body.error ?? `Sandblocks request failed (${response.status})`)}${placement}`);
|
|
14207
|
+
}
|
|
13679
14208
|
return body;
|
|
13680
14209
|
}
|
|
13681
14210
|
async function leaseRequest(context, pathname, init = {}) {
|
|
@@ -13820,7 +14349,7 @@ function resolvedServiceDomains(sandboxId, deploymentId, preview) {
|
|
|
13820
14349
|
]));
|
|
13821
14350
|
}
|
|
13822
14351
|
function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
|
|
13823
|
-
const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${
|
|
14352
|
+
const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${service}`).digest("hex").slice(0, 8)}`;
|
|
13824
14353
|
const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
|
|
13825
14354
|
return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
|
|
13826
14355
|
}
|
|
@@ -15120,7 +15649,16 @@ var HELP = `Sandblocks CLI
|
|
|
15120
15649
|
sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
|
|
15121
15650
|
[--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
|
|
15122
15651
|
sandblocks artifact push <directory> --project <id> --stack <id> [--pool <pool>] [--json]
|
|
15652
|
+
sandblocks worker <list|create|rotate-secret|drain|resume|approve-update> --organization <id>
|
|
15653
|
+
[--worker <uuid>] [--name <name>] [--type <build|deployment>]
|
|
15654
|
+
[--pool-id <uuid>] [--project <id>] [--version <version>] [--json]
|
|
15655
|
+
sandblocks registry <list|create|assign> --organization <id> [--project <id>]
|
|
15656
|
+
[--name <name>] [--endpoint <url>] [--registry <uuid>]
|
|
15657
|
+
[--purpose <build-cache|artifact-publication|deployment-pull>] [--json]
|
|
15123
15658
|
sandblocks sandbox up [directory] [--environment <id>] [--project <id>] [--build-pool <pool>]
|
|
15659
|
+
sandblocks sandbox dev [directory] [--environment <id>] [--checks <smoke|full|none>]
|
|
15660
|
+
sandblocks sandbox sync [directory] [--environment <id>] [--watch] [--checks <smoke|full|none>]
|
|
15661
|
+
sandblocks sandbox check [directory] [--environment <id>] [--profile <smoke|full>]
|
|
15124
15662
|
sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
|
|
15125
15663
|
sandblocks sandbox deploy [directory] [--environment <id>] [--build-pool <pool>]
|
|
15126
15664
|
[--artifact <image@sha256:digest>] [--deployment-id <uuid>] [--resume]
|
|
@@ -15179,10 +15717,7 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
15179
15717
|
parse: parse2,
|
|
15180
15718
|
option: option2,
|
|
15181
15719
|
load,
|
|
15182
|
-
source:
|
|
15183
|
-
const bundle = await createSourceTar(root);
|
|
15184
|
-
return { bundle, files: sourceFileCount };
|
|
15185
|
-
}
|
|
15720
|
+
source: createSourceTar
|
|
15186
15721
|
})
|
|
15187
15722
|
});
|
|
15188
15723
|
else if (command === "runtime")
|
|
@@ -15193,6 +15728,10 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
15193
15728
|
await secret(args);
|
|
15194
15729
|
else if (command === "artifact")
|
|
15195
15730
|
await artifact(args);
|
|
15731
|
+
else if (command === "worker")
|
|
15732
|
+
await worker(args);
|
|
15733
|
+
else if (command === "registry")
|
|
15734
|
+
await registry(args);
|
|
15196
15735
|
else if (command === "sandbox")
|
|
15197
15736
|
await sandbox(args);
|
|
15198
15737
|
else if (command === "sdk")
|
|
@@ -15303,21 +15842,21 @@ async function doctor(args) {
|
|
|
15303
15842
|
{ label: "artifact publish", kind: "workspace.artifact.publish", role: "build", memory: 1024 ** 3 },
|
|
15304
15843
|
{ label: "service deploy", kind: "workspace.service.deploy", role: "runtime", memory: 6 * 1024 ** 3 }
|
|
15305
15844
|
];
|
|
15306
|
-
const explanations = await Promise.all(checks.map(async (
|
|
15845
|
+
const explanations = await Promise.all(checks.map(async (check2) => {
|
|
15307
15846
|
const query = new URLSearchParams({
|
|
15308
|
-
kind:
|
|
15309
|
-
role:
|
|
15310
|
-
memoryReservationBytes: String(
|
|
15847
|
+
kind: check2.kind,
|
|
15848
|
+
role: check2.role,
|
|
15849
|
+
memoryReservationBytes: String(check2.memory)
|
|
15311
15850
|
});
|
|
15312
15851
|
const result = await sandblocksRequest(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/placement/explain?${query}`);
|
|
15313
|
-
return { ...
|
|
15852
|
+
return { ...check2, explanation: result.explanation };
|
|
15314
15853
|
}));
|
|
15315
15854
|
if (options.json)
|
|
15316
15855
|
console.log(JSON.stringify({ projectId, explanations }, null, 2));
|
|
15317
15856
|
else
|
|
15318
|
-
for (const
|
|
15319
|
-
const explanation = objectRecord(
|
|
15320
|
-
console.log(`${
|
|
15857
|
+
for (const check2 of explanations) {
|
|
15858
|
+
const explanation = objectRecord(check2.explanation);
|
|
15859
|
+
console.log(`${check2.label.padEnd(16)} ${explanation.eligible ? "eligible" : "blocked"}`);
|
|
15321
15860
|
if (!explanation.eligible && Array.isArray(explanation.candidates))
|
|
15322
15861
|
for (const candidate of explanation.candidates) {
|
|
15323
15862
|
const row = objectRecord(candidate);
|
|
@@ -15479,6 +16018,102 @@ async function artifact(args) {
|
|
|
15479
16018
|
console.log(`deploy with: sandblocks sandbox deploy . --artifact ${result.image} --deployment-id ${result.deploymentId}`);
|
|
15480
16019
|
}
|
|
15481
16020
|
}
|
|
16021
|
+
async function worker(args) {
|
|
16022
|
+
const [action = "list", ...rest] = args;
|
|
16023
|
+
const options = parse2(rest);
|
|
16024
|
+
const organizationId = option2(options, "organization");
|
|
16025
|
+
const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
16026
|
+
const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
16027
|
+
if (!organizationId)
|
|
16028
|
+
throw new Error("worker command requires --organization");
|
|
16029
|
+
if (!apiUrl || !apiKey)
|
|
16030
|
+
throw new Error("worker command requires Sandblocks API URL and key");
|
|
16031
|
+
const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
|
|
16032
|
+
let body;
|
|
16033
|
+
if (action === "list")
|
|
16034
|
+
body = await client2.listWorkers(organizationId);
|
|
16035
|
+
else if (action === "create") {
|
|
16036
|
+
const name = option2(options, "name");
|
|
16037
|
+
const type = option2(options, "type");
|
|
16038
|
+
if (!name || type !== "build" && type !== "deployment")
|
|
16039
|
+
throw new Error("worker create requires --name and --type build|deployment");
|
|
16040
|
+
body = await client2.createWorker({
|
|
16041
|
+
organizationId,
|
|
16042
|
+
name,
|
|
16043
|
+
type,
|
|
16044
|
+
...option2(options, "pool-id") ? { poolId: option2(options, "pool-id") } : {},
|
|
16045
|
+
...option2(options, "project") ? { projectIds: [option2(options, "project")] } : {}
|
|
16046
|
+
});
|
|
16047
|
+
} else {
|
|
16048
|
+
const workerId = option2(options, "worker");
|
|
16049
|
+
if (!workerId)
|
|
16050
|
+
throw new Error(`worker ${action} requires --worker`);
|
|
16051
|
+
if (action === "rotate-secret")
|
|
16052
|
+
body = await client2.rotateWorkerSecret(organizationId, workerId);
|
|
16053
|
+
else if (action === "move") {
|
|
16054
|
+
const targetWorkerIds = (option2(options, "target-workers") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
16055
|
+
if (!targetWorkerIds.length)
|
|
16056
|
+
throw new Error("worker move requires --target-workers <id,id,...>");
|
|
16057
|
+
body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/move`, { method: "POST", body: JSON.stringify({ targetWorkerIds }) });
|
|
16058
|
+
} else if (action === "approve-update") {
|
|
16059
|
+
const version = option2(options, "version");
|
|
16060
|
+
if (!version)
|
|
16061
|
+
throw new Error("worker approve-update requires --version");
|
|
16062
|
+
body = await client2.approveWorkerUpdate(organizationId, workerId, version);
|
|
16063
|
+
} else if (action === "drain" || action === "resume")
|
|
16064
|
+
body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/${action}`, { method: "POST" });
|
|
16065
|
+
else
|
|
16066
|
+
throw new Error(`unsupported worker command '${action}'`);
|
|
16067
|
+
}
|
|
16068
|
+
if (options.json || action !== "list")
|
|
16069
|
+
console.log(JSON.stringify(body, null, 2));
|
|
16070
|
+
else
|
|
16071
|
+
for (const candidate of Array.isArray(body.workers) ? body.workers : []) {
|
|
16072
|
+
const row = objectRecord(candidate);
|
|
16073
|
+
console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.type ?? "unknown"} ${row.state ?? "unknown"}`);
|
|
16074
|
+
}
|
|
16075
|
+
}
|
|
16076
|
+
async function registry(args) {
|
|
16077
|
+
const [action = "list", ...rest] = args;
|
|
16078
|
+
const options = parse2(rest);
|
|
16079
|
+
const organizationId = option2(options, "organization");
|
|
16080
|
+
const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
16081
|
+
const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
16082
|
+
if (!organizationId)
|
|
16083
|
+
throw new Error("registry command requires --organization");
|
|
16084
|
+
if (!apiUrl || !apiKey)
|
|
16085
|
+
throw new Error("registry command requires Sandblocks API URL and key");
|
|
16086
|
+
const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
|
|
16087
|
+
let body;
|
|
16088
|
+
if (action === "list")
|
|
16089
|
+
body = await client2.listRegistries(organizationId);
|
|
16090
|
+
else if (action === "create") {
|
|
16091
|
+
const name = option2(options, "name");
|
|
16092
|
+
const endpoint = option2(options, "endpoint");
|
|
16093
|
+
if (!name || !endpoint)
|
|
16094
|
+
throw new Error("registry create requires --name and --endpoint");
|
|
16095
|
+
body = await client2.createRegistry({ organizationId, name, endpoint });
|
|
16096
|
+
} else if (action === "assign") {
|
|
16097
|
+
const projectId = option2(options, "project");
|
|
16098
|
+
const registryId = option2(options, "registry");
|
|
16099
|
+
const purpose = option2(options, "purpose");
|
|
16100
|
+
if (!projectId || !registryId || !["build-cache", "artifact-publication", "deployment-pull"].includes(purpose ?? ""))
|
|
16101
|
+
throw new Error("registry assign requires --project, --registry, and a valid --purpose");
|
|
16102
|
+
body = await client2.assignProjectRegistry(projectId, {
|
|
16103
|
+
purpose,
|
|
16104
|
+
registryId,
|
|
16105
|
+
...option2(options, "repository-prefix") ? { repositoryPrefix: option2(options, "repository-prefix") } : {}
|
|
16106
|
+
});
|
|
16107
|
+
} else
|
|
16108
|
+
throw new Error(`unsupported registry command '${action}'`);
|
|
16109
|
+
if (options.json || action !== "list")
|
|
16110
|
+
console.log(JSON.stringify(body, null, 2));
|
|
16111
|
+
else
|
|
16112
|
+
for (const candidate of Array.isArray(body.registries) ? body.registries : []) {
|
|
16113
|
+
const row = objectRecord(candidate);
|
|
16114
|
+
console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.endpoint ?? "unknown"}`);
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
15482
16117
|
function printResources(kind, action, body) {
|
|
15483
16118
|
if (action === "settings") {
|
|
15484
16119
|
console.log(JSON.stringify(body.settings ?? {}, null, 2));
|
|
@@ -15505,10 +16140,7 @@ async function sandbox(args) {
|
|
|
15505
16140
|
parse: parse2,
|
|
15506
16141
|
option: option2,
|
|
15507
16142
|
load,
|
|
15508
|
-
source:
|
|
15509
|
-
const bundle2 = await createSourceTar(root2);
|
|
15510
|
-
return { bundle: bundle2, files: sourceFileCount };
|
|
15511
|
-
}
|
|
16143
|
+
source: createSourceTar
|
|
15512
16144
|
});
|
|
15513
16145
|
}
|
|
15514
16146
|
if (subcommand === "deploy")
|
|
@@ -15533,7 +16165,8 @@ async function sandbox(args) {
|
|
|
15533
16165
|
throw new Error("sandbox import requires --api-url or SANDBLOCKS_API_URL");
|
|
15534
16166
|
if (!apiKey)
|
|
15535
16167
|
throw new Error("sandbox import requires --api-key or SANDBLOCKS_API_KEY");
|
|
15536
|
-
const
|
|
16168
|
+
const source = await createSourceTar(root);
|
|
16169
|
+
const bundle = source.bundle;
|
|
15537
16170
|
const response = await fetch(`${apiUrl}/v1/projects/${encodeURIComponent(projectId)}/workspaces/import`, {
|
|
15538
16171
|
method: "POST",
|
|
15539
16172
|
headers: {
|
|
@@ -15550,11 +16183,11 @@ async function sandbox(args) {
|
|
|
15550
16183
|
if (!response.ok)
|
|
15551
16184
|
throw new Error(String(submitted.error ?? `sandbox import failed (${response.status})`));
|
|
15552
16185
|
const operation = options.wait ? await waitForOperation(apiUrl, apiKey, String(submitted.operation.id)) : submitted.operation;
|
|
15553
|
-
const output = { workspaceId, files:
|
|
16186
|
+
const output = { workspaceId, files: source.files, bytes: bundle.byteLength, operation };
|
|
15554
16187
|
if (options.json)
|
|
15555
16188
|
console.log(JSON.stringify(output, null, 2));
|
|
15556
16189
|
else {
|
|
15557
|
-
console.log(`source ${
|
|
16190
|
+
console.log(`source ${source.files} files (${bundle.byteLength} bytes)`);
|
|
15558
16191
|
console.log(`workspace ${workspaceId}`);
|
|
15559
16192
|
console.log(`operation ${operation.id} (${operation.state})`);
|
|
15560
16193
|
if (operation.result?.container)
|
|
@@ -15647,7 +16280,6 @@ async function sandboxPromote(args) {
|
|
|
15647
16280
|
else
|
|
15648
16281
|
console.log(`promoted ${sandboxId} to production`);
|
|
15649
16282
|
}
|
|
15650
|
-
var sourceFileCount = 0;
|
|
15651
16283
|
async function createPrebuiltTar(root) {
|
|
15652
16284
|
const names = [];
|
|
15653
16285
|
const visit2 = async (directory, prefix = "") => {
|
|
@@ -15693,7 +16325,7 @@ async function createPrebuiltTar(root) {
|
|
|
15693
16325
|
}
|
|
15694
16326
|
return output;
|
|
15695
16327
|
}
|
|
15696
|
-
async function createSourceTar(root) {
|
|
16328
|
+
async function createSourceTar(root, previous) {
|
|
15697
16329
|
const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
15698
16330
|
cwd: root,
|
|
15699
16331
|
stdout: "pipe",
|
|
@@ -15714,6 +16346,7 @@ async function createSourceTar(root) {
|
|
|
15714
16346
|
if (names.length > 20000)
|
|
15715
16347
|
throw new Error("local source contains more than 20000 files");
|
|
15716
16348
|
const chunks = [];
|
|
16349
|
+
const manifest = {};
|
|
15717
16350
|
let total = 1024;
|
|
15718
16351
|
for (const name of names) {
|
|
15719
16352
|
if (name.includes("\x00") || name.includes("\\") || name.startsWith("/") || name.split("/").includes("..")) {
|
|
@@ -15733,6 +16366,11 @@ async function createSourceTar(root) {
|
|
|
15733
16366
|
if (!info.isFile())
|
|
15734
16367
|
continue;
|
|
15735
16368
|
const contents = new Uint8Array(await readFile8(file));
|
|
16369
|
+
const mode = info.mode & 511;
|
|
16370
|
+
const sha256 = createHash3("sha256").update(contents).digest("hex");
|
|
16371
|
+
manifest[name] = { sha256, mode, bytes: contents.byteLength };
|
|
16372
|
+
if (previous?.[name]?.sha256 === sha256 && previous[name]?.mode === mode)
|
|
16373
|
+
continue;
|
|
15736
16374
|
const header = tarHeader(name, contents.byteLength, info.mode, Math.floor(info.mtimeMs / 1000));
|
|
15737
16375
|
const padding = (512 - contents.byteLength % 512) % 512;
|
|
15738
16376
|
total += 512 + contents.byteLength + padding;
|
|
@@ -15740,9 +16378,15 @@ async function createSourceTar(root) {
|
|
|
15740
16378
|
throw new Error("local source bundle exceeds 256 MiB");
|
|
15741
16379
|
chunks.push(header, contents, new Uint8Array(padding));
|
|
15742
16380
|
}
|
|
15743
|
-
|
|
15744
|
-
if (!sourceFileCount)
|
|
16381
|
+
if (!Object.keys(manifest).length)
|
|
15745
16382
|
throw new Error("local directory has no regular source files");
|
|
16383
|
+
if (previous) {
|
|
16384
|
+
const contents = new TextEncoder().encode(JSON.stringify({ version: 1, files: manifest }));
|
|
16385
|
+
const name = ".sandblocks-sync-manifest.json";
|
|
16386
|
+
const padding = (512 - contents.byteLength % 512) % 512;
|
|
16387
|
+
total += 512 + contents.byteLength + padding;
|
|
16388
|
+
chunks.push(tarHeader(name, contents.byteLength, 384, Math.floor(Date.now() / 1000)), contents, new Uint8Array(padding));
|
|
16389
|
+
}
|
|
15746
16390
|
chunks.push(new Uint8Array(1024));
|
|
15747
16391
|
const output = new Uint8Array(total);
|
|
15748
16392
|
let offset = 0;
|
|
@@ -15750,7 +16394,7 @@ async function createSourceTar(root) {
|
|
|
15750
16394
|
output.set(chunk, offset);
|
|
15751
16395
|
offset += chunk.byteLength;
|
|
15752
16396
|
}
|
|
15753
|
-
return output;
|
|
16397
|
+
return { bundle: output, files: Object.keys(manifest).length, manifest };
|
|
15754
16398
|
}
|
|
15755
16399
|
async function gitIgnoredFiles(root, names) {
|
|
15756
16400
|
if (!names.length)
|
|
@@ -15936,7 +16580,8 @@ function parse2(args) {
|
|
|
15936
16580
|
"allow-host",
|
|
15937
16581
|
"rootless",
|
|
15938
16582
|
"allow-unpinned-images",
|
|
15939
|
-
"read-only-root"
|
|
16583
|
+
"read-only-root",
|
|
16584
|
+
"resume"
|
|
15940
16585
|
].includes(rawKey)) {
|
|
15941
16586
|
output[rawKey] = inline === undefined ? true : inline !== "false";
|
|
15942
16587
|
continue;
|
|
@@ -16003,4 +16648,4 @@ export {
|
|
|
16003
16648
|
parse2 as parse
|
|
16004
16649
|
};
|
|
16005
16650
|
|
|
16006
|
-
//# debugId=
|
|
16651
|
+
//# debugId=ACB6B270856F84F064756E2164756E21
|