@theholocron/cli 5.0.0-alpha.97 → 5.0.0-alpha.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -313,6 +313,23 @@ Order comes from the `CI_ORDER` constant in `@theholocron/astromech`
313
313
  expands into its sub-jobs (each under its own `audit / …` check context)
314
314
  unless the repo ships its own `"audit"` script.
315
315
 
316
+ ### `holocron deploy`
317
+
318
+ `holocron deploy <branch>` (a Git-linked deployment) and `holocron deploy
319
+ --files <dir>` (a directory of build output, no linked repo) both trigger a
320
+ deployment through the configured `deployment` capability, then **wait for
321
+ it to finish**. The command reports success, exit code 0, only when the
322
+ deployment reaches `ready`. A deployment that ends `error` or `cancelled`,
323
+ or is still building after 10 minutes, is a failure: the exit code is
324
+ non-zero and the output includes the provider's own reason
325
+ (`DeploymentRecord.errorMessage`, e.g. Vercel's `Command "npm install"
326
+ exited with 1`). Providers build asynchronously, so accepting a deployment
327
+ isn't the same as it going live (holocron#911).
328
+
329
+ Provider plugins support this through the `Deployment` capability's
330
+ existing `getDeployment(id)`. They should set `errorMessage` on the
331
+ returned `DeploymentRecord` whenever the provider exposes one.
332
+
316
333
  ### Lint parity
317
334
 
318
335
  The `lint` task takes an optional `linters` array — one list that drives
package/dist/cli.mjs CHANGED
@@ -1265,7 +1265,47 @@ function commandsInContext(context) {
1265
1265
  * deployment capability (and, for `--files`, `deployFunction`) is
1266
1266
  * present, so operators can sanity-check the wiring without spinning
1267
1267
  * up a build.
1268
+ *
1269
+ * **Both modes wait for the deployment to finish** (holocron#911): a
1270
+ * provider accepting the upload/trigger is not the deployment succeeding.
1271
+ * Vercel builds asynchronously — an `npm install` failure there leaves
1272
+ * the deployment in `ERROR` while production keeps serving the previous
1273
+ * one, and this command used to report `ok` regardless (found live:
1274
+ * Sentinel's holocron#910 deploy "succeeded" in CI, never went live). So
1275
+ * after triggering, {@link waitForDeployment} polls
1276
+ * `Deployment.getDeployment()` until a terminal state; only `ready` is
1277
+ * `ok`, while `error` / `cancelled` / a timeout are `fail` (non-zero exit
1278
+ * via `cli.ts`).
1268
1279
  */
1280
+ const DEFAULT_WAIT_TIMEOUT_MS = 6e5;
1281
+ const DEFAULT_WAIT_INTERVAL_MS = 5e3;
1282
+ const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1283
+ "ready",
1284
+ "error",
1285
+ "cancelled"
1286
+ ]);
1287
+ /**
1288
+ * Polls `getDeployment(id)` until the deployment reaches a terminal status
1289
+ * (`ready` / `error` / `cancelled`). Throws on timeout — the caller turns
1290
+ * that into a `fail` report, same as a provider throwing mid-trigger.
1291
+ */
1292
+ async function waitForDeployment(deploy, deploymentId, options = {}) {
1293
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
1294
+ const intervalMs = options.intervalMs ?? DEFAULT_WAIT_INTERVAL_MS;
1295
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1296
+ const now = options.now ?? Date.now;
1297
+ const deadline = now() + timeoutMs;
1298
+ for (;;) {
1299
+ const record = await deploy.getDeployment(deploymentId);
1300
+ if (TERMINAL_STATUSES.has(record.status)) return record;
1301
+ if (now() >= deadline) throw new Error(`deployment ${deploymentId} still ${record.status} after ${Math.round(timeoutMs / 1e3)}s — check the provider's dashboard`);
1302
+ await sleep(intervalMs);
1303
+ }
1304
+ }
1305
+ /** One terminal, non-ready record as a single human-readable failure line. */
1306
+ function terminalFailure(record) {
1307
+ return `deployment ${record.id} ended ${record.status}${record.errorMessage ? `: ${record.errorMessage}` : ""}`;
1308
+ }
1269
1309
  /**
1270
1310
  * Provider `url` fields (Vercel's REST API, at least) come back as bare
1271
1311
  * hostnames — `"my-app.vercel.app"`, no scheme — which terminals don't
@@ -1302,17 +1342,37 @@ async function runDeploy(input) {
1302
1342
  };
1303
1343
  }
1304
1344
  try {
1305
- const record = await withSpinner(`Deploying ${input.branch}${input.target ? ` → ${input.target}` : ""}…`, () => deploy.triggerDeployment({
1345
+ const triggered = await withSpinner(`Deploying ${input.branch}${input.target ? ` → ${input.target}` : ""}…`, () => deploy.triggerDeployment({
1306
1346
  projectId: input.projectId,
1307
1347
  branch: input.branch,
1308
1348
  ...input.target ? { target: input.target } : {}
1309
1349
  }));
1310
- print(` ${style.success(`${record.status} — ${withScheme(record.url)}`)}`);
1350
+ logger.info({
1351
+ status: triggered.status,
1352
+ url: triggered.url,
1353
+ id: triggered.id
1354
+ }, "deploy: triggered");
1355
+ const record = TERMINAL_STATUSES.has(triggered.status) ? triggered : await withSpinner(`Waiting for ${withScheme(triggered.url)} to finish…`, () => waitForDeployment(deploy, triggered.id, input.wait));
1356
+ if (record.status !== "ready") {
1357
+ const message = terminalFailure(record);
1358
+ print(` ${style.fail(`${message} — ${withScheme(triggered.url)}`)}`);
1359
+ logger.warn({
1360
+ id: record.id,
1361
+ status: record.status,
1362
+ reason: record.errorMessage
1363
+ }, "deploy: failed");
1364
+ return {
1365
+ deployment: record,
1366
+ status: "fail",
1367
+ message
1368
+ };
1369
+ }
1370
+ print(` ${style.success(`${record.status} — ${withScheme(triggered.url)}`)}`);
1311
1371
  logger.info({
1312
1372
  status: record.status,
1313
- url: record.url,
1373
+ url: triggered.url,
1314
1374
  id: record.id
1315
- }, "deploy: triggered");
1375
+ }, "deploy: ready");
1316
1376
  return {
1317
1377
  deployment: record,
1318
1378
  status: "ok"
@@ -1389,11 +1449,30 @@ async function runDeployFromFiles(input) {
1389
1449
  files,
1390
1450
  ...input.target ? { target: input.target } : {}
1391
1451
  }));
1392
- print(` ${style.success(withScheme(result.url))}`);
1393
1452
  logger.info({
1394
1453
  url: result.url,
1395
1454
  id: result.deploymentId
1396
1455
  }, "deploy: triggered (files)");
1456
+ const record = await withSpinner(`Waiting for ${withScheme(result.url)} to finish…`, () => waitForDeployment(deploy, result.deploymentId, input.wait));
1457
+ if (record.status !== "ready") {
1458
+ const message = terminalFailure(record);
1459
+ print(` ${style.fail(`${message} — ${withScheme(result.url)}`)}`);
1460
+ logger.warn({
1461
+ id: record.id,
1462
+ status: record.status,
1463
+ reason: record.errorMessage
1464
+ }, "deploy: failed (files)");
1465
+ return {
1466
+ deployment: result,
1467
+ status: "fail",
1468
+ message
1469
+ };
1470
+ }
1471
+ print(` ${style.success(withScheme(result.url))}`);
1472
+ logger.info({
1473
+ url: result.url,
1474
+ id: result.deploymentId
1475
+ }, "deploy: ready (files)");
1397
1476
  return {
1398
1477
  deployment: result,
1399
1478
  status: "ok"