@sandblocks/cli 0.5.0-b.9 → 0.6.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/dist/cli.js +742 -96
- package/dist/cli.js.map +8 -8
- package/package.json +2 -2
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,99 @@ 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.sourceSyncInitialized === true ? context.state.sourceManifest : undefined);
|
|
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
|
+
context.state.sourceSyncInitialized = true;
|
|
13199
|
+
await writeState(context.file, context.state);
|
|
13200
|
+
console.log(`sync revision=${context.state.sourceRevision} changed=${completed.result?.changed ?? 0} deleted=${completed.result?.deleted ?? 0}`);
|
|
13201
|
+
if (context.options.checks !== "none") {
|
|
13202
|
+
const changedPaths = [
|
|
13203
|
+
...Array.isArray(completed.result?.changedPaths) ? completed.result.changedPaths : [],
|
|
13204
|
+
...Array.isArray(completed.result?.deletedPaths) ? completed.result.deletedPaths : []
|
|
13205
|
+
].filter((value) => typeof value === "string");
|
|
13206
|
+
await runDevelopmentChecks(context, dependencies, undefined, changedPaths);
|
|
13207
|
+
}
|
|
13208
|
+
};
|
|
13209
|
+
await apply();
|
|
13210
|
+
if (context.options.watch !== true)
|
|
13211
|
+
return;
|
|
13212
|
+
console.log("watch waiting for source changes (Ctrl-C keeps the sandbox running)");
|
|
13213
|
+
while (true) {
|
|
13214
|
+
await waitForSourceChange(context.root);
|
|
13215
|
+
await apply();
|
|
13216
|
+
}
|
|
13217
|
+
}
|
|
13218
|
+
async function check(args, dependencies) {
|
|
13219
|
+
const context = await getContext(withDefaultEnvironment(args, "develop"), dependencies);
|
|
13220
|
+
await runDevelopmentChecks(context, dependencies, dependencies.option(context.options, "profile"));
|
|
13221
|
+
}
|
|
13222
|
+
async function waitForSourceChange(root) {
|
|
13223
|
+
await new Promise((resolveChange) => {
|
|
13224
|
+
let settled = false;
|
|
13225
|
+
let debounce;
|
|
13226
|
+
const watcher = watchFiles(root, { recursive: true }, (_event, filename) => {
|
|
13227
|
+
const path6 = String(filename ?? "").replaceAll("\\", "/");
|
|
13228
|
+
if (!path6 || path6 === ".git" || path6.startsWith(".git/") || path6 === ".sandblocks" || path6.startsWith(".sandblocks/") || path6 === "node_modules" || path6.includes("/node_modules/")) {
|
|
13229
|
+
return;
|
|
13230
|
+
}
|
|
13231
|
+
if (debounce)
|
|
13232
|
+
clearTimeout(debounce);
|
|
13233
|
+
debounce = setTimeout(() => {
|
|
13234
|
+
if (settled)
|
|
13235
|
+
return;
|
|
13236
|
+
settled = true;
|
|
13237
|
+
watcher.close();
|
|
13238
|
+
resolveChange();
|
|
13239
|
+
}, 150);
|
|
13240
|
+
});
|
|
13241
|
+
watcher.on("error", () => {
|
|
13242
|
+
if (settled)
|
|
13243
|
+
return;
|
|
13244
|
+
settled = true;
|
|
13245
|
+
watcher.close();
|
|
13246
|
+
setTimeout(resolveChange, 1000);
|
|
13247
|
+
});
|
|
13248
|
+
});
|
|
13249
|
+
}
|
|
13250
|
+
function withDefaultEnvironment(args, environment) {
|
|
13251
|
+
return withDefaultOption(args, "environment", environment);
|
|
13252
|
+
}
|
|
13253
|
+
function withDefaultOption(args, option2, value) {
|
|
13254
|
+
return args.some((candidate) => candidate === `--${option2}` || candidate.startsWith(`--${option2}=`)) ? args : [...args, `--${option2}`, value];
|
|
13255
|
+
}
|
|
13016
13256
|
async function create(args, dependencies) {
|
|
13017
13257
|
const options = dependencies.parse(args);
|
|
13018
13258
|
const root = path5.resolve(options.positionals[0] ?? process.cwd());
|
|
@@ -13053,9 +13293,10 @@ async function create(args, dependencies) {
|
|
|
13053
13293
|
source: dependencies.source,
|
|
13054
13294
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13055
13295
|
});
|
|
13056
|
-
const hostId =
|
|
13057
|
-
|
|
13058
|
-
|
|
13296
|
+
const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13297
|
+
const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13298
|
+
if (!workerId)
|
|
13299
|
+
throw new Error("workspace import did not bind a worker");
|
|
13059
13300
|
const state = {
|
|
13060
13301
|
version: 1,
|
|
13061
13302
|
environment,
|
|
@@ -13065,14 +13306,17 @@ async function create(args, dependencies) {
|
|
|
13065
13306
|
repository,
|
|
13066
13307
|
leaseId: lease.lease.id,
|
|
13067
13308
|
leaseToken: lease.token,
|
|
13068
|
-
hostId,
|
|
13309
|
+
...hostId ? { hostId } : {},
|
|
13310
|
+
workerId,
|
|
13311
|
+
sourceRevision: typeof imported.operation.result?.sha === "string" ? imported.operation.result.sha : undefined,
|
|
13312
|
+
sourceManifest: imported.manifest,
|
|
13069
13313
|
sequence: 0,
|
|
13070
13314
|
createdAt: new Date().toISOString()
|
|
13071
13315
|
};
|
|
13072
13316
|
await writeState(file, state);
|
|
13073
13317
|
console.log(`sandbox ${sandboxId}`);
|
|
13074
13318
|
console.log(`workspace ${workspaceId}`);
|
|
13075
|
-
console.log(`
|
|
13319
|
+
console.log(`worker ${workerId}`);
|
|
13076
13320
|
} catch (error) {
|
|
13077
13321
|
await request(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${sandboxId}`, {
|
|
13078
13322
|
method: "DELETE"
|
|
@@ -13095,8 +13339,14 @@ async function status2(args, dependencies) {
|
|
|
13095
13339
|
console.log(`state ${path5.relative(context.root, context.file)}`);
|
|
13096
13340
|
console.log(`sandbox ${context.state.sandboxId} (${sandbox.sandbox.status})`);
|
|
13097
13341
|
console.log(`workspace ${context.state.workspaceId}`);
|
|
13098
|
-
console.log(`
|
|
13342
|
+
console.log(`worker ${context.state.workerId}`);
|
|
13099
13343
|
console.log(`lease expires ${session.session.expiresAt}`);
|
|
13344
|
+
if (context.state.runtimeMode)
|
|
13345
|
+
console.log(`mode ${context.state.runtimeMode}`);
|
|
13346
|
+
if (context.state.sourceRevision)
|
|
13347
|
+
console.log(`revision ${context.state.sourceRevision}`);
|
|
13348
|
+
if (context.state.quality)
|
|
13349
|
+
console.log(`quality ${context.state.quality.status} (${context.state.quality.sourceRevision})`);
|
|
13100
13350
|
for (const route of context.state.previewUrls ?? [])
|
|
13101
13351
|
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13102
13352
|
}
|
|
@@ -13125,15 +13375,22 @@ async function withDeploymentLock(args, dependencies, action) {
|
|
|
13125
13375
|
}
|
|
13126
13376
|
async function deployUnlocked(args, dependencies) {
|
|
13127
13377
|
const context = await getContext(args, dependencies);
|
|
13128
|
-
|
|
13378
|
+
const resume = context.options.resume === true;
|
|
13379
|
+
if (context.state.deploymentId && !(resume && context.state.candidate?.deploymentId === context.state.deploymentId))
|
|
13129
13380
|
throw new Error("preview is already deployed; use sandbox redeploy");
|
|
13130
13381
|
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
13131
13382
|
const stack = loaded.manifest.name ?? context.state.repository;
|
|
13132
13383
|
const preview = await applyManagedRoutingPolicy(context, stack, selectPreview(loaded, context.state.environment));
|
|
13384
|
+
await assertEnvironmentBranch(context.root, preview);
|
|
13385
|
+
context.state.managedEnvironment = preview.managedEnvironment;
|
|
13386
|
+
if (preview.mode === "development") {
|
|
13387
|
+
await deployDevelopmentUnlocked(context, loaded, preview);
|
|
13388
|
+
return;
|
|
13389
|
+
}
|
|
13133
13390
|
const deploymentTarget = await resolveDeploymentTarget(context, process.env.SANDBLOCKS_SANDBOX_TARGET_HOST ?? preview.targetHost);
|
|
13134
13391
|
const targetHost = deploymentTarget.targetHost;
|
|
13135
13392
|
context.state.deploymentHostId = deploymentTarget.hostId;
|
|
13136
|
-
|
|
13393
|
+
context.state.deploymentWorkerId = deploymentTarget.workerId;
|
|
13137
13394
|
const previousCandidate = resume ? context.state.candidate : undefined;
|
|
13138
13395
|
if (resume && !previousCandidate)
|
|
13139
13396
|
throw new Error("no resumable candidate exists for this sandbox");
|
|
@@ -13143,13 +13400,14 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13143
13400
|
throw new Error("deployment ID must be a UUID");
|
|
13144
13401
|
if (previousCandidate && requestedDeploymentId && requestedDeploymentId !== previousCandidate.deploymentId)
|
|
13145
13402
|
throw new Error("--deployment-id does not match the resumable candidate");
|
|
13146
|
-
const environmentSnapshotId = previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
|
|
13147
|
-
context.state.candidate = previousCandidate
|
|
13403
|
+
const environmentSnapshotId = resume ? await createEnvironmentSnapshot(context) : previousCandidate?.environmentSnapshotId ?? await createEnvironmentSnapshot(context);
|
|
13404
|
+
context.state.candidate = previousCandidate ? { ...previousCandidate, environmentSnapshotId } : { deploymentId, environmentSnapshotId };
|
|
13148
13405
|
await writeState(context.file, context.state);
|
|
13149
13406
|
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
13150
13407
|
const serviceSteps = resolveServiceSteps(loaded.manifest, context.state.environment);
|
|
13151
13408
|
let buildWorkspaceId = previousCandidate?.buildWorkspaceId;
|
|
13152
13409
|
let buildHostId = previousCandidate?.buildHostId;
|
|
13410
|
+
let buildWorkerId = previousCandidate?.buildWorkerId;
|
|
13153
13411
|
const suppliedArtifact = dependencies.option(context.options, "artifact");
|
|
13154
13412
|
if (suppliedArtifact && !/@sha256:[a-f0-9]{64}$/.test(suppliedArtifact))
|
|
13155
13413
|
throw new Error("--artifact must be an immutable OCI image digest");
|
|
@@ -13173,10 +13431,17 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13173
13431
|
source: dependencies.source,
|
|
13174
13432
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13175
13433
|
});
|
|
13176
|
-
buildHostId =
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13434
|
+
buildHostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13435
|
+
buildWorkerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13436
|
+
if (!buildWorkerId)
|
|
13437
|
+
throw new Error("build workspace did not bind a build worker");
|
|
13438
|
+
context.state.candidate = {
|
|
13439
|
+
deploymentId,
|
|
13440
|
+
environmentSnapshotId,
|
|
13441
|
+
buildWorkspaceId,
|
|
13442
|
+
buildHostId,
|
|
13443
|
+
buildWorkerId
|
|
13444
|
+
};
|
|
13180
13445
|
await writeState(context.file, context.state);
|
|
13181
13446
|
const stepOperation = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13182
13447
|
method: "POST",
|
|
@@ -13184,7 +13449,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13184
13449
|
body: JSON.stringify({
|
|
13185
13450
|
kind: "workspace.step.run",
|
|
13186
13451
|
stack: context.state.repository,
|
|
13187
|
-
environmentId: context
|
|
13452
|
+
environmentId: managedEnvironmentId(context),
|
|
13188
13453
|
environmentSnapshotId,
|
|
13189
13454
|
payload: {
|
|
13190
13455
|
workspaceId: buildWorkspaceId,
|
|
@@ -13194,7 +13459,11 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13194
13459
|
serviceUrls,
|
|
13195
13460
|
steps: serviceSteps,
|
|
13196
13461
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
|
|
13197
|
-
placement: {
|
|
13462
|
+
placement: {
|
|
13463
|
+
workerId: buildWorkerId,
|
|
13464
|
+
...buildHostId ? { hostId: buildHostId } : {},
|
|
13465
|
+
memoryReservationBytes: 2 * 1024 * 1024 * 1024
|
|
13466
|
+
}
|
|
13198
13467
|
},
|
|
13199
13468
|
maxAttempts: 1
|
|
13200
13469
|
})
|
|
@@ -13205,22 +13474,29 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13205
13474
|
environmentSnapshotId,
|
|
13206
13475
|
buildWorkspaceId,
|
|
13207
13476
|
buildHostId,
|
|
13477
|
+
buildWorkerId,
|
|
13208
13478
|
buildCompleted: true
|
|
13209
13479
|
};
|
|
13210
13480
|
await writeState(context.file, context.state);
|
|
13211
13481
|
}
|
|
13212
|
-
if (!
|
|
13213
|
-
throw new Error("resumable build
|
|
13482
|
+
if (!buildWorkerId)
|
|
13483
|
+
throw new Error("resumable build worker is unavailable");
|
|
13214
13484
|
const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13215
13485
|
method: "POST",
|
|
13216
|
-
headers: {
|
|
13486
|
+
headers: {
|
|
13487
|
+
"idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
|
|
13488
|
+
},
|
|
13217
13489
|
body: JSON.stringify({
|
|
13218
13490
|
kind: "workspace.artifact.publish",
|
|
13219
13491
|
payload: {
|
|
13220
13492
|
workspaceId: buildWorkspaceId,
|
|
13221
13493
|
deploymentId,
|
|
13222
13494
|
stack,
|
|
13223
|
-
placement: {
|
|
13495
|
+
placement: {
|
|
13496
|
+
workerId: buildWorkerId,
|
|
13497
|
+
...buildHostId ? { hostId: buildHostId } : {},
|
|
13498
|
+
memoryReservationBytes: 1024 * 1024 * 1024
|
|
13499
|
+
}
|
|
13224
13500
|
},
|
|
13225
13501
|
maxAttempts: 1
|
|
13226
13502
|
})
|
|
@@ -13234,6 +13510,7 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13234
13510
|
environmentSnapshotId,
|
|
13235
13511
|
buildWorkspaceId,
|
|
13236
13512
|
buildHostId,
|
|
13513
|
+
buildWorkerId,
|
|
13237
13514
|
buildCompleted: true,
|
|
13238
13515
|
artifactImage
|
|
13239
13516
|
};
|
|
@@ -13257,7 +13534,9 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13257
13534
|
if (!artifactImage) {
|
|
13258
13535
|
const publish = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13259
13536
|
method: "POST",
|
|
13260
|
-
headers: {
|
|
13537
|
+
headers: {
|
|
13538
|
+
"idempotency-key": resume ? `preview:${deploymentId}:artifact:resume:${randomUUID2()}` : `preview:${deploymentId}:artifact`
|
|
13539
|
+
},
|
|
13261
13540
|
body: JSON.stringify({
|
|
13262
13541
|
kind: "workspace.artifact.publish",
|
|
13263
13542
|
payload: {
|
|
@@ -13265,7 +13544,8 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13265
13544
|
deploymentId,
|
|
13266
13545
|
stack,
|
|
13267
13546
|
placement: {
|
|
13268
|
-
|
|
13547
|
+
workerId: context.state.workerId,
|
|
13548
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13269
13549
|
memoryReservationBytes: 1024 * 1024 * 1024
|
|
13270
13550
|
}
|
|
13271
13551
|
},
|
|
@@ -13282,12 +13562,12 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13282
13562
|
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13283
13563
|
method: "POST",
|
|
13284
13564
|
headers: {
|
|
13285
|
-
"idempotency-key": `preview:${deploymentId}:deploy`
|
|
13565
|
+
"idempotency-key": resume ? `preview:${deploymentId}:deploy:resume:${randomUUID2()}` : `preview:${deploymentId}:deploy`
|
|
13286
13566
|
},
|
|
13287
13567
|
body: JSON.stringify({
|
|
13288
13568
|
kind: "workspace.service.deploy",
|
|
13289
13569
|
stack: context.state.repository,
|
|
13290
|
-
environmentId: context
|
|
13570
|
+
environmentId: managedEnvironmentId(context),
|
|
13291
13571
|
environmentSnapshotId,
|
|
13292
13572
|
payload: {
|
|
13293
13573
|
workspaceId: context.state.workspaceId,
|
|
@@ -13301,7 +13581,8 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13301
13581
|
serviceUrls,
|
|
13302
13582
|
services: preview.services,
|
|
13303
13583
|
placement: {
|
|
13304
|
-
|
|
13584
|
+
workerId: deploymentTarget.workerId,
|
|
13585
|
+
...deploymentTarget.hostId ? { hostId: deploymentTarget.hostId } : {},
|
|
13305
13586
|
requiredWorkerRole: "runtime",
|
|
13306
13587
|
pool: deploymentTarget.pool,
|
|
13307
13588
|
memoryReservationBytes: serviceMemoryReservation(preview.services)
|
|
@@ -13313,14 +13594,14 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13313
13594
|
operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
|
|
13314
13595
|
} catch (error) {
|
|
13315
13596
|
const message = error instanceof Error ? error.message : String(error);
|
|
13316
|
-
throw new Error(`${message}; retry with
|
|
13597
|
+
throw new Error(`${message}; retry with \`sandblocks sandbox deploy . --resume\``);
|
|
13317
13598
|
}
|
|
13318
13599
|
context.state.deploymentId = deploymentId;
|
|
13319
13600
|
await writeState(context.file, context.state);
|
|
13320
13601
|
const routes = operation.result?.serviceRoutes ?? [];
|
|
13321
13602
|
const images = operation.result?.images ?? [];
|
|
13322
13603
|
if (typeof preview.ssoEnabled === "boolean") {
|
|
13323
|
-
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.
|
|
13604
|
+
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.managedEnvironment)}`, { method: "PUT", body: JSON.stringify({ ssoEnabled: preview.ssoEnabled }) });
|
|
13324
13605
|
}
|
|
13325
13606
|
let sandbox = await recordSandbox(context, loaded, preview, routes, operation.result?.healthEvidence ?? [], images, preview.steps.length ? "active" : "available");
|
|
13326
13607
|
let checkEvidence = [];
|
|
@@ -13334,15 +13615,15 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13334
13615
|
route.url
|
|
13335
13616
|
]
|
|
13336
13617
|
] : []));
|
|
13337
|
-
const
|
|
13618
|
+
const check2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13338
13619
|
method: "POST",
|
|
13339
13620
|
headers: {
|
|
13340
|
-
"idempotency-key": `preview:${deploymentId}:checks`
|
|
13621
|
+
"idempotency-key": resume ? `preview:${deploymentId}:checks:resume:${randomUUID2()}` : `preview:${deploymentId}:checks`
|
|
13341
13622
|
},
|
|
13342
13623
|
body: JSON.stringify({
|
|
13343
13624
|
kind: "workspace.check.run",
|
|
13344
13625
|
stack: context.state.repository,
|
|
13345
|
-
environmentId: context
|
|
13626
|
+
environmentId: managedEnvironmentId(context),
|
|
13346
13627
|
environmentSnapshotId,
|
|
13347
13628
|
payload: {
|
|
13348
13629
|
workspaceId: buildWorkspaceId ?? context.state.workspaceId,
|
|
@@ -13351,12 +13632,15 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13351
13632
|
runtimeType: preview.deploymentType,
|
|
13352
13633
|
checks: preview.steps,
|
|
13353
13634
|
serviceUrls: serviceUrls2,
|
|
13354
|
-
placement: {
|
|
13635
|
+
placement: {
|
|
13636
|
+
workerId: buildWorkerId ?? context.state.workerId,
|
|
13637
|
+
...buildHostId ?? context.state.hostId ? { hostId: buildHostId ?? context.state.hostId } : {}
|
|
13638
|
+
}
|
|
13355
13639
|
},
|
|
13356
13640
|
maxAttempts: 1
|
|
13357
13641
|
})
|
|
13358
13642
|
});
|
|
13359
|
-
const checked2 = await wait(context.apiUrl, context.apiKey,
|
|
13643
|
+
const checked2 = await wait(context.apiUrl, context.apiKey, check2.operation.id, 60 * 60000);
|
|
13360
13644
|
checkEvidence = checked2.result?.checks ?? [];
|
|
13361
13645
|
if (checked2.result?.passed !== true) {
|
|
13362
13646
|
throw new Error(`one or more post-deploy checks failed: ${JSON.stringify(checkEvidence)}`);
|
|
@@ -13367,13 +13651,6 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13367
13651
|
await recordSandbox(context, loaded, preview, routes, [...operation.result?.healthEvidence ?? [], ...checkEvidence], images, "failed").catch(() => {
|
|
13368
13652
|
return;
|
|
13369
13653
|
});
|
|
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
13654
|
throw error;
|
|
13378
13655
|
}
|
|
13379
13656
|
if (buildWorkspaceId)
|
|
@@ -13388,6 +13665,219 @@ async function deployUnlocked(args, dependencies) {
|
|
|
13388
13665
|
for (const route of previewUrls)
|
|
13389
13666
|
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13390
13667
|
}
|
|
13668
|
+
async function deployDevelopmentUnlocked(context, loaded, preview) {
|
|
13669
|
+
if (context.state.deploymentId)
|
|
13670
|
+
throw new Error("development sandbox is already running; use sandbox sync");
|
|
13671
|
+
const stack = loaded.manifest.name ?? context.state.repository;
|
|
13672
|
+
const target = await resolveDevelopmentTarget(context, preview.targetHost);
|
|
13673
|
+
const deploymentId = randomUUID2();
|
|
13674
|
+
const environmentSnapshotId = await createEnvironmentSnapshot(context);
|
|
13675
|
+
context.state.deploymentId = deploymentId;
|
|
13676
|
+
context.state.deploymentHostId = target.hostId;
|
|
13677
|
+
context.state.deploymentWorkerId = target.workerId;
|
|
13678
|
+
context.state.runtimeMode = "development";
|
|
13679
|
+
await writeState(context.file, context.state);
|
|
13680
|
+
try {
|
|
13681
|
+
const serviceUrls = expectedPreviewServiceUrls(context.state.sandboxId, deploymentId, context.state.repository, preview);
|
|
13682
|
+
const steps = resolveServiceSteps(loaded.manifest, context.state.environment);
|
|
13683
|
+
if (steps.length) {
|
|
13684
|
+
const submitted2 = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13685
|
+
method: "POST",
|
|
13686
|
+
headers: { "idempotency-key": `development:${deploymentId}:setup` },
|
|
13687
|
+
body: JSON.stringify({
|
|
13688
|
+
kind: "workspace.step.run",
|
|
13689
|
+
stack,
|
|
13690
|
+
environmentId: managedEnvironmentId(context),
|
|
13691
|
+
environmentSnapshotId,
|
|
13692
|
+
payload: {
|
|
13693
|
+
workspaceId: context.state.workspaceId,
|
|
13694
|
+
sandboxId: context.state.sandboxId,
|
|
13695
|
+
revisionId: context.state.sourceRevision ?? deploymentId,
|
|
13696
|
+
runtimeType: preview.deploymentType,
|
|
13697
|
+
serviceUrls,
|
|
13698
|
+
steps,
|
|
13699
|
+
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {},
|
|
13700
|
+
placement: {
|
|
13701
|
+
workerId: context.state.workerId,
|
|
13702
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13703
|
+
requiredWorkerRole: "development"
|
|
13704
|
+
}
|
|
13705
|
+
},
|
|
13706
|
+
maxAttempts: 1
|
|
13707
|
+
})
|
|
13708
|
+
});
|
|
13709
|
+
await wait(context.apiUrl, context.apiKey, submitted2.operation.id, 30 * 60000);
|
|
13710
|
+
}
|
|
13711
|
+
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13712
|
+
method: "POST",
|
|
13713
|
+
headers: { "idempotency-key": `development:${deploymentId}:services` },
|
|
13714
|
+
body: JSON.stringify({
|
|
13715
|
+
kind: "workspace.service.deploy",
|
|
13716
|
+
stack,
|
|
13717
|
+
environmentId: managedEnvironmentId(context),
|
|
13718
|
+
environmentSnapshotId,
|
|
13719
|
+
payload: {
|
|
13720
|
+
workspaceId: context.state.workspaceId,
|
|
13721
|
+
deploymentId,
|
|
13722
|
+
sandboxId: context.state.sandboxId,
|
|
13723
|
+
runtimeType: preview.deploymentType,
|
|
13724
|
+
stack,
|
|
13725
|
+
workspace: preview.workspace,
|
|
13726
|
+
mutableDevelopment: true,
|
|
13727
|
+
sourceRevision: context.state.sourceRevision ?? deploymentId,
|
|
13728
|
+
targetHost: target.targetHost,
|
|
13729
|
+
serviceUrls,
|
|
13730
|
+
services: preview.services,
|
|
13731
|
+
placement: {
|
|
13732
|
+
workerId: context.state.workerId,
|
|
13733
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13734
|
+
requiredWorkerRole: "development",
|
|
13735
|
+
memoryReservationBytes: serviceMemoryReservation(preview.services)
|
|
13736
|
+
}
|
|
13737
|
+
},
|
|
13738
|
+
maxAttempts: 1
|
|
13739
|
+
})
|
|
13740
|
+
});
|
|
13741
|
+
const operation = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 30 * 60000);
|
|
13742
|
+
if (typeof preview.ssoEnabled === "boolean") {
|
|
13743
|
+
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.managedEnvironment)}`, { method: "PUT", body: JSON.stringify({ ssoEnabled: preview.ssoEnabled }) });
|
|
13744
|
+
}
|
|
13745
|
+
const sandbox = await recordSandbox(context, loaded, preview, operation.result?.serviceRoutes ?? [], operation.result?.healthEvidence ?? [], operation.result?.images ?? [], "available");
|
|
13746
|
+
const previewUrls = sandbox.previewUrls ?? [];
|
|
13747
|
+
context.state.previewUrls = previewUrls;
|
|
13748
|
+
await writeState(context.file, context.state);
|
|
13749
|
+
console.log(`development ${deploymentId} available`);
|
|
13750
|
+
for (const route of previewUrls)
|
|
13751
|
+
console.log(`${route.service ?? "service"} ${route.url}`);
|
|
13752
|
+
await runDevelopmentChecks(context, {
|
|
13753
|
+
parse: () => context.options,
|
|
13754
|
+
option: (options, key) => typeof options[key] === "string" ? options[key] : undefined,
|
|
13755
|
+
load: async () => loaded,
|
|
13756
|
+
source: async () => ({ bundle: new Uint8Array, files: 0 })
|
|
13757
|
+
});
|
|
13758
|
+
} catch (error) {
|
|
13759
|
+
context.state.runtimeMode = "development";
|
|
13760
|
+
await writeState(context.file, context.state);
|
|
13761
|
+
throw error;
|
|
13762
|
+
} finally {
|
|
13763
|
+
await releaseEnvironmentSnapshot(context, environmentSnapshotId).catch(() => {
|
|
13764
|
+
return;
|
|
13765
|
+
});
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13768
|
+
async function runDevelopmentChecks(context, dependencies, requestedProfile, changedPaths = []) {
|
|
13769
|
+
if (!context.state.deploymentId || !context.state.previewUrls?.length) {
|
|
13770
|
+
throw new Error("development services must be available before checks run");
|
|
13771
|
+
}
|
|
13772
|
+
const loaded = await dependencies.load(context.root, dependencies.option(context.options, "config") ?? dependencies.option(context.options, "manifest"));
|
|
13773
|
+
const environment = resolveEnvironment(loaded.manifest, context.state.environment);
|
|
13774
|
+
if (environment.mode !== "development")
|
|
13775
|
+
throw new Error("continuous checks require a development environment");
|
|
13776
|
+
const profile = requestedProfile ?? environment.checkProfile;
|
|
13777
|
+
if (!["none", "smoke", "full"].includes(profile))
|
|
13778
|
+
throw new Error("check profile must be none, smoke, or full");
|
|
13779
|
+
const sourceRevision = context.state.sourceRevision ?? context.state.deploymentId;
|
|
13780
|
+
if (profile === "none") {
|
|
13781
|
+
context.state.quality = { sourceRevision, status: "unchecked", checks: [] };
|
|
13782
|
+
await writeState(context.file, context.state);
|
|
13783
|
+
console.log(`quality unchecked revision=${sourceRevision}`);
|
|
13784
|
+
return;
|
|
13785
|
+
}
|
|
13786
|
+
const profileChecks = environment.steps.filter((candidate) => profile === "full" || candidate.profile === "smoke");
|
|
13787
|
+
const checks = profile === "full" || !changedPaths.length ? profileChecks : profileChecks.filter((candidate) => !candidate.inputs.length || candidate.inputs.some((input) => changedPaths.some((path6) => repositoryPatternMatches(input, path6))));
|
|
13788
|
+
if (!profileChecks.length)
|
|
13789
|
+
throw new Error(`environment '${environment.id}' has no ${profile} checks`);
|
|
13790
|
+
if (!checks.length) {
|
|
13791
|
+
context.state.quality = {
|
|
13792
|
+
sourceRevision,
|
|
13793
|
+
status: context.state.quality?.status === "failed" ? "failed" : "passed",
|
|
13794
|
+
checks: context.state.quality?.checks ?? []
|
|
13795
|
+
};
|
|
13796
|
+
await writeState(context.file, context.state);
|
|
13797
|
+
console.log(`quality reused revision=${sourceRevision} (no affected application suites)`);
|
|
13798
|
+
return;
|
|
13799
|
+
}
|
|
13800
|
+
await waitForPublishedRoutes(context.state.previewUrls);
|
|
13801
|
+
const serviceUrls = Object.fromEntries(context.state.previewUrls.flatMap((route) => route.service ? [[`SANDBLOCKS_SERVICE_${route.service.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`, route.url]] : []));
|
|
13802
|
+
context.state.sequence += 1;
|
|
13803
|
+
const snapshotId = await createEnvironmentSnapshot(context);
|
|
13804
|
+
try {
|
|
13805
|
+
const submitted = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/operations`, {
|
|
13806
|
+
method: "POST",
|
|
13807
|
+
headers: {
|
|
13808
|
+
"idempotency-key": `development:${context.state.deploymentId}:checks:${sourceRevision}:${profile}:${context.state.sequence}`
|
|
13809
|
+
},
|
|
13810
|
+
body: JSON.stringify({
|
|
13811
|
+
kind: "workspace.check.run",
|
|
13812
|
+
stack: loaded.manifest.name ?? context.state.repository,
|
|
13813
|
+
environmentId: managedEnvironmentId(context),
|
|
13814
|
+
environmentSnapshotId: snapshotId,
|
|
13815
|
+
payload: {
|
|
13816
|
+
workspaceId: context.state.workspaceId,
|
|
13817
|
+
deploymentId: context.state.deploymentId,
|
|
13818
|
+
sandboxId: context.state.sandboxId,
|
|
13819
|
+
runtimeType: environment.deploymentType,
|
|
13820
|
+
sourceRevision,
|
|
13821
|
+
checks,
|
|
13822
|
+
serviceUrls,
|
|
13823
|
+
placement: {
|
|
13824
|
+
workerId: context.state.workerId,
|
|
13825
|
+
...context.state.hostId ? { hostId: context.state.hostId } : {},
|
|
13826
|
+
requiredWorkerRole: "development"
|
|
13827
|
+
}
|
|
13828
|
+
},
|
|
13829
|
+
maxAttempts: 1
|
|
13830
|
+
})
|
|
13831
|
+
});
|
|
13832
|
+
const completed = await wait(context.apiUrl, context.apiKey, submitted.operation.id, 60 * 60000);
|
|
13833
|
+
const checksResult = Array.isArray(completed.result?.checks) ? completed.result.checks : [];
|
|
13834
|
+
const selectedIds = new Set(checks.map((item) => item.id));
|
|
13835
|
+
const reused = (context.state.quality?.checks ?? []).filter((item) => item && !selectedIds.has(String(item.check ?? ""))).map((item) => ({
|
|
13836
|
+
...item,
|
|
13837
|
+
sourceRevision,
|
|
13838
|
+
status: item.status === "passed" || item.status === "reused" ? "reused" : item.status,
|
|
13839
|
+
reusedFromRevision: item.sourceRevision
|
|
13840
|
+
}));
|
|
13841
|
+
const combined = [...checksResult, ...reused];
|
|
13842
|
+
const status3 = completed.result?.passed === true && combined.every((item) => item.required === false || ["passed", "reused"].includes(item.status)) ? "passed" : "failed";
|
|
13843
|
+
context.state.quality = { sourceRevision, status: status3, checks: combined };
|
|
13844
|
+
await writeState(context.file, context.state);
|
|
13845
|
+
console.log(`quality ${status3} revision=${sourceRevision}`);
|
|
13846
|
+
for (const result of checksResult) {
|
|
13847
|
+
const item = result;
|
|
13848
|
+
console.log(` ${String(item.application ?? item.check ?? "check").padEnd(12)} ${item.status} ${item.durationMs ?? 0}ms`);
|
|
13849
|
+
}
|
|
13850
|
+
} finally {
|
|
13851
|
+
await releaseEnvironmentSnapshot(context, snapshotId).catch(() => {
|
|
13852
|
+
return;
|
|
13853
|
+
});
|
|
13854
|
+
}
|
|
13855
|
+
}
|
|
13856
|
+
async function assertEnvironmentBranch(root, environment) {
|
|
13857
|
+
if (!environment.branches.length)
|
|
13858
|
+
return;
|
|
13859
|
+
const result = Bun.spawnSync(["git", "branch", "--show-current"], { cwd: root });
|
|
13860
|
+
const branch = result.exitCode === 0 ? result.stdout.toString().trim() : "";
|
|
13861
|
+
if (!branch)
|
|
13862
|
+
throw new Error(`environment '${environment.id}' requires a named Git branch`);
|
|
13863
|
+
const allowed = environment.branches.some((pattern) => {
|
|
13864
|
+
const expression = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
|
|
13865
|
+
return expression.test(branch);
|
|
13866
|
+
});
|
|
13867
|
+
if (!allowed) {
|
|
13868
|
+
throw new Error(`branch '${branch}' cannot deploy to environment '${environment.id}' (allowed: ${environment.branches.join(", ")})`);
|
|
13869
|
+
}
|
|
13870
|
+
}
|
|
13871
|
+
function escapeRegExp(value) {
|
|
13872
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
13873
|
+
}
|
|
13874
|
+
function repositoryPatternMatches(pattern, path6) {
|
|
13875
|
+
const normalized = pattern.replace(/^\.\//, "").replace(/\/\*\*$/, "").replace(/\/$/, "");
|
|
13876
|
+
if (!normalized.includes("*"))
|
|
13877
|
+
return path6 === normalized || path6.startsWith(`${normalized}/`);
|
|
13878
|
+
const expression = new RegExp(`^${normalized.split("**").map((part) => part.split("*").map(escapeRegExp).join("[^/]*")).join(".*")}(?:/.*)?$`);
|
|
13879
|
+
return expression.test(path6);
|
|
13880
|
+
}
|
|
13391
13881
|
async function redeploy(args, dependencies) {
|
|
13392
13882
|
const context = await getContext(args, dependencies);
|
|
13393
13883
|
const routing = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-aliases`);
|
|
@@ -13415,10 +13905,12 @@ async function redeploy(args, dependencies) {
|
|
|
13415
13905
|
source: dependencies.source,
|
|
13416
13906
|
..."image" in loaded.manifest.workspace && loaded.manifest.workspace.image ? { workspaceImage: loaded.manifest.workspace.image } : {}
|
|
13417
13907
|
});
|
|
13418
|
-
const hostId =
|
|
13419
|
-
|
|
13420
|
-
|
|
13908
|
+
const hostId = typeof imported.operation.assignedHostId === "string" ? imported.operation.assignedHostId : undefined;
|
|
13909
|
+
const workerId = String(imported.operation.assignedWorkerId ?? imported.operation.assignedHostId ?? "");
|
|
13910
|
+
if (!workerId)
|
|
13911
|
+
throw new Error("workspace re-import did not bind a worker");
|
|
13421
13912
|
context.state.hostId = hostId;
|
|
13913
|
+
context.state.workerId = workerId;
|
|
13422
13914
|
await writeState(context.file, context.state);
|
|
13423
13915
|
await deploy(args, dependencies);
|
|
13424
13916
|
if (previousDeploymentId)
|
|
@@ -13494,25 +13986,47 @@ async function down(args, dependencies) {
|
|
|
13494
13986
|
await rm4(context.file, { force: true });
|
|
13495
13987
|
console.log(`sandbox ${context.state.sandboxId} destroyed`);
|
|
13496
13988
|
}
|
|
13989
|
+
async function resolveDevelopmentTarget(context, fallback) {
|
|
13990
|
+
const project = encodeURIComponent(context.state.projectId);
|
|
13991
|
+
const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
|
|
13992
|
+
const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
|
|
13993
|
+
const worker = workers.find((candidate) => candidate?.id === context.state.workerId);
|
|
13994
|
+
if (!worker || worker.status !== "online")
|
|
13995
|
+
throw new Error("development workspace worker is offline");
|
|
13996
|
+
const configuredTarget = worker?.configuration?.targetAddress;
|
|
13997
|
+
const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
|
|
13998
|
+
if (!targetHost)
|
|
13999
|
+
throw new Error("development worker targetAddress or environment targetHost is required");
|
|
14000
|
+
return {
|
|
14001
|
+
workerId: String(worker.id),
|
|
14002
|
+
...worker.hostId ? { hostId: String(worker.hostId) } : {},
|
|
14003
|
+
targetHost
|
|
14004
|
+
};
|
|
14005
|
+
}
|
|
13497
14006
|
async function resolveDeploymentTarget(context, fallback) {
|
|
13498
14007
|
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
|
-
]);
|
|
14008
|
+
const workerBody = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/workers`);
|
|
13503
14009
|
const workers = Array.isArray(workerBody.workers) ? workerBody.workers : [];
|
|
13504
14010
|
const requestedPool = process.env.SANDBLOCKS_DEPLOYMENT_WORKER_POOL?.trim() || process.env.SANDBLOCKS_RUNTIME_WORKER_POOL?.trim();
|
|
13505
14011
|
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?.
|
|
14012
|
+
const worker = eligible.find((candidate) => typeof fallback === "string" && candidate?.configuration?.targetAddress === fallback.trim()) ?? eligible.find((candidate) => requestedPool && candidate.pool === requestedPool) ?? eligible[0];
|
|
14013
|
+
if (!worker?.id)
|
|
13508
14014
|
throw new Error("no online deployment worker is available");
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
14015
|
+
let configuredTarget = worker?.configuration?.targetAddress;
|
|
14016
|
+
if (!configuredTarget && worker.hostId) {
|
|
14017
|
+
const legacyHosts = await request(context.apiUrl, context.apiKey, `/v1/projects/${project}/hosts`);
|
|
14018
|
+
const host = Array.isArray(legacyHosts.hosts) ? legacyHosts.hosts.find((candidate) => candidate?.id === worker.hostId) : undefined;
|
|
14019
|
+
configuredTarget = host?.labels?.targetHost;
|
|
14020
|
+
}
|
|
13512
14021
|
const targetHost = typeof fallback === "string" && fallback.trim() ? fallback.trim() : typeof configuredTarget === "string" && configuredTarget.trim() ? configuredTarget.trim() : undefined;
|
|
13513
14022
|
if (!targetHost)
|
|
13514
|
-
throw new Error("deployment
|
|
13515
|
-
return {
|
|
14023
|
+
throw new Error("deployment worker targetAddress or preview targetHost is required");
|
|
14024
|
+
return {
|
|
14025
|
+
workerId: String(worker.id),
|
|
14026
|
+
...worker.hostId ? { hostId: String(worker.hostId) } : {},
|
|
14027
|
+
targetHost,
|
|
14028
|
+
pool: String(worker.pool ?? "unpooled")
|
|
14029
|
+
};
|
|
13516
14030
|
}
|
|
13517
14031
|
async function recordSandbox(context, loaded, preview, serviceRoutes, healthEvidence, images, status3) {
|
|
13518
14032
|
const deploymentId = context.state.deploymentId;
|
|
@@ -13553,7 +14067,7 @@ async function createEnvironmentSnapshot(context) {
|
|
|
13553
14067
|
body: JSON.stringify({
|
|
13554
14068
|
stack: context.state.repository,
|
|
13555
14069
|
app: "*",
|
|
13556
|
-
environmentId: context
|
|
14070
|
+
environmentId: managedEnvironmentId(context)
|
|
13557
14071
|
})
|
|
13558
14072
|
});
|
|
13559
14073
|
const id2 = body.snapshot?.id;
|
|
@@ -13562,6 +14076,9 @@ async function createEnvironmentSnapshot(context) {
|
|
|
13562
14076
|
}
|
|
13563
14077
|
return id2;
|
|
13564
14078
|
}
|
|
14079
|
+
function managedEnvironmentId(context) {
|
|
14080
|
+
return context.state.managedEnvironment ?? context.state.environment;
|
|
14081
|
+
}
|
|
13565
14082
|
async function releaseEnvironmentSnapshot(context, snapshotId) {
|
|
13566
14083
|
await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/environment-snapshots/${snapshotId}`, { method: "DELETE" });
|
|
13567
14084
|
}
|
|
@@ -13581,7 +14098,13 @@ async function destroyDeploymentById(context, deploymentId) {
|
|
|
13581
14098
|
kind: "workspace.service.destroy",
|
|
13582
14099
|
payload: {
|
|
13583
14100
|
deploymentId,
|
|
13584
|
-
placement:
|
|
14101
|
+
placement: context.state.deploymentWorkerId ? {
|
|
14102
|
+
workerId: context.state.deploymentWorkerId,
|
|
14103
|
+
requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
|
|
14104
|
+
} : {
|
|
14105
|
+
hostId: context.state.deploymentHostId ?? context.state.hostId,
|
|
14106
|
+
requiredWorkerRole: context.state.runtimeMode === "development" ? "development" : "runtime"
|
|
14107
|
+
}
|
|
13585
14108
|
},
|
|
13586
14109
|
maxAttempts: 1
|
|
13587
14110
|
})
|
|
@@ -13629,6 +14152,7 @@ async function importSource(input) {
|
|
|
13629
14152
|
return {
|
|
13630
14153
|
files: source.files,
|
|
13631
14154
|
bytes: source.bundle.byteLength,
|
|
14155
|
+
manifest: source.manifest,
|
|
13632
14156
|
operation: await wait(input.apiUrl, input.apiKey, submitted.operation.id)
|
|
13633
14157
|
};
|
|
13634
14158
|
}
|
|
@@ -13651,6 +14175,10 @@ async function getContext(args, dependencies) {
|
|
|
13651
14175
|
let state;
|
|
13652
14176
|
try {
|
|
13653
14177
|
state = JSON.parse(await readFile5(file, "utf8"));
|
|
14178
|
+
if (!state.workerId && state.hostId)
|
|
14179
|
+
state.workerId = state.hostId;
|
|
14180
|
+
if (!state.workerId)
|
|
14181
|
+
throw new Error("sandbox state has no bound worker");
|
|
13654
14182
|
} catch {
|
|
13655
14183
|
throw new Error(`sandbox '${environment}' does not exist; use sandbox create or up`);
|
|
13656
14184
|
}
|
|
@@ -13674,8 +14202,10 @@ async function request(apiUrl, apiKey, pathname, init = {}) {
|
|
|
13674
14202
|
headers.set("content-type", "application/json");
|
|
13675
14203
|
const response = await fetchWithRetry(`${apiUrl}${pathname}`, { ...init, headers });
|
|
13676
14204
|
const body = await response.json().catch(() => ({}));
|
|
13677
|
-
if (!response.ok)
|
|
13678
|
-
|
|
14205
|
+
if (!response.ok) {
|
|
14206
|
+
const placement = body.placement ? `: ${JSON.stringify(body.placement)}` : "";
|
|
14207
|
+
throw new Error(`${String(body.error ?? `Sandblocks request failed (${response.status})`)}${placement}`);
|
|
14208
|
+
}
|
|
13679
14209
|
return body;
|
|
13680
14210
|
}
|
|
13681
14211
|
async function leaseRequest(context, pathname, init = {}) {
|
|
@@ -13781,7 +14311,7 @@ async function loadLocalEnvironment(root) {
|
|
|
13781
14311
|
}
|
|
13782
14312
|
}
|
|
13783
14313
|
async function applyManagedRoutingPolicy(context, stack, preview) {
|
|
13784
|
-
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.
|
|
14314
|
+
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.managedEnvironment)}`);
|
|
13785
14315
|
const policy = body.policy;
|
|
13786
14316
|
const primaryDomains = Object.fromEntries(Object.entries(policy.domains ?? {}).flatMap(([service, domains]) => {
|
|
13787
14317
|
const domain = Array.isArray(domains) ? domains[0] : domains;
|
|
@@ -13801,7 +14331,7 @@ function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
|
|
|
13801
14331
|
const name = service.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
13802
14332
|
const host = resolvedServiceDomain(sandboxId, deploymentId, service.id, preview);
|
|
13803
14333
|
const configuredStableHosts = preview.stableDomains[service.id];
|
|
13804
|
-
const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.
|
|
14334
|
+
const stableHost = (Array.isArray(configuredStableHosts) ? configuredStableHosts[0] : configuredStableHosts) ?? `${preview.managedEnvironment}-${stack}-${service.id}.${preview.baseDomain}`;
|
|
13805
14335
|
return [
|
|
13806
14336
|
[`SANDBLOCKS_SERVICE_${name}_URL`, `https://${host}`],
|
|
13807
14337
|
[`SANDBLOCKS_STABLE_SERVICE_${name}_URL`, `https://${stableHost}`]
|
|
@@ -13809,7 +14339,7 @@ function expectedPreviewServiceUrls(sandboxId, deploymentId, stack, preview) {
|
|
|
13809
14339
|
}));
|
|
13810
14340
|
}
|
|
13811
14341
|
async function managedServiceDomains(context, stack, preview, deploymentId) {
|
|
13812
|
-
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.
|
|
14342
|
+
const body = await request(context.apiUrl, context.apiKey, `/v1/projects/${encodeURIComponent(context.state.projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(preview.managedEnvironment)}`);
|
|
13813
14343
|
const domains = body.policy.domains ?? {};
|
|
13814
14344
|
return Object.keys(domains).length ? domains : resolvedServiceDomains(context.state.sandboxId, deploymentId, preview);
|
|
13815
14345
|
}
|
|
@@ -13820,7 +14350,7 @@ function resolvedServiceDomains(sandboxId, deploymentId, preview) {
|
|
|
13820
14350
|
]));
|
|
13821
14351
|
}
|
|
13822
14352
|
function resolvedServiceDomain(sandboxId, deploymentId, service, preview) {
|
|
13823
|
-
const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${
|
|
14353
|
+
const routeId = `${sandboxId.replaceAll("-", "").slice(0, 8)}-${createHash2("sha256").update(`${sandboxId}/${service}`).digest("hex").slice(0, 8)}`;
|
|
13824
14354
|
const template = preview.domains[service] ?? `{route}.${preview.baseDomain}`;
|
|
13825
14355
|
return template.replaceAll("{route}", routeId).replaceAll("{sandbox}", sandboxId.replaceAll("-", "").slice(0, 8)).replaceAll("{revision}", deploymentId.replaceAll("-", "").slice(0, 8)).replaceAll("{service}", service).toLowerCase();
|
|
13826
14356
|
}
|
|
@@ -15120,7 +15650,16 @@ var HELP = `Sandblocks CLI
|
|
|
15120
15650
|
sandblocks secret <list|set|delete> --project <id> --stack <id> --app <id>
|
|
15121
15651
|
[--environment <id>] [--name <name>] [--value <value>] [--id <id>] [--json]
|
|
15122
15652
|
sandblocks artifact push <directory> --project <id> --stack <id> [--pool <pool>] [--json]
|
|
15653
|
+
sandblocks worker <list|create|rotate-secret|drain|resume|approve-update> --organization <id>
|
|
15654
|
+
[--worker <uuid>] [--name <name>] [--type <build|deployment>]
|
|
15655
|
+
[--pool-id <uuid>] [--project <id>] [--version <version>] [--json]
|
|
15656
|
+
sandblocks registry <list|create|assign> --organization <id> [--project <id>]
|
|
15657
|
+
[--name <name>] [--endpoint <url>] [--registry <uuid>]
|
|
15658
|
+
[--purpose <build-cache|artifact-publication|deployment-pull>] [--json]
|
|
15123
15659
|
sandblocks sandbox up [directory] [--environment <id>] [--project <id>] [--build-pool <pool>]
|
|
15660
|
+
sandblocks sandbox dev [directory] [--environment <id>] [--checks <smoke|full|none>]
|
|
15661
|
+
sandblocks sandbox sync [directory] [--environment <id>] [--watch] [--checks <smoke|full|none>]
|
|
15662
|
+
sandblocks sandbox check [directory] [--environment <id>] [--profile <smoke|full>]
|
|
15124
15663
|
sandblocks sandbox create [directory] [--environment <id>] [--project <id>]
|
|
15125
15664
|
sandblocks sandbox deploy [directory] [--environment <id>] [--build-pool <pool>]
|
|
15126
15665
|
[--artifact <image@sha256:digest>] [--deployment-id <uuid>] [--resume]
|
|
@@ -15179,10 +15718,7 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
15179
15718
|
parse: parse2,
|
|
15180
15719
|
option: option2,
|
|
15181
15720
|
load,
|
|
15182
|
-
source:
|
|
15183
|
-
const bundle = await createSourceTar(root);
|
|
15184
|
-
return { bundle, files: sourceFileCount };
|
|
15185
|
-
}
|
|
15721
|
+
source: createSourceTar
|
|
15186
15722
|
})
|
|
15187
15723
|
});
|
|
15188
15724
|
else if (command === "runtime")
|
|
@@ -15193,6 +15729,10 @@ async function run3(argv = process.argv.slice(2)) {
|
|
|
15193
15729
|
await secret(args);
|
|
15194
15730
|
else if (command === "artifact")
|
|
15195
15731
|
await artifact(args);
|
|
15732
|
+
else if (command === "worker")
|
|
15733
|
+
await worker(args);
|
|
15734
|
+
else if (command === "registry")
|
|
15735
|
+
await registry(args);
|
|
15196
15736
|
else if (command === "sandbox")
|
|
15197
15737
|
await sandbox(args);
|
|
15198
15738
|
else if (command === "sdk")
|
|
@@ -15303,21 +15843,21 @@ async function doctor(args) {
|
|
|
15303
15843
|
{ label: "artifact publish", kind: "workspace.artifact.publish", role: "build", memory: 1024 ** 3 },
|
|
15304
15844
|
{ label: "service deploy", kind: "workspace.service.deploy", role: "runtime", memory: 6 * 1024 ** 3 }
|
|
15305
15845
|
];
|
|
15306
|
-
const explanations = await Promise.all(checks.map(async (
|
|
15846
|
+
const explanations = await Promise.all(checks.map(async (check2) => {
|
|
15307
15847
|
const query = new URLSearchParams({
|
|
15308
|
-
kind:
|
|
15309
|
-
role:
|
|
15310
|
-
memoryReservationBytes: String(
|
|
15848
|
+
kind: check2.kind,
|
|
15849
|
+
role: check2.role,
|
|
15850
|
+
memoryReservationBytes: String(check2.memory)
|
|
15311
15851
|
});
|
|
15312
15852
|
const result = await sandblocksRequest(apiUrl, apiKey, `/v1/projects/${encodeURIComponent(projectId)}/placement/explain?${query}`);
|
|
15313
|
-
return { ...
|
|
15853
|
+
return { ...check2, explanation: result.explanation };
|
|
15314
15854
|
}));
|
|
15315
15855
|
if (options.json)
|
|
15316
15856
|
console.log(JSON.stringify({ projectId, explanations }, null, 2));
|
|
15317
15857
|
else
|
|
15318
|
-
for (const
|
|
15319
|
-
const explanation = objectRecord(
|
|
15320
|
-
console.log(`${
|
|
15858
|
+
for (const check2 of explanations) {
|
|
15859
|
+
const explanation = objectRecord(check2.explanation);
|
|
15860
|
+
console.log(`${check2.label.padEnd(16)} ${explanation.eligible ? "eligible" : "blocked"}`);
|
|
15321
15861
|
if (!explanation.eligible && Array.isArray(explanation.candidates))
|
|
15322
15862
|
for (const candidate of explanation.candidates) {
|
|
15323
15863
|
const row = objectRecord(candidate);
|
|
@@ -15479,6 +16019,102 @@ async function artifact(args) {
|
|
|
15479
16019
|
console.log(`deploy with: sandblocks sandbox deploy . --artifact ${result.image} --deployment-id ${result.deploymentId}`);
|
|
15480
16020
|
}
|
|
15481
16021
|
}
|
|
16022
|
+
async function worker(args) {
|
|
16023
|
+
const [action = "list", ...rest] = args;
|
|
16024
|
+
const options = parse2(rest);
|
|
16025
|
+
const organizationId = option2(options, "organization");
|
|
16026
|
+
const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
16027
|
+
const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
16028
|
+
if (!organizationId)
|
|
16029
|
+
throw new Error("worker command requires --organization");
|
|
16030
|
+
if (!apiUrl || !apiKey)
|
|
16031
|
+
throw new Error("worker command requires Sandblocks API URL and key");
|
|
16032
|
+
const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
|
|
16033
|
+
let body;
|
|
16034
|
+
if (action === "list")
|
|
16035
|
+
body = await client2.listWorkers(organizationId);
|
|
16036
|
+
else if (action === "create") {
|
|
16037
|
+
const name = option2(options, "name");
|
|
16038
|
+
const type = option2(options, "type");
|
|
16039
|
+
if (!name || type !== "build" && type !== "deployment")
|
|
16040
|
+
throw new Error("worker create requires --name and --type build|deployment");
|
|
16041
|
+
body = await client2.createWorker({
|
|
16042
|
+
organizationId,
|
|
16043
|
+
name,
|
|
16044
|
+
type,
|
|
16045
|
+
...option2(options, "pool-id") ? { poolId: option2(options, "pool-id") } : {},
|
|
16046
|
+
...option2(options, "project") ? { projectIds: [option2(options, "project")] } : {}
|
|
16047
|
+
});
|
|
16048
|
+
} else {
|
|
16049
|
+
const workerId = option2(options, "worker");
|
|
16050
|
+
if (!workerId)
|
|
16051
|
+
throw new Error(`worker ${action} requires --worker`);
|
|
16052
|
+
if (action === "rotate-secret")
|
|
16053
|
+
body = await client2.rotateWorkerSecret(organizationId, workerId);
|
|
16054
|
+
else if (action === "move") {
|
|
16055
|
+
const targetWorkerIds = (option2(options, "target-workers") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
16056
|
+
if (!targetWorkerIds.length)
|
|
16057
|
+
throw new Error("worker move requires --target-workers <id,id,...>");
|
|
16058
|
+
body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/move`, { method: "POST", body: JSON.stringify({ targetWorkerIds }) });
|
|
16059
|
+
} else if (action === "approve-update") {
|
|
16060
|
+
const version = option2(options, "version");
|
|
16061
|
+
if (!version)
|
|
16062
|
+
throw new Error("worker approve-update requires --version");
|
|
16063
|
+
body = await client2.approveWorkerUpdate(organizationId, workerId, version);
|
|
16064
|
+
} else if (action === "drain" || action === "resume")
|
|
16065
|
+
body = await sandblocksRequest(apiUrl, apiKey, `/v1/organizations/${encodeURIComponent(organizationId)}/workers/${encodeURIComponent(workerId)}/${action}`, { method: "POST" });
|
|
16066
|
+
else
|
|
16067
|
+
throw new Error(`unsupported worker command '${action}'`);
|
|
16068
|
+
}
|
|
16069
|
+
if (options.json || action !== "list")
|
|
16070
|
+
console.log(JSON.stringify(body, null, 2));
|
|
16071
|
+
else
|
|
16072
|
+
for (const candidate of Array.isArray(body.workers) ? body.workers : []) {
|
|
16073
|
+
const row = objectRecord(candidate);
|
|
16074
|
+
console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.type ?? "unknown"} ${row.state ?? "unknown"}`);
|
|
16075
|
+
}
|
|
16076
|
+
}
|
|
16077
|
+
async function registry(args) {
|
|
16078
|
+
const [action = "list", ...rest] = args;
|
|
16079
|
+
const options = parse2(rest);
|
|
16080
|
+
const organizationId = option2(options, "organization");
|
|
16081
|
+
const apiUrl = (option2(options, "api-url") ?? process.env.SANDBLOCKS_API_URL ?? "").replace(/\/$/, "");
|
|
16082
|
+
const apiKey = option2(options, "api-key") ?? process.env.SANDBLOCKS_API_KEY;
|
|
16083
|
+
if (!organizationId)
|
|
16084
|
+
throw new Error("registry command requires --organization");
|
|
16085
|
+
if (!apiUrl || !apiKey)
|
|
16086
|
+
throw new Error("registry command requires Sandblocks API URL and key");
|
|
16087
|
+
const client2 = new SandblocksClient({ baseUrl: apiUrl, apiKey });
|
|
16088
|
+
let body;
|
|
16089
|
+
if (action === "list")
|
|
16090
|
+
body = await client2.listRegistries(organizationId);
|
|
16091
|
+
else if (action === "create") {
|
|
16092
|
+
const name = option2(options, "name");
|
|
16093
|
+
const endpoint = option2(options, "endpoint");
|
|
16094
|
+
if (!name || !endpoint)
|
|
16095
|
+
throw new Error("registry create requires --name and --endpoint");
|
|
16096
|
+
body = await client2.createRegistry({ organizationId, name, endpoint });
|
|
16097
|
+
} else if (action === "assign") {
|
|
16098
|
+
const projectId = option2(options, "project");
|
|
16099
|
+
const registryId = option2(options, "registry");
|
|
16100
|
+
const purpose = option2(options, "purpose");
|
|
16101
|
+
if (!projectId || !registryId || !["build-cache", "artifact-publication", "deployment-pull"].includes(purpose ?? ""))
|
|
16102
|
+
throw new Error("registry assign requires --project, --registry, and a valid --purpose");
|
|
16103
|
+
body = await client2.assignProjectRegistry(projectId, {
|
|
16104
|
+
purpose,
|
|
16105
|
+
registryId,
|
|
16106
|
+
...option2(options, "repository-prefix") ? { repositoryPrefix: option2(options, "repository-prefix") } : {}
|
|
16107
|
+
});
|
|
16108
|
+
} else
|
|
16109
|
+
throw new Error(`unsupported registry command '${action}'`);
|
|
16110
|
+
if (options.json || action !== "list")
|
|
16111
|
+
console.log(JSON.stringify(body, null, 2));
|
|
16112
|
+
else
|
|
16113
|
+
for (const candidate of Array.isArray(body.registries) ? body.registries : []) {
|
|
16114
|
+
const row = objectRecord(candidate);
|
|
16115
|
+
console.log(`${row.id ?? "unknown"} ${row.name ?? "unknown"} ${row.endpoint ?? "unknown"}`);
|
|
16116
|
+
}
|
|
16117
|
+
}
|
|
15482
16118
|
function printResources(kind, action, body) {
|
|
15483
16119
|
if (action === "settings") {
|
|
15484
16120
|
console.log(JSON.stringify(body.settings ?? {}, null, 2));
|
|
@@ -15505,10 +16141,7 @@ async function sandbox(args) {
|
|
|
15505
16141
|
parse: parse2,
|
|
15506
16142
|
option: option2,
|
|
15507
16143
|
load,
|
|
15508
|
-
source:
|
|
15509
|
-
const bundle2 = await createSourceTar(root2);
|
|
15510
|
-
return { bundle: bundle2, files: sourceFileCount };
|
|
15511
|
-
}
|
|
16144
|
+
source: createSourceTar
|
|
15512
16145
|
});
|
|
15513
16146
|
}
|
|
15514
16147
|
if (subcommand === "deploy")
|
|
@@ -15533,7 +16166,8 @@ async function sandbox(args) {
|
|
|
15533
16166
|
throw new Error("sandbox import requires --api-url or SANDBLOCKS_API_URL");
|
|
15534
16167
|
if (!apiKey)
|
|
15535
16168
|
throw new Error("sandbox import requires --api-key or SANDBLOCKS_API_KEY");
|
|
15536
|
-
const
|
|
16169
|
+
const source = await createSourceTar(root);
|
|
16170
|
+
const bundle = source.bundle;
|
|
15537
16171
|
const response = await fetch(`${apiUrl}/v1/projects/${encodeURIComponent(projectId)}/workspaces/import`, {
|
|
15538
16172
|
method: "POST",
|
|
15539
16173
|
headers: {
|
|
@@ -15550,11 +16184,11 @@ async function sandbox(args) {
|
|
|
15550
16184
|
if (!response.ok)
|
|
15551
16185
|
throw new Error(String(submitted.error ?? `sandbox import failed (${response.status})`));
|
|
15552
16186
|
const operation = options.wait ? await waitForOperation(apiUrl, apiKey, String(submitted.operation.id)) : submitted.operation;
|
|
15553
|
-
const output = { workspaceId, files:
|
|
16187
|
+
const output = { workspaceId, files: source.files, bytes: bundle.byteLength, operation };
|
|
15554
16188
|
if (options.json)
|
|
15555
16189
|
console.log(JSON.stringify(output, null, 2));
|
|
15556
16190
|
else {
|
|
15557
|
-
console.log(`source ${
|
|
16191
|
+
console.log(`source ${source.files} files (${bundle.byteLength} bytes)`);
|
|
15558
16192
|
console.log(`workspace ${workspaceId}`);
|
|
15559
16193
|
console.log(`operation ${operation.id} (${operation.state})`);
|
|
15560
16194
|
if (operation.result?.container)
|
|
@@ -15647,7 +16281,6 @@ async function sandboxPromote(args) {
|
|
|
15647
16281
|
else
|
|
15648
16282
|
console.log(`promoted ${sandboxId} to production`);
|
|
15649
16283
|
}
|
|
15650
|
-
var sourceFileCount = 0;
|
|
15651
16284
|
async function createPrebuiltTar(root) {
|
|
15652
16285
|
const names = [];
|
|
15653
16286
|
const visit2 = async (directory, prefix = "") => {
|
|
@@ -15693,7 +16326,7 @@ async function createPrebuiltTar(root) {
|
|
|
15693
16326
|
}
|
|
15694
16327
|
return output;
|
|
15695
16328
|
}
|
|
15696
|
-
async function createSourceTar(root) {
|
|
16329
|
+
async function createSourceTar(root, previous) {
|
|
15697
16330
|
const process2 = Bun.spawn(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
15698
16331
|
cwd: root,
|
|
15699
16332
|
stdout: "pipe",
|
|
@@ -15714,6 +16347,7 @@ async function createSourceTar(root) {
|
|
|
15714
16347
|
if (names.length > 20000)
|
|
15715
16348
|
throw new Error("local source contains more than 20000 files");
|
|
15716
16349
|
const chunks = [];
|
|
16350
|
+
const manifest = {};
|
|
15717
16351
|
let total = 1024;
|
|
15718
16352
|
for (const name of names) {
|
|
15719
16353
|
if (name.includes("\x00") || name.includes("\\") || name.startsWith("/") || name.split("/").includes("..")) {
|
|
@@ -15733,6 +16367,11 @@ async function createSourceTar(root) {
|
|
|
15733
16367
|
if (!info.isFile())
|
|
15734
16368
|
continue;
|
|
15735
16369
|
const contents = new Uint8Array(await readFile8(file));
|
|
16370
|
+
const mode = info.mode & 511;
|
|
16371
|
+
const sha256 = createHash3("sha256").update(contents).digest("hex");
|
|
16372
|
+
manifest[name] = { sha256, mode, bytes: contents.byteLength };
|
|
16373
|
+
if (previous?.[name]?.sha256 === sha256 && previous[name]?.mode === mode)
|
|
16374
|
+
continue;
|
|
15736
16375
|
const header = tarHeader(name, contents.byteLength, info.mode, Math.floor(info.mtimeMs / 1000));
|
|
15737
16376
|
const padding = (512 - contents.byteLength % 512) % 512;
|
|
15738
16377
|
total += 512 + contents.byteLength + padding;
|
|
@@ -15740,9 +16379,15 @@ async function createSourceTar(root) {
|
|
|
15740
16379
|
throw new Error("local source bundle exceeds 256 MiB");
|
|
15741
16380
|
chunks.push(header, contents, new Uint8Array(padding));
|
|
15742
16381
|
}
|
|
15743
|
-
|
|
15744
|
-
if (!sourceFileCount)
|
|
16382
|
+
if (!Object.keys(manifest).length)
|
|
15745
16383
|
throw new Error("local directory has no regular source files");
|
|
16384
|
+
if (previous) {
|
|
16385
|
+
const contents = new TextEncoder().encode(JSON.stringify({ version: 1, files: manifest }));
|
|
16386
|
+
const name = ".sandblocks-sync-manifest.json";
|
|
16387
|
+
const padding = (512 - contents.byteLength % 512) % 512;
|
|
16388
|
+
total += 512 + contents.byteLength + padding;
|
|
16389
|
+
chunks.push(tarHeader(name, contents.byteLength, 384, Math.floor(Date.now() / 1000)), contents, new Uint8Array(padding));
|
|
16390
|
+
}
|
|
15746
16391
|
chunks.push(new Uint8Array(1024));
|
|
15747
16392
|
const output = new Uint8Array(total);
|
|
15748
16393
|
let offset = 0;
|
|
@@ -15750,7 +16395,7 @@ async function createSourceTar(root) {
|
|
|
15750
16395
|
output.set(chunk, offset);
|
|
15751
16396
|
offset += chunk.byteLength;
|
|
15752
16397
|
}
|
|
15753
|
-
return output;
|
|
16398
|
+
return { bundle: output, files: Object.keys(manifest).length, manifest };
|
|
15754
16399
|
}
|
|
15755
16400
|
async function gitIgnoredFiles(root, names) {
|
|
15756
16401
|
if (!names.length)
|
|
@@ -15936,7 +16581,8 @@ function parse2(args) {
|
|
|
15936
16581
|
"allow-host",
|
|
15937
16582
|
"rootless",
|
|
15938
16583
|
"allow-unpinned-images",
|
|
15939
|
-
"read-only-root"
|
|
16584
|
+
"read-only-root",
|
|
16585
|
+
"resume"
|
|
15940
16586
|
].includes(rawKey)) {
|
|
15941
16587
|
output[rawKey] = inline === undefined ? true : inline !== "false";
|
|
15942
16588
|
continue;
|
|
@@ -16003,4 +16649,4 @@ export {
|
|
|
16003
16649
|
parse2 as parse
|
|
16004
16650
|
};
|
|
16005
16651
|
|
|
16006
|
-
//# debugId=
|
|
16652
|
+
//# debugId=B268A7EFF33E724C64756E2164756E21
|