@rightkit/release 0.3.0 → 0.4.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 (83) hide show
  1. package/README.md +74 -10
  2. package/addon-command.mjs +3 -4
  3. package/adoption-evidence.mjs +650 -0
  4. package/android-play-upload.mjs +2 -1
  5. package/apple-profile-create.mjs +113 -10
  6. package/apple-profile-renew.mjs +11 -3
  7. package/artifact-storage.mjs +35 -2
  8. package/bin/darwin/rightkit-swap +0 -0
  9. package/binary-license-notices.mjs +161 -0
  10. package/build-release.mjs +138 -47
  11. package/cargo-license-notices.mjs +111 -0
  12. package/cli/right-release.mjs +21 -2
  13. package/codesign-environment.mjs +2 -2
  14. package/content-digest.mjs +209 -0
  15. package/development.mjs +17 -6
  16. package/direct-bootstrap.mjs +5 -6
  17. package/disk-command.mjs +1 -0
  18. package/disk-policy.json +6 -2
  19. package/disk-stable-root.mjs +13 -9
  20. package/github-updater-release.mjs +70 -7
  21. package/in-place-install.mjs +17 -14
  22. package/install-app-windows.mjs +24 -7
  23. package/install-app.mjs +21 -40
  24. package/ios-asc-credentials.mjs +163 -0
  25. package/ios-build-context.mjs +129 -0
  26. package/ios-build-upload.mjs +282 -0
  27. package/ios-credentials-command.mjs +70 -0
  28. package/ios-dev.mjs +248 -0
  29. package/ios-host.mjs +76 -0
  30. package/ios-lock.mjs +75 -0
  31. package/ios-package-recipe.mjs +707 -0
  32. package/ios-testflight.mjs +266 -0
  33. package/ios-upload.mjs +313 -0
  34. package/local-publish.mjs +222 -53
  35. package/managed-cargo.mjs +8 -0
  36. package/mirror-root-artifact.mjs +5 -1
  37. package/native-cargo-layout.mjs +47 -4
  38. package/ownership-command.mjs +2 -1
  39. package/ownership-dependency-allowlist.json +3 -1
  40. package/ownership-evidence.mjs +30 -18
  41. package/ownership-key-command.mjs +69 -2
  42. package/ownership-mandatory-set.json +117 -67
  43. package/ownership-mandatory-set.mjs +141 -21
  44. package/ownership-repositories.json +1 -1
  45. package/ownership-scope.mjs +94 -0
  46. package/ownership.mjs +49 -7
  47. package/package-recipe-targets.mjs +41 -1
  48. package/package.json +1 -1
  49. package/pipeline-admission.mjs +375 -0
  50. package/pipeline-config.mjs +100 -0
  51. package/pipeline-generators.json +4 -0
  52. package/pipeline-generators.mjs +137 -0
  53. package/pipeline-scan.mjs +285 -0
  54. package/pipeline-tools.mjs +171 -0
  55. package/preflight.mjs +14 -7
  56. package/production-mode.mjs +42 -0
  57. package/project-type.mjs +3 -1
  58. package/protected-credentials.mjs +28 -1
  59. package/publication-artifacts.mjs +30 -5
  60. package/publication-gate.mjs +94 -32
  61. package/publish-cargo.mjs +117 -25
  62. package/publish-swift.mjs +12 -3
  63. package/publish-update.mjs +15 -2
  64. package/qa-build-features.mjs +16 -13
  65. package/qa-contract.mjs +30 -2
  66. package/qa-control-scan.mjs +8 -0
  67. package/release-config.mjs +330 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +88 -21
  71. package/release.mjs +82 -20
  72. package/rightkit-ownership.json +1 -1
  73. package/rightkit-sync.mjs +8 -0
  74. package/rightkit-versions.json +428 -194
  75. package/runtime-budget-crates.mjs +14 -0
  76. package/runtime-budgets.mjs +248 -47
  77. package/sign-updater.mjs +56 -59
  78. package/source-gate.mjs +8 -1
  79. package/stage-sidecar.mjs +207 -40
  80. package/swap-helper-bundle.mjs +7 -3
  81. package/target-distribution.mjs +5 -0
  82. package/upload-release.mjs +7 -2
  83. package/bin/darwin/rightkit-swap.sign.json +0 -6
package/release.mjs CHANGED
@@ -11,8 +11,8 @@ import { validateRightKitCargoContract } from "./cargo-contract.mjs";
11
11
  import { assertCargoProfileContract } from "./cargo-profile-contract.mjs";
12
12
  import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
13
13
  import { assertLegalReleaseContract } from "./legal-contract.mjs";
14
- import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
15
- import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
14
+ import { assertNativeIosPrepareHasManifest, assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
15
+ import { declaredCargoManifest, hasDefaultTauriManifest, resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
16
16
  import {
17
17
  canonicalPackageIdentity,
18
18
  createNativeFinalizationReceipt,
@@ -48,9 +48,13 @@ import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs
48
48
  import { assertReleaseDependencies } from "./release-dependencies.mjs";
49
49
  import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
50
50
  import { expandRecipeBuildCommands } from "./recipe-build.mjs";
51
- import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
51
+ import { assertNoReleaseSwitches, commandTokens, localCrateDirsFromTree, resolveFeatureClosure } from "./release-switch-scan.mjs";
52
+ import { iosOwnershipScope } from "./ownership-scope.mjs";
52
53
  import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
53
54
  import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
55
+ import { runIosPackageRecipe } from "./ios-package-recipe.mjs";
56
+ import { releaseSwitchInputs } from "./ios-build-context.mjs";
57
+ import { productionBuildEnv, productionModeProblems } from "./production-mode.mjs";
54
58
 
55
59
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
56
60
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -180,11 +184,14 @@ const SUPERSEDE_JOB_HEAD = /^\n supersede:\n needs: \[private-repo-guard\]\n
180
184
 
181
185
  const root = path.dirname(configPath);
182
186
  const workdir = path.resolve(root, config.workdir ?? ".");
183
- const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
187
+ // A native iOS app (xcodegen project; Rust built by iosPackage.prepare) with no src-tauri and no declared nativeAssembly
188
+ // manifest owns no Cargo layout: null, so the Cargo-derived preflight, sweep and package-contract steps are skipped for it.
189
+ const nativeIos = opts.platform === "ios" && !hasDefaultTauriManifest({ appRoot: root });
190
+ const nativeLayout = nativeIos ? null : resolveNativeCargoLayout({ appRoot: root, config });
184
191
  const nativeAssembly = config.nativeAssembly;
185
192
  const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
186
- // A SwiftPM app without a package.json has no Node package contract to validate.
187
- if (!(resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.platform] }) === "swiftpm" && !existsSync(path.join(root, "package.json")))) {
193
+ // A SwiftPM app or a native iOS app without a package.json has no Node package contract to validate.
194
+ if (!((nativeIos || resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.platform] }) === "swiftpm") && !existsSync(path.join(root, "package.json")))) {
188
195
  await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
189
196
  }
190
197
  validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
@@ -257,12 +264,18 @@ let androidTarget; // validated Android AAB target (android only; see android-re
257
264
  if (opts.platform === "android") {
258
265
  try { androidTarget = validateAndroidTarget(target, { root }); } catch (error) { fail(`${config.app ?? "app"} android ${error.message}`); }
259
266
  }
267
+ let iosSpec; // validated iOS iosPackage spec (ios only; package-recipe-targets.mjs expands targets.ios, ios-package-recipe.mjs runs it)
268
+ if (opts.platform === "ios") {
269
+ if (target.recipe?.kind !== "iosPackage") fail(`${config.app ?? "app"} ios must declare targets.ios.iosPackage; there is no hand-written iOS package script`);
270
+ iosSpec = target.recipe.spec;
271
+ if (nativeIos) { try { assertNativeIosPrepareHasManifest(iosSpec, config, root); } catch (error) { fail(`${config.app ?? "app"} ios ${error.message}`); } }
272
+ }
260
273
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
261
274
  /**
262
275
  * Assemble preflight inputs from the app's own files. Kept here (not in
263
276
  * preflight.mjs) so the check module stays free of release.mjs's config shape.
264
277
  */
265
- function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout, projectTarget = config.targets?.[platform] }) {
278
+ function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout, projectTarget = config.targets?.[platform], pinnedCargoLocks = [] }) {
266
279
  let version;
267
280
  try {
268
281
  version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
@@ -273,6 +286,7 @@ function releasePreflight({ config, configPath, root, repoRoot, platform, native
273
286
  // them means the build needs a real Windows Perl.
274
287
  const cargoLockPaths = [
275
288
  ...(suppliedNativeLayout?.lockCandidates ?? []),
289
+ ...pinnedCargoLocks,
276
290
  ...(config.cargoLocks ?? []).map((entry) => path.resolve(root, entry)),
277
291
  ].filter((entry, index, entries) => entries.indexOf(entry) === index);
278
292
  return collectPreflight({
@@ -286,12 +300,14 @@ function releasePreflight({ config, configPath, root, repoRoot, platform, native
286
300
  nativeLayout: suppliedNativeLayout,
287
301
  targetLink: suppliedNativeLayout?.targetLink,
288
302
  projectType: resolveProjectType({ appRoot: root, config, target: projectTarget }),
303
+ rust: Boolean(suppliedNativeLayout) || pinnedCargoLocks.length > 0,
304
+ bridge: Boolean(suppliedNativeLayout),
289
305
  msix: targetUsesMsix(projectTarget),
290
306
  });
291
307
  }
292
308
 
293
309
  if (opts.doctor) {
294
- const { buildInputs, nativeLayout: resolvedNativeLayout } = resolveReleaseBuildInputs({
310
+ const { buildInputs, nativeLayout: resolvedNativeLayout, cargoLockPaths: pinnedCargoLocks } = resolveReleaseBuildInputs({
295
311
  repoRoot: doctorInvocation.repoRoot,
296
312
  appRoot: root,
297
313
  configPath,
@@ -335,7 +351,7 @@ if (opts.doctor) {
335
351
 
336
352
  // Config above, MACHINE below. Printing config never told anyone whether this
337
353
  // box could finish a build; these checks do.
338
- const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform, nativeLayout: resolvedNativeLayout });
354
+ const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform, nativeLayout: resolvedNativeLayout, pinnedCargoLocks });
339
355
  console.log("preflight:");
340
356
  console.log(formatPreflight(checks));
341
357
  const failures = preflightFailures(checks);
@@ -350,10 +366,15 @@ let prePackScanned = false; // set once a pre-pack payload scan has seen a nativ
350
366
  // or app Cargo manifest enables devtools, qa-native, qa-control or dev-native (also in --dry-run).
351
367
  // DevTools: refused for every app except an allow-listed one that declares `devtools: "content-only"` (devtools-policy.mjs).
352
368
  let devtoolsAllowance = { mode: "denied" };
369
+ // The Rust a build compiles is checked from the manifest it compiles: targets.<os>.cargoManifest, else the declared
370
+ // nativeAssembly.cargoManifest. iosPackage.prepare commands run cargo for a native iOS app, so they are scanned like any build command.
371
+ const switchInputs = releaseSwitchInputs({ appRoot: root, config, target, prepare: iosSpec?.prepare ?? [], nativeIos });
372
+ if (switchInputs.problems.length) fail(`${config.app ?? "app"} ios release-switch scan cannot see what is built:\n- ${switchInputs.problems.join("\n- ")}`);
353
373
  try {
354
374
  devtoolsAllowance = resolveDevtoolsAllowance({ config, root });
355
375
  if (devtoolsAllowance.mode === "content-only") console.log(`right-release: devtools allowance recorded: ${JSON.stringify(devtoolsEvidence(devtoolsAllowance))}`);
356
- assertNoForbiddenBuildFeatures({ root, tier: "release/update", devtools: devtoolsAllowance, cargoManifest: target.cargoManifest, manifest: config.releaseSwitches?.manifest, commands: [
376
+ assertNoForbiddenBuildFeatures({ root, tier: "release/update", devtools: devtoolsAllowance, cargoManifest: switchInputs.cargoManifest, manifest: config.releaseSwitches?.manifest, commands: [
377
+ ...switchInputs.commands,
357
378
  { label: "target.prePackage", command: target.prePackage },
358
379
  { label: "target.package", command: target.package },
359
380
  { label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
@@ -362,6 +383,13 @@ try {
362
383
  { label: "target.gradle", command: target.gradle },
363
384
  ] });
364
385
  } catch (error) { fail(error.message); }
386
+ // M12: the frontend build resolves in production mode only (development-only conditional exports such as
387
+ // `@rightkit/license/testing` never resolve). `run` pins NODE_ENV=production for build children; this refuses a
388
+ // Vite config that lists the `development` resolve condition explicitly, which the pin cannot override.
389
+ {
390
+ const modeProblems = productionModeProblems({ appRoot: root });
391
+ if (modeProblems.length) fail(`${config.app ?? "app"} release frontend is not production-only:\n- ${modeProblems.join("\n- ")}`);
392
+ }
365
393
 
366
394
  // D4 (M21/M23) source layer: every environment/argument read in app source is registered in the app's switches.json and
367
395
  // every S1-S4 selector is compiled out of release. The inventory is `config.releaseSwitches.inventory` (relative to the
@@ -374,17 +402,18 @@ const releaseSwitchInventoryText = existsSync(releaseSwitchInventoryPath) ? read
374
402
  try {
375
403
  // A `node recipe-build.mjs --plan <JSON>` prePackage hides each step's `--features` in one argument: the closure and the
376
404
  // command token scan see every prepare / build / postBuild step (a plan that cannot be read or validated fails closed).
377
- const closureCommands = [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, ...commandList(target.postSign), target.gradle].filter(Boolean).flatMap((c) => expandRecipeBuildCommands(c, { root }));
405
+ const closureCommands = [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, ...commandList(target.postSign), target.gradle, ...switchInputs.cargoCommands].filter(Boolean).flatMap((c) => expandRecipeBuildCommands(c, { root }));
378
406
  assertReleaseDependencies({ root, config, target, commands: closureCommands });
379
407
  let treeText;
380
408
  try {
381
409
  const closure = resolveFeatureClosure({
382
410
  root,
383
411
  manifest: config.releaseSwitches?.manifest,
384
- cargoManifest: target.cargoManifest,
412
+ cargoManifest: switchInputs.cargoManifest,
385
413
  rust: config.releaseSwitches?.rust,
386
414
  features: config.releaseSwitches?.features ?? [],
387
- triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
415
+ // iOS ships one device triple; "all" is refused by the build broker and would pull in desktop-only targets.
416
+ triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? (opts.platform === "ios" ? "aarch64-apple-ios" : undefined),
388
417
  commandTexts: closureCommands,
389
418
  });
390
419
  if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
@@ -394,9 +423,27 @@ try {
394
423
  console.log(`right-release: dry run: ${error.message}`);
395
424
  }
396
425
  // remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
397
- const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: closureCommands.map((c) => commandTokens(c)) };
426
+ // remoteLoopback: the daemon `remote-loopback` feature is a separate declaration (releaseSwitches.remoteLoopback: true); remoteHost does not cover it.
427
+ const closureInput = { treeText, devtools: devtoolsAllowance, remoteHost: config.releaseSwitches?.remoteHost, remoteLoopback: config.releaseSwitches?.remoteLoopback, commands: closureCommands.map((c) => commandTokens(c)) };
398
428
  if (releaseSwitchInventoryText != null) {
399
- assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
429
+ // iOS: only what the iPhone app compiles — the Xcode project and its local packages, plus the local crates in the
430
+ // resolved aarch64-apple-ios cargo graph (feature-aware, unlike the ownership closure). Other targets walk the app root.
431
+ let sourceRoots;
432
+ if (opts.platform === "ios") {
433
+ const repoTop = git(root, ["rev-parse", "--show-toplevel"]);
434
+ // Swift side only: the Rust side is the resolved cargo graph below. The ownership scope also folds in the whole
435
+ // nativeAssembly workspace, which would scan crates (desktop tools, the TUI) the iPhone never links.
436
+ const swiftSide = (iosOwnershipScope({ repoRoot: repoTop, appRoot: root, target: config.targets?.ios }) ?? []).filter((dir) => dir !== ".rightkit").map((dir) => path.join(repoTop, dir));
437
+ sourceRoots = [...new Set([...swiftSide, ...localCrateDirsFromTree(treeText, repoTop)])].sort();
438
+ // A prepare step can stage sources from anywhere (a web bundle, a generated resource) into the app; its working directory must be
439
+ // inside the scanned roots, or the release fails instead of shipping what the scan never saw. Add the directory to ownershipScope.
440
+ for (const step of config.targets?.ios?.iosPackage?.prepare ?? []) {
441
+ const cwd = path.resolve(root, step?.cwd ?? ".");
442
+ if (!sourceRoots.some((dir) => cwd === dir || cwd.startsWith(dir + path.sep))) throw new Error(`source switch inventory: targets.ios.iosPackage.prepare runs in ${path.relative(repoTop, cwd) || "."}, outside the iOS scan roots (${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}); list that directory in targets.ios.ownershipScope`);
443
+ }
444
+ console.log(`right-release: source switch scan (ios): ${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}`);
445
+ }
446
+ assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, sourceRoots, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
400
447
  } else if (treeText != null || closureInput.commands.length) {
401
448
  assertNoReleaseSwitches({ ...closureInput, appForbid: config.releaseSwitches?.forbid ?? [] }, "feature closure");
402
449
  }
@@ -408,7 +455,7 @@ try {
408
455
  const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
409
456
  const mode = "package";
410
457
  const command = target.package;
411
- if (!command && opts.platform !== "android" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
458
+ if (!command && opts.platform !== "android" && opts.platform !== "ios" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
412
459
  if (opts.upload && !target.publish && !target.upload) {
413
460
  fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
414
461
  }
@@ -524,6 +571,16 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
524
571
  receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
525
572
  allowContentOnlyDevtools: devtoolsAllowance.mode === "content-only",
526
573
  });
574
+ } else if (opts.platform === "ios") {
575
+ // Archive and export with Xcode automatic signing in the GUI session, then the IPA verification and the receipt (macOS only;
576
+ // --dry-run prints the plan anywhere). Upload is a separate command: `right-release upload --platform ios`.
577
+ await runIosPackageRecipe({
578
+ root, app: config.app ?? path.basename(root), spec: iosSpec, dryRun: opts.dryRun,
579
+ receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
580
+ scanFiles: (label, files, scanOptions) => assertNoQaControlBinaries(label, files, scanOptions),
581
+ // The build number counter outlives every release id: it lives in the vault state root that right-release build exports.
582
+ ...(process.env.RIGHT_RELEASE_VAULT_STATE_ROOT ? { counterDir: path.join(path.resolve(process.env.RIGHT_RELEASE_VAULT_STATE_ROOT), "ios-build-numbers") } : {}),
583
+ });
527
584
  } else if (nativeAssembly) {
528
585
  if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
529
586
  if (nativeFinalizer) {
@@ -647,7 +704,7 @@ function assertNoQaControlBinaries(label, files, { requireBinary = false, prePac
647
704
  try {
648
705
  const stats = assertNoQaControl(files, label, { requireBinary, allowContentOnlyDevtools: devtoolsAllowance.mode === "content-only" });
649
706
  // D4 binary layer, same inputs: no test-mode marker and no S1-S4 selector name in any native binary.
650
- assertNoReleaseSwitches({ binaryPaths: files, inventoryText: releaseSwitchInventoryText ?? undefined, appForbid: config.releaseSwitches?.forbid ?? [], requireBinary: false }, `${label} (release switches)`);
707
+ assertNoReleaseSwitches({ binaryPaths: files, devtools: devtoolsAllowance, inventoryText: releaseSwitchInventoryText ?? undefined, appForbid: config.releaseSwitches?.forbid ?? [], requireBinary: false }, `${label} (release switches)`);
651
708
  if (prePack && stats.binaries > 0) prePackScanned = true;
652
709
  for (const opaque of stats.opaque) {
653
710
  console.log(`right-release: ${label}: ${opaque.kind.toUpperCase()} payload is compressed and cannot be scanned (${opaque.file}); the raw payload was scanned before packing`);
@@ -908,6 +965,8 @@ function hasNativeTargetSpecificPrePackage(target, packageHook) {
908
965
  // publish so it cannot race an artifact, and is best-effort: a missing
909
966
  // cargo-sweep or a sweep failure must never fail a release.
910
967
  function sweepStaleRustArtifacts() {
968
+ // No Cargo host, nothing of ours to sweep.
969
+ if (!nativeLayout) return;
911
970
  const candidates = [...new Set([root, workdir, nativeLayout.manifestDir, nativeLayout.workspaceRoot])];
912
971
  const projects = candidates.filter((dir) => existsSync(path.join(dir, "target")));
913
972
  for (const dir of projects) {
@@ -929,8 +988,8 @@ function sweepStaleRustArtifacts() {
929
988
  }
930
989
 
931
990
  async function runInstall(pm, cwd) {
932
- if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd);
933
- else if (pm === "npm") await run("npm", ["ci"], cwd);
991
+ if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd, {}, { install: true });
992
+ else if (pm === "npm") await run("npm", ["ci"], cwd, {}, { install: true });
934
993
  else fail(`unsupported package manager: ${pm}`);
935
994
  }
936
995
 
@@ -1432,6 +1491,8 @@ function expandEnv(value) {
1432
1491
  // notices and the source offer, optionally run the app's stage command, and (before signing) check the staged files.
1433
1492
  async function prepareMediaSidecar(root, { stage = true } = {}) {
1434
1493
  if (!mediaSidecarConfig) return null;
1494
+ // Unreachable for iOS (mediaSidecar is mac and win only, refused above); a Cargo-less layout must still never be dereferenced.
1495
+ if (!nativeLayout) fail(`${config.app ?? "app"} ${opts.platform} mediaSidecar needs a native Cargo manifest to read the sidecar catalog, and this app has none`);
1435
1496
  let plan;
1436
1497
  try {
1437
1498
  const loaded = loadSidecarCatalog({ config: mediaSidecarConfig, root, manifestPath: nativeLayout.manifestPath });
@@ -1720,7 +1781,8 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
1720
1781
  const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
1721
1782
  const child = spawn(cmd, runArgs, {
1722
1783
  cwd,
1723
- env: macosCodesignEnvironment({ ...process.env, ...releaseEnv, ...env }),
1784
+ // Build children resolve in production mode; package installs keep the caller's NODE_ENV (production drops devDependencies).
1785
+ env: macosCodesignEnvironment(options.install ? { ...process.env, ...releaseEnv, ...env } : productionBuildEnv({ ...process.env, ...releaseEnv, ...env })),
1724
1786
  stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
1725
1787
  shell: useShell,
1726
1788
  windowsHide: true,
@@ -2979,7 +2979,7 @@
2979
2979
  {
2980
2980
  "package": "rightkit-spend",
2981
2981
  "capability": "spend-ledger",
2982
- "summary": "Spend reservation, allowance and compare-and-swap ledger on SQLite: integer micro-units, idempotent reserve on request id, in-flight and overrun handling, opaque allowance periods, optional account key and per-reservation currency, billed-unknown commit, caller-transaction (`_in`) variants, strict transition mode",
2982
+ "summary": "Spend reservation, allowance and compare-and-swap ledger on SQLite: integer micro-units, idempotent reserve on request id, in-flight and overrun handling, opaque allowance periods, optional account key and per-reservation currency, billed-unknown commit, caller-transaction (`_in`/`_on`) variants, strict transition mode, carry-over, atomic multi-cap reserve with epoch-reset device allowance, caller and process budgets, provider-reported cost and invoice reconciliation, cap-setting audit, optional overrun gate",
2983
2983
  "fingerprints": [],
2984
2984
  "detection": "declaration-only",
2985
2985
  "policyNotes": [
package/rightkit-sync.mjs CHANGED
@@ -192,6 +192,14 @@ export function stageSourceVersions(entries, { file = VERSIONS_FILE, root = righ
192
192
  return { changes, copies, written };
193
193
  }
194
194
 
195
+ /** Feed file and consumer-copy root of one requested workspace. recordPublished/stageSourceVersions default to the
196
+ * location of THIS module, which is inside the package store when right-release runs from an installed
197
+ * @rightkit/release; every caller that knows its workspace passes these explicitly. */
198
+ export function workspaceFeedOptions(workspaceRoot) {
199
+ const root = path.resolve(workspaceRoot);
200
+ return { root, file: path.join(root, "packages", "release", "rightkit-versions.json") };
201
+ }
202
+
195
203
  /** Record one just-published version (the registry index can lag the upload) and refresh the copies.
196
204
  * Called by right-release after a verified cargo or npm publish; never fails the publish itself unless `strict` is set. */
197
205
  export function recordPublished({ registry, name, version }, { file = VERSIONS_FILE, root = rightkitRoot, log = (line) => process.stdout.write(`${line}\n`), strict = false } = {}) {