bulletin-deploy 0.6.8 → 0.6.9-rc.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/README.md CHANGED
@@ -12,6 +12,8 @@ bulletin-deploy ./dist my-app00.dot
12
12
 
13
13
  Your site is live at `https://my-app00.dot.li`
14
14
 
15
+ > **Stable vs release candidate.** `npm install -g bulletin-deploy` (and `@latest`) always resolves to the latest stable version — never an RC. Release candidates are published under the `rc` dist-tag and can be installed explicitly with `npm install -g bulletin-deploy@rc` (or a literal version pin like `@0.6.9-rc.0`). Only use RCs for testing.
16
+
15
17
  ## Prerequisites
16
18
 
17
19
  - **Node.js 22+**
@@ -134,7 +136,7 @@ If you see **"Requires Full Personhood verification"**, the deploy will fail ear
134
136
  | Variable | Default | Description |
135
137
  |---|---|---|
136
138
  | `BULLETIN_RPC` | `wss://paseo-bulletin-rpc.polkadot.io` | Bulletin chain WebSocket RPC |
137
- | `BULLETIN_DEPLOY_TELEMETRY` | `1` (enabled) | Set to `0` to disable Sentry telemetry |
139
+ | `BULLETIN_DEPLOY_TELEMETRY` | _(off for external users, on for internal)_ | `1` to opt in, `0` to force off see **Telemetry** below |
138
140
  | `BULLETIN_DEPLOY_UPDATE_CHECK` | `1` (enabled) | Set to `0` to disable version check on errors |
139
141
  | `DOTNS_STATUS` | `full` (testnet) / `none` (mainnet) | PoP level to self-grant before registration: `none`, `lite`, or `full` |
140
142
  | `IPFS_CID` | _(none)_ | Skip storage, use pre-existing CID |
@@ -221,7 +223,15 @@ Set `BULLETIN_DEPLOY_UPDATE_CHECK=0` to disable version checking.
221
223
 
222
224
  ## Telemetry
223
225
 
