@stigmer/cli 3.32.0 → 3.34.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/local/artifact.d.ts +10 -3
- package/local/artifact.d.ts.map +1 -1
- package/local/artifact.js +62 -23
- package/local/artifact.js.map +1 -1
- package/local/temporal/download.d.ts +7 -1
- package/local/temporal/download.d.ts.map +1 -1
- package/local/temporal/download.js +6 -1
- package/local/temporal/download.js.map +1 -1
- package/package.json +7 -7
- package/src/local/artifact.ts +80 -22
- package/src/local/temporal/download.test.ts +116 -0
- package/src/local/temporal/download.ts +8 -1
package/local/artifact.d.ts
CHANGED
|
@@ -21,12 +21,19 @@ export interface TarballBinarySource {
|
|
|
21
21
|
label: string;
|
|
22
22
|
/** Override the fetch implementation (tests). */
|
|
23
23
|
fetchImpl?: typeof fetch;
|
|
24
|
+
/** Override the wait between download attempts (tests). */
|
|
25
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
26
|
}
|
|
27
|
+
/** Attempts per download before a transient failure is reported. */
|
|
28
|
+
export declare const DOWNLOAD_ATTEMPTS = 4;
|
|
29
|
+
/** The wait before the second attempt; it doubles before each later one (1 s, 2 s, 4 s). */
|
|
30
|
+
export declare const DOWNLOAD_RETRY_BASE_DELAY_MS = 1000;
|
|
25
31
|
/**
|
|
26
32
|
* Download a `.tar.gz`, optionally verify its sha256, extract the named entry,
|
|
27
|
-
* and write it to `binPath` with executable permissions.
|
|
28
|
-
*
|
|
29
|
-
* failure
|
|
33
|
+
* and write it to `binPath` with executable permissions. Transient network
|
|
34
|
+
* failures are retried (see the module header). Throws a {@link CliExitError}
|
|
35
|
+
* with the URL on a persistent network failure, a non-transient HTTP status, a
|
|
36
|
+
* checksum mismatch, or an extraction failure.
|
|
30
37
|
*/
|
|
31
38
|
export declare function fetchTarballBinary(src: TarballBinarySource): Promise<void>;
|
|
32
39
|
/** Lowercase hex sha256 of `bytes`. */
|
package/local/artifact.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/local/artifact.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/local/artifact.ts"],"names":[],"mappings":"AAyBA,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,CAS9D;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,4FAA4F;AAC5F,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAElD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BhF;AAED,uCAAuC;AACvC,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEnD;AAmED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOrE;AASD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAoBhF"}
|
package/local/artifact.js
CHANGED
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
// second consumer until the server became an npm artifact.) The tar reader is a
|
|
9
9
|
// tiny POSIX/ustar implementation: no native `tar` dependency, which keeps the
|
|
10
10
|
// base install lean (DD-002).
|
|
11
|
+
//
|
|
12
|
+
// Retrying a transient failure is part of the contract. A connection that resets
|
|
13
|
+
// before or during the body, a 5xx or a 429 is tried again with a doubling wait,
|
|
14
|
+
// so a network blip does not fail a first `stigmer up`, a CI lane or an image
|
|
15
|
+
// build. Any other HTTP status and a checksum mismatch are answers: they fail at
|
|
16
|
+
// once.
|
|
11
17
|
import { createHash } from "node:crypto";
|
|
12
18
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
19
|
import { dirname } from "node:path";
|
|
20
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
14
21
|
import { gunzipSync } from "fflate";
|
|
15
22
|
import { CliExitError } from "../errors/cli-exit-error.js";
|
|
16
23
|
import { ExitCode } from "../errors/exit-codes.js";
|
|
@@ -36,18 +43,23 @@ export function mapReleaseArch(arch) {
|
|
|
36
43
|
return arch;
|
|
37
44
|
}
|
|
38
45
|
}
|
|
46
|
+
/** Attempts per download before a transient failure is reported. */
|
|
47
|
+
export const DOWNLOAD_ATTEMPTS = 4;
|
|
48
|
+
/** The wait before the second attempt; it doubles before each later one (1 s, 2 s, 4 s). */
|
|
49
|
+
export const DOWNLOAD_RETRY_BASE_DELAY_MS = 1_000;
|
|
39
50
|
/**
|
|
40
51
|
* Download a `.tar.gz`, optionally verify its sha256, extract the named entry,
|
|
41
|
-
* and write it to `binPath` with executable permissions.
|
|
42
|
-
*
|
|
43
|
-
* failure
|
|
52
|
+
* and write it to `binPath` with executable permissions. Transient network
|
|
53
|
+
* failures are retried (see the module header). Throws a {@link CliExitError}
|
|
54
|
+
* with the URL on a persistent network failure, a non-transient HTTP status, a
|
|
55
|
+
* checksum mismatch, or an extraction failure.
|
|
44
56
|
*/
|
|
45
57
|
export async function fetchTarballBinary(src) {
|
|
46
|
-
const
|
|
47
|
-
const archive = await fetchBytes(
|
|
58
|
+
const net = { doFetch: src.fetchImpl ?? fetch, sleep: src.sleep ?? delay };
|
|
59
|
+
const archive = await fetchBytes(net, src.url, src.label);
|
|
48
60
|
if (src.checksumUrl !== undefined) {
|
|
49
61
|
const archiveName = archiveBasename(src.url);
|
|
50
|
-
const expected = parseShasum(await fetchText(
|
|
62
|
+
const expected = parseShasum(await fetchText(net, src.checksumUrl, `${src.label} checksum`), archiveName);
|
|
51
63
|
const actual = sha256Hex(archive);
|
|
52
64
|
if (expected === "" || expected !== actual) {
|
|
53
65
|
throw new CliExitError(`${src.label} checksum mismatch — refusing to use the download`, ExitCode.General, [
|
|
@@ -72,26 +84,53 @@ export async function fetchTarballBinary(src) {
|
|
|
72
84
|
export function sha256Hex(bytes) {
|
|
73
85
|
return createHash("sha256").update(bytes).digest("hex");
|
|
74
86
|
}
|
|
75
|
-
async function fetchBytes(
|
|
76
|
-
|
|
77
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
87
|
+
async function fetchBytes(net, url, label) {
|
|
88
|
+
return fetchWithRetry(net, url, label, async (res) => new Uint8Array(await res.arrayBuffer()));
|
|
78
89
|
}
|
|
79
|
-
async function fetchText(
|
|
80
|
-
|
|
81
|
-
return res.text();
|
|
90
|
+
async function fetchText(net, url, label) {
|
|
91
|
+
return fetchWithRetry(net, url, label, (res) => res.text());
|
|
82
92
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// One download, with its body read inside the attempt: a connection that drops
|
|
94
|
+
// after the headers is as transient as one that never connects, and is retried
|
|
95
|
+
// the same way. The error after the last attempt carries the last failure's
|
|
96
|
+
// cause, which for Node's fetch is the socket error behind "fetch failed".
|
|
97
|
+
async function fetchWithRetry(net, url, label, read) {
|
|
98
|
+
let lastFailure = "";
|
|
99
|
+
for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt++) {
|
|
100
|
+
if (attempt > 1)
|
|
101
|
+
await net.sleep(DOWNLOAD_RETRY_BASE_DELAY_MS * 2 ** (attempt - 2));
|
|
102
|
+
let res;
|
|
103
|
+
try {
|
|
104
|
+
res = await net.doFetch(url);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
lastFailure = describeFailure(err);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
if (!isTransientStatus(res.status)) {
|
|
112
|
+
throw new CliExitError(`failed to download ${label}: HTTP ${res.status} from ${url}`, ExitCode.General);
|
|
113
|
+
}
|
|
114
|
+
lastFailure = `HTTP ${res.status}`;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await read(res);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
lastFailure = describeFailure(err);
|
|
122
|
+
}
|
|
93
123
|
}
|
|
94
|
-
|
|
124
|
+
throw new CliExitError(`failed to download ${label} from ${url} after ${DOWNLOAD_ATTEMPTS} attempts: ${lastFailure}`, ExitCode.General, ["check the network connection, then run the command again"]);
|
|
125
|
+
}
|
|
126
|
+
// A server error or a rate limit may clear on its own; any other status will not.
|
|
127
|
+
function isTransientStatus(status) {
|
|
128
|
+
return status >= 500 || status === 429;
|
|
129
|
+
}
|
|
130
|
+
function describeFailure(err) {
|
|
131
|
+
if (!(err instanceof Error))
|
|
132
|
+
return String(err);
|
|
133
|
+
return err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message;
|
|
95
134
|
}
|
|
96
135
|
/**
|
|
97
136
|
* The sha256 a `shasum`/`sha256sum`-format file records for `filename`, lowercase
|
package/local/artifact.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../../src/local/artifact.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,2EAA2E;AAC3E,iEAAiE;AACjE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,8BAA8B;
|
|
1
|
+
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../../src/local/artifact.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { extractTarEntry } from "../artifact.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The Temporal CLI version every channel runs: `stigmer up`, the all-in-one
|
|
4
|
+
* image, and the CI lanes and developer machines that install it with
|
|
5
|
+
* `make install-temporal-cli`. Bump it deliberately; it moves all of them.
|
|
6
|
+
*/
|
|
3
7
|
export declare const DEFAULT_TEMPORAL_VERSION = "1.5.1";
|
|
4
8
|
/** The release's `sha256sum`-format digest file, one line per published asset. */
|
|
5
9
|
export declare const TEMPORAL_CHECKSUMS_FILE = "checksums.txt";
|
|
@@ -14,6 +18,8 @@ export interface TemporalDownloadTarget {
|
|
|
14
18
|
arch?: string;
|
|
15
19
|
/** Override the fetch implementation (tests). */
|
|
16
20
|
fetchImpl?: typeof fetch;
|
|
21
|
+
/** Override the wait between download attempts (tests). */
|
|
22
|
+
sleep?: (ms: number) => Promise<void>;
|
|
17
23
|
}
|
|
18
24
|
/** The release asset name for a version on a platform, as Temporal publishes it. */
|
|
19
25
|
export declare function temporalArchiveName(version: string, platform: NodeJS.Platform, arch: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/local/temporal/download.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/local/temporal/download.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAEhD,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AAEvD,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpG;AAED,kEAAkE;AAClE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,yEAAyE;AACzE,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvF;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D"}
|
|
@@ -15,7 +15,11 @@ import { fetchTarballBinary, mapReleaseArch, mapReleaseOs } from "../artifact.js
|
|
|
15
15
|
// Re-exported so existing consumers (temporal/index.ts, download.test.ts) keep
|
|
16
16
|
// importing the tar reader from here.
|
|
17
17
|
export { extractTarEntry } from "../artifact.js";
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The Temporal CLI version every channel runs: `stigmer up`, the all-in-one
|
|
20
|
+
* image, and the CI lanes and developer machines that install it with
|
|
21
|
+
* `make install-temporal-cli`. Bump it deliberately; it moves all of them.
|
|
22
|
+
*/
|
|
19
23
|
export const DEFAULT_TEMPORAL_VERSION = "1.5.1";
|
|
20
24
|
/** The release's `sha256sum`-format digest file, one line per published asset. */
|
|
21
25
|
export const TEMPORAL_CHECKSUMS_FILE = "checksums.txt";
|
|
@@ -37,6 +41,7 @@ export async function downloadTemporalCli(target) {
|
|
|
37
41
|
binPath: target.binPath,
|
|
38
42
|
label: "Temporal CLI",
|
|
39
43
|
fetchImpl: target.fetchImpl,
|
|
44
|
+
sleep: target.sleep,
|
|
40
45
|
});
|
|
41
46
|
}
|
|
42
47
|
/** True if a Temporal binary is already present at `binPath`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.js","sourceRoot":"","sources":["../../../src/local/temporal/download.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,uEAAuE;AACvE,mDAAmD;AACnD,EAAE;AACF,wEAAwE;AACxE,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAElF,+EAA+E;AAC/E,sCAAsC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"download.js","sourceRoot":"","sources":["../../../src/local/temporal/download.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,uEAAuE;AACvE,mDAAmD;AACnD,EAAE;AACF,wEAAwE;AACxE,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAElF,+EAA+E;AAC/E,sCAAsC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAEhD,kFAAkF;AAClF,MAAM,CAAC,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAiBvD,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,QAAyB,EAAE,IAAY;IAC1F,OAAO,gBAAgB,OAAO,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC;AAC5F,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,uBAAuB,CAAC,OAAe,EAAE,KAAa;IACpE,OAAO,wDAAwD,OAAO,IAAI,KAAK,EAAE,CAAC;AACpF,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAA8B;IACtE,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtH,MAAM,kBAAkB,CAAC;QACvB,GAAG,EAAE,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;QACrD,WAAW,EAAE,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,uBAAuB,CAAC;QAC7E,SAAS,EAAE,UAAU;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,cAAc;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC,CAAC;AACL,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.0",
|
|
4
4
|
"description": "Stigmer command-line interface — manage agents, workflows, MCP servers, skills, and executions from the terminal",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"@connectrpc/connect": "^2.0.0",
|
|
36
36
|
"@connectrpc/connect-node": "^2.1.1",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
38
|
-
"@stigmer/ink": "3.
|
|
39
|
-
"@stigmer/mcp-server": "3.
|
|
40
|
-
"@stigmer/plugin-package": "3.
|
|
41
|
-
"@stigmer/protos": "3.
|
|
42
|
-
"@stigmer/sdk": "3.
|
|
43
|
-
"@stigmer/zip-structure": "3.
|
|
38
|
+
"@stigmer/ink": "3.34.0",
|
|
39
|
+
"@stigmer/mcp-server": "3.34.0",
|
|
40
|
+
"@stigmer/plugin-package": "3.34.0",
|
|
41
|
+
"@stigmer/protos": "3.34.0",
|
|
42
|
+
"@stigmer/sdk": "3.34.0",
|
|
43
|
+
"@stigmer/zip-structure": "3.34.0",
|
|
44
44
|
"commander": "^12.1.0",
|
|
45
45
|
"fflate": "^0.8.3",
|
|
46
46
|
"ink": "^7.0.0",
|
package/src/local/artifact.ts
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
// second consumer until the server became an npm artifact.) The tar reader is a
|
|
9
9
|
// tiny POSIX/ustar implementation: no native `tar` dependency, which keeps the
|
|
10
10
|
// base install lean (DD-002).
|
|
11
|
+
//
|
|
12
|
+
// Retrying a transient failure is part of the contract. A connection that resets
|
|
13
|
+
// before or during the body, a 5xx or a 429 is tried again with a doubling wait,
|
|
14
|
+
// so a network blip does not fail a first `stigmer up`, a CI lane or an image
|
|
15
|
+
// build. Any other HTTP status and a checksum mismatch are answers: they fail at
|
|
16
|
+
// once.
|
|
11
17
|
|
|
12
18
|
import { createHash } from "node:crypto";
|
|
13
19
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
20
|
import { dirname } from "node:path";
|
|
21
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
15
22
|
import { gunzipSync } from "fflate";
|
|
16
23
|
import { CliExitError } from "../errors/cli-exit-error.js";
|
|
17
24
|
import { ExitCode } from "../errors/exit-codes.js";
|
|
@@ -59,21 +66,30 @@ export interface TarballBinarySource {
|
|
|
59
66
|
label: string;
|
|
60
67
|
/** Override the fetch implementation (tests). */
|
|
61
68
|
fetchImpl?: typeof fetch;
|
|
69
|
+
/** Override the wait between download attempts (tests). */
|
|
70
|
+
sleep?: (ms: number) => Promise<void>;
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
/** Attempts per download before a transient failure is reported. */
|
|
74
|
+
export const DOWNLOAD_ATTEMPTS = 4;
|
|
75
|
+
|
|
76
|
+
/** The wait before the second attempt; it doubles before each later one (1 s, 2 s, 4 s). */
|
|
77
|
+
export const DOWNLOAD_RETRY_BASE_DELAY_MS = 1_000;
|
|
78
|
+
|
|
64
79
|
/**
|
|
65
80
|
* Download a `.tar.gz`, optionally verify its sha256, extract the named entry,
|
|
66
|
-
* and write it to `binPath` with executable permissions.
|
|
67
|
-
*
|
|
68
|
-
* failure
|
|
81
|
+
* and write it to `binPath` with executable permissions. Transient network
|
|
82
|
+
* failures are retried (see the module header). Throws a {@link CliExitError}
|
|
83
|
+
* with the URL on a persistent network failure, a non-transient HTTP status, a
|
|
84
|
+
* checksum mismatch, or an extraction failure.
|
|
69
85
|
*/
|
|
70
86
|
export async function fetchTarballBinary(src: TarballBinarySource): Promise<void> {
|
|
71
|
-
const
|
|
72
|
-
const archive = await fetchBytes(
|
|
87
|
+
const net: Download = { doFetch: src.fetchImpl ?? fetch, sleep: src.sleep ?? delay };
|
|
88
|
+
const archive = await fetchBytes(net, src.url, src.label);
|
|
73
89
|
|
|
74
90
|
if (src.checksumUrl !== undefined) {
|
|
75
91
|
const archiveName = archiveBasename(src.url);
|
|
76
|
-
const expected = parseShasum(await fetchText(
|
|
92
|
+
const expected = parseShasum(await fetchText(net, src.checksumUrl, `${src.label} checksum`), archiveName);
|
|
77
93
|
const actual = sha256Hex(archive);
|
|
78
94
|
if (expected === "" || expected !== actual) {
|
|
79
95
|
throw new CliExitError(`${src.label} checksum mismatch — refusing to use the download`, ExitCode.General, [
|
|
@@ -102,27 +118,69 @@ export function sha256Hex(bytes: Uint8Array): string {
|
|
|
102
118
|
return createHash("sha256").update(bytes).digest("hex");
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
// The network a download runs over: the fetch it calls and the wait between attempts.
|
|
122
|
+
interface Download {
|
|
123
|
+
doFetch: typeof fetch;
|
|
124
|
+
sleep: (ms: number) => Promise<void>;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
async function
|
|
111
|
-
|
|
112
|
-
return res.text();
|
|
127
|
+
async function fetchBytes(net: Download, url: string, label: string): Promise<Uint8Array> {
|
|
128
|
+
return fetchWithRetry(net, url, label, async (res) => new Uint8Array(await res.arrayBuffer()));
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
async function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
async function fetchText(net: Download, url: string, label: string): Promise<string> {
|
|
132
|
+
return fetchWithRetry(net, url, label, (res) => res.text());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// One download, with its body read inside the attempt: a connection that drops
|
|
136
|
+
// after the headers is as transient as one that never connects, and is retried
|
|
137
|
+
// the same way. The error after the last attempt carries the last failure's
|
|
138
|
+
// cause, which for Node's fetch is the socket error behind "fetch failed".
|
|
139
|
+
async function fetchWithRetry<T>(
|
|
140
|
+
net: Download,
|
|
141
|
+
url: string,
|
|
142
|
+
label: string,
|
|
143
|
+
read: (res: Response) => Promise<T>,
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
let lastFailure = "";
|
|
146
|
+
for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt++) {
|
|
147
|
+
if (attempt > 1) await net.sleep(DOWNLOAD_RETRY_BASE_DELAY_MS * 2 ** (attempt - 2));
|
|
148
|
+
|
|
149
|
+
let res: Response;
|
|
150
|
+
try {
|
|
151
|
+
res = await net.doFetch(url);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
lastFailure = describeFailure(err);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
if (!isTransientStatus(res.status)) {
|
|
158
|
+
throw new CliExitError(`failed to download ${label}: HTTP ${res.status} from ${url}`, ExitCode.General);
|
|
159
|
+
}
|
|
160
|
+
lastFailure = `HTTP ${res.status}`;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return await read(res);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
lastFailure = describeFailure(err);
|
|
167
|
+
}
|
|
124
168
|
}
|
|
125
|
-
|
|
169
|
+
throw new CliExitError(
|
|
170
|
+
`failed to download ${label} from ${url} after ${DOWNLOAD_ATTEMPTS} attempts: ${lastFailure}`,
|
|
171
|
+
ExitCode.General,
|
|
172
|
+
["check the network connection, then run the command again"],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A server error or a rate limit may clear on its own; any other status will not.
|
|
177
|
+
function isTransientStatus(status: number): boolean {
|
|
178
|
+
return status >= 500 || status === 429;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function describeFailure(err: unknown): string {
|
|
182
|
+
if (!(err instanceof Error)) return String(err);
|
|
183
|
+
return err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message;
|
|
126
184
|
}
|
|
127
185
|
|
|
128
186
|
/**
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
// The Temporal CLI download end to end over a scripted `fetch`: asset naming,
|
|
2
|
+
// verification against the release's checksums.txt, extraction, and the retry
|
|
3
|
+
// contract `../artifact.ts` gives every release download. The retry cases pin
|
|
4
|
+
// which failures are transient (a dropped connection before or during the body,
|
|
5
|
+
// a 5xx, a 429) and which are answers (a 404, a bad digest), the backoff, and
|
|
6
|
+
// the error a persistent failure ends in. A recording `sleep` keeps them instant.
|
|
7
|
+
|
|
1
8
|
import { createHash } from "node:crypto";
|
|
2
9
|
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
|
3
10
|
import { tmpdir } from "node:os";
|
|
4
11
|
import { join } from "node:path";
|
|
5
12
|
import { gzipSync } from "fflate";
|
|
6
13
|
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { CliExitError } from "../../errors/cli-exit-error.js";
|
|
7
15
|
import {
|
|
8
16
|
TEMPORAL_CHECKSUMS_FILE,
|
|
9
17
|
downloadTemporalCli,
|
|
@@ -130,3 +138,111 @@ describe("downloadTemporalCli", () => {
|
|
|
130
138
|
await expect(downloadTemporalCli({ version: "9.9.9", binPath, fetchImpl })).rejects.toThrow(/HTTP 404/);
|
|
131
139
|
});
|
|
132
140
|
});
|
|
141
|
+
|
|
142
|
+
type Failure = "reject" | "body" | number;
|
|
143
|
+
|
|
144
|
+
// Wraps a release so requests for one asset fail as scripted before the real
|
|
145
|
+
// release answers: each entry of `failures` is consumed by one request for
|
|
146
|
+
// `asset`. "reject" is a connection that never answers (Node's fetch rejects
|
|
147
|
+
// with the socket error as `cause`), "body" one that drops after the headers,
|
|
148
|
+
// a number an HTTP status. `calls` counts the requests for `asset`.
|
|
149
|
+
function flaky(inner: typeof fetch, asset: string, failures: Failure[]): { fetchImpl: typeof fetch; calls: () => number } {
|
|
150
|
+
let calls = 0;
|
|
151
|
+
const fetchImpl = (async (input: string | URL | Request) => {
|
|
152
|
+
const url = String(input);
|
|
153
|
+
if (!url.endsWith(`/${asset}`)) return inner(input);
|
|
154
|
+
calls += 1;
|
|
155
|
+
const failure = failures[calls - 1];
|
|
156
|
+
if (failure === "reject") throw new TypeError("fetch failed", { cause: new Error("read ECONNRESET") });
|
|
157
|
+
if (failure === "body") {
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
status: 200,
|
|
161
|
+
arrayBuffer: async () => {
|
|
162
|
+
throw new TypeError("terminated", { cause: new Error("other side closed") });
|
|
163
|
+
},
|
|
164
|
+
} as unknown as Response;
|
|
165
|
+
}
|
|
166
|
+
if (typeof failure === "number") {
|
|
167
|
+
return { ok: false, status: failure, arrayBuffer: async () => new ArrayBuffer(0) } as unknown as Response;
|
|
168
|
+
}
|
|
169
|
+
return inner(input);
|
|
170
|
+
}) as unknown as typeof fetch;
|
|
171
|
+
return { fetchImpl, calls: () => calls };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe("downloadTemporalCli over an unreliable network", () => {
|
|
175
|
+
const linuxArm = temporalArchiveName("1.5.1", "linux", "arm64");
|
|
176
|
+
const arm = archiveFor(Buffer.from("the-arm64-binary"));
|
|
177
|
+
const good = release("1.5.1", { [linuxArm]: arm }, `${sha256(arm)} ${linuxArm}\n`);
|
|
178
|
+
|
|
179
|
+
function install(fetchImpl: typeof fetch): { binPath: string; delays: number[]; run: Promise<void> } {
|
|
180
|
+
const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-temporal-")), "temporal");
|
|
181
|
+
const delays: number[] = [];
|
|
182
|
+
const sleep = async (ms: number): Promise<void> => {
|
|
183
|
+
delays.push(ms);
|
|
184
|
+
};
|
|
185
|
+
const run = downloadTemporalCli({ version: "1.5.1", binPath, platform: "linux", arch: "arm64", fetchImpl, sleep });
|
|
186
|
+
return { binPath, delays, run };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
it("retries a connection that never answers, then installs the verified binary", async () => {
|
|
190
|
+
const net = flaky(good, linuxArm, ["reject"]);
|
|
191
|
+
const { binPath, delays, run } = install(net.fetchImpl);
|
|
192
|
+
await run;
|
|
193
|
+
expect(readFileSync(binPath, "utf8")).toBe("the-arm64-binary");
|
|
194
|
+
expect(net.calls()).toBe(2);
|
|
195
|
+
expect(delays).toEqual([1000]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("retries a connection that drops after the headers", async () => {
|
|
199
|
+
const net = flaky(good, linuxArm, ["body"]);
|
|
200
|
+
const { binPath, run } = install(net.fetchImpl);
|
|
201
|
+
await run;
|
|
202
|
+
expect(readFileSync(binPath, "utf8")).toBe("the-arm64-binary");
|
|
203
|
+
expect(net.calls()).toBe(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("retries a 5xx and a 429, doubling the wait", async () => {
|
|
207
|
+
const net = flaky(good, linuxArm, [503, 429]);
|
|
208
|
+
const { binPath, delays, run } = install(net.fetchImpl);
|
|
209
|
+
await run;
|
|
210
|
+
expect(readFileSync(binPath, "utf8")).toBe("the-arm64-binary");
|
|
211
|
+
expect(net.calls()).toBe(3);
|
|
212
|
+
expect(delays).toEqual([1000, 2000]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("retries the checksums.txt fetch the same way", async () => {
|
|
216
|
+
const net = flaky(good, TEMPORAL_CHECKSUMS_FILE, ["reject"]);
|
|
217
|
+
const { binPath, run } = install(net.fetchImpl);
|
|
218
|
+
await run;
|
|
219
|
+
expect(readFileSync(binPath, "utf8")).toBe("the-arm64-binary");
|
|
220
|
+
expect(net.calls()).toBe(2);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("takes a 404 as the answer and does not retry it", async () => {
|
|
224
|
+
const net = flaky(good, linuxArm, [404]);
|
|
225
|
+
const { delays, run } = install(net.fetchImpl);
|
|
226
|
+
await expect(run).rejects.toThrow(/HTTP 404/);
|
|
227
|
+
expect(net.calls()).toBe(1);
|
|
228
|
+
expect(delays).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("gives up after four attempts, saying what failed and what to do", async () => {
|
|
232
|
+
const net = flaky(good, linuxArm, ["reject", "reject", "reject", "reject"]);
|
|
233
|
+
const { binPath, delays, run } = install(net.fetchImpl);
|
|
234
|
+
const err: unknown = await run.then(
|
|
235
|
+
() => undefined,
|
|
236
|
+
(e: unknown) => e,
|
|
237
|
+
);
|
|
238
|
+
expect(err).toBeInstanceOf(CliExitError);
|
|
239
|
+
const exit = err as CliExitError;
|
|
240
|
+
expect(exit.message).toBe(
|
|
241
|
+
`failed to download Temporal CLI from ${temporalReleaseAssetUrl("1.5.1", linuxArm)} after 4 attempts: fetch failed: read ECONNRESET`,
|
|
242
|
+
);
|
|
243
|
+
expect(exit.hints).toEqual(["check the network connection, then run the command again"]);
|
|
244
|
+
expect(net.calls()).toBe(4);
|
|
245
|
+
expect(delays).toEqual([1000, 2000, 4000]);
|
|
246
|
+
expect(existsSync(binPath)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -18,7 +18,11 @@ import { fetchTarballBinary, mapReleaseArch, mapReleaseOs } from "../artifact.js
|
|
|
18
18
|
// importing the tar reader from here.
|
|
19
19
|
export { extractTarEntry } from "../artifact.js";
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The Temporal CLI version every channel runs: `stigmer up`, the all-in-one
|
|
23
|
+
* image, and the CI lanes and developer machines that install it with
|
|
24
|
+
* `make install-temporal-cli`. Bump it deliberately; it moves all of them.
|
|
25
|
+
*/
|
|
22
26
|
export const DEFAULT_TEMPORAL_VERSION = "1.5.1";
|
|
23
27
|
|
|
24
28
|
/** The release's `sha256sum`-format digest file, one line per published asset. */
|
|
@@ -35,6 +39,8 @@ export interface TemporalDownloadTarget {
|
|
|
35
39
|
arch?: string;
|
|
36
40
|
/** Override the fetch implementation (tests). */
|
|
37
41
|
fetchImpl?: typeof fetch;
|
|
42
|
+
/** Override the wait between download attempts (tests). */
|
|
43
|
+
sleep?: (ms: number) => Promise<void>;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/** The release asset name for a version on a platform, as Temporal publishes it. */
|
|
@@ -58,6 +64,7 @@ export async function downloadTemporalCli(target: TemporalDownloadTarget): Promi
|
|
|
58
64
|
binPath: target.binPath,
|
|
59
65
|
label: "Temporal CLI",
|
|
60
66
|
fetchImpl: target.fetchImpl,
|
|
67
|
+
sleep: target.sleep,
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
|