@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 +17 -0
- package/dist/cli.mjs +84 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin/capabilities.d.mts +2 -0
- package/package.json +4 -4
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
|
|
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
|
-
|
|
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:
|
|
1373
|
+
url: triggered.url,
|
|
1314
1374
|
id: record.id
|
|
1315
|
-
}, "deploy:
|
|
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"
|