bulletin-deploy 0.21.0 → 0.22.0-rc.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.
Files changed (45) hide show
  1. package/bin/bulletin-deploy +12 -8
  2. package/dist/auth-config.js +3 -3
  3. package/dist/bug-report.js +4 -4
  4. package/dist/chain-health.js +3 -3
  5. package/dist/{chunk-IOLWREZB.js → chunk-4FAL7DFB.js} +5 -5
  6. package/dist/{chunk-CQA3JPYX.js → chunk-6RQY4VPG.js} +4 -4
  7. package/dist/{chunk-E77HESGN.js → chunk-7G475SPF.js} +1 -1
  8. package/dist/{chunk-7QQZ57E6.js → chunk-BMUDRQYV.js} +1 -1
  9. package/dist/{chunk-K57H4IRT.js → chunk-DKTKQS2N.js} +1 -1
  10. package/dist/{chunk-WYZUDE6I.js → chunk-FUU5U5GB.js} +2 -2
  11. package/dist/{chunk-GL7JKE7O.js → chunk-KDVHMIKK.js} +1 -1
  12. package/dist/{chunk-4FIMYLWV.js → chunk-KXYSKT5U.js} +142 -45
  13. package/dist/{chunk-EWQ2QZ44.js → chunk-LTUECTW2.js} +66 -18
  14. package/dist/{chunk-UPENVZ7J.js → chunk-NTJRL6TA.js} +1 -1
  15. package/dist/{chunk-BDUL42WH.js → chunk-OFR4K7P3.js} +115 -1
  16. package/dist/{chunk-GLLLSFMA.js → chunk-RMO7G7X2.js} +3 -3
  17. package/dist/{chunk-KOLWOUWP.js → chunk-S2N25SYI.js} +1 -1
  18. package/dist/{chunk-LPG3QSOM.js → chunk-Z6EIQVNP.js} +1 -1
  19. package/dist/chunk-probe.js +3 -3
  20. package/dist/commands/login.js +13 -13
  21. package/dist/commands/logout.js +4 -4
  22. package/dist/commands/transfer.js +6 -6
  23. package/dist/commands/whoami.js +3 -3
  24. package/dist/deploy-actors.js +7 -7
  25. package/dist/deploy.js +13 -13
  26. package/dist/dotns.js +6 -6
  27. package/dist/index.js +26 -26
  28. package/dist/manifest/product-preflight.js +3 -3
  29. package/dist/manifest/publish.js +17 -17
  30. package/dist/memory-report.js +2 -2
  31. package/dist/merkle.js +13 -13
  32. package/dist/personhood/bootstrap.js +9 -9
  33. package/dist/personhood/people-client.js +6 -6
  34. package/dist/pool.d.ts +62 -1
  35. package/dist/pool.js +21 -5
  36. package/dist/run-state.js +1 -1
  37. package/dist/sss-allowance-cache.js +4 -4
  38. package/dist/storage-signer.js +13 -13
  39. package/dist/telemetry.d.ts +4 -2
  40. package/dist/telemetry.js +6 -2
  41. package/dist/version-check.js +3 -3
  42. package/dist/ws.js +4 -4
  43. package/package.json +1 -1
  44. package/tools/release-retry-wrapper.mjs +8 -0
  45. package/dist/{chunk-DBDOISNQ.js → chunk-EHAL4CJM.js} +3 -3
@@ -7,7 +7,7 @@
7
7
  import "../dist/suppress-localstorage-warning.js";
8
8
 
9
9
  import { deploy, DEFAULT_BULLETIN_RPC, DEFAULT_POOL_SIZE, NonRetryableError, EXIT_CODE_NO_RETRY, isConnectionError, isBenignTeardownError, resolveEffectiveMnemonic, mnemonicConflictNotice, resolveEnvId, shouldPublishManifest, nonInteractivePhoneConfirmationError, printDeploymentCompleteBanner } from "../dist/deploy.js";
10
- import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, setRunStateActive, markRelaunchOomHintShown, setVersionFreshness } from "../dist/telemetry.js";
10
+ import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, markDeployFatal, setRunStateActive, markRelaunchOomHintShown, setVersionFreshness } from "../dist/telemetry.js";
11
11
  import { handleFailedDeploy, handlePreflightVersionCheck, fetchVersionInfo, preReleaseWarning, checkNodeVersion, classifyVersionFreshness, isInteractive } from "../dist/version-check.js";
12
12
  import { setDeployContext, installLogCapture, buildCliFlagsSummary } from "../dist/bug-report.js";