224
- Sentry telemetry is enabled by default for deploy observability. Set `BULLETIN_DEPLOY_TELEMETRY=0` to disable.
226
+ Sentry telemetry is **off by default for external users**. It's automatically on for deploys originating from inside Parity — either Parity CI, or local development trees whose git origin points at `paritytech/*`, `w3f/*`, or `polkadot-fellows/*`.
227
+
228
+ - `BULLETIN_DEPLOY_TELEMETRY=1` — explicit opt-in (useful if you want to help us debug an issue you're seeing).
229
+ - `BULLETIN_DEPLOY_TELEMETRY=0` — force off regardless of context.
230
+
231
+ Detection signals (OR'd together):
232
+ 1. `GITHUB_REPOSITORY` matches a known-internal org — Parity-owned CI workflow.
233
+ 2. `RUNNER_NAME` starts with `parity-` — Parity self-hosted runner.
234
+ 3. `git remote get-url origin` points at a known-internal org — internal local dev.
225
235
 
226
236
  What's tracked:
227
237
  - Deploy duration and success/failure
@@ -3,7 +3,7 @@
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
5
  import { VERSION } from "../dist/telemetry.js";
6
- import { handleFailedDeploy } from "../dist/version-check.js";
6
+ import { handleFailedDeploy, preReleaseWarning } from "../dist/version-check.js";
7
7
  import { setDeployContext } from "../dist/bug-report.js";
8
8
  import * as fs from "fs";
9
9
 
@@ -48,6 +48,9 @@ Options:
48
48
  process.exit(0);
49
49
  }
50
50
 
51
+ const rcWarning = preReleaseWarning(VERSION);
52
+ if (rcWarning) console.error(rcWarning);
53
+
51
54
  try {
52
55
  if (flags.bootstrap) {
53
56
  const rpc = flags.rpc ?? process.env.BULLETIN_RPC ?? DEFAULT_BULLETIN_RPC;
@@ -9,6 +9,10 @@ interface DeployContext {
9
9
  sentryTraceId?: string;
10
10
  }
11
11
  declare function setDeployContext(ctx: Partial<DeployContext>): void;
12
+ declare function buildReportBody(error: Error): string;
13
+ declare function buildTitle(error: Error): string;
14
+ declare function buildLabels(error: Error): string[];
15
+ declare function createGhIssue(title: string, body: string, labels: string[]): void;
12
16
  declare function offerBugReport(error: Error): Promise<void>;
13
17
 
14
- export { offerBugReport, setDeployContext };
18
+ export { buildLabels, buildReportBody, buildTitle, createGhIssue, offerBugReport, setDeployContext };
@@ -2,10 +2,10 @@ import {
2
2
  classifyErrorArea,
3
3
  isInteractive,
4
4
  promptYesNo
5
- } from "./chunk-SYJ2L6RU.js";
5
+ } from "./chunk-X7WJJEXW.js";
6
6
  import {
7
7
  VERSION
8
- } from "./chunk-SLARK556.js";
8
+ } from "./chunk-3W2COGJ6.js";
9
9
  import "./chunk-QGM4M3NI.js";
10
10
 
11
11
  // src/bug-report.ts
@@ -63,6 +63,20 @@ function buildLabels(error) {
63
63
  if (area) labels.push(area);
64
64
  return labels;
65
65
  }
66
+ function createGhIssue(title, body, labels) {
67
+ const args = [
68
+ "issue",
69
+ "create",
70
+ "--repo",
71
+ "paritytech/bulletin-deploy",
72
+ "--title",
73
+ title,
74
+ ...labels.flatMap((l) => ["--label", l]),
75
+ "--body-file",
76
+ "-"
77
+ ];
78
+ execFileSync("gh", args, { input: body, stdio: ["pipe", "inherit", "inherit"] });
79
+ }
66
80
  async function offerBugReport(error) {
67
81
  if (!isInteractive()) return;
68
82
  const yes = await promptYesNo("\n This looks like a bug. Open an issue with debug info? [Y/n] ");
@@ -72,22 +86,17 @@ async function offerBugReport(error) {
72
86
  const labels = buildLabels(error);
73
87
  if (hasGhCli()) {
74
88
  try {
75
- const args = [
76
- "issue",
77
- "create",
78
- "--repo",
79
- "paritytech/bulletin-deploy",
80
- "--title",
81
- title,
82
- ...labels.flatMap((l) => ["--label", l]),
83
- "--body-file",
84
- "-"
85
- ];
86
- execFileSync("gh", args, { input: body, stdio: ["pipe", "inherit", "inherit"] });
89
+ createGhIssue(title, body, labels);
87
90
  console.error(" Issue created.");
88
91
  } catch {
89
- console.error(" Failed to create issue. Debug info below:\n");
90
- printFallback(title, body, labels);
92
+ try {
93
+ console.error(" Retrying without labels...");
94
+ createGhIssue(title, body, []);
95
+ console.error(" Issue created (without labels).");
96
+ } catch {
97
+ console.error(" Failed to create issue. Debug info below:\n");
98
+ printFallback(title, body, labels);
99
+ }
91
100
  }
92
101
  } else {
93
102
  console.error("\n gh CLI not found. Debug info below \u2014 paste into a new issue:\n");
@@ -103,6 +112,10 @@ function printFallback(title, body, labels) {
103
112
  console.error(body);
104
113
  }
105
114
  export {
115
+ buildLabels,
116
+ buildReportBody,
117
+ buildTitle,
118
+ createGhIssue,
106
119
  offerBugReport,
107
120
  setDeployContext
108
121
  };
@@ -1,12 +1,13 @@
1
1
  // src/telemetry.ts
2
2
  import { execSync } from "child_process";
3
+ import { createHash } from "crypto";
3
4
  import * as fs from "fs";
4
5
  import * as path from "path";
5
6
 
6
7
  // package.json
7
8
  var package_default = {
8
9
  name: "bulletin-deploy",
9
- version: "0.6.8",
10
+ version: "0.6.9-rc.0",
10
11
  private: false,
11
12
  repository: {
12
13
  type: "git",
@@ -70,7 +71,75 @@ var package_default = {
70
71
  // src/telemetry.ts
71
72
  var VERSION = package_default.version;
72
73
  var DEFAULT_DSN = "https://e021c025d79c4c3ade2862a11f13c40b@o4511059872841728.ingest.de.sentry.io/4511093597405264";
73
- var DISABLED = process.env.BULLETIN_DEPLOY_TELEMETRY === "0";
74
+ var INTERNAL_ORG_RE = /^(paritytech|w3f|polkadot-fellows)\//i;
75
+ function extractRepoSlug(url) {
76
+ return url.replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
77
+ }
78
+ function tryGitRemote() {
79
+ try {
80
+ return extractRepoSlug(execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim());
81
+ } catch {
82
+ return void 0;
83
+ }
84
+ }
85
+ function isInternalContextFromSignals(signals) {
86
+ if (INTERNAL_ORG_RE.test(signals.githubRepository ?? "")) return true;
87
+ if (signals.runnerName?.startsWith("parity-")) return true;
88
+ if (signals.gitRemote && INTERNAL_ORG_RE.test(signals.gitRemote)) return true;
89
+ return false;
90
+ }
91
+ function isInternalContext() {
92
+ return isInternalContextFromSignals({
93
+ githubRepository: process.env.GITHUB_REPOSITORY,
94
+ runnerName: process.env.RUNNER_NAME,
95
+ gitRemote: tryGitRemote()
96
+ });
97
+ }
98
+ var OPT_OUT = process.env.BULLETIN_DEPLOY_TELEMETRY === "0";
99
+ var OPT_IN = process.env.BULLETIN_DEPLOY_TELEMETRY === "1";
100
+ var DISABLED = OPT_OUT || !OPT_IN && !isInternalContext();
101
+ var CONVENTIONAL_BRANCH_PREFIXES = /* @__PURE__ */ new Set([
102
+ "fix",
103
+ "feat",
104
+ "chore",
105
+ "docs",
106
+ "test",
107
+ "refactor",
108
+ "release",
109
+ "bump",
110
+ "perf",
111
+ "style",
112
+ "ci",
113
+ "build",
114
+ "revert"
115
+ ]);
116
+ function scrubPaths(msg) {
117
+ if (!msg) return msg;
118
+ return msg.replace(/\/Users\/[^\/\s"'`]+/g, "/Users/<redacted>").replace(/\/home\/[^\/\s"'`]+/g, "/home/<redacted>");
119
+ }
120
+ function truncateAddress(ss58) {
121
+ if (!ss58) return ss58;
122
+ return ss58.length > 8 ? `${ss58.slice(0, 8)}\u2026` : ss58;
123
+ }
124
+ function sanitizeBranch(name) {
125
+ if (!name) return name;
126
+ const slash = name.indexOf("/");
127
+ if (slash === -1) return name;
128
+ const prefix = name.slice(0, slash).toLowerCase();
129
+ if (CONVENTIONAL_BRANCH_PREFIXES.has(prefix)) return name;
130
+ return name.slice(slash + 1);
131
+ }
132
+ function sanitizeRepo(slug) {
133
+ if (!slug) return slug;
134
+ if (INTERNAL_ORG_RE.test(slug)) return slug;
135
+ const slash = slug.indexOf("/");
136
+ if (slash === -1) {
137
+ return `ext/${createHash("sha256").update(slug).digest("hex").slice(0, 12)}`;
138
+ }
139
+ const org = slug.slice(0, slash);
140
+ const repo = slug.slice(slash + 1);
141
+ return `${org}/${createHash("sha256").update(repo).digest("hex").slice(0, 12)}`;
142
+ }
74
143
  var Sentry = null;
75
144
  if (!DISABLED) {
76
145
  try {
@@ -84,19 +153,35 @@ function initTelemetry() {
84
153
  dsn: process.env.SENTRY_DSN || DEFAULT_DSN,
85
154
  release: `${package_default.name}@${VERSION}`,
86
155
  tracesSampleRate: 1,
87
- environment: process.env.CI ? "ci" : "local"
156
+ environment: process.env.CI ? "ci" : "local",
157
+ // Sentry Node SDK captures os.hostname() by default, which leaks personal
158
+ // machine names (e.g. "Mac.fritz.box"). Override to something anonymous.
159
+ serverName: process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local",
160
+ beforeSend(event) {
161
+ if (event.server_name) event.server_name = process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local";
162
+ if (event.message) event.message = scrubPaths(event.message);
163
+ for (const ex of event.exception?.values ?? []) {
164
+ if (ex.value) ex.value = scrubPaths(ex.value);
165
+ }
166
+ for (const bc of event.breadcrumbs ?? []) {
167
+ if (bc.message) bc.message = scrubPaths(bc.message);
168
+ }
169
+ return event;
170
+ },
171
+ beforeSendTransaction(event) {
172
+ const spans = event.spans ?? [];
173
+ for (const span of spans) {
174
+ const attrs = span.data;
175
+ if (!attrs) continue;
176
+ for (const k of Object.keys(attrs)) {
177
+ const v = attrs[k];
178
+ if (typeof v === "string") attrs[k] = scrubPaths(v);
179
+ }
180
+ }
181
+ return event;
182
+ }
88
183
  });
89
184
  }
90
- function extractRepoSlug(url) {
91
- return url.replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
92
- }
93
- function tryGitRemote() {
94
- try {
95
- return extractRepoSlug(execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim());
96
- } catch {
97
- return void 0;
98
- }
99
- }
100
185
  function tryPackageJsonRepo() {
101
186
  try {
102
187
  const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf-8"));
@@ -128,17 +213,20 @@ function resolveRunnerType() {
128
213
  }
129
214
  function getDeployAttributes(domain) {
130
215
  return {
131
- "deploy.repo": resolveRepo(domain),
132
- "deploy.branch": process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || tryGitBranch(),
216
+ "deploy.repo": sanitizeRepo(resolveRepo(domain)),
217
+ "deploy.branch": sanitizeBranch(process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || tryGitBranch()),
133
218
  "deploy.source": process.env.CI ? "ci" : "local",
134
219
  "deploy.pr": process.env.GITHUB_PR_NUMBER || void 0,
135
220
  "deploy.tool_version": VERSION,
136
221
  "deploy.runner": resolveRunner(),
137
- "deploy.runner_type": resolveRunnerType()
222
+ "deploy.runner_type": resolveRunnerType(),
223
+ // Seed "false" so successful spans form the %SAD denominator; the catch block and
224
+ // captureWarning flip it to "true" on friction.
225
+ "deploy.sad": "false"
138
226
  };
139
227
  }
140
228
  function isExpectedError(msg) {
141
- return /personhood|owned by|owner mismatch|reserved for original|invalid domain label|not authorized for bulletin|insufficient balance/i.test(msg);
229
+ return /personhood|owned by|owner mismatch|reserved for original|invalid domain label|not authorized for bulletin|insufficient balance|quota exhausted|insufficient .* authorization/i.test(msg);
142
230
  }
143
231
  async function withSpan(op, description, attributes, fn) {
144
232
  if (!Sentry) return fn();
@@ -171,7 +259,7 @@ async function withDeploySpan(domain, fn) {
171
259
  const msg = error.message;
172
260
  span.setAttribute("deploy.status", "error");
173
261
  span.setAttribute("deploy.error", msg.slice(0, 200));
174
- span.setAttribute("deploy.sad", true);
262
+ span.setAttribute("deploy.sad", "true");
175
263
  const isExpected = isExpectedError(msg);
176
264
  if (!isExpected) {
177
265
  span.setStatus({ code: 2, message: "internal_error" });
@@ -194,7 +282,7 @@ function captureWarning(message, context) {
194
282
  Sentry.addBreadcrumb({ level: "warning", message, data: context });
195
283
  Sentry.captureMessage(message, { level: "warning", extra: context });
196
284
  const root = Sentry.getRootSpan(Sentry.getActiveSpan());
197
- if (root) root.setAttribute("deploy.sad", true);
285
+ if (root) root.setAttribute("deploy.sad", "true");
198
286
  } catch {
199
287
  }
200
288
  }
@@ -205,10 +293,17 @@ async function flush() {
205
293
 
206
294
  export {
207
295
  VERSION,
296
+ isInternalContextFromSignals,
297
+ isInternalContext,
298
+ scrubPaths,
299
+ truncateAddress,
300
+ sanitizeBranch,
301
+ sanitizeRepo,
208
302
  initTelemetry,
209
303
  resolveRepo,
210
304
  resolveRunner,
211
305
  resolveRunnerType,
306
+ getDeployAttributes,
212
307
  isExpectedError,
213
308
  withSpan,
214
309
  withDeploySpan,
@@ -55,13 +55,13 @@ async function fetchPoolAuthorizations(api, accounts) {
55
55
  );
56
56
  return results;
57
57
  }
58
- async function ensureAuthorized(api, address, bulletinRpc, label) {
58
+ async function ensureAuthorized(api, address, bulletinRpc, label, minimum = { txs: TOPUP_THRESHOLD_TXS, bytes: TOPUP_THRESHOLD_BYTES }) {
59
59
  const auth = await api.query.TransactionStorage.Authorizations.getValue(
60
60
  Enum("Account", address)
61
61
  );
62
62
  const txsRemaining = auth ? BigInt(auth.extent.transactions) : 0n;
63
63
  const bytesRemaining = auth ? auth.extent.bytes : 0n;
64
- if (txsRemaining >= TOPUP_THRESHOLD_TXS && bytesRemaining >= TOPUP_THRESHOLD_BYTES) {
64
+ if (txsRemaining >= minimum.txs && bytesRemaining >= minimum.bytes) {
65
65
  return;
66
66
  }
67
67
  console.log(` Auto-authorizing ${label ?? "account"} (${address.slice(0, 8)}...)...`);
@@ -4,7 +4,7 @@ import {
4
4
  TX_TIMEOUT_MS,
5
5
  fetchNonce,
6
6
  validateDomainLabel
7
- } from "./chunk-KHLH5WQY.js";
7
+ } from "./chunk-WOHUOD3E.js";
8
8
  import {
9
9
  merkleizeJS
10
10
  } from "./chunk-GZ5UUECB.js";
@@ -13,7 +13,7 @@ import {
13
13
  ensureAuthorized,
14
14
  fetchPoolAuthorizations,
15
15
  selectAccount
16
- } from "./chunk-LGPTJYA3.js";
16
+ } from "./chunk-CUFLI45S.js";
17
17
  import {
18
18
  VERSION,
19
19
  captureWarning,
@@ -21,9 +21,10 @@ import {
21
21
  resolveRunner,
22
22
  resolveRunnerType,
23
23
  setDeployAttribute,
24
+ truncateAddress,
24
25
  withDeploySpan,
25
26
  withSpan
26
- } from "./chunk-SLARK556.js";
27
+ } from "./chunk-3W2COGJ6.js";
27
28
 
28
29
  // src/deploy.ts
29
30
  import { Buffer } from "buffer";
@@ -306,6 +307,12 @@ var NonRetryableError = class extends Error {
306
307
  }
307
308
  };
308
309
  var EXIT_CODE_NO_RETRY = 78;
310
+ function friendlyChainError(msg) {
311
+ if (/"type":\s*"Invalid"[\s\S]*?"type":\s*"Payment"/i.test(msg)) {
312
+ return "authorization quota exhausted (Bulletin signed extension rejected the tx \u2014 signer is out of authorized txs or bytes)";
313
+ }
314
+ return msg;
315
+ }
309
316
  var DEFAULT_BULLETIN_RPC = "wss://paseo-bulletin-rpc.polkadot.io";
310
317
  var DEFAULT_POOL_SIZE = 10;
311
318
  var BULLETIN_RPC = DEFAULT_BULLETIN_RPC;
@@ -414,7 +421,7 @@ async function getProvider() {
414
421
  }
415
422
  console.log(` Using pool account ${selected.index}: ${selected.address}`);
416
423
  setDeployAttribute("deploy.signer.mode", "pool");
417
- setDeployAttribute("deploy.pool.account", selected.address);
424
+ setDeployAttribute("deploy.pool.account", truncateAddress(selected.address));
418
425
  setDeployAttribute("deploy.pool.index", selected.index);
419
426
  return { client, unsafeApi, signer: selected.signer, ss58: selected.address };
420
427
  } catch (e) {
@@ -439,7 +446,7 @@ async function getDirectProvider(mnemonic) {
439
446
  }
440
447
  console.log(` Authorization: ${txsRemaining} txs, ${Number(bytesRemaining) / 1e6}MB remaining`);
441
448
  setDeployAttribute("deploy.signer.mode", "direct");
442
- setDeployAttribute("deploy.signer.address", ss58);
449
+ setDeployAttribute("deploy.signer.address", truncateAddress(ss58));
443
450
  return { client, unsafeApi, signer, ss58 };
444
451
  }
445
452
  var MAX_BEST_CHAIN_DROPS = 5;
@@ -503,7 +510,7 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
503
510
  },
504
511
  error: (e) => {
505
512
  const msg = e?.message || String(e).slice(0, 500);
506
- settle(reject)(new Error(`${label} subscription error: ${msg}`));
513
+ settle(reject)(new Error(`${label} subscription error: ${friendlyChainError(msg)}`));
507
514
  }
508
515
  });
509
516
  });
@@ -574,20 +581,21 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
574
581
  ownsClient = true;
575
582
  }
576
583
  const requiredTxs = BigInt(chunks.length + 1);
584
+ const requiredBytes = BigInt(totalBytes);
577
585
  const auth = await unsafeApi.query.TransactionStorage.Authorizations.getValue(
578
586
  Enum("Account", ss58)
579
587
  );
580
588
  const txsRemaining = auth ? BigInt(auth.extent.transactions) : 0n;
581
589
  const bytesRemaining = auth ? auth.extent.bytes : 0n;
582
- if (txsRemaining < requiredTxs || bytesRemaining < BigInt(totalBytes)) {
590
+ if (txsRemaining < requiredTxs || bytesRemaining < requiredBytes) {
583
591
  console.log(`
584
592
  Account has insufficient authorization for this upload (need ${requiredTxs} txs / ${(totalBytes / 1e6).toFixed(1)}MB, have ${txsRemaining} txs / ${Number(bytesRemaining) / 1e6}MB)`);
585
593
  console.log(` Attempting to re-authorize with Alice...`);
586
594
  try {
587
- await ensureAuthorized(unsafeApi, ss58, BULLETIN_RPC);
595
+ await ensureAuthorized(unsafeApi, ss58, BULLETIN_RPC, void 0, { txs: requiredTxs, bytes: requiredBytes });
588
596
  console.log(` Re-authorization successful`);
589
597
  } catch (e) {
590
- throw new Error(`Re-authorization failed: ${e.message}`);
598
+ throw new NonRetryableError(`Account ${ss58} has insufficient Bulletin authorization quota and auto-authorization via Alice failed (${e.message}). Authorize the account on-chain, or run 'bulletin-deploy --bootstrap --mnemonic "..."' in a dev network.`);
591
599
  }
592
600
  }
593
601
  let reconnectionsUsed = 0;
@@ -689,7 +697,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
689
697
  }
690
698
  }
691
699
  }
692
- setDeployAttribute("deploy.pool.account", ss58);
700
+ setDeployAttribute("deploy.pool.account", truncateAddress(ss58));
693
701
  console.log(`
694
702
  All ${chunks.length} chunks included in block`);
695
703
  console.log(` Verifying chunk integrity...`);
@@ -786,15 +794,16 @@ async function merkleize(directoryPath, outputCarPath) {
786
794
  async function storeDirectory(directoryPath, provider = {}, password, jsMerkle) {
787
795
  let carContent;
788
796
  let ipfsCid;
797
+ const dirBasename = path.basename(directoryPath);
789
798
  if (jsMerkle) {
790
- const result = await withSpan("deploy.merkleize", "1a. merkleize (js)", { "deploy.directory": directoryPath }, async () => {
799
+ const result = await withSpan("deploy.merkleize", "1a. merkleize (js)", { "deploy.directory": dirBasename }, async () => {
791
800
  return merkleizeJS(directoryPath);
792
801
  });
793
802
  carContent = result.carBytes;
794
803
  ipfsCid = result.cid;
795
804
  } else {
796
805
  const carPath = path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.car`);
797
- const { cid } = await withSpan("deploy.merkleize", "1a. merkleize", { "deploy.directory": directoryPath }, async () => {
806
+ const { cid } = await withSpan("deploy.merkleize", "1a. merkleize", { "deploy.directory": dirBasename }, async () => {
798
807
  return merkleize(directoryPath, carPath);
799
808
  });
800
809
  ipfsCid = cid;
@@ -1006,6 +1015,7 @@ Or deploy with the original account, or use a different domain name.`);
1006
1015
  export {
1007
1016
  NonRetryableError,
1008
1017
  EXIT_CODE_NO_RETRY,
1018
+ friendlyChainError,
1009
1019
  DEFAULT_BULLETIN_RPC,
1010
1020
  DEFAULT_POOL_SIZE,
1011
1021
  isConnectionError,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  captureWarning,
3
3
  withSpan
4
- } from "./chunk-SLARK556.js";
4
+ } from "./chunk-3W2COGJ6.js";
5
5
 
6
6
  // src/dotns.ts
7
7
  import crypto from "crypto";
@@ -414,7 +414,7 @@ var DotNS = class {
414
414
  return this;
415
415
  } catch (e) {
416
416
  lastError = e;
417
- captureWarning("DotNS RPC endpoint failed, trying next", { endpoint: rpc, error: e.message, remainingEndpoints: endpoints.length - endpoints.indexOf(rpc) - 1 });
417
+ captureWarning("DotNS RPC endpoint failed, trying next", { endpoint: rpc, error: e.message?.slice(0, 200), remainingEndpoints: endpoints.length - endpoints.indexOf(rpc) - 1 });
418
418
  console.log(` Failed to connect to ${rpc}: ${e.message}`);
419
419
  if (this.client) {
420
420
  try {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-SLARK556.js";
3
+ } from "./chunk-3W2COGJ6.js";
4
4
 
5
5
  // src/version-check.ts
6
6
  import { execSync, execFileSync } from "child_process";
@@ -17,6 +17,19 @@ function compareSemver(a, b) {
17
17
  }
18
18
  return 0;
19
19
  }
20
+ function isPreReleaseVersion(version) {
21
+ return version.includes("-");
22
+ }
23
+ function preReleaseWarning(version) {
24
+ if (!isPreReleaseVersion(version)) return null;
25
+ return [
26
+ "",
27
+ `\u26A0\uFE0F Running bulletin-deploy ${version} (release candidate).`,
28
+ " This version is not recommended for production deploys.",
29
+ " For stable: npm install -g bulletin-deploy@latest",
30
+ ""
31
+ ].join("\n");
32
+ }
20
33
  async function fetchJson(url) {
21
34
  try {
22
35
  const controller = new AbortController();
@@ -55,12 +68,38 @@ function isInternalUser() {
55
68
  function isInteractive() {
56
69
  return Boolean(process.stdin.isTTY && !process.env.CI);
57
70
  }
58
- async function promptYesNo(question) {
59
- const rl = createInterface({ input: process.stdin, output: process.stderr });
71
+ async function promptYesNo(question, input) {
72
+ const stdin = input ?? process.stdin;
73
+ if (!input && !process.stdin.isTTY) return false;
74
+ const rl = createInterface({ input: stdin, output: process.stderr });
75
+ let answered = false;
60
76
  return new Promise((resolve) => {
77
+ rl.on("close", () => {
78
+ if (!answered) {
79
+ answered = true;
80
+ resolve(false);
81
+ }
82
+ });
61
83
  rl.question(question, (answer) => {
62
- rl.close();
63
- resolve(!answer || answer.toLowerCase().startsWith("y"));
84
+ if (answered) return;
85
+ const a = answer.trim().toLowerCase();
86
+ if (a === "" || a === "y" || a === "yes") {
87
+ answered = true;
88
+ rl.close();
89
+ resolve(true);
90
+ } else if (a === "n" || a === "no") {
91
+ answered = true;
92
+ rl.close();
93
+ resolve(false);
94
+ } else {
95
+ rl.question(" Please answer Y or N: ", (retry) => {
96
+ if (answered) return;
97
+ const r = retry.trim().toLowerCase();
98
+ answered = true;
99
+ rl.close();
100
+ resolve(r === "" || r === "y" || r === "yes");
101
+ });
102
+ }
64
103
  });
65
104
  });
66
105
  }
@@ -137,6 +176,8 @@ async function handleFailedDeploy(error) {
137
176
 
138
177
  export {
139
178
  compareSemver,
179
+ isPreReleaseVersion,
180
+ preReleaseWarning,
140
181
  isInternalUser,
141
182
  isInteractive,
142
183
  promptYesNo,
package/dist/deploy.d.ts CHANGED
@@ -12,6 +12,7 @@ declare class NonRetryableError extends Error {
12
12
  constructor(message: string);
13
13
  }
14
14
  declare const EXIT_CODE_NO_RETRY = 78;
15
+ declare function friendlyChainError(msg: string): string;
15
16
  interface ProviderResult {
16
17
  client: any;
17
18
  unsafeApi: any;
@@ -70,4 +71,4 @@ interface DeployOptions {
70
71
  }
71
72
  declare function deploy(content: DeployContent, domainName?: string | null, options?: DeployOptions): Promise<DeployResult>;
72
73
 
73
- export { DEFAULT_BULLETIN_RPC, DEFAULT_POOL_SIZE, type DeployContent, type DeployOptions, type DeployResult, ENCRYPT_KEY_LEN, ENCRYPT_MAGIC, ENCRYPT_NONCE_LEN, ENCRYPT_PBKDF2_ITERATIONS, ENCRYPT_SALT_LEN, ENCRYPT_TAG_LEN, EXIT_CODE_NO_RETRY, NonRetryableError, chunk, createCID, deploy, deriveRootSigner, encodeContenthash, encryptContent, hasIPFS, isConnectionError, merkleize, storeChunkedContent, storeDirectory, storeFile };
74
+ export { DEFAULT_BULLETIN_RPC, DEFAULT_POOL_SIZE, type DeployContent, type DeployOptions, type DeployResult, ENCRYPT_KEY_LEN, ENCRYPT_MAGIC, ENCRYPT_NONCE_LEN, ENCRYPT_PBKDF2_ITERATIONS, ENCRYPT_SALT_LEN, ENCRYPT_TAG_LEN, EXIT_CODE_NO_RETRY, NonRetryableError, chunk, createCID, deploy, deriveRootSigner, encodeContenthash, encryptContent, friendlyChainError, hasIPFS, isConnectionError, merkleize, storeChunkedContent, storeDirectory, storeFile };
package/dist/deploy.js CHANGED
@@ -15,17 +15,18 @@ import {
15
15
  deriveRootSigner,
16
16
  encodeContenthash,
17
17
  encryptContent,
18
+ friendlyChainError,
18
19
  hasIPFS,
19
20
  isConnectionError,
20
21
  merkleize,
21
22
  storeChunkedContent,
22
23
  storeDirectory,
23
24
  storeFile
24
- } from "./chunk-LVCOE5CC.js";
25
- import "./chunk-KHLH5WQY.js";
25
+ } from "./chunk-IHFIWF2A.js";
26
+ import "./chunk-WOHUOD3E.js";
26
27
  import "./chunk-GZ5UUECB.js";
27
- import "./chunk-LGPTJYA3.js";
28
- import "./chunk-SLARK556.js";
28
+ import "./chunk-CUFLI45S.js";
29
+ import "./chunk-3W2COGJ6.js";
29
30
  import "./chunk-QGM4M3NI.js";
30
31
  export {
31
32
  DEFAULT_BULLETIN_RPC,
@@ -44,6 +45,7 @@ export {
44
45
  deriveRootSigner,
45
46
  encodeContenthash,
46
47
  encryptContent,
48
+ friendlyChainError,
47
49
  hasIPFS,
48
50
  isConnectionError,
49
51
  merkleize,
package/dist/dotns.js CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  sanitizeDomainLabel,
20
20
  stripTrailingDigits,
21
21
  validateDomainLabel
22
- } from "./chunk-KHLH5WQY.js";
23
- import "./chunk-SLARK556.js";
22
+ } from "./chunk-WOHUOD3E.js";
23
+ import "./chunk-3W2COGJ6.js";
24
24
  import "./chunk-QGM4M3NI.js";
25
25
  export {
26
26
  CONNECTION_TIMEOUT_MS,
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-LVCOE5CC.js";
3
+ } from "./chunk-IHFIWF2A.js";
4
4
  import {
5
5
  DotNS
6
- } from "./chunk-KHLH5WQY.js";
6
+ } from "./chunk-WOHUOD3E.js";
7
7
  import {
8
8
  merkleizeJS
9
9
  } from "./chunk-GZ5UUECB.js";
@@ -13,8 +13,8 @@ import {
13
13
  ensureAuthorized,
14
14
  fetchPoolAuthorizations,
15
15
  selectAccount
16
- } from "./chunk-LGPTJYA3.js";
17
- import "./chunk-SLARK556.js";
16
+ } from "./chunk-CUFLI45S.js";
17
+ import "./chunk-3W2COGJ6.js";
18
18
  import "./chunk-QGM4M3NI.js";
19
19
  export {
20
20
  DotNS,
package/dist/pool.d.ts CHANGED
@@ -14,7 +14,11 @@ interface PoolAuthorization extends PoolAccount {
14
14
  declare function derivePoolAccounts(poolSize?: number, mnemonic?: string): PoolAccount[];
15
15
  declare function selectAccount(authorizations: PoolAuthorization[]): PoolAuthorization | null;
16
16
  declare function fetchPoolAuthorizations(api: any, accounts: PoolAccount[]): Promise<PoolAuthorization[]>;
17
- declare function ensureAuthorized(api: any, address: string, bulletinRpc: string, label?: string): Promise<void>;
17
+ interface AuthorizationNeeds {
18
+ txs: bigint;
19
+ bytes: bigint;
20
+ }
21
+ declare function ensureAuthorized(api: any, address: string, bulletinRpc: string, label?: string, minimum?: AuthorizationNeeds): Promise<void>;
18
22
  declare function bootstrapPool(bulletinRpc: string, poolSize?: number, mnemonic?: string): Promise<void>;
19
23
 
20
- export { type PoolAccount, type PoolAuthorization, bootstrapPool, derivePoolAccounts, ensureAuthorized, fetchPoolAuthorizations, selectAccount };
24
+ export { type AuthorizationNeeds, type PoolAccount, type PoolAuthorization, bootstrapPool, derivePoolAccounts, ensureAuthorized, fetchPoolAuthorizations, selectAccount };
package/dist/pool.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  ensureAuthorized,
5
5
  fetchPoolAuthorizations,
6
6
  selectAccount
7
- } from "./chunk-LGPTJYA3.js";
7
+ } from "./chunk-CUFLI45S.js";
8
8
  import "./chunk-QGM4M3NI.js";
9
9
  export {
10
10
  bootstrapPool,
@@ -1,8 +1,20 @@
1
1
  declare const VERSION: string;
2
+ interface InternalContextSignals {
3
+ githubRepository?: string;
4
+ runnerName?: string;
5
+ gitRemote?: string;
6
+ }
7
+ declare function isInternalContextFromSignals(signals: InternalContextSignals): boolean;
8
+ declare function isInternalContext(): boolean;
9
+ declare function scrubPaths(msg: string): string;
10
+ declare function truncateAddress(ss58: string | undefined): string | undefined;
11
+ declare function sanitizeBranch(name: string | undefined): string | undefined;
12
+ declare function sanitizeRepo(slug: string | undefined): string | undefined;
2
13
  declare function initTelemetry(): void;
3
14
  declare function resolveRepo(domain: string): string;
4
15
  declare function resolveRunner(): string;
5
16
  declare function resolveRunnerType(): string;
17
+ declare function getDeployAttributes(domain: string): Record<string, string | number | boolean | undefined>;
6
18
  declare function isExpectedError(msg: string): boolean;
7
19
  declare function withSpan<T>(op: string, description: string, attributes: Record<string, string | number | boolean | undefined>, fn: () => T | Promise<T>): Promise<T>;
8
20
  declare function withDeploySpan<T>(domain: string, fn: () => T | Promise<T>): Promise<T>;
@@ -10,4 +22,4 @@ declare function setDeployAttribute(key: string, value: string | number | boolea
10
22
  declare function captureWarning(message: string, context?: Record<string, unknown>): void;
11
23
  declare function flush(): Promise<void>;
12
24
 
13
- export { VERSION, captureWarning, flush, initTelemetry, isExpectedError, resolveRepo, resolveRunner, resolveRunnerType, setDeployAttribute, withDeploySpan, withSpan };
25
+ export { type InternalContextSignals, VERSION, captureWarning, flush, getDeployAttributes, initTelemetry, isExpectedError, isInternalContext, isInternalContextFromSignals, resolveRepo, resolveRunner, resolveRunnerType, sanitizeBranch, sanitizeRepo, scrubPaths, setDeployAttribute, truncateAddress, withDeploySpan, withSpan };
package/dist/telemetry.js CHANGED
@@ -2,26 +2,40 @@ import {
2
2
  VERSION,
3
3
  captureWarning,
4
4
  flush,
5
+ getDeployAttributes,
5
6
  initTelemetry,
6
7
  isExpectedError,
8
+ isInternalContext,
9
+ isInternalContextFromSignals,
7
10
  resolveRepo,
8
11
  resolveRunner,
9
12
  resolveRunnerType,
13
+ sanitizeBranch,
14
+ sanitizeRepo,
15
+ scrubPaths,
10
16
  setDeployAttribute,
17
+ truncateAddress,
11
18
  withDeploySpan,
12
19
  withSpan
13
- } from "./chunk-SLARK556.js";
20
+ } from "./chunk-3W2COGJ6.js";
14
21
  import "./chunk-QGM4M3NI.js";
15
22
  export {
16
23
  VERSION,
17
24
  captureWarning,
18
25
  flush,
26
+ getDeployAttributes,
19
27
  initTelemetry,
20
28
  isExpectedError,
29
+ isInternalContext,
30
+ isInternalContextFromSignals,
21
31
  resolveRepo,
22
32
  resolveRunner,
23
33
  resolveRunnerType,
34
+ sanitizeBranch,
35
+ sanitizeRepo,
36
+ scrubPaths,
24
37
  setDeployAttribute,
38
+ truncateAddress,
25
39
  withDeploySpan,
26
40
  withSpan
27
41
  };
@@ -1,3 +1,5 @@
1
+ import { Readable } from 'node:stream';
2
+
1
3
  interface VersionInfo {
2
4
  latest: string;
3
5
  minimumFromRegistry: string | null;
@@ -5,9 +7,11 @@ interface VersionInfo {
5
7
  killSwitchMessage: string | null;
6
8
  }
7
9
  declare function compareSemver(a: string, b: string): number;
10
+ declare function isPreReleaseVersion(version: string): boolean;
11
+ declare function preReleaseWarning(version: string): string | null;
8
12
  declare function isInternalUser(): boolean;
9
13
  declare function isInteractive(): boolean;
10
- declare function promptYesNo(question: string): Promise<boolean>;
14
+ declare function promptYesNo(question: string, input?: Readable): Promise<boolean>;
11
15
  declare function classifyErrorArea(msg: string): string | null;
12
16
  type VersionVerdict = {
13
17
  action: "forced_update";
@@ -28,4 +32,4 @@ type VersionVerdict = {
28
32
  declare function assessVersion(currentVersion: string, info: VersionInfo, internal: boolean): VersionVerdict;
29
33
  declare function handleFailedDeploy(error: Error): Promise<void>;
30
34
 
31
- export { type VersionVerdict, assessVersion, classifyErrorArea, compareSemver, handleFailedDeploy, isInteractive, isInternalUser, promptYesNo };
35
+ export { type VersionVerdict, assessVersion, classifyErrorArea, compareSemver, handleFailedDeploy, isInteractive, isInternalUser, isPreReleaseVersion, preReleaseWarning, promptYesNo };
@@ -5,9 +5,11 @@ import {
5
5
  handleFailedDeploy,
6
6
  isInteractive,
7
7
  isInternalUser,
8
+ isPreReleaseVersion,
9
+ preReleaseWarning,
8
10
  promptYesNo
9
- } from "./chunk-SYJ2L6RU.js";
10
- import "./chunk-SLARK556.js";
11
+ } from "./chunk-X7WJJEXW.js";
12
+ import "./chunk-3W2COGJ6.js";
11
13
  import "./chunk-QGM4M3NI.js";
12
14
  export {
13
15
  assessVersion,
@@ -16,5 +18,7 @@ export {
16
18
  handleFailedDeploy,
17
19
  isInteractive,
18
20
  isInternalUser,
21
+ isPreReleaseVersion,
22
+ preReleaseWarning,
19
23
  promptYesNo
20
24
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulletin-deploy",
3
- "version": "0.6.8",
3
+ "version": "0.6.9-rc.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",