@rightkit/release 0.2.174 → 0.4.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.
Files changed (82) hide show
  1. package/README.md +200 -2
  2. package/addon-command.mjs +3 -4
  3. package/android-play-upload.mjs +2 -1
  4. package/apple-profile-renew.mjs +11 -3
  5. package/artifact-storage.mjs +35 -2
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +1 -1
  8. package/build-release.mjs +138 -47
  9. package/cli/right-release.mjs +40 -5
  10. package/codesign-environment.mjs +2 -2
  11. package/command-spec.mjs +80 -0
  12. package/content-digest.mjs +209 -0
  13. package/deps-cargo.mjs +55 -0
  14. package/deps.mjs +7 -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/dist-scan.mjs +102 -0
  21. package/dmg-build.mjs +53 -4
  22. package/github-updater-release.mjs +70 -7
  23. package/icon-pack.mjs +117 -0
  24. package/in-place-install.mjs +17 -14
  25. package/init-command.mjs +116 -0
  26. package/install-app-windows.mjs +24 -7
  27. package/install-app.mjs +56 -42
  28. package/ios-asc-credentials.mjs +163 -0
  29. package/ios-build-context.mjs +129 -0
  30. package/ios-build-upload.mjs +282 -0
  31. package/ios-credentials-command.mjs +70 -0
  32. package/ios-dev.mjs +248 -0
  33. package/ios-host.mjs +76 -0
  34. package/ios-lock.mjs +75 -0
  35. package/ios-package-recipe.mjs +707 -0
  36. package/ios-testflight.mjs +266 -0
  37. package/ios-upload.mjs +313 -0
  38. package/local-publish.mjs +390 -47
  39. package/macho-approval-hash.mjs +181 -0
  40. package/managed-cargo.mjs +57 -1
  41. package/media-sidecar.mjs +20 -4
  42. package/mirror-root-artifact.mjs +74 -22
  43. package/native-cargo-layout.mjs +47 -4
  44. package/npm-license-notices.mjs +159 -0
  45. package/ownership-command.mjs +2 -1
  46. package/ownership-dependency-allowlist.json +2 -1
  47. package/ownership-evidence.mjs +30 -18
  48. package/ownership-key-command.mjs +69 -2
  49. package/ownership-mandatory-set.mjs +66 -16
  50. package/ownership-repositories.json +1 -1
  51. package/ownership-scope.mjs +94 -0
  52. package/ownership.mjs +86 -11
  53. package/package-recipe-targets.mjs +56 -2
  54. package/package.json +1 -1
  55. package/pe-content.mjs +133 -0
  56. package/preflight.mjs +14 -7
  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 +83 -5
  61. package/publish-cargo.mjs +124 -18
  62. package/publish-update.mjs +15 -2
  63. package/qa-build-features.mjs +16 -13
  64. package/qa-contract.mjs +30 -2
  65. package/qa-control-scan.mjs +4 -0
  66. package/release-dependencies-command.mjs +45 -0
  67. package/release-dependencies.mjs +60 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +62 -24
  71. package/release.mjs +144 -21
  72. package/rightkit-ownership.json +46 -2
  73. package/rightkit-versions.json +380 -189
  74. package/runtime-budgets.mjs +134 -23
  75. package/sign-updater.mjs +56 -59
  76. package/source-gate.mjs +8 -1
  77. package/stage-sidecar.mjs +634 -69
  78. package/target-distribution.mjs +5 -0
  79. package/test-command.mjs +190 -0
  80. package/toolchain-swift.mjs +239 -0
  81. package/upload-release.mjs +7 -2
  82. package/web-build.mjs +67 -0
package/build-release.mjs CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  closeSync,
6
6
  copyFileSync,
7
7
  existsSync,
8
- mkdtempSync,
9
8
  mkdirSync,
10
9
  openSync,
11
10
  readFileSync,
@@ -18,8 +17,8 @@ import {
18
17
  unlinkSync,
19
18
  writeFileSync,
20
19
  } from "node:fs";
21
- import { tmpdir } from "node:os";
22
20
  import path from "node:path";
21
+ import { iosOwnershipScope } from "./ownership-scope.mjs";
23
22
  import { spawn, spawnSync } from "./background-process.mjs";
24
23
  import { fileURLToPath, pathToFileURL } from "node:url";
25
24
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
@@ -37,6 +36,7 @@ import { ensureLegacyTargetLink, resolveReleaseCacheMode } from "./release-cache
37
36
  import { resolveTargetRoot } from "./cargo-target.mjs";
38
37
  import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
39
38
  import { collectSwiftToolVersions, planSwiftBuild } from "./swift-build-context.mjs";
39
+ import { planIosNativeBuild, resolveIosBuildContext } from "./ios-build-context.mjs";
40
40
  import { packageCommandOptional, requiredBuildExecutables, resolveProjectType, targetUsesMsix } from "./project-type.mjs";
41
41
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
42
42
  import { assertCleanSource } from "./source-gate.mjs";
@@ -46,6 +46,15 @@ import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimi
46
46
  import { beginArtifactLease, completeArtifactLease, materializeArtifacts, retainArtifacts, resolveArtifactDir, resolveManagedRoot } from "./artifact-storage.mjs";
47
47
  import { macosCodesignEnvironment } from "./codesign-environment.mjs";
48
48
  import { ANDROID_RECEIPT_NAME, androidSealEntry, validateAndroidTarget, verifyAndroidReceipt } from "./android-release.mjs";
49
+ import { IOS_RECEIPT_NAME, iosSealEntry, verifyIosReceipt } from "./ios-package-recipe.mjs";
50
+ import { expandIosPackageTarget } from "./package-recipe-targets.mjs";
51
+ import { assertUpdaterSignature } from "./github-updater-release.mjs";
52
+
53
+ // The iOS lane's own packaging code, kept out of the win/mac fingerprint for the same reason as the Android one.
54
+ const IOS_PIPELINE_FINGERPRINT = createHash("sha256")
55
+ .update(["ios-package-recipe.mjs", "ios-host.mjs"].map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
56
+ .digest("hex")
57
+ .slice(0, 16);
49
58
 
50
59
  // Fingerprint of the pipeline code that can change the bytes we ship or the way
51
60
  // they are signed — deliberately NOT the package version, which moves for docs
@@ -66,15 +75,18 @@ const ANDROID_PIPELINE_FINGERPRINT = createHash("sha256")
66
75
  .slice(0, 16);
67
76
 
68
77
  /** Assemble preflight inputs from the app's own files (mirrors release.mjs). */
69
- function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativeLayout, target }) {
78
+ function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativeLayout, target, pinnedCargoLocks = [] }) {
70
79
  let version;
71
80
  try {
72
81
  version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
73
82
  } catch {
74
83
  version = undefined;
75
84
  }
85
+ // An iOS target is built from its Xcode project and local packages only; the ownership scan covers just those roots.
86
+ const ownershipScope = platform === "ios" ? iosOwnershipScope({ repoRoot, appRoot, target, nativeAssembly: config?.nativeAssembly }) : null;
76
87
  return collectPreflight({
77
88
  platform,
89
+ ownershipScope,
78
90
  appRoot,
79
91
  repoRoot,
80
92
  app: config.app,
@@ -83,8 +95,11 @@ function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativ
83
95
  // collision is already enforced downstream, and duplicating it here would
84
96
  // reject a legitimate resume of an in-flight build.
85
97
  version: undefined,
86
- cargoLockPaths: nativeLayout?.lockCandidates ?? [],
98
+ cargoLockPaths: [...(nativeLayout?.lockCandidates ?? []), ...pinnedCargoLocks],
87
99
  projectType: resolveProjectType({ appRoot, config, target }),
100
+ // A native iOS app has no broker target or bridge; it still gets the Rust budget and lock checks when it pins a Cargo.lock.
101
+ rust: Boolean(nativeLayout) || pinnedCargoLocks.length > 0,
102
+ bridge: Boolean(nativeLayout),
88
103
  msix: targetUsesMsix(target),
89
104
  });
90
105
  }
@@ -109,7 +124,7 @@ for (let i = 0; i < args.length; i += 1) {
109
124
  else if (arg === "-h" || arg === "--help") usage(0);
110
125
  else fail(`unknown argument: ${arg}`);
111
126
  }
112
- if (platform !== "win" && platform !== "mac" && platform !== "android") fail("--platform must be win, mac or android");
127
+ if (platform !== "win" && platform !== "mac" && platform !== "android" && platform !== "ios") fail("--platform must be win, mac, android or ios");
113
128
  if (resumeFrom && !["prepackage", "sign", "package", "post-sign", "harden"].includes(resumeFrom)) fail("--resume-from must be prepackage, sign, package, post-sign, or harden");
114
129
 
115
130
  const configPath = path.resolve(configName);
@@ -141,8 +156,8 @@ if (dryRun) {
141
156
  const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
142
157
  if (!config?.app || !config?.version) fail("release config must expose app and version");
143
158
  const target = config.targets?.[platform];
144
- // Android builds with target.gradle (validated by release.mjs), not a package command.
145
- if (platform !== "android" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
159
+ // Android builds with target.gradle (validated by release.mjs), not a package command; iOS with targets.ios.iosPackage.
160
+ if (platform !== "android" && platform !== "ios" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
146
161
  console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
147
162
  console.log(`config: ${configPath}`);
148
163
  console.log(`commit: ${commit}`);
@@ -166,7 +181,7 @@ try {
166
181
  // turn an already-built, signed installer into a needless full rebuild.
167
182
  if (!resumeFrom) {
168
183
  assertCleanSource({
169
- status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]),
184
+ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { trim: false }), // untrimmed: a leading space is the first record's X column (#782)
170
185
  commandId: "right-release build",
171
186
  });
172
187
  }
@@ -181,24 +196,41 @@ try {
181
196
  fail("app and version must be safe release-id components");
182
197
  }
183
198
  const target = config.targets?.[platform];
184
- if (platform !== "android" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
199
+ if (platform !== "android" && platform !== "ios" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
200
+ // iOS: the same strict validation release.mjs applies; yields the IPA path and the team and bundle id the seal pins.
201
+ let iosSpec = null;
202
+ if (platform === "ios") {
203
+ try { iosSpec = expandIosPackageTarget(target, { appRoot }).recipe?.spec ?? null; } catch (error) { fail(`${config.app} ios ${error.message}`); }
204
+ if (!iosSpec) fail(`${config.app} ios must declare targets.ios.iosPackage`);
205
+ }
185
206
  // Same validation release.mjs applies; yields the normalised upload-certificate pin and the AAB path.
186
207
  let androidTarget = null;
187
208
  if (platform === "android") {
188
209
  try { androidTarget = validateAndroidTarget(target, { root: appRoot }); } catch (error) { fail(`${config.app} android ${error.message}`); }
189
210
  }
190
211
  const isSwiftBuild = resolveProjectType({ appRoot, config, target }) === "swiftpm";
191
- const nativeLayout = resolveNativeCargoLayout({ appRoot, config });
212
+ // A native iOS app (xcodegen project; no Rust library unless nativeAssembly.cargoManifest declares one) has no src-tauri and no
213
+ // declared nativeAssembly manifest: it owns no Cargo layout (null), and every Cargo-derived value below is skipped for it.
214
+ // The Xcode tools are asserted present, then probed, inside resolveIosBuildContext (a missing tool fails with the normal message).
215
+ const iosContext = iosSpec !== null
216
+ ? resolveIosBuildContext({ appRoot, config, target, iosSpec, exec: (cmd, runArgs) => commandOutputAt(cmd, runArgs, appRoot), assertExecutables })
217
+ : null;
218
+ const nativeIos = iosContext?.nativeIos ?? false;
219
+ const nativeLayout = iosContext ? iosContext.nativeLayout : resolveNativeCargoLayout({ appRoot, config });
192
220
  if (platform === "win" && target.signingContract === "windows-raw-exe-authenticode-before-nsis-v1") {
193
221
  assertNsisInPlaceUpgradeContract(appRoot, target.nsisUpgradeContract);
194
222
  }
195
- const { requiredInputs, buildInputs } = resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target });
223
+ const { requiredInputs, buildInputs, cargoLockPaths: pinnedCargoLocks } = resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target });
196
224
  const dirtyInputs = dirtyBuildInputs(repoRoot, buildInputs, commit);