13
13
  import { loadRunState, writeRunState, shouldSkipStaleWarning, shouldShowOomHint, probablyOomRssMb } from "../dist/run-state.js";
@@ -355,9 +355,6 @@ if (!flags.help && !flags.version) {
355
355
  if (finalizing) return;
356
356
  finalizing = true;
357
357
  try {
358
- setDeployAttribute("deploy.status", "killed");
359
- setDeployAttribute("deploy.killed", reason);
360
- setDeployAttribute("deploy.sad", "true");
361
358
  captureWarning(`deploy process terminated: ${reason}`);
362
359
  } catch { /* telemetry best-effort */ }
363
360
  try { await closeTelemetry(1000); } catch { /* ignore */ }
@@ -390,7 +387,7 @@ if (!flags.help && !flags.version) {
390
387
  } catch { /* telemetry best-effort */ }
391
388
  return;
392
389
  }
393
- try { setDeployAttribute("deploy.error", msgStr.slice(0, 200)); } catch {}
390
+ try { markDeployFatal(e); } catch {}
394
391
  finalize(kind, 2);
395
392
  };
396
393
  process.on("uncaughtException", (e) => handleUnhandled(e, "uncaught"));
@@ -400,9 +397,16 @@ if (!flags.help && !flags.version) {
400
397
  // and are retryable from the consumer's perspective. Contrast with
401
398
  // NonRetryableError exits (code 78, POSIX EX_CONFIG) which callers must
402
399
  // NOT retry — see EXIT_CODE_NO_RETRY in src/errors.ts.
403
- process.on("SIGINT", () => finalize("SIGINT", 130));
404
- process.on("SIGTERM", () => finalize("SIGTERM", 143));
405
- process.on("SIGHUP", () => finalize("SIGHUP", 129));
400
+ const onSignal = (sig, exitCode) => {
401
+ try {
402
+ setDeployAttribute("deploy.status", "killed");
403
+ setDeployAttribute("deploy.killed", sig);
404
+ } catch { /* telemetry best-effort */ }
405
+ finalize(sig, exitCode);
406
+ };
407
+ process.on("SIGINT", () => onSignal("SIGINT", 130));
408
+ process.on("SIGTERM", () => onSignal("SIGTERM", 143));
409
+ process.on("SIGHUP", () => onSignal("SIGHUP", 129));
406
410
  }
407
411
 
408
412
  try {
@@ -10,10 +10,10 @@ import {
10
10
  hasPersistedSession,
11
11
  readNetworkSuffix,
12
12
  resolveBulletinEndpoints
13
- } from "./chunk-GL7JKE7O.js";
13
+ } from "./chunk-KDVHMIKK.js";
14
14
  import "./chunk-3NR3T7US.js";
15
- import "./chunk-4FIMYLWV.js";
16
- import "./chunk-7QQZ57E6.js";
15
+ import "./chunk-KXYSKT5U.js";
16
+ import "./chunk-BMUDRQYV.js";
17
17
  import "./chunk-SH2LG52Z.js";
18
18
  import "./chunk-DNXE3Q5D.js";
19
19
  export {
@@ -10,10 +10,10 @@ import {
10
10
  offerBugReport,
11
11
  scrubSecrets,
12
12
  setDeployContext
13
- } from "./chunk-GLLLSFMA.js";
14
- import "./chunk-KOLWOUWP.js";
15
- import "./chunk-4FIMYLWV.js";
16
- import "./chunk-7QQZ57E6.js";
13
+ } from "./chunk-RMO7G7X2.js";
14
+ import "./chunk-S2N25SYI.js";
15
+ import "./chunk-KXYSKT5U.js";
16
+ import "./chunk-BMUDRQYV.js";
17
17
  import "./chunk-SH2LG52Z.js";
18
18
  export {
19
19
  buildCliFlagsSummary,
@@ -3,9 +3,9 @@ import {
3
3
  DEFAULT_WINDOW_BLOCKS,
4
4
  MAX_SPANS_PER_SAMPLER,
5
5
  endpointHost
6
- } from "./chunk-UPENVZ7J.js";
7
- import "./chunk-4FIMYLWV.js";
8
- import "./chunk-7QQZ57E6.js";
6
+ } from "./chunk-NTJRL6TA.js";
7
+ import "./chunk-KXYSKT5U.js";
8
+ import "./chunk-BMUDRQYV.js";
9
9
  import "./chunk-SH2LG52Z.js";
10
10
  export {
11
11
  ChainHealthSampler,
@@ -8,11 +8,11 @@ import {
8
8
  import {
9
9
  isTestnetSpecName,
10
10
  testnetFromNetworkField
11
- } from "./chunk-BDUL42WH.js";
11
+ } from "./chunk-OFR4K7P3.js";
12
12
  import {
13
13
  getWsProvider,
14
14
  parseRpcFrame
15
- } from "./chunk-WYZUDE6I.js";
15
+ } from "./chunk-FUU5U5GB.js";
16
16
  import {
17
17
  HIGHEST_VERIFIED_DOTNS_RELEASE,
18
18
  POP_CONTROLLER_PROBE_ABI,
@@ -27,7 +27,7 @@ import {
27
27
  } from "./chunk-3NR3T7US.js";
28
28
  import {
29
29
  ChainHealthSampler
30
- } from "./chunk-UPENVZ7J.js";
30
+ } from "./chunk-NTJRL6TA.js";
31
31
  import {
32
32
  captureWarning,
33
33
  markCodePath,
@@ -35,13 +35,13 @@ import {
35
35
  setDeploySentryTag,
36
36
  truncateAddress,
37
37
  withSpan
38
- } from "./chunk-4FIMYLWV.js";
38
+ } from "./chunk-KXYSKT5U.js";
39
39
  import {
40
40
  FALLBACK_MAX_COMMITMENT_AGE_SECONDS,
41
41
  clearCommitmentRecord,
42
42
  loadCommitmentRecord,
43
43
  writeCommitmentRecord
44
- } from "./chunk-7QQZ57E6.js";
44
+ } from "./chunk-BMUDRQYV.js";
45
45
  import {
46
46
  NonRetryableError
47
47
  } from "./chunk-SH2LG52Z.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  verifyEmbeddedAppManifests
3
- } from "./chunk-DBDOISNQ.js";
3
+ } from "./chunk-EHAL4CJM.js";
4
4
  import {
5
5
  pessimisticSizePreflight
6
6
  } from "./chunk-YTRNJMI4.js";
@@ -19,13 +19,13 @@ import {
19
19
  setBulletinEndpoints,
20
20
  storeDirectory,
21
21
  storeFile
22
- } from "./chunk-EWQ2QZ44.js";
22
+ } from "./chunk-LTUECTW2.js";
23
23
  import {
24
24
  DEFAULT_TLD,
25
25
  DotNS,
26
26
  parseDomainName,
27
27
  stripTldSuffix
28
- } from "./chunk-IOLWREZB.js";
28
+ } from "./chunk-4FAL7DFB.js";
29
29
  import {
30
30
  DEFAULT_ENV_ID,
31
31
  getPopSelfServeConfig,
@@ -36,7 +36,7 @@ import {
36
36
  setDeployAttribute,
37
37
  withManifestSpan,
38
38
  withSpan
39
- } from "./chunk-4FIMYLWV.js";
39
+ } from "./chunk-KXYSKT5U.js";
40
40
  import {
41
41
  NonRetryableError
42
42
  } from "./chunk-SH2LG52Z.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-MRZL4IXT.js";
4
4
  import {
5
5
  DOT_DAPP_ID
6
- } from "./chunk-GL7JKE7O.js";
6
+ } from "./chunk-KDVHMIKK.js";
7
7
 
8
8
  // src/sss-allowance-cache.ts
9
9
  import { mkdir, readFile, writeFile, unlink } from "fs/promises";
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bulletin-deploy",
9
- version: "0.21.0",
9
+ version: "0.22.0-rc.1",
10
10
  private: false,
11
11
  repository: {
12
12
  type: "git",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureWarning
3
- } from "./chunk-4FIMYLWV.js";
3
+ } from "./chunk-KXYSKT5U.js";
4
4
 
5
5
  // src/chunk-probe.ts
6
6
  import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  endpointHost
3
- } from "./chunk-UPENVZ7J.js";
3
+ } from "./chunk-NTJRL6TA.js";
4
4
  import {
5
5
  captureWarning
6
- } from "./chunk-4FIMYLWV.js";
6
+ } from "./chunk-KXYSKT5U.js";
7
7
 
8
8
  // src/ws.ts
9
9
  import { getWsProvider as papiGetWsProvider } from "polkadot-api/ws";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3NR3T7US.js";
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-4FIMYLWV.js";
6
+ } from "./chunk-KXYSKT5U.js";
7
7
  import {
8
8
  CLI_NAME
9
9
  } from "./chunk-DNXE3Q5D.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-7QQZ57E6.js";
4
+ } from "./chunk-BMUDRQYV.js";
5
5
  import {
6
6
  FALLBACK_CONFIG_ORIGIN,
7
7
  SHIPPED_CONFIG_ORIGIN
@@ -131,6 +131,11 @@ function setVersionFreshness(status) {
131
131
  versionFreshness = status;
132
132
  }
133
133
  async function closeTelemetry(timeoutMs) {
134
+ if (deployRootSpan) {
135
+ if (deployRootSpan === deploySpan) deployRootSpan.setAttribute("deploy.outcome", computeDeployOutcome(currentErrorCategory, currentDeploySad, currentSadReason));
136
+ deployRootSpan.end();
137
+ deployRootSpan = null;
138
+ }
134
139
  if (!Sentry) return;
135
140
  try {
136
141
  await Sentry.close(timeoutMs);
@@ -152,6 +157,9 @@ function initTelemetry() {
152
157
  // Sentry Node SDK captures os.hostname() by default, which leaks personal
153
158
  // machine names (e.g. "Mac.fritz.box"). Override to something anonymous.
154
159
  serverName: process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local",
160
+ // Library mode registers no uncaughtException listener, so Sentry's fatal
161
+ // handler is what exits the process; report the deploy as failed first.
162
+ integrations: [Sentry.onUncaughtExceptionIntegration({ onFatalError: exitOnFatalError })],
155
163
  beforeSend(event) {
156
164
  if (event.server_name) event.server_name = process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local";
157
165
  if (event.message) event.message = scrubPaths(event.message);
@@ -284,6 +292,9 @@ var DEPLOY_SEED_POOL = {
284
292
  "deploy.pool.nonce_collision_missing": 0,
285
293
  "deploy.pool.nonce_collision_reupload_count": 0
286
294
  };
295
+ var DEPLOY_SEED_POOL_HEALTH = {
296
+ "deploy.pool.stuck_skipped": "none"
297
+ };
287
298
  var DEPLOY_SEED_MANIFEST = {
288
299
  "deploy.manifest.fetch_source": "none",
289
300
  "deploy.manifest.fetch_attempts": "0",
@@ -327,6 +338,7 @@ function getDeployAttributes(domain) {
327
338
  ...DEPLOY_SEED_STORAGE,
328
339
  ...DEPLOY_SEED_PROBE,
329
340
  ...DEPLOY_SEED_POOL,
341
+ ...DEPLOY_SEED_POOL_HEALTH,
330
342
  ...DEPLOY_SEED_MANIFEST,
331
343
  ...DEPLOY_SEED_BULLETIN_UPLOAD,
332
344
  ...DEPLOY_SEED_BULLETIN_AUTH,
@@ -346,7 +358,12 @@ function getDeployAttributes(domain) {
346
358
  // #1539: unconditional (both-values rule) so count_if(deploy.is_e2e_fixture,
347
359
  // "true") / "false" both work — see isE2eFixtureTag above for the full
348
360
  // rationale, and setDeployAttribute below for the options.tag/--tag path.
349
- "deploy.is_e2e_fixture": String(isE2eFixtureTag(process.env.DEPLOY_TAG))
361
+ "deploy.is_e2e_fixture": String(isE2eFixtureTag(process.env.DEPLOY_TAG)),
362
+ // #1626: the E2E harness sets BULLETIN_E2E_EXPECT_FAIL=1 on a deploy it
363
+ // asserts will fail (S3 owned-elsewhere, governance-reserved, subdomain
364
+ // orphan…), so the E2E dashboard can exclude those deliberate error spans.
365
+ // A tag only: nothing else reads the var. Unconditional (both-values rule).
366
+ "deploy.expected_error": String(process.env.BULLETIN_E2E_EXPECT_FAIL === "1")
350
367
  };
351
368
  if (hostApp) attrs["deploy.host_app"] = hostApp;
352
369
  const hostAppVersion = process.env.BULLETIN_DEPLOY_HOST_APP_VERSION;
@@ -369,23 +386,19 @@ function cameFromOurConfig(msg) {
369
386
  const lower = msg.toLowerCase();
370
387
  return lower.includes(`came from ${SHIPPED_CONFIG_ORIGIN.toLowerCase()}`) || lower.includes(`came from ${FALLBACK_CONFIG_ORIGIN.toLowerCase()}`);
371
388
  }
372
- var GENERIC_USER_ERROR = new RegExp(`personhood|owned by|owner mismatch|reserved for original|invalid domain label|not authorized for bulletin|insufficient balance|insufficient funds|quota exhausted|insufficient .* authorization|bip39 mnemonic|ipfs cli not installed|base name is \\d+ chars|cannot register it|NameNotAvailable|name must be lowercase|${PHONE_NONINTERACTIVE_FRAGMENT}`, "i");
389
+ function contractAddressCategory(msg) {
390
+ if (CONTRACT_ADDRESS_FAILURE.test(msg) && /this address came from/i.test(msg) && !cameFromOurConfig(msg)) return "user";
391
+ return /could not determine the DotNS ABI profile/i.test(msg) ? "environment" : "unknown";
392
+ }
393
+ function categoryOf(kind, msg) {
394
+ if (kind === "naming.contract_unavailable" || kind === "naming.contract_function_removed") return contractAddressCategory(msg);
395
+ return ERROR_KIND_CATEGORY[kind];
396
+ }
373
397
  function isExpectedError(msg, variant) {
374
- const structuralCategory = structuralCategoryOf(variant);
375
- if (structuralCategory) return structuralCategory === "user";
376
- if (GENERIC_USER_ERROR.test(msg)) return true;
377
- if (CONTRACT_ADDRESS_FAILURE.test(msg)) {
378
- return /this address came from/i.test(msg) && !cameFromOurConfig(msg);
379
- }
380
- return false;
398
+ return classifyDeployError(msg, variant) === "user";
381
399
  }
382
400
  function classifyDeployError(msg, variant) {
383
- const structuralCategory = structuralCategoryOf(variant);
384
- if (structuralCategory) return structuralCategory;
385
- if (isExpectedError(msg)) return "user";
386
- if (/chunk.*failed after.*retr|tx dropped from best chain|timed out after \d+s waiting for block|Contract reverted|Contract execution would revert|dotns register failed|All promises were rejected|"type"\s*:\s*"Invalid"|Commitment still too new|not finalised after \d+s|chain may have (dropped|evicted)|ReviveApi.*timed out|ReviveApi.*returned empty result|\b(?:commit|register|setSubnodeOwner|setResolver|setContenthash|setText|Revive\.call|Utility\.batch_all) timed out after \d+ms|transaction watcher silent for|could not determine the DotNS ABI profile/i.test(msg)) return "environment";
387
- if (/javascript heap out of memory|allocation failed.*heap|External signer mode is not supported with dotns-cli/i.test(msg)) return "internal";
388
- return "unknown";
401
+ return structuralCategoryOf(variant) ?? categoryOf(classifyErrorKind(msg, variant), msg);
389
402
  }
390
403
  function classifySadReason(message) {
391
404
  if (/process terminated: SIG/i.test(message)) return "killed";
@@ -450,8 +463,17 @@ var ERROR_KIND_RULES = [
450
463
  // Ahead of the generic contract-revert rule: a soulbound refusal is a
451
464
  // permanent naming fact, not a transient chain error.
452
465
  [/is soulbound and cannot be transferred/i, "naming.soulbound"],
466
+ // Operator-fixable phrasings, above contract-revert so the actionable kind wins over the wrapper.
467
+ // formatUnregistrableReason's phrase (#1185); above the label rules since it can quote the PopError it explains.
468
+ [/cannot register it|base name is \d+ chars|reserved for original/i, "naming.governance_reserved"],
469
+ [/NameNotAvailable/i, "naming.name_unavailable"],
470
+ [/invalid domain label|name must be lowercase/i, "naming.invalid_label"],
471
+ [/insufficient balance|insufficient funds|signer balance .* < estimated fee/i, "funding.insufficient_balance"],
472
+ [/insufficient .* authorization/i, "storage.allowance_insufficient"],
473
+ [/bip39 mnemonic/i, "signer.invalid_mnemonic"],
474
+ [/ipfs cli not installed/i, "tool.ipfs_missing"],
453
475
  [/Contract reverted|Contract execution would revert|revert(?:ed|ing)?\s*\(flags=[0-9]+\)|"type"\s*:\s*"ContractReverted"/i, "contract-revert"],
454
- [/timed out after \d+s waiting for block|Transaction not included after \d+s|Transaction did not settle within|Commitment still too new after \d+s/i, "chain-timeout"],
476
+ [/timed out after \d+s waiting for block|Transaction not included after \d+s|Transaction did not settle within|Commitment still too new/i, "chain-timeout"],
455
477
  [/\bstale\b.*nonce|nonce.*\bstale\b|"type"\s*:\s*"(?:Future|Stale)"|Invalid::Future|tx rejected by pool/i, "nonce-stale"],
456
478
  // Invalid::BadProof: the extrinsic's signature didn't verify (wrong genesis
457
479
  // hash / mortality window / spec version, or a re-signed payload mismatch).
@@ -465,6 +487,8 @@ var ERROR_KIND_RULES = [
465
487
  [/short names are not on sale on|Short names are not for sale/i, "naming.short_names_closed"],
466
488
  [/requires ProofOfPersonhood(?:Full|Lite|Light),\s*but this signer is NoStatus/i, "naming.pop_required"],
467
489
  [/requires NoStatus,\s*but this signer is ProofOfPersonhood/i, "naming.nostatus_required"],
490
+ // Other personhood phrasings ("Requires Full Personhood verification", "personhood check failed"): after both specific rules above so their kinds win.
491
+ [/personhood/i, "naming.pop_required"],
468
492
  // #1345 (2026-09-01 previewnet TLD change, fixed in #1324): checkTldNodeConsistency's
469
493
  // safety-net throw (src/dotns.ts). Was 100% of the 48-span 'unknown' majority in the
470
494
  // 7d sweep — anchored on the stable prefix, never on the interpolated namehash values.
@@ -473,6 +497,11 @@ var ERROR_KIND_RULES = [
473
497
  // Anchored on the stable "DotNS signer ... PAS free; needs ≥" shape, never on the
474
498
  // interpolated address/amounts — those vary per signer and per fee floor.
475
499
  [/DotNS signer .+ PAS free; needs ≥/i, "funding.insufficient_balance"],
500
+ // Revive's TransferFailed module error, raised by register / finalize-registration
501
+ // (formatDispatchError JSON) when concurrent deploys on one shared signer spent its
502
+ // balance after the preflight check passed. Matches the variant name alone: the
503
+ // enclosing JSON can be cut by truncation, the innermost variant is the tail.
504
+ [/"type"\s*:\s*"TransferFailed"/i, "funding.transfer_failed"],
476
505
  [/Cannot decode zero data.*with ABI parameters/i, "dotns.abi_decode_empty"],
477
506
  // Issue #1060: contractCall's own empty-`0x`-data guard (src/dotns.ts, #729) throws
478
507
  // an actionable wrapper instead of letting the raw viem message above reach a
@@ -512,33 +541,34 @@ var ERROR_KIND_RULES = [
512
541
  // coming back with empty data. Genuinely a missing-contract config problem,
513
542
  // not a removed-function one — stays naming.contract_unavailable.
514
543
  [/Invalid contract address for \w+ in environment/i, "naming.contract_unavailable"],
515
- [new RegExp(`Domain\\s+\\S+\\.${TLD_FRAGMENT}\\s+is already owned by\\s+0x[a-fA-F0-9]+`, "i"), "naming.already_owned"],
516
- // #1185: formatUnregistrableReason's distinctive phrase — present in both
517
- // the unregistered ("...is not registered, and bulletin-deploy cannot
518
- // register it: ...") and owned-by-another-account ("...is owned by
519
- // 0x..., and bulletin-deploy cannot register it for a different account:
520
- // ...") variants, so one rule classifies both. Ordered before
521
- // naming.subdomain_orphan (a different message shape entirely) and after
522
- // naming.already_owned (whose "already owned by 0x..." phrasing this rule
523
- // never produces, so there's no overlap either way).
524
- [/cannot register it/i, "naming.governance_reserved"],
525
544
  [new RegExp(`Cannot deploy\\s+[\\w.-]+\\.${TLD_FRAGMENT}:\\s*parent\\s+[\\w.-]+\\.${TLD_FRAGMENT}\\s+is owned by`, "i"), "naming.subdomain_orphan"],
545
+ // After subdomain_orphan, whose "is owned by" it would otherwise claim.
546
+ [/owner mismatch|\bowned by\b/i, "naming.already_owned"],
526
547
  [/Post-deploy verification failed for .+: on-chain contenthash is /i, "verify.contenthash_mismatch"],
527
548
  [/Deploy verification failed:\s*DAG-PB root.+not finalised/i, "verify.dagpb_not_finalised"],
528
549
  [/Retry budget exhausted:.*recovery attempts/i, "network.recovery_exhausted"],
529
550
  [/Account auto-mapping did not take effect on-chain/i, "account.mapping_pending"],
530
- [/ReviveApi\.\w+ timed out after \d+ms/i, "chain.api_timeout"],
531
- [/ReviveApi\.\w+ returned empty result/i, "chain.api_timeout"],
532
- [/transaction watcher silent for \d+s/i, "chain.tx_silent"],
551
+ [/ReviveApi.*timed out/i, "chain.api_timeout"],
552
+ [/ReviveApi.*returned empty result/i, "chain.api_timeout"],
553
+ [/transaction watcher silent for/i, "chain.tx_silent"],
533
554
  [/(?:commit|register|setSubnodeOwner|setResolver|setContenthash|setText|Revive\.call|Utility\.batch_all) timed out after \d+ms/i, "chain.tx_timeout"],
534
555
  // Prefix, not the full `AncientBirthBlock` variant name: at 17 chars it is
535
556
  // long enough to be cut in half by the 100-char truncation both src/deploy.ts
536
557
  // producer sites apply to the inner chain error, which sent a live span to
537
558
  // `unknown` with the message ending `"type": "AncientBirth`.
538
559
  [/AncientBirth/i, "chain.extrinsic_expired"],
539
- [/Bulletin quota exhausted/i, "chain.quota_exhausted"],
560
+ [/quota exhausted/i, "chain.quota_exhausted"],
540
561
  [/Mobile (?:transaction )?signing (?:failed|rejected).*message too big/i, "signer.message_too_large"],
541
- [/^INVARIANT FAILED:/i, "tool.invariant"]
562
+ [/^INVARIANT FAILED:/i, "tool.invariant"],
563
+ // Broad fallbacks, last.
564
+ [/javascript heap out of memory|allocation failed.*heap/i, "tool.out_of_memory"],
565
+ [/External signer mode is not supported with dotns-cli/i, "tool.external_signer_unsupported"],
566
+ [/All promises were rejected/i, "network.endpoints_failed"],
567
+ [/tx dropped from best chain|chain may have (?:dropped|evicted)/i, "chain.tx_dropped"],
568
+ [/not finalised after \d+s/i, "chain.not_finalised"],
569
+ [/chunk.*failed after.*retr/i, "chain.chunk_failed"],
570
+ [/"type"\s*:\s*"Invalid"/i, "chain.tx_invalid"],
571
+ [/dotns register failed/i, "dotns.register_failed"]
542
572
  ];
543
573
  var INVALID_TRANSACTION_VARIANT_KIND = {
544
574
  Future: "nonce-stale",
@@ -555,6 +585,57 @@ function classifyErrorKind(msg, variant) {
555
585
  }
556
586
  return "unknown";
557
587
  }
588
+ var ERROR_KIND_CATEGORY = {
589
+ "contract-revert": "environment",
590
+ "chain-timeout": "environment",
591
+ "nonce-stale": "environment",
592
+ "connection": "environment",
593
+ "naming.pop_required": "user",
594
+ "naming.nostatus_required": "user",
595
+ "naming.contract_unavailable": "unknown",
596
+ // see contractAddressCategory
597
+ "naming.contract_function_removed": "unknown",
598
+ // see contractAddressCategory
599
+ "dotns.abi_decode_empty": "unknown",
600
+ // never attributed; kept visible
601
+ "naming.already_owned": "user",
602
+ "naming.governance_reserved": "user",
603
+ "naming.short_names_closed": "user",
604
+ "naming.soulbound": "user",
605
+ "naming.subdomain_orphan": "user",
606
+ "naming.invalid_label": "user",
607
+ "naming.name_unavailable": "user",
608
+ "naming.tld_mismatch": "environment",
609
+ "funding.insufficient_balance": "user",
610
+ "funding.transfer_failed": "user",
611
+ "verify.contenthash_mismatch": "environment",
612
+ "verify.dagpb_not_finalised": "environment",
613
+ "network.recovery_exhausted": "environment",
614
+ "network.endpoints_failed": "environment",
615
+ "account.mapping_pending": "environment",
616
+ "chain.api_timeout": "environment",
617
+ "chain.tx_timeout": "environment",
618
+ "chain.tx_silent": "environment",
619
+ "chain.extrinsic_expired": "environment",
620
+ "chain.quota_exhausted": "user",
621
+ "chain.bad_proof": "environment",
622
+ "chain.chunk_failed": "environment",
623
+ "chain.tx_dropped": "environment",
624
+ "chain.tx_invalid": "environment",
625
+ "chain.not_finalised": "environment",
626
+ "dotns.register_failed": "environment",
627
+ "signer.message_too_large": "environment",
628
+ "signer.invalid_mnemonic": "user",
629
+ "storage.rejected": "user",
630
+ "storage.allowance_insufficient": "user",
631
+ "user.aborted": "user",
632
+ "signer.phone_confirmation_unavailable": "user",
633
+ "tool.invariant": "internal",
634
+ "tool.ipfs_missing": "user",
635
+ "tool.out_of_memory": "internal",
636
+ "tool.external_signer_unsupported": "internal",
637
+ "unknown": "unknown"
638
+ };
558
639
  function chainErrorVariantOf(error) {
559
640
  return error?.chainErrorVariant;
560
641
  }
@@ -609,6 +690,7 @@ async function withSpan(op, description, attributes, fn) {
609
690
  }
610
691
  var memoryPeak = null;
611
692
  var deployRootSpan = null;
693
+ var deploySpan = null;
612
694
  var stageSamples = {};
613
695
  var reportContext = {};
614
696
  var currentErrorCategory = null;
@@ -674,6 +756,7 @@ async function withDeploySpan(domain, fn) {
674
756
  try {
675
757
  return await Sentry.startSpan({ op: "deploy", name: `deploy ${domain}`, attributes: attrs }, async (span) => {
676
758
  deployRootSpan = span;
759
+ deploySpan = span;
677
760
  span.setAttribute("deploy.tool_version", VERSION);
678
761
  if (relaunchOomHintShown) {
679
762
  span.setAttribute("deploy.relaunch.oom_hint_shown", "true");
@@ -694,19 +777,7 @@ async function withDeploySpan(domain, fn) {
694
777
  span.setAttribute("deploy.status", "ok");
695
778
  return result;
696
779
  } catch (error) {
697
- const msg = error.message ?? String(error);
698
- const variant = chainErrorVariantOf(error);
699
- span.setAttribute("deploy.status", "error");
700
- setDeployErrorOnSpan(span, msg, variant);
701
- const errorCategory = classifyDeployError(msg, variant);
702
- span.setAttribute("deploy.error_category", errorCategory);
703
- currentErrorCategory = errorCategory;
704
- const isExpected = isExpectedError(msg, variant);
705
- span.setAttribute("deploy.expected", isExpected ? "true" : "false");
706
- span.setAttribute("deploy.sad", isExpected ? "false" : "true");
707
- if (!isExpected) {
708
- span.setStatus({ code: 2, message: "internal_error" });
709
- }
780
+ recordDeployFailure(span, error);
710
781
  throw error;
711
782
  } finally {
712
783
  sampleMemory("end");
@@ -757,6 +828,7 @@ async function withDeploySpan(domain, fn) {
757
828
  } finally {
758
829
  memoryPeak = null;
759
830
  deployRootSpan = null;
831
+ deploySpan = null;
760
832
  stageSamples = {};
761
833
  reportContext = {};
762
834
  currentErrorCategory = null;
@@ -828,6 +900,29 @@ function setDeployError(msg, variant) {
828
900
  if (!deployRootSpan) return;
829
901
  setDeployErrorOnSpan(deployRootSpan, msg, variant);
830
902
  }
903
+ function recordDeployFailure(span, error) {
904
+ const msg = error?.message ?? String(error);
905
+ const variant = chainErrorVariantOf(error);
906
+ span.setAttribute("deploy.status", "error");
907
+ setDeployErrorOnSpan(span, msg, variant);
908
+ const errorCategory = classifyDeployError(msg, variant);
909
+ span.setAttribute("deploy.error_category", errorCategory);
910
+ currentErrorCategory = errorCategory;
911
+ const isExpected = isExpectedError(msg, variant);
912
+ span.setAttribute("deploy.expected", isExpected ? "true" : "false");
913
+ span.setAttribute("deploy.sad", isExpected ? "false" : "true");
914
+ if (!isExpected) {
915
+ span.setStatus({ code: 2, message: "internal_error" });
916
+ }
917
+ }
918
+ function markDeployFatal(error) {
919
+ if (deployRootSpan && deployRootSpan === deploySpan) recordDeployFailure(deployRootSpan, error);
920
+ }
921
+ function exitOnFatalError(error) {
922
+ console.error(error);
923
+ markDeployFatal(error);
924
+ void closeTelemetry(2e3).finally(() => process.exit(1));
925
+ }
831
926
  function __setDeployRootSpanForTest(span) {
832
927
  deployRootSpan = span;
833
928
  }
@@ -1035,6 +1130,7 @@ export {
1035
1130
  computeDeployOutcome,
1036
1131
  classifiableErrorKinds,
1037
1132
  classifyErrorKind,
1133
+ ERROR_KIND_CATEGORY,
1038
1134
  sanitizeErrorMessage,
1039
1135
  analyseErrorPattern,
1040
1136
  withSpan,
@@ -1044,6 +1140,7 @@ export {
1044
1140
  setDeployReportContext,
1045
1141
  setDeployAttribute,
1046
1142
  setDeployError,
1143
+ markDeployFatal,
1047
1144
  __setDeployRootSpanForTest,
1048
1145
  __setSentryForTest,
1049
1146
  getCurrentSentryTraceId,