unfazed 0.8.0 → 0.8.1
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/CHANGELOG.md +36 -0
- package/README.md +3 -3
- package/REFERENCE.md +25 -11
- package/dist/cli/login.d.ts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/generated/gql.d.ts +14 -2
- package/dist/generated/gql.js +5 -2
- package/dist/generated/gql.js.map +1 -1
- package/dist/generated/graphql.d.ts +82 -1
- package/dist/generated/graphql.js +87 -1
- package/dist/generated/graphql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +29 -1
- package/dist/operations.js +70 -1
- package/dist/operations.js.map +1 -1
- package/dist/reference.generated.js +1 -1
- package/dist/reference.generated.js.map +1 -1
- package/dist/unfazed.d.ts +71 -17
- package/dist/unfazed.js +204 -50
- package/dist/unfazed.js.map +1 -1
- package/package.json +1 -1
- package/schema/SOURCE.json +3 -3
- package/schema/schema.graphqls +81 -5
package/dist/unfazed.js
CHANGED
|
@@ -464,9 +464,24 @@ export class Unfazed {
|
|
|
464
464
|
* the account already holds its cap of waits in progress (16), it polls
|
|
465
465
|
* instead (`waitOptions.mode: "poll"` forces that), every 0.5 s backing
|
|
466
466
|
* off to 3 s by default.
|
|
467
|
+
*
|
|
468
|
+
* Pass a {@link CommitDeploymentTarget} instead of an id to wait for the
|
|
469
|
+
* deploy a git import makes of a commit — a restore, a push or a sync
|
|
470
|
+
* deploys before any id exists. The wait covers the import too, and
|
|
471
|
+
* {@link UnfazedDeployError} carries the import's error when it failed on
|
|
472
|
+
* the commit.
|
|
473
|
+
*
|
|
474
|
+
* ```ts
|
|
475
|
+
* const back = await unfazed.restoreVersion({ workspaceId: ws.id, sha });
|
|
476
|
+
* await unfazed.waitForDeployment({
|
|
477
|
+
* workspaceId: back.workspaceID, gitCommit: back.sha, alias: back.deployAlias, after: back.waitAfter,
|
|
478
|
+
* });
|
|
479
|
+
* ```
|
|
467
480
|
*/
|
|
468
|
-
async waitForDeployment(
|
|
469
|
-
|
|
481
|
+
async waitForDeployment(target, waitOptions = {}) {
|
|
482
|
+
if (typeof target !== "string")
|
|
483
|
+
return (await this.#commitWait(target, waitOptions)).deployment;
|
|
484
|
+
return (await this.#waitTerminal(target, waitOptions)).deployment;
|
|
470
485
|
}
|
|
471
486
|
async #waitTerminal(deploymentId, waitOptions) {
|
|
472
487
|
const now = waitOptions.now ?? (() => Date.now());
|
|
@@ -518,7 +533,11 @@ export class Unfazed {
|
|
|
518
533
|
}
|
|
519
534
|
throw e;
|
|
520
535
|
}
|
|
536
|
+
// By id the platform always answers the deployment; null is only for a
|
|
537
|
+
// wait by commit before the import made it.
|
|
521
538
|
const d = answer.deployment;
|
|
539
|
+
if (!d)
|
|
540
|
+
throw new UnfazedNotFoundError(`unfazed: deployment ${deploymentId} not found`);
|
|
522
541
|
last = d;
|
|
523
542
|
if (answer.timedOut) {
|
|
524
543
|
landing = d.status === "READY";
|
|
@@ -529,6 +548,61 @@ export class Unfazed {
|
|
|
529
548
|
throw new UnfazedDeployError("FAILED", deploymentId, d, `unfazed: deployment ${deploymentId} ${d.status}${d.error ? `: ${d.error}` : ""}`);
|
|
530
549
|
}
|
|
531
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* The server-side wait by commit: for the import to make the commit's
|
|
553
|
+
* deployment, then for it, one budget for both. There is nothing cheaper to
|
|
554
|
+
* poll, so a refusal past the platform's cap on waits in progress backs off
|
|
555
|
+
* (0.5 s up to 3 s) and asks again.
|
|
556
|
+
*/
|
|
557
|
+
async #commitWait(target, waitOptions) {
|
|
558
|
+
const resolved = resolveWaitOptions(waitOptions);
|
|
559
|
+
const deadline = resolved.now() + resolved.timeoutMs;
|
|
560
|
+
const commit = target.gitCommit.slice(0, 7);
|
|
561
|
+
const what = `the deploy of ${commit}${target.alias ? ` to ${target.alias}` : ""}`;
|
|
562
|
+
let last = null;
|
|
563
|
+
let status = "IMPORTING";
|
|
564
|
+
let backoff = 500;
|
|
565
|
+
for (let requests = 1;; requests++) {
|
|
566
|
+
if (resolved.signal?.aborted)
|
|
567
|
+
throw new UnfazedTimeoutError("unfazed: wait aborted");
|
|
568
|
+
const remaining = deadline - resolved.now();
|
|
569
|
+
if (requests > 1 && remaining <= 0) {
|
|
570
|
+
throw new UnfazedDeployError("TIMEOUT", last?.id ?? "", last, `unfazed: ${what} still ${status} after ${resolved.timeoutMs}ms`);
|
|
571
|
+
}
|
|
572
|
+
const seconds = Math.max(1, Math.min(SERVER_WAIT_SECONDS, Math.ceil(remaining / 1000)));
|
|
573
|
+
let answer;
|
|
574
|
+
try {
|
|
575
|
+
answer = (await this.client.query(ops.WaitForCommitDeploymentDocument, {
|
|
576
|
+
workspaceID: target.workspaceId,
|
|
577
|
+
gitCommit: target.gitCommit,
|
|
578
|
+
alias: target.alias ?? null,
|
|
579
|
+
after: target.after ?? null,
|
|
580
|
+
timeoutSeconds: seconds,
|
|
581
|
+
}, { ...(resolved.signal ? { signal: resolved.signal } : {}), timeoutMs: (seconds + 30) * 1000 })).waitForDeployment;
|
|
582
|
+
}
|
|
583
|
+
catch (e) {
|
|
584
|
+
if (!isTooManyWaits(e))
|
|
585
|
+
throw e;
|
|
586
|
+
await resolved.sleep(Math.max(0, Math.min(backoff, deadline - resolved.now())), resolved.signal);
|
|
587
|
+
backoff = Math.min(backoff * 1.5, 3_000);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const d = answer.deployment;
|
|
591
|
+
status = answer.status;
|
|
592
|
+
if (d)
|
|
593
|
+
last = d;
|
|
594
|
+
if (answer.timedOut)
|
|
595
|
+
continue;
|
|
596
|
+
if (!d) {
|
|
597
|
+
// Not timed out and no deployment: the import failed on the commit
|
|
598
|
+
// and nothing imports any more, so no deployment of it is coming.
|
|
599
|
+
throw new UnfazedDeployError("FAILED", "", null, `unfazed: the import of ${commit} failed${answer.importError ? `: ${answer.importError}` : ""}`);
|
|
600
|
+
}
|
|
601
|
+
if (d.status === "READY")
|
|
602
|
+
return { deployment: d, requests };
|
|
603
|
+
throw new UnfazedDeployError("FAILED", d.id, d, `unfazed: deployment ${d.id} (${what}) ${d.status}${d.error ? `: ${d.error}` : ""}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
532
606
|
async #pollWait(deploymentId, waitOptions) {
|
|
533
607
|
const resolved = resolveWaitOptions({ initialIntervalMs: 500, maxIntervalMs: 3_000, ...waitOptions });
|
|
534
608
|
const requestOptions = resolved.signal ? { signal: resolved.signal } : {};
|
|
@@ -557,12 +631,16 @@ export class Unfazed {
|
|
|
557
631
|
return { deployment: full, requests: requests + 1, mode: "poll" };
|
|
558
632
|
}
|
|
559
633
|
/**
|
|
560
|
-
* The platform's measured phases of a deploy ({@link PlatformTimings}),
|
|
561
|
-
*
|
|
562
|
-
*
|
|
634
|
+
* The platform's measured phases of a deploy ({@link PlatformTimings}), as
|
|
635
|
+
* it parses them from all of the deployment's timing events
|
|
636
|
+
* (`Deployment.timings`). Null when it recorded none (a deploy that is
|
|
637
|
+
* still building, or one from before timings existed), or for a deployment
|
|
638
|
+
* you cannot see.
|
|
563
639
|
*/
|
|
564
640
|
async getDeploymentTimings(deploymentId, options) {
|
|
565
|
-
|
|
641
|
+
const data = await this.client.query(ops.DeploymentTimingsDocument, { id: deploymentId }, options);
|
|
642
|
+
const timings = data.deployment?.timings;
|
|
643
|
+
return timings ? toPlatformTimings(timings) : null;
|
|
566
644
|
}
|
|
567
645
|
/**
|
|
568
646
|
* The timings of a deploy that just went READY. `deployment.phases` is
|
|
@@ -964,9 +1042,12 @@ export class Unfazed {
|
|
|
964
1042
|
* When the app deploys every push (`deploys: true`), the new commit is
|
|
965
1043
|
* imported and deployed like a push. That deploy is queued behind the
|
|
966
1044
|
* restore, so the platform answers before it exists; with `wait: true` the
|
|
967
|
-
* SDK waits for
|
|
968
|
-
*
|
|
969
|
-
*
|
|
1045
|
+
* SDK waits for it by commit on the platform ({@link Unfazed.waitForDeployment}
|
|
1046
|
+
* with `gitCommit: sha`, `alias: deployAlias`, `after: waitAfter`) and
|
|
1047
|
+
* resolves with it READY, or throws {@link UnfazedDeployError} with its
|
|
1048
|
+
* build error or the import's. A link with unfazed.json `apps[]` names no
|
|
1049
|
+
* `deployAlias` and deploys once per app: then it follows the import until
|
|
1050
|
+
* every deployment it made is done.
|
|
970
1051
|
*
|
|
971
1052
|
* ```ts
|
|
972
1053
|
* const page = await unfazed.listCommits(ws.id);
|
|
@@ -985,18 +1066,19 @@ export class Unfazed {
|
|
|
985
1066
|
const restored = data.restoreAppVersion;
|
|
986
1067
|
if (!options.wait || !restored.deploys)
|
|
987
1068
|
return restored;
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
};
|
|
1069
|
+
const waitOptions = { ...options.waitOptions, ...(options.signal ? { signal: options.signal } : {}) };
|
|
1070
|
+
if (restored.deployAlias) {
|
|
1071
|
+
const { deployment } = await this.#commitWait({ workspaceId: restored.workspaceID, gitCommit: restored.sha, alias: restored.deployAlias, after: restored.waitAfter }, waitOptions);
|
|
1072
|
+
return { ...restored, deployments: [deployment] };
|
|
1073
|
+
}
|
|
1074
|
+
return { ...restored, deployments: await this.#waitForImport(restored.workspaceID, restored.sha, waitOptions) };
|
|
995
1075
|
}
|
|
996
1076
|
/**
|
|
997
1077
|
* Waits until the app's git link has imported `sha` (READY: its deploys
|
|
998
1078
|
* are done) and returns the deployments of that commit; throws on a FAILED
|
|
999
|
-
* import of it.
|
|
1079
|
+
* import of it. Only for a restore whose link names no alias (unfazed.json
|
|
1080
|
+
* `apps[]`): the wait by commit waits for one app's deploy, and which apps
|
|
1081
|
+
* a commit deploys is known only once its import is done.
|
|
1000
1082
|
*/
|
|
1001
1083
|
async #waitForImport(workspaceId, sha, waitOptions) {
|
|
1002
1084
|
const resolved = resolveWaitOptions({ initialIntervalMs: 500, maxIntervalMs: 3_000, ...waitOptions });
|
|
@@ -1443,51 +1525,123 @@ function toDeploymentEvent(event) {
|
|
|
1443
1525
|
message: event.message,
|
|
1444
1526
|
vmID: event.vmID ?? null,
|
|
1445
1527
|
metadata: event.metadata ?? null,
|
|
1528
|
+
timings: event.timings ? toPlatformTimings(event.timings) : null,
|
|
1446
1529
|
createdAt: event.createdAt,
|
|
1447
1530
|
};
|
|
1448
1531
|
}
|
|
1532
|
+
const BUILD_FINISHED_EVENT = "deployment.build_finished";
|
|
1533
|
+
const DEPLOY_PHASES_EVENT = "deployment.phases";
|
|
1449
1534
|
/**
|
|
1450
|
-
* A
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1535
|
+
* A duration as the platform accepts one: a whole, non-negative number of
|
|
1536
|
+
* milliseconds (a number, or a string of digits as metadata carries it). A
|
|
1537
|
+
* fraction or anything else is not a duration; a negative one is a clock gone
|
|
1538
|
+
* backwards. Both are dropped, as the platform drops them.
|
|
1454
1539
|
*/
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
if (
|
|
1540
|
+
function durationMs(value) {
|
|
1541
|
+
let n;
|
|
1542
|
+
if (typeof value === "number")
|
|
1543
|
+
n = value;
|
|
1544
|
+
else if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim()))
|
|
1545
|
+
n = Number(value.trim());
|
|
1546
|
+
else
|
|
1460
1547
|
return null;
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
return null;
|
|
1466
|
-
const n = Number(value);
|
|
1467
|
-
return Number.isFinite(n) ? n : null;
|
|
1468
|
-
};
|
|
1548
|
+
return Number.isSafeInteger(n) && n >= 0 ? n + 0 : null; // + 0: -0 is 0
|
|
1549
|
+
}
|
|
1550
|
+
/** The platform's structured timings, with every duration checked like the metadata's. */
|
|
1551
|
+
function toPlatformTimings(t) {
|
|
1469
1552
|
const phases = {};
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const n = Number(value);
|
|
1477
|
-
if (Number.isFinite(n))
|
|
1478
|
-
phases[name] = n;
|
|
1553
|
+
if (t.phases && typeof t.phases === "object") {
|
|
1554
|
+
for (const [name, value] of Object.entries(t.phases)) {
|
|
1555
|
+
const ms = durationMs(value);
|
|
1556
|
+
if (ms !== null)
|
|
1557
|
+
phases[name] = ms;
|
|
1558
|
+
}
|
|
1479
1559
|
}
|
|
1480
|
-
const memory =
|
|
1560
|
+
const memory = durationMs(t.buildMemoryMiB);
|
|
1481
1561
|
return {
|
|
1482
|
-
totalMs:
|
|
1483
|
-
buildMs:
|
|
1484
|
-
promoteMs:
|
|
1562
|
+
totalMs: durationMs(t.totalMs),
|
|
1563
|
+
buildMs: durationMs(t.buildMs),
|
|
1564
|
+
promoteMs: durationMs(t.promoteMs),
|
|
1485
1565
|
phases,
|
|
1486
|
-
buildMode:
|
|
1487
|
-
artifactTransport:
|
|
1488
|
-
buildMemoryMiB:
|
|
1566
|
+
buildMode: t.buildMode || null,
|
|
1567
|
+
artifactTransport: t.artifactTransport || null,
|
|
1568
|
+
buildMemoryMiB: memory ? memory : null,
|
|
1489
1569
|
};
|
|
1490
1570
|
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Timing-event metadata read exactly as the platform reads it
|
|
1573
|
+
* (internal/usecases/deploy_timings.go): every `<name>_ms` that is a whole,
|
|
1574
|
+
* non-negative number; `total` only from the READY summary.
|
|
1575
|
+
*/
|
|
1576
|
+
function timingsFromMetadata(metadata, ready) {
|
|
1577
|
+
const out = {
|
|
1578
|
+
totalMs: null,
|
|
1579
|
+
buildMs: null,
|
|
1580
|
+
promoteMs: null,
|
|
1581
|
+
phases: {},
|
|
1582
|
+
buildMode: metadata["build_mode"] || null,
|
|
1583
|
+
artifactTransport: metadata["artifact_transport"] || null,
|
|
1584
|
+
buildMemoryMiB: null,
|
|
1585
|
+
};
|
|
1586
|
+
for (const [key, raw] of Object.entries(metadata)) {
|
|
1587
|
+
if (!key.endsWith("_ms") || key === "_ms")
|
|
1588
|
+
continue;
|
|
1589
|
+
const ms = durationMs(raw);
|
|
1590
|
+
if (ms === null)
|
|
1591
|
+
continue;
|
|
1592
|
+
const name = key.slice(0, -3);
|
|
1593
|
+
if (name === "total") {
|
|
1594
|
+
if (ready)
|
|
1595
|
+
out.totalMs = ms;
|
|
1596
|
+
}
|
|
1597
|
+
else if (name === "build")
|
|
1598
|
+
out.buildMs = ms;
|
|
1599
|
+
else if (name === "promote")
|
|
1600
|
+
out.promoteMs = ms;
|
|
1601
|
+
else
|
|
1602
|
+
out.phases[name] = ms;
|
|
1603
|
+
}
|
|
1604
|
+
const memory = durationMs(metadata["build_memory_mib"]);
|
|
1605
|
+
if (memory)
|
|
1606
|
+
out.buildMemoryMiB = memory;
|
|
1607
|
+
return out;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* A deploy's phases from its events, as the platform reads them: the latest
|
|
1611
|
+
* `deployment.phases` (written when it went READY: build, promotion and its
|
|
1612
|
+
* split, total) over the latest `deployment.build_finished` (the build VM's
|
|
1613
|
+
* own phases). It prefers the `timings` the platform parsed into each event
|
|
1614
|
+
* ({@link Unfazed.getEvents} reads them); events without them (an older
|
|
1615
|
+
* platform, events built by hand) are parsed from their `*_ms` metadata the
|
|
1616
|
+
* same way. Durations are whole, non-negative milliseconds: anything else is
|
|
1617
|
+
* dropped, as the platform drops it. Null when neither event is there.
|
|
1618
|
+
*/
|
|
1619
|
+
export function parseDeploymentTimings(events) {
|
|
1620
|
+
const latest = (type) => {
|
|
1621
|
+
const event = [...events].reverse().find((e) => e.type === type);
|
|
1622
|
+
return event && (event.timings || event.metadata) ? event : null;
|
|
1623
|
+
};
|
|
1624
|
+
const build = latest(BUILD_FINISHED_EVENT);
|
|
1625
|
+
const ready = latest(DEPLOY_PHASES_EVENT);
|
|
1626
|
+
if (!build && !ready)
|
|
1627
|
+
return null;
|
|
1628
|
+
if ((!build || build.timings) && (!ready || ready.timings)) {
|
|
1629
|
+
const b = build?.timings ? toPlatformTimings(build.timings) : null;
|
|
1630
|
+
const r = ready?.timings ? toPlatformTimings(ready.timings) : null;
|
|
1631
|
+
return {
|
|
1632
|
+
totalMs: r?.totalMs ?? null,
|
|
1633
|
+
buildMs: r?.buildMs ?? b?.buildMs ?? null,
|
|
1634
|
+
promoteMs: r?.promoteMs ?? b?.promoteMs ?? null,
|
|
1635
|
+
phases: { ...b?.phases, ...r?.phases },
|
|
1636
|
+
buildMode: r?.buildMode ?? b?.buildMode ?? null,
|
|
1637
|
+
artifactTransport: r?.artifactTransport ?? b?.artifactTransport ?? null,
|
|
1638
|
+
buildMemoryMiB: r?.buildMemoryMiB ?? b?.buildMemoryMiB ?? null,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
// The platform merges the two events' metadata, the READY summary's values
|
|
1642
|
+
// over the build's, and parses the result.
|
|
1643
|
+
return timingsFromMetadata({ ...build?.metadata, ...ready?.metadata }, ready !== null);
|
|
1644
|
+
}
|
|
1491
1645
|
function errorGroupInput(ref) {
|
|
1492
1646
|
return { workspaceID: ref.workspaceId, alias: ref.alias, groupID: ref.groupId };
|
|
1493
1647
|
}
|