bulletin-deploy 0.7.3 → 0.7.5
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 +26 -0
- package/bin/bulletin-deploy +94 -1
- package/dist/bug-report.js +4 -3
- package/dist/chunk-GQFH2NRB.js +155 -0
- package/dist/{chunk-EPNPPAMS.js → chunk-KQ75CSJJ.js} +71 -78
- package/dist/chunk-SAMH7JFG.js +896 -0
- package/dist/{chunk-4EQERQRG.js → chunk-UXKC7JAM.js} +2 -2
- package/dist/{chunk-BYIVK52G.js → chunk-WOJRQCQV.js} +67 -75
- package/dist/{chunk-MDYSENTW.js → chunk-XOKNNK6E.js} +1 -1
- package/dist/deploy.d.ts +4 -2
- package/dist/deploy.js +8 -5
- package/dist/dotns.d.ts +13 -40
- package/dist/dotns.js +11 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +24 -6
- package/dist/memory-report.js +2 -1
- package/dist/run-state.d.ts +22 -0
- package/dist/run-state.js +21 -0
- package/dist/telemetry.d.ts +5 -1
- package/dist/telemetry.js +8 -1
- package/dist/version-check.js +3 -2
- package/package.json +4 -3
- package/dist/chunk-M3H3F4FY.js +0 -1048
package/README.md
CHANGED
|
@@ -274,6 +274,32 @@ What's tracked:
|
|
|
274
274
|
- Source metadata (repo, branch, PR number, CI vs local)
|
|
275
275
|
- Tool version (`deploy.tool_version`)
|
|
276
276
|
|
|
277
|
+
### Using bulletin-deploy as a library under your own Sentry
|
|
278
|
+
|
|
279
|
+
If your tool embeds bulletin-deploy as a library and already runs its own Sentry SDK (for example, Parity's `playground-cli`), you can route bulletin-deploy's deploy spans, tags, and diagnostics through your existing Sentry client rather than clobbering it with ours. Set two env vars **before** importing or invoking bulletin-deploy:
|
|
280
|
+
|
|
281
|
+
```sh
|
|
282
|
+
BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1
|
|
283
|
+
BULLETIN_DEPLOY_HOST_APP=<your-app-name>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
What happens:
|
|
287
|
+
|
|
288
|
+
- `BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1` makes `initTelemetry()` skip its own `Sentry.init()` call. bulletin-deploy reuses the global Sentry client your app already configured. All deploy spans, breadcrumbs, `captureWarning`/`captureMessage` calls route to your DSN.
|
|
289
|
+
- `BULLETIN_DEPLOY_HOST_APP=<name>` attaches `deploy.host_app: <name>` to every deploy span **and** as a Sentry scope tag, so downstream events in the same process carry it too. Use it to facet dashboards by host.
|
|
290
|
+
|
|
291
|
+
**Requirements:**
|
|
292
|
+
|
|
293
|
+
- Your app must call its own `Sentry.init()` **before** importing or spawning bulletin-deploy; otherwise there is no ambient client to reuse and telemetry is effectively off.
|
|
294
|
+
- Your Sentry project should live in the same Sentry organisation as `bulletin-deploy` (`o4511059872841728.ingest.de.sentry.io`) if you want our cross-project dashboards to aggregate your traffic. Different org = different world; no cross-aggregation is possible.
|
|
295
|
+
- If your consumer app is maintained by Parity, we can add its name to the `PARITY_HOST_APPS` allowlist in `src/telemetry.ts` so end-user installs of the compiled binary qualify for the same diagnostics as our internal CI. Today: `playground-cli`.
|
|
296
|
+
|
|
297
|
+
**Gotchas:**
|
|
298
|
+
|
|
299
|
+
- Quotas are per-project. A traffic spike in your project will eat your quota and can drop bulletin-deploy spans routing through it without any signal in our dashboards.
|
|
300
|
+
- Issue-feed fingerprints don't dedupe across projects: the same error in your project and ours surfaces as two separate Sentry issues.
|
|
301
|
+
- `@sentry/node` major version must be compatible with ours (currently v8.x). Skew risks runtime errors on the first span call.
|
|
302
|
+
|
|
277
303
|
### Tagging test and benchmark runs
|
|
278
304
|
|
|
279
305
|
Real-user deploys and automated test/benchmark deploys share the same telemetry pipeline. Use `--tag` (or the `DEPLOY_TAG` env var) to label non-production runs so Sentry dashboards can filter them out:
|
package/bin/bulletin-deploy
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { deploy, DEFAULT_BULLETIN_RPC, DEFAULT_POOL_SIZE, NonRetryableError, EXIT_CODE_NO_RETRY } from "../dist/deploy.js";
|
|
4
4
|
import { bootstrapPool } from "../dist/pool.js";
|
|
5
|
-
import { VERSION } from "../dist/telemetry.js";
|
|
5
|
+
import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, setRunStateActive, markRelaunchOomHintShown } from "../dist/telemetry.js";
|
|
6
6
|
import { handleFailedDeploy, preReleaseWarning } from "../dist/version-check.js";
|
|
7
7
|
import { setDeployContext, installLogCapture, buildCliFlagsSummary } from "../dist/bug-report.js";
|
|
8
|
+
import { loadRunState, writeRunState, shouldSkipStaleWarning, shouldShowOomHint, probablyOomRssMb } from "../dist/run-state.js";
|
|
8
9
|
import * as fs from "fs";
|
|
9
10
|
|
|
10
11
|
// Install early so anything printed during flag parsing / preflight is
|
|
@@ -60,6 +61,92 @@ Options:
|
|
|
60
61
|
const rcWarning = preReleaseWarning(VERSION);
|
|
61
62
|
if (rcWarning) console.error(rcWarning);
|
|
62
63
|
|
|
64
|
+
// ── Crash capture (issue #154) ───────────────────────────────────
|
|
65
|
+
// Only wire crash capture for actual deploy / bootstrap runs — skip for
|
|
66
|
+
// --help / --version (which exit above) and --bootstrap (one-off ops op,
|
|
67
|
+
// no background work worth hinting OOM for).
|
|
68
|
+
if (!flags.help && !flags.version && !flags.bootstrap) {
|
|
69
|
+
// Sanitised argv — positional args + presence-only flag summary. Never
|
|
70
|
+
// puts a mnemonic/password/RPC/derivation-path on disk, even if the user
|
|
71
|
+
// passes one on the command line.
|
|
72
|
+
const sanitizedArgv = [
|
|
73
|
+
...positional,
|
|
74
|
+
...buildCliFlagsSummary(flags).split(" ").filter(Boolean),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Check if the previous run left a stale "running" or "crashed" marker
|
|
78
|
+
// (i.e. this process is a relaunch after a SIGKILL/OOM). Print a hint
|
|
79
|
+
// BEFORE resetting state so the user sees what happened.
|
|
80
|
+
const prev = loadRunState();
|
|
81
|
+
if (prev && (prev.status === "running" || prev.status === "crashed") && !shouldSkipStaleWarning(prev)) {
|
|
82
|
+
if (shouldShowOomHint(prev)) {
|
|
83
|
+
const peak = prev.lastPeakRssMb ?? "unknown";
|
|
84
|
+
const stage = prev.lastStage ?? "unknown";
|
|
85
|
+
const threshold = probablyOomRssMb();
|
|
86
|
+
console.error("");
|
|
87
|
+
console.error(` Warning: previous deploy did not exit cleanly (peak RSS ${peak} MB at stage "${stage}", threshold ${threshold} MB).`);
|
|
88
|
+
console.error(` This looks like an out-of-memory kill. Retry with a larger heap:`);
|
|
89
|
+
console.error(` NODE_OPTIONS='--max-old-space-size=8192' bulletin-deploy ...`);
|
|
90
|
+
console.error("");
|
|
91
|
+
markRelaunchOomHintShown();
|
|
92
|
+
} else if (prev.status === "crashed" && prev.reason) {
|
|
93
|
+
console.error(` Previous deploy exited via ${prev.reason}. Continuing.`);
|
|
94
|
+
} else if (prev.status === "running") {
|
|
95
|
+
console.error(` Previous deploy did not exit cleanly. Continuing.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reset the state file to reflect THIS run. Any previous status (crashed,
|
|
100
|
+
// succeeded, etc.) is overwritten — we already surfaced the warning above.
|
|
101
|
+
writeRunState({
|
|
102
|
+
status: "running",
|
|
103
|
+
pid: process.pid,
|
|
104
|
+
startedAt: Date.now(),
|
|
105
|
+
endedAt: undefined,
|
|
106
|
+
toolVersion: VERSION,
|
|
107
|
+
argv: sanitizedArgv,
|
|
108
|
+
lastPeakRssMb: null,
|
|
109
|
+
lastStage: null,
|
|
110
|
+
reason: undefined,
|
|
111
|
+
});
|
|
112
|
+
setRunStateActive(true);
|
|
113
|
+
|
|
114
|
+
// ── Signal / uncaught handlers ──────────────────────────────
|
|
115
|
+
// Guard against re-entry: if a handler itself throws (e.g. Sentry close
|
|
116
|
+
// rejects), unhandledRejection must not recursively call finalize.
|
|
117
|
+
let finalizing = false;
|
|
118
|
+
const finalize = async (reason, exitCode) => {
|
|
119
|
+
if (finalizing) return;
|
|
120
|
+
finalizing = true;
|
|
121
|
+
try {
|
|
122
|
+
setDeployAttribute("deploy.killed", reason);
|
|
123
|
+
setDeployAttribute("deploy.sad", "true");
|
|
124
|
+
captureWarning(`deploy process terminated: ${reason}`);
|
|
125
|
+
} catch { /* telemetry best-effort */ }
|
|
126
|
+
try { await closeTelemetry(1000); } catch { /* ignore */ }
|
|
127
|
+
try {
|
|
128
|
+
writeRunState({ status: "crashed", endedAt: Date.now(), reason });
|
|
129
|
+
} catch { /* fs best-effort */ }
|
|
130
|
+
process.exit(exitCode);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
process.on("uncaughtException", (e) => {
|
|
134
|
+
try { setDeployAttribute("deploy.error", (e?.message ?? String(e)).slice(0, 200)); } catch {}
|
|
135
|
+
finalize("uncaught", 2);
|
|
136
|
+
});
|
|
137
|
+
process.on("unhandledRejection", (e) => {
|
|
138
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
139
|
+
try { setDeployAttribute("deploy.error", msg.slice(0, 200)); } catch {}
|
|
140
|
+
finalize("unhandled", 2);
|
|
141
|
+
});
|
|
142
|
+
// POSIX exit codes: 128 + signal number. Matches shell conventions so
|
|
143
|
+
// callers (e.g. consumer CI with EXIT_CODE_NO_RETRY=75) treat them as
|
|
144
|
+
// retryable.
|
|
145
|
+
process.on("SIGINT", () => finalize("SIGINT", 130));
|
|
146
|
+
process.on("SIGTERM", () => finalize("SIGTERM", 143));
|
|
147
|
+
process.on("SIGHUP", () => finalize("SIGHUP", 129));
|
|
148
|
+
}
|
|
149
|
+
|
|
63
150
|
try {
|
|
64
151
|
if (flags.bootstrap) {
|
|
65
152
|
const rpc = flags.rpc ?? process.env.BULLETIN_RPC ?? DEFAULT_BULLETIN_RPC;
|
|
@@ -112,10 +199,16 @@ try {
|
|
|
112
199
|
console.log(`CID: ${result.cid}`);
|
|
113
200
|
console.log(`Domain: ${result.domainName}`);
|
|
114
201
|
}
|
|
202
|
+
if (!flags.help && !flags.version && !flags.bootstrap) {
|
|
203
|
+
try { writeRunState({ status: "succeeded", endedAt: Date.now() }); } catch {}
|
|
204
|
+
}
|
|
115
205
|
process.exit(0);
|
|
116
206
|
} catch (error) {
|
|
117
207
|
const noRetry = error instanceof NonRetryableError;
|
|
118
208
|
console.error(`Deployment failed${noRetry ? " (not retryable)" : ""}:`, error.message);
|
|
119
209
|
await handleFailedDeploy(error);
|
|
210
|
+
if (!flags.help && !flags.version && !flags.bootstrap) {
|
|
211
|
+
try { writeRunState({ status: "failed", endedAt: Date.now(), reason: (error?.message ?? String(error)).slice(0, 200) }); } catch {}
|
|
212
|
+
}
|
|
120
213
|
process.exit(noRetry ? EXIT_CODE_NO_RETRY : 1);
|
|
121
214
|
}
|
package/dist/bug-report.js
CHANGED
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
offerBugReport,
|
|
10
10
|
scrubSecrets,
|
|
11
11
|
setDeployContext
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-UXKC7JAM.js";
|
|
13
|
+
import "./chunk-XOKNNK6E.js";
|
|
14
|
+
import "./chunk-WOJRQCQV.js";
|
|
15
|
+
import "./chunk-GQFH2NRB.js";
|
|
15
16
|
import "./chunk-QGM4M3NI.js";
|
|
16
17
|
export {
|
|
17
18
|
buildCliFlagsSummary,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/run-state.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "bulletin-deploy",
|
|
9
|
+
version: "0.7.5",
|
|
10
|
+
private: false,
|
|
11
|
+
repository: {
|
|
12
|
+
type: "git",
|
|
13
|
+
url: "https://github.com/paritytech/bulletin-deploy.git"
|
|
14
|
+
},
|
|
15
|
+
publishConfig: {
|
|
16
|
+
registry: "https://registry.npmjs.org",
|
|
17
|
+
access: "public"
|
|
18
|
+
},
|
|
19
|
+
type: "module",
|
|
20
|
+
main: "./dist/index.js",
|
|
21
|
+
types: "./dist/index.d.ts",
|
|
22
|
+
bin: {
|
|
23
|
+
"bulletin-deploy": "./bin/bulletin-deploy"
|
|
24
|
+
},
|
|
25
|
+
exports: {
|
|
26
|
+
".": {
|
|
27
|
+
types: "./dist/index.d.ts",
|
|
28
|
+
import: "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
files: [
|
|
32
|
+
"dist",
|
|
33
|
+
"bin"
|
|
34
|
+
],
|
|
35
|
+
scripts: {
|
|
36
|
+
build: "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts src/memory-report.ts src/merkle.ts src/gh-pages-mirror.ts src/version-check.ts src/bug-report.ts src/run-state.ts --format esm --dts --clean --target node22",
|
|
37
|
+
prepare: "npm run build",
|
|
38
|
+
test: "npm run build && node --test test/test.js test/pool.test.js test/helpers/e2e-helpers.test.js",
|
|
39
|
+
"test:e2e": "npm run build && node --test test/e2e.test.js",
|
|
40
|
+
"test:e2e:smoke": "bash scripts/e2e-pass.sh smoke",
|
|
41
|
+
"test:e2e:pr": "bash scripts/e2e-pass.sh pr",
|
|
42
|
+
"test:e2e:nightly": "bash scripts/e2e-pass.sh nightly",
|
|
43
|
+
benchmark: "npm run build && node benchmark.js"
|
|
44
|
+
},
|
|
45
|
+
dependencies: {
|
|
46
|
+
"@ipld/car": "^5.4.3",
|
|
47
|
+
"@ipld/dag-pb": "^4.1.3",
|
|
48
|
+
"@noble/hashes": "^1.7.2",
|
|
49
|
+
"@parity/dotns-cli": "0.5.6",
|
|
50
|
+
"@polkadot-api/substrate-bindings": "^0.16.5",
|
|
51
|
+
"@polkadot-labs/hdkd": "^0.0.25",
|
|
52
|
+
"@polkadot-labs/hdkd-helpers": "^0.0.26",
|
|
53
|
+
"@polkadot/keyring": "^13.0.0",
|
|
54
|
+
"@polkadot/util-crypto": "^13.0.0",
|
|
55
|
+
"@sentry/node": "^9.14.0",
|
|
56
|
+
"ipfs-unixfs": "^11.2.0",
|
|
57
|
+
"ipfs-unixfs-importer": "^16.1.4",
|
|
58
|
+
multiformats: "^13.4.1",
|
|
59
|
+
"polkadot-api": "^1.23.1",
|
|
60
|
+
viem: "^2.30.5"
|
|
61
|
+
},
|
|
62
|
+
devDependencies: {
|
|
63
|
+
"@types/node": "^22.0.0",
|
|
64
|
+
tsup: "^8.5.0",
|
|
65
|
+
typescript: "^5.9.3"
|
|
66
|
+
},
|
|
67
|
+
minimumVersion: "0.5.6",
|
|
68
|
+
engines: {
|
|
69
|
+
node: ">=22"
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/run-state.ts
|
|
74
|
+
var VERSION = package_default.version;
|
|
75
|
+
function resolveStateDir() {
|
|
76
|
+
if (process.platform === "darwin") {
|
|
77
|
+
return path.join(os.homedir(), "Library", "Application Support", "bulletin-deploy");
|
|
78
|
+
}
|
|
79
|
+
if (process.platform === "win32") {
|
|
80
|
+
const base2 = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
81
|
+
return path.join(base2, "bulletin-deploy");
|
|
82
|
+
}
|
|
83
|
+
const base = process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.length > 0 ? process.env.XDG_STATE_HOME : path.join(os.homedir(), ".local", "state");
|
|
84
|
+
return path.join(base, "bulletin-deploy");
|
|
85
|
+
}
|
|
86
|
+
function stateFilePath() {
|
|
87
|
+
return path.join(resolveStateDir(), "last-run.json");
|
|
88
|
+
}
|
|
89
|
+
function loadRunState() {
|
|
90
|
+
try {
|
|
91
|
+
const raw = fs.readFileSync(stateFilePath(), "utf-8");
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
94
|
+
return parsed;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function writeRunState(patch) {
|
|
100
|
+
try {
|
|
101
|
+
const dir = resolveStateDir();
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
const file = stateFilePath();
|
|
104
|
+
let existing = {};
|
|
105
|
+
try {
|
|
106
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
107
|
+
const parsed = JSON.parse(raw);
|
|
108
|
+
if (parsed && typeof parsed === "object") existing = parsed;
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
const merged = { ...existing, ...patch };
|
|
112
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
113
|
+
fs.writeFileSync(tmp, JSON.stringify(merged), { encoding: "utf-8" });
|
|
114
|
+
fs.renameSync(tmp, file);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function isPidAlive(pid) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const code = err.code;
|
|
124
|
+
if (code === "EPERM") return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function shouldSkipStaleWarning(prev) {
|
|
129
|
+
if (prev.pid && isPidAlive(prev.pid)) return true;
|
|
130
|
+
if (prev.toolVersion !== VERSION) return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function probablyOomRssMb(override) {
|
|
134
|
+
if (typeof override === "number" && Number.isFinite(override)) return override;
|
|
135
|
+
const env = process.env.BULLETIN_DEPLOY_OOM_HINT_RSS_MB;
|
|
136
|
+
const parsed = env != null ? Number(env) : NaN;
|
|
137
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
138
|
+
return 1800;
|
|
139
|
+
}
|
|
140
|
+
function shouldShowOomHint(prev) {
|
|
141
|
+
if (prev.lastPeakRssMb == null) return false;
|
|
142
|
+
return prev.lastPeakRssMb >= probablyOomRssMb();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
package_default,
|
|
147
|
+
VERSION,
|
|
148
|
+
resolveStateDir,
|
|
149
|
+
stateFilePath,
|
|
150
|
+
loadRunState,
|
|
151
|
+
writeRunState,
|
|
152
|
+
shouldSkipStaleWarning,
|
|
153
|
+
probablyOomRssMb,
|
|
154
|
+
shouldShowOomHint
|
|
155
|
+
};
|