197
225
  if (dirtyInputs.length > 0) fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
198
- const toolVersions = isSwiftBuild ? collectSwiftToolVersions({ packageManager: config.packageManager, exec: commandOutput }) : collectToolVersions(config.packageManager ?? "pnpm");
226
+ const hasPackageJson = existsSync(path.join(appRoot, "package.json"));
227
+ // iOS records the Xcode toolchain (and xcodegen when a project.yml is named) beside the Node and Rust tools.
228
+ const toolVersions = isSwiftBuild ? collectSwiftToolVersions({ packageManager: config.packageManager, exec: commandOutput })
229
+ : iosContext ? iosContext.toolVersions
230
+ : collectToolVersions(config.packageManager ?? "pnpm", appRoot);
199
231
  const inputHashes = hashInputs(requiredInputs);
200
232
  const releaseId = `${config.app}-${config.version}-${shortCommit}`;
201
- const platformDir = platform === "win" ? "windows" : platform === "android" ? "android" : "mac";
233
+ const platformDir = platform === "win" ? "windows" : platform === "android" ? "android" : platform === "ios" ? "ios" : "mac";
202
234
  const stateRoot = path.join(vaultRoot, "state", releaseId, platformDir);
203
235
  const signingIdentity = platform === "win" ? {
204
236
  contract: target.signingContract ?? "<missing>",
@@ -230,17 +262,27 @@ try {
230
262
  uploadCertSha256: androidTarget.signing.expectedCertSha256,
231
263
  // release.mjs writes the AAB receipt into RIGHT_RELEASE_STATE_ROOT (= stateRoot) as android-aab.json.
232
264
  receiptInputs: [path.resolve(stateRoot, ANDROID_RECEIPT_NAME)],
265
+ } : platform === "ios" ? {
266
+ contract: "ios-xcodebuild-archive-automatic-v1",
267
+ pipeline: IOS_PIPELINE_FINGERPRINT,
268
+ configSha256: hashFile(configPath),
269
+ // The team and bundle id the IPA receipt must carry; the upload re-checks both from the sealed manifest.
270
+ teamId: iosSpec.teamId,
271
+ bundleId: iosSpec.bundleId,
272
+ // release.mjs writes the IPA receipt into RIGHT_RELEASE_STATE_ROOT (= stateRoot) as ios-archive.json.
273
+ receiptInputs: [path.resolve(stateRoot, IOS_RECEIPT_NAME)],
233
274
  } : null;
234
275
  if (signingIdentity) inputHashes[".right-release/signing-identity.json"] = hashFileText(JSON.stringify(signingIdentity));
235
- const cargoLock = nativeLayout.lockPath;
236
- const cargoToml = nativeLayout.manifestPath;
276
+ const cargoLock = nativeLayout?.lockPath;
277
+ const cargoToml = nativeLayout?.manifestPath;
237
278
  // A broker-managed host is not identified by RIGHTKIT_BUILD_BROKER_SOCKET alone:
238
279
  // that variable is exported by login shells only, while the managed cargo shim is
239
280
  // on PATH for every shell. Detecting on the variable alone made agent (non-login)
240
281
  // shells take the Cache V2 branch, bridge resolved native target into release cache,
241
282
  // and then lose CARGO_TARGET_DIR to the broker's unconditional override — so the
242
283
  // app bundle landed in the broker workspace and packaging failed on an empty cache.
243
- const swiftPlan = isSwiftBuild ? planSwiftBuild({ appRoot, vaultRoot, platform, target, toolVersions }) : null;
284
+ // swiftPlan is the "no Rust host" plan for SwiftPM and for a Cargo-less native iOS app alike: same shape, same skips.
285
+ const swiftPlan = isSwiftBuild ? planSwiftBuild({ appRoot, vaultRoot, platform, target, toolVersions }) : nativeIos ? planIosNativeBuild({ appRoot, vaultRoot, platform, toolVersions }) : null;
244
286
  // SwiftPM targets have no Cargo manifest: no broker cargo target, no Rust cache identity.
245
287
  const managedCargoTarget = swiftPlan ? swiftPlan.managedCargoTarget : brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
246
288
  const cacheIdentity = swiftPlan ? swiftPlan.identity : resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
@@ -267,7 +309,7 @@ try {
267
309
  // Preflight BEFORE any compile. These preconditions used to be discovered one
268
310
  // per build cycle, minutes deep, with errors that named a symptom instead of a
269
311
  // cause. Blocking problems stop here, in seconds, naming the fix.
270
- const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform, nativeLayout, target });
312
+ const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform, nativeLayout, target, pinnedCargoLocks });
271
313
  const preflightBlockers = preflightFailures(preflight);
272
314
  if (preflightBlockers.length > 0) {
273
315
  fail(`preflight found ${preflightBlockers.length} blocking problem(s):\n${formatPreflight(preflightBlockers)}`);
@@ -323,6 +365,7 @@ try {
323
365
  env.RIGHT_RELEASE_ARTIFACT_DIR = managedArtifactDir;
324
366
  const buildRoot = managedCargoTarget ? null : path.join(vaultRoot, "build", config.app, config.version, shortCommit, platformDir);
325
367
  env.RIGHT_RELEASE_STATE_ROOT = stateRoot;
368
+ env.RIGHT_RELEASE_VAULT_STATE_ROOT = path.join(vaultRoot, "state");
326
369
  const lockPayload = {
327
370
  schema: 1,
328
371
  releaseId,
@@ -336,7 +379,7 @@ try {
336
379
  tools: toolVersions,
337
380
  createdAt: new Date().toISOString(),
338
381
  };
339
- const targetLink = nativeLayout.targetLink;
382
+ const targetLink = nativeLayout?.targetLink;
340
383
  // ownedRoot is the per-app cache parent holding every fingerprint dir, so a
341
384
  // link left by an earlier fingerprint is recognised as ours and repointed
342
385
  // instead of hard-stopping the build.
@@ -367,7 +410,7 @@ try {
367
410
  const volumes = inspectWriteVolumes({ repoRoot, vaultRoot, cacheRoot: sharedCacheRoot });
368
411
  assertWriteVolumeFloors({ volumes, policy, configuredRepoMinimumBytes: Math.max(Number(target.preflight?.minFreeGb ?? 25), 25) * 1024 ** 3 });
369
412
  } else assertDiskSpace(repoRoot, target.preflight?.minFreeGb ?? 20);
370
- assertExecutables([...requiredBuildExecutables({ projectType: resolveProjectType({ appRoot, config, target }), packageManager: config.packageManager ?? "pnpm", usesPackageManager: Boolean(config.packageManager) || existsSync(path.join(appRoot, "package.json")) }), ...(target.preflight?.executables ?? [])]);
413
+ assertExecutables([...requiredBuildExecutables({ projectType: resolveProjectType({ appRoot, config, target }), packageManager: config.packageManager ?? "pnpm", usesPackageManager: nativeIos ? hasPackageJson : Boolean(config.packageManager) || hasPackageJson, usesCargo: !nativeIos }), ...(iosContext?.prepareExecutables ?? []), ...(target.preflight?.executables ?? [])]);
371
414
  for (const name of target.preflight?.env ?? []) if (!process.env[name]) fail(`missing required environment variable: ${name}`);
372
415
  for (const command of target.preflight?.commands ?? []) runChecked(command.cmd, command.args ?? [], path.resolve(appRoot, command.cwd ?? "."), env);
373
416
  if (buildRoot) mkdirSync(buildRoot, { recursive: true });
@@ -394,7 +437,8 @@ try {
394
437
  if (linked.action === "refused") fail(linked.reason);
395
438
  if (linked.action === "moved-aside") console.error(`[right-release] hosted CI: moved existing ${targetLink} aside to ${linked.aside}`);
396
439
  }
397
- if (resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager) await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget, path.join(stateRoot, "stall-install.log"));
440
+ // A Cargo-less native iOS app installs Node dependencies only when it has a package.json.
441
+ if (nativeIos ? hasPackageJson : resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager) await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget, path.join(stateRoot, "stall-install.log"));
398
442
  },
399
443
  build: async () => {
400
444
  throwIfInterrupted();
@@ -409,7 +453,10 @@ try {
409
453
  );
410
454
  checkpoint(stateRoot, "build_complete");
411
455
  checkpoint(stateRoot, "signed");
412
- checkpoint(stateRoot, "hardened");
456
+ // Truthful checkpoint: the worker (release.mjs) runs the hardening scan only when it has scan
457
+ // targets and fails the whole build when the scan fails, so reaching here with targets means
458
+ // the scan passed. With no targets the worker ran no scan — never claim "hardened".
459
+ if (hardeningScanTargets(target).length > 0) checkpoint(stateRoot, "hardened");
413
460
  },
414
461
  // Product-specific checks that must run after the signed/hardened bytes
415
462
  // exist but before they become immutable. This is intentionally a small
@@ -427,7 +474,7 @@ try {
427
474
  });
428
475
  },
429
476
  seal: async ({ sealedDir }) => {
430
- sealRelease({ configRoot: appRoot, managedCargoTarget, androidTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot });
477
+ sealRelease({ configRoot: appRoot, managedCargoTarget, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot });
431
478
  verifySealedRelease(sealedDir);
432
479
  if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
433
480
  checkpoint(stateRoot, "sealed");
@@ -449,7 +496,14 @@ try {
449
496
  }
450
497
 
451
498
  function hashInputs(files) {
452
- return Object.fromEntries(files.map((file) => [path.relative(path.dirname(path.dirname(file)), file), hashFile(file)]));
499
+ const hashes = {};
500
+ for (const file of files) {
501
+ const key = path.relative(path.dirname(path.dirname(file)), file);
502
+ // Two inputs (an app file and a shared crate's file, say) must never share a key: one would silently replace the other.
503
+ if (Object.hasOwn(hashes, key)) fail(`release inputs share the hash key ${key}; rename one so every input is sealed`);
504
+ hashes[key] = hashFile(file);
505
+ }
506
+ return hashes;
453
507
  }
454
508
 
455
509
  function hashFile(file) {
@@ -467,7 +521,7 @@ function sqlCipherFeatures(text) {
467
521
  .filter((value) => /sqlcipher|openssl/i.test(value));
468
522
  }
469
523
 
470
- function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot }) {
524
+ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot }) {
471
525
  // Every app's right-release.config.mjs names installer/updater artifacts
472
526
  // relative to its resolved native target. On a broker-managed host that
473
527
  // directory is never populated (the broker owns CARGO_TARGET_DIR and
@@ -476,6 +530,7 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
476
530
  // own target root instead, or every managed build fails at seal after a
477
531
  // full compile, sign, and notarize.
478
532
  const targetPrefix = "src-tauri/target/";
533
+ // A Cargo-less iOS app has no native layout and no managed cargo target: only the default prefix is ever compared.
479
534
  const effectiveTargetPrefix = nativeLayout?.targetPrefix ?? targetPrefix;
480
535
  const resolveArtifactPath = (file) => {
481
536
  const normalized = file.replaceAll(path.win32.sep, "/");
@@ -507,14 +562,28 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
507
562
  // updater block: `target.artifacts` entries (paths or { file }) seal with the
508
563
  // "portable" role. On android the declared AAB seals with the "aab" role instead.
509
564
  const aabSource = platform === "android" ? resolveArtifactPath(androidTarget.aab.file) : null;
565
+ const ipaSource = platform === "ios" ? resolveArtifactPath(iosSpec.ipa) : null;
510
566
  for (const artifact of target.artifacts ?? []) {
511
567
  const file = typeof artifact === "string" ? artifact : artifact?.file;
512
568
  if (typeof file !== "string" || !file) fail("target.artifacts entries must be paths or { file }");
513
569
  const source = resolveArtifactPath(file);
514
- addSource(source, source === aabSource ? "aab" : "portable");
570
+ addSource(source, source === aabSource ? "aab" : source === ipaSource ? "ipa" : "portable");
515
571
  }
516
572
  if (sources.size === 0) fail(`nothing to seal: declare installer.artifacts, updater.artifacts or artifacts for the ${platform} target`);
517
573
  for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
574
+ // Every minisign updater requires an explicit trust root & full-byte signature
575
+ // verification before any seal directory or artifact copy is created.
576
+ const updaterPublicKey = config.distribution?.updaterPublicKey ?? config.distribution?.updaterPubkey ?? target?.updater?.pubkey ?? target?.updater?.publicKey;
577
+ if ((target.updater?.artifacts?.length ?? 0) > 0 && target.updater?.kind !== "appinstaller") {
578
+ if (typeof updaterPublicKey !== "string" || !updaterPublicKey.trim()) fail("updater public key is required to verify updater artifacts before sealing");
579
+ for (const artifact of target.updater?.artifacts ?? []) {
580
+ const source = resolveArtifactPath(artifact.file);
581
+ const signature = readFileSync(resolveArtifactPath(artifact.signature), "utf8").trim();
582
+ if (!signature) fail(`updater signature is empty, refusing to seal: ${artifact.signature}`);
583
+ try { assertUpdaterSignature(readFileSync(source), signature, updaterPublicKey, `signature of ${path.basename(artifact.file)}`); }
584
+ catch (error) { fail(`updater signature rejected at seal: ${error.message}`); }
585
+ }
586
+ }
518
587
  // Android: the build receipt must describe exactly the AAB being sealed (bytes re-hashed) and its signer must be the pinned
519
588
  // upload certificate. Checked before anything is copied, so a mismatch leaves no partial seal.
520
589
  let androidReceipt = null;
@@ -525,6 +594,16 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
525
594
  try { androidReceipt = verifyAndroidReceipt(receiptPath, aabSource); } catch (error) { fail(`android AAB receipt rejected: ${error.message}`); }
526
595
  if (androidReceipt.signer?.certSha256 !== signingIdentity.uploadCertSha256) fail("android AAB receipt signer certificate is not the pinned upload certificate");
527
596
  }
597
+ // iOS: the receipt must describe exactly the IPA being sealed (bytes re-hashed), for the pinned team and bundle id.
598
+ let iosReceipt = null;
599
+ if (platform === "ios") {
600
+ if (!sources.has(ipaSource)) fail("ios seal requires targets.ios.artifacts to list iosPackage.ipa");
601
+ const [receiptPath] = signingIdentity.receiptInputs;
602
+ if (!existsSync(receiptPath)) fail(`no signing receipts found to seal; expected at least ${receiptPath}`);
603
+ try { iosReceipt = verifyIosReceipt(receiptPath, ipaSource); } catch (error) { fail(`ios IPA receipt rejected: ${error.message}`); }
604
+ if (iosReceipt.teamId !== signingIdentity.teamId || iosReceipt.bundleId !== signingIdentity.bundleId) fail("ios IPA receipt team or bundle id is not the one pinned by the release config");
605
+ if (iosReceipt.sourceCommit !== commit) fail(`ios IPA receipt was built from ${String(iosReceipt.sourceCommit)}, not the release commit ${commit}`);
606
+ }
528
607
  // rightkit-ffmpeg decode sidecar: release.mjs signed the binaries with the app and wrote media-sidecar.json. The seal
529
608
  // refuses a release without it, re-checks its shape against the configuration, and (Windows) that every sidecar's
530
609
  // signed digest is one the sealed raw-EXE signing receipt recorded. The hashes then live in the sealed manifest.
@@ -557,6 +636,8 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
557
636
  copyFileSync(source, path.join(temp, name));
558
637
  files.push(androidReceipt && source === aabSource && name === androidReceipt.file.name
559
638
  ? androidSealEntry(androidReceipt)
639
+ : iosReceipt && source === ipaSource && name === iosReceipt.file.name
640
+ ? iosSealEntry(iosReceipt)
560
641
  : { role: [...roles].sort().join("+"), name, sha256: hashFile(source), sizeBytes: statSync(source).size });
561
642
  artifactInputs.push({ source, name });
562
643
  }
@@ -575,7 +656,12 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
575
656
  routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: signatureSource ? sourceNames.get(signatureSource) : undefined, bucket: "public", key: patchKey, platform: artifact.platform });
576
657
  routes.update.push({ role: "updater", name: sourceNames.get(source), signature: signatureSource ? sourceNames.get(signatureSource) : undefined, bucket: "private", key: routeKey, platform: artifact.platform });
577
658
  }
578
- writeJson(path.join(temp, "hardening.json"), { schema: 1, passed: true, checkedAt: new Date().toISOString(), files: files.map((file) => file.sha256) });
659
+ // Derived from the worker's actual scan, never asserted unconditionally: release.mjs scans exactly
660
+ // `target.hardening ?? target.artifacts` and fails the build on scan failure, so a completed build with
661
+ // targets passed; with no targets no scan ran and the status is "skipped", not passed.
662
+ const hardeningTargets = hardeningScanTargets(target);
663
+ const hardeningScanned = hardeningTargets.length > 0;
664
+ writeJson(path.join(temp, "hardening.json"), { schema: 1, status: hardeningScanned ? "passed" : "skipped", passed: hardeningScanned, checkedAt: new Date().toISOString(), targets: hardeningTargets.length, files: hardeningScanned ? files.map((file) => file.sha256) : [] });
579
665
  const manifest = {
580
666
  schema: 1,
581
667
  releaseId,
@@ -606,7 +692,7 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
606
692
  routes,
607
693
  inputs: inputHashes,
608
694
  tools: toolVersions,
609
- checkpoints: ["preflight_complete", "build_complete", "signed", "hardened", "sealed"],
695
+ checkpoints: ["preflight_complete", "build_complete", "signed", ...(hardeningScanned ? ["hardened"] : []), "sealed"],
610
696
  sealedAt: new Date().toISOString(),
611
697
  };
612
698
  writeJson(path.join(temp, "release-manifest.json"), manifest);
@@ -627,21 +713,26 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, nativeLayo
627
713
  else if (retention.removed.length) console.log(`[right-release] artifact retention removed ${retention.removed.join(", ")}`);
628
714
  }
629
715
 
630
- function collectToolVersions(packageManager) {
631
- const probeDir = mkdtempSync(path.join(tmpdir(), "right-release-tool-probe-"));
632
- try {
633
- const rustcVerbose = commandOutputAt("rustc", ["-vV"], probeDir);
634
- return {
635
- node: process.version,
636
- packageManager: `${packageManager} ${commandOutput(packageManager, ["--version"])}`,
637
- cargo: commandOutputAt("cargo", ["--version"], probeDir),
638
- rustc: rustcVerbose,
639
- rustHost: rustcVerbose.match(/^host:\s*(.+)$/m)?.[1] ?? "unknown",
640
- sccache: commandExists("sccache") ? commandOutput("sccache", ["--version"]) : null,
641
- };
642
- } finally {
643
- rmSync(probeDir, { recursive: true, force: true });
644
- }
716
+ /** The exact scan-target expression the worker uses (release.mjs: `target.hardening ?? target.artifacts ?? []`).
717
+ * Recipe expansion in the worker preserves both fields (`...rest`), so the raw config target gives the same list. */
718
+ function hardeningScanTargets(target) {
719
+ return target?.hardening ?? target?.artifacts ?? [];
720
+ }
721
+
722
+ // Probes run with cwd = the app root (bogusyogi/rightkit#783): on an agent host `cargo` is the
723
+ // RustRight shim, which refuses any cwd outside a primary checkout, so a $TMPDIR probe dir failed
724
+ // with RK_NOT_PRIMARY_CHECKOUT before the first build step. The app root also makes rustc report
725
+ // the toolchain the app's rust-toolchain.toml selects, which is the one the build uses.
726
+ function collectToolVersions(packageManager, probeDir = process.cwd()) {
727
+ const rustcVerbose = commandOutputAt("rustc", ["-vV"], probeDir);
728
+ return {
729
+ node: process.version,
730
+ packageManager: `${packageManager} ${commandOutput(packageManager, ["--version"])}`,
731
+ cargo: commandOutputAt("cargo", ["--version"], probeDir),
732
+ rustc: rustcVerbose,
733
+ rustHost: rustcVerbose.match(/^host:\s*(.+)$/m)?.[1] ?? "unknown",
734
+ sccache: commandExists("sccache") ? commandOutput("sccache", ["--version"]) : null,
735
+ };
645
736
  }
646
737
 
647
738
  async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null, { livenessOwnerTimeoutMs = null } = {}) {
@@ -801,14 +892,14 @@ function commandOutput(cmd, runArgs) {
801
892
  }
802
893
  }
803
894
 
804
- function git(cwd, runArgs) {
805
- return commandOutputAt("git", runArgs, cwd);
895
+ function git(cwd, runArgs, { trim = true } = {}) {
896
+ return commandOutputAt("git", runArgs, cwd, { trim });
806
897
  }
807
898
 
808
- function commandOutputAt(cmd, runArgs, cwd) {
899
+ function commandOutputAt(cmd, runArgs, cwd, { trim = true } = {}) {
809
900
  const result = spawnSync(cmd, runArgs, { cwd, encoding: "utf8", windowsHide: true });
810
901
  if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${(result.stderr ?? "").trim()}`);
811
- return result.stdout.trim();
902
+ return trim ? result.stdout.trim() : result.stdout;
812
903
  }
813
904
 
814
905
  function runChecked(cmd, runArgs, cwd, env = process.env) {
@@ -843,7 +934,7 @@ function writeJson(file, value) {
843
934
  }
844
935
 
845
936
  function usage(code) {
846
- console.log("usage: right-release build --platform win|mac|android [--config right-release.config.mjs] [--dry-run]");
937
+ console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.mjs] [--dry-run]");
847
938
  process.exit(code);
848
939
  }
849
940
 
@@ -22,10 +22,17 @@ const commands = new Map([
22
22
  ["sign-candidate", "sign-candidate.mjs"],
23
23
  ["engine-locate", "engine-locator.mjs"],
24
24
  ["stage-sidecar", "stage-sidecar-command.mjs"],
25
+ ["init", "init-command.mjs"],
26
+ ["pe", "pe-content.mjs"],
27
+ ["icons", "icon-pack.mjs"],
25
28
  ["recipe-build", "recipe-build.mjs"],
26
29
  ["signing-agent", "signing-agent-command.mjs"],
27
30
  ["create-mac-updater", "create-mac-updater.mjs"],
28
31
  ["mirror-root-artifact", "mirror-root-artifact.mjs"],
32
+ ["test", "test-command.mjs"],
33
+ ["web-build", "web-build.mjs"],
34
+ ["license-notices", "npm-license-notices.mjs"],
35
+ ["toolchain", "toolchain-swift.mjs"],
29
36
  ["github", "github-release.mjs"],
30
37
  ["cargo-target", "cargo-target.mjs"],
31
38
  ["qa", "release-qa-command.mjs"],
@@ -35,8 +42,11 @@ const commands = new Map([
35
42
  ["ownership-mandatory", "ownership-mandatory-set.mjs"],
36
43
  ["swift-package", "swift-package-command.mjs"],
37
44
  ["bundle-scan", "runtime-bundle-scan.mjs"],
45
+ ["check-release-dependencies", "release-dependencies-command.mjs"],
46
+ ["dist-scan", "dist-scan.mjs"],
38
47
  ["identity-scan", "packaged-app-identity.mjs"],
39
48
  ["apple-profile-create", "apple-profile-create.mjs"],
49
+ ["ios-credentials", "ios-credentials-command.mjs"],
40
50
  ]);
41
51
 
42
52
  const args = process.argv.slice(2);
@@ -47,7 +57,7 @@ if (first === "dev") {
47
57
  process.exitCode = 1;
48
58
  });
49
59
  } else {
50
- const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
60
+ const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target", "web-build", "test"]);
51
61
 
52
62
  try {
53
63
  if (githubBuildCommands.has(first)
@@ -124,6 +134,8 @@ if (first === "--version" || first === "-v") {
124
134
  runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
125
135
  } else if (first === "generate-dmg-background") {
126
136
  runBinary("python3", [path.join(packageRoot, "generate-dmg-background.py"), ...args.slice(1)]);
137
+ } else if (first === "build" && args.includes("--web")) {
138
+ run("web-build.mjs", args.slice(1).filter((arg) => arg !== "--web"));
127
139
  } else if (commands.has(first)) {
128
140
  run(commands.get(first), args.slice(1));
129
141
  } else if (first.startsWith("-")) {
@@ -183,11 +195,15 @@ function printHelp() {
183
195
  console.log(`right-release <command> [options]
184
196
 
185
197
  Commands:
198
+ pe digest <file> [--algorithm sha256|sha384|sha512] [--json] | pe imports <file> [--allow dll,...] [--allow-file <path>] Signature-independent PE content digest; import-closure check
199
+ icons ico|icns --out <file> --frame <PNG>... [--dir <folder>] Pack hand-tuned per-size PNG frames into an ICO or ICNS
200
+ init --name <Product Name> --id <reverse.dns.bundle.id> [--slug <lowercase>] [--dir <root>] [--dry-run] Rename the desktop app template into a real app, in place
186
201
  stage-sidecar [--config <file>] [--target <triple>] [--json] Stage declarative sidecars into src-tauri/binaries (Tauri externalBin naming)
187
202
  recipe-build --plan <json> | --plan-file <path> [--app-root <dir>] Run a package recipe: prepare steps, the Tauri build, postBuild steps (argv, no shell, fail closed)
188
203
  dmg build --app <.app> --out <dmg> --volname <name> [--background <png>] [--spec <appdmg.json>]
189
204
  [--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h] [--window-position x,y]
190
- Build branded UDZO image without Finder
205
+ [--sign [--identifier <codesign-identifier>] [--receipt <path>]]
206
+ Build branded UDZO image without Finder; --sign then Developer ID signs the image (secure timestamp, no hardened runtime; never notarizes)
191
207
  install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id <id>] [--dest /Applications]
192
208
  [--relaunch|--no-relaunch] [--json]
193
209
  Verify a signed DMG, quit, replace & relaunch hidden; no UI, no osascript (macOS)
@@ -196,12 +212,21 @@ Commands:
196
212
  Windows: run the installer in place at the fixed folder; payload-only rollback (never the whole folder, user data beside the app is never copied)
197
213
  build [--platform mac|win] [--skip-checks] [--resume-from <phase>]
198
214
  Resume macOS at prepackage, sign, package, post-sign, or harden
215
+ build --web [--config <file>] [--dry-run] Web frontend build only (config web.build steps); no Tauri, signing or packaging (alias: web-build)
216
+ test [--config <file>] [--suite <name>[,<name>]]... [--runner <name>] [--list] [--json] [-- <args>]
217
+ Run the app's declared test suites (config tests.suites / tests.runners) in order; stops at the first failure
218
+ license-notices npm [--config <file>] [--output <file>] [--product-name <name>] [--check]
219
+ Write the third-party npm license appendix HTML from pnpm licenses (config legal.npmNotices); --check only compares
220
+ toolchain swift --version <x.y.z> --sha256 <pin> [--url <https url>] [--github-env] [--json]
221
+ Windows: install the pinned swift.org Swift toolchain (sha256 verified before it runs), check swift --version, find the runtime bin; --github-env exports what the installer changed to GITHUB_ENV / GITHUB_PATH
199
222
  dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
200
223
  Incremental unsigned build & optional local install
201
224
  swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [--dry-run] [--no-sign] [--sign] [--notarize] [--profile <name>]
202
225
  Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
203
226
  bundle-scan [--allow-name <basename>]... <bundle-root>...
204
227
  Fail when a bundle carries Node/Python runtimes or script sources
228
+ dist-scan --dist <dir> --forbid <string>... [--forbid-file <path>] [--forbid-name <regex>]... [--ext js,css,...]
229
+ Fail when a built web dist/ carries a forbidden marker string or mock/fixture chunk name (dev, mock or fixture code in release)
205
230
  sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]
206
231
  Developer ID sign an existing CI dev artifact per file with fixed identifiers; never builds, notarizes or publishes (macOS)
207
232
  sign-candidate --platform win|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]
@@ -212,20 +237,29 @@ Commands:
212
237
  Assert codesign Identifier/Team/cdhash/lipo archs; reject ad-hoc
213
238
  upload --release <id> --platform mac|win --tier patch|update
214
239
  Upload one sealed release; never builds
240
+ upload --platform ios --release <id> [--apply] Sealed IPA to TestFlight, then the exact build into the beta groups; dry run unless --apply (macOS)
241
+ ios-credentials store --key-id <KEYID> --issuer <uuid> | check
242
+ Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
215
243
  qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt (a missing receipt warns and is recorded; it never blocks)
216
244
  qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List source/installed QA status per release; exit 1 if an uploaded build lacks passing QA
217
245
  publish-update --release <id> --platform mac|win --tier patch|update
218
246
  Upload sealed bytes to R2 & register RightApps
219
247
  release Compatibility alias for build
220
248
  publish Compatibility alias for upload
249
+ check-release-dependencies [--platform mac|win|linux | --all-targets] Check app-forbidden release crates
221
250
  publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish
222
251
  publish npm --package @rightkit/<name> --publish --tarball <gated.tgz>
223
252
  Publish the gated tarball with the Keychain token
224
253
  (npm-rightkit-publish) and verify registry bytes
225
254
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
226
255
  publish npm --package @rightkit/<name> --local Mac-only local publish under the committed local-build grant
227
- publish cargo --crate <name> --local (.rightkit-local-build.json): runs the unit's CI tests, packs, writes darwin-only
228
- evidence, then the same publication gate and publisher; refuses without the grant
256
+ publish cargo --crate <name> --local (.rightkit-local-build.json), on the Mac or Windows: runs the unit's CI tests, packs,
257
+ writes darwin-only (Mac) or win32-only (Windows) evidence, then the same
258
+ publication gate and publisher; refuses without the grant
259
+ publish cargo --crate <name> --local --qualify-only --bundle <new-dir> [--jobs <1-64>]
260
+ Verify on source host; export crate bytes & gate evidence without uploading
261
+ publish cargo --crate <name> --from-evidence <gate.json>
262
+ Reuse exact HEAD/artifact proof on upload host; recheck archive hash & registry bytes
229
263
  publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]
230
264
  Check main, a clean tree, and the publication evidence, then tag RightKitSwift at its VERSION on origin
231
265
  inventory check [--json] [--warn-only] [--md <file>] [--no-cache]
@@ -268,7 +302,8 @@ Commands:
268
302
  lsclean <AppName.app> Clear macOS LaunchServices duplicates
269
303
  generate-dmg-background <options> Generate the branded multi-resolution DMG background
270
304
  mirror-root-artifact --file <path> --package-root <dir>
271
- Persist artifact in RIGHT_RELEASE_ARTIFACT_DIR
305
+ mirror-root-artifact --config <file> --platform mac|win [--package-root <dir>]
306
+ Persist artifact(s) in RIGHT_RELEASE_ARTIFACT_DIR (config form: targets.<platform>.mirror entries, optional pruneSuffix)
272
307
  github --release <id> --tier patch|update --platform mac|win [--config <file>] [--repo owner/repo] [--dry-run]
273
308
  Same checks and publisher as upload (explicit tier, github-releases provider + configured repository)
274
309
  github --addon <config> --platform mac|win [--repo owner/repo] [--dry-run]
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { detectMacSigningSession } from "./macos-signing-session.mjs";
6
6
  import { ensurePrivateDirectory, privatePath } from "./rightkit-signing-agent.mjs";
7
- import { resolveManagedRoot } from "./artifact-storage.mjs";
7
+ import { canonicalManagedRoot, resolveManagedRoot } from "./artifact-storage.mjs";
8
8
 
9
9
  const quote = (text) => `'${String(text).replaceAll("'", "'\\''")}'`;
10
10
  export function codesignShimScript({ route, node = process.execPath,
@@ -35,7 +35,7 @@ fi
35
35
  exec /bin/bash "$@"\n`;
36
36
  // Immutable wrappers avoid races between GUI & SSH builds or package versions.
37
37
  const identity = createHash("sha256").update(script).update(automationScript).update(bashScript).digest("hex").slice(0, 24);
38
- const bin = path.join(resolveManagedRoot({ env }), "signing", "codesign-bin", identity);
38
+ const bin = path.join(canonicalManagedRoot(resolveManagedRoot({ env })), "signing", "codesign-bin", identity);
39
39
  ensurePrivateDirectory(bin, { io });
40
40
  const file = path.join(bin, "codesign");
41
41
  if (io.existsSync(file)) {