@rightkit/release 0.2.164 → 0.2.172

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 (77) hide show
  1. package/README.md +121 -4
  2. package/addon-command.mjs +6 -13
  3. package/android-aab-scan.mjs +265 -0
  4. package/android-dev.mjs +111 -0
  5. package/android-ndk.mjs +77 -0
  6. package/android-play-upload.mjs +262 -0
  7. package/android-release.mjs +344 -0
  8. package/android-test-fixtures.mjs +79 -0
  9. package/apple-profile-create.mjs +363 -0
  10. package/bin/darwin/rightkit-swap +0 -0
  11. package/bin/darwin/rightkit-swap.sign.json +6 -0
  12. package/build-release.mjs +73 -13
  13. package/cargo-guard.mjs +2 -0
  14. package/cargo-profile-contract.mjs +183 -0
  15. package/cargo-target.mjs +8 -0
  16. package/cli/right-release.mjs +14 -4
  17. package/committed-source.mjs +1 -1
  18. package/dev-artifact-refresh.mjs +176 -0
  19. package/dev-artifact-support.mjs +61 -0
  20. package/development-signing.mjs +71 -4
  21. package/development.mjs +78 -46
  22. package/direct-bootstrap.mjs +26 -41
  23. package/disk-command.mjs +315 -13
  24. package/disk-policy.json +7 -5
  25. package/disk-policy.mjs +141 -22
  26. package/disk-stable-root.mjs +19 -16
  27. package/engine-locator.mjs +116 -0
  28. package/external-bin.mjs +133 -0
  29. package/github-release.mjs +2 -2
  30. package/github-updater-release.mjs +154 -0
  31. package/hardeningscan.mjs +1 -1
  32. package/in-place-install.mjs +56 -0
  33. package/install-app-windows.mjs +63 -0
  34. package/install-app.mjs +53 -23
  35. package/local-publish.mjs +11 -1
  36. package/mac-signing-matrix.mjs +23 -11
  37. package/managed-cargo.mjs +6 -0
  38. package/media-sidecar-catalog.mjs +45 -0
  39. package/media-sidecar.mjs +306 -0
  40. package/ownership-approval-command.mjs +10 -44
  41. package/ownership-approval.mjs +3 -42
  42. package/ownership-catalog-config.json +2 -1
  43. package/ownership-catalog.mjs +4 -2
  44. package/ownership-command.mjs +7 -9
  45. package/ownership-dependency-allowlist.json +25 -1
  46. package/ownership-evidence.mjs +8 -5
  47. package/ownership-git-inventory.mjs +41 -7
  48. package/ownership-lockfiles.mjs +91 -8
  49. package/ownership-mandatory-set.json +36 -16
  50. package/ownership-report.mjs +5 -6
  51. package/ownership-scan-worker.mjs +2 -2
  52. package/ownership-verdict.mjs +10 -7
  53. package/ownership.mjs +47 -29
  54. package/package.json +7 -4
  55. package/portable-development.mjs +32 -115
  56. package/preflight.mjs +4 -6
  57. package/publication-artifacts.mjs +34 -0
  58. package/publish-update.mjs +72 -11
  59. package/qa-adoption-exemptions.json +5 -0
  60. package/qa-build-features.mjs +7 -0
  61. package/qa-contract.mjs +182 -0
  62. package/qa-control-scan.mjs +3 -3
  63. package/release-qa-command.mjs +38 -12
  64. package/release-qa-installed.mjs +136 -0
  65. package/release-qa-receipt.mjs +23 -2
  66. package/release.mjs +283 -14
  67. package/rightkit-ownership.json +537 -5
  68. package/rightkit-sync.mjs +26 -0
  69. package/rightkit-versions.json +110 -62
  70. package/runtime-budget-classes.json +28 -0
  71. package/runtime-budgets.mjs +4 -1
  72. package/sign-candidate.mjs +161 -0
  73. package/sign-dev-artifact.mjs +198 -0
  74. package/stage-binary.mjs +60 -0
  75. package/swap-helper-bundle.mjs +105 -0
  76. package/templates/cargo-profiles.toml +28 -0
  77. package/upload-release.mjs +11 -1
package/release.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath, pathToFileURL } from "node:url";
7
8
  import { spawn, spawnSync } from "./background-process.mjs";
8
9
  import { validateRightKitCargoContract } from "./cargo-contract.mjs";
9
- import { assertQaBackdoorContract } from "./qa-contract.mjs";
10
+ import { assertCargoProfileContract } from "./cargo-profile-contract.mjs";
11
+ import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
10
12
  import { assertLegalReleaseContract } from "./legal-contract.mjs";
11
13
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
12
14
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
@@ -28,6 +30,9 @@ import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
28
30
  import { terminateProcessTree } from "./heavy-command.mjs";
29
31
  import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
30
32
  import { signingReceiptPath } from "./signing-receipts.mjs";
33
+ import { MEDIA_SIDECAR_OFFER_NAME, MEDIA_SIDECAR_RECEIPT_NAME, buildMediaSidecarReceipt, macSidecarSigningProblems, mediaSidecarNestedCode, planMediaSidecar, sidecarFileEvidence, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices } from "./media-sidecar.mjs";
34
+ import { loadSidecarCatalog } from "./media-sidecar-catalog.mjs";
35
+ import { EXTERNAL_BIN_RECEIPT_NAME, buildExternalBinReceipt, externalBinNestedCode, planExternalBin, validateExternalBinConfig, verifyStagedExternalBins } from "./external-bin.mjs";
31
36
  import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
32
37
  import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
33
38
  import { fileEvidence } from "./macos-file-evidence.mjs";
@@ -42,6 +47,7 @@ import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs
42
47
  import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
43
48
  import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
44
49
  import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
50
+ import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
45
51
 
46
52
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
47
53
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -51,7 +57,8 @@ const SIGN_MACOS = path.resolve(TOOL_ROOT, "sign-macos.mjs");
51
57
  const NOTARIZE_MACOS = path.resolve(TOOL_ROOT, "notarize-macos.mjs");
52
58
  const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
53
59
  const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
54
- const VERSION = RIGHTKIT_VERSIONS.stagedNpm?.["@rightkit/release"] ?? RIGHTKIT_VERSIONS.npm["@rightkit/release"];
60
+ // The running package's own version: the bundled feed lags by one release (it is written after the publish).
61
+ const VERSION = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "package.json"), "utf8")).version;
55
62
  const TIERS = new Set(["patch", "update"]);
56
63
  const RIGHTKIT_ALLOWED = buildAllowedVersions(
57
64
  RIGHTKIT_VERSIONS.npm,
@@ -86,6 +93,7 @@ const SWIFT_PRIVATE_PARALLEL_DEV_MAC = {
86
93
  };
87
94
  const SWIFT_PRIVATE_JOBS = new Map([
88
95
  ["ci.yml", new Map([["ci", SWIFT_PRIVATE_ANY_PROFILE], ["dev-artifact-mac", SWIFT_PRIVATE_PARALLEL_DEV_MAC], ["test", SWIFT_PRIVATE_NODE_PROFILE]])],
96
+ ["swift-check.yml", new Map([["swift-check", SWIFT_PRIVATE_ANY_PROFILE]])],
89
97
  ["qualification.yml", new Map([["qualification", SWIFT_PRIVATE_ANY_PROFILE]])],
90
98
  ["release-candidate.yml", new Map([["candidate", SWIFT_PRIVATE_ANY_PROFILE]])],
91
99
  ["regenerate.yml", new Map([["regenerate", SWIFT_PRIVATE_ANY_PROFILE]])],
@@ -157,6 +165,16 @@ const config = (await import(pathToFileURL(configPath))).default;
157
165
  if (!config) usage(2, `config did not export default: ${configPath}`);
158
166
  if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<missing>"} (expected 1)`);
159
167
 
168
+ // Hosted-workflow policy patterns: defined before the top-level await below uses them.
169
+ const SIGN_EXISTING_SECRETS = new Set(["APPLE_CERTIFICATE_BASE64", "APPLE_CERTIFICATE_PASSWORD", "APPLE_KEYCHAIN_PASSWORD"]);
170
+ const SIGN_EXISTING_ACTIONS = new Set(["actions/checkout", "actions/setup-node", "pnpm/action-setup", "actions/download-artifact", "actions/upload-artifact"]);
171
+ const SIGN_EXISTING_RIGHT_RELEASE_COMMANDS = new Set(["ownership", "sign-dev-artifact"]);
172
+ const SIGN_EXISTING_BUILD_RE = /\b(?:cargo|rustc|xcodebuild|tauri|cmake|msbuild|gradle|notarytool|stapler)\b|swift build|right-release build|--release\b|\b(?:npm|pnpm|yarn) (?:run|build)\b/;
173
+ const SIGN_EXISTING_GUARD = " private-repo-guard:\n runs-on: ubuntu-24.04\n steps:\n - name: Refuse private repository\n if: ${{ github.event.repository.private }}\n run: exit 1\n";
174
+ const SIGN_EXISTING_JOB_HEAD = /^(?: #[^\n]*\n)* needs: \[private-repo-guard\]\n if: \$\{\{ github\.event_name == 'workflow_dispatch' && github\.ref_name == github\.event\.repository\.default_branch \}\}\n environment: [A-Za-z0-9][A-Za-z0-9._-]*\n runs-on: (?:macos|xcode)-[A-Za-z0-9._-]+\n(?: timeout-minutes: \d+\n)? permissions:\n contents: read\n actions: read\n steps:\n/;
175
+ // Head of the managed `supersede` job (the span starts at the preceding newline, before the job key line).
176
+ const SUPERSEDE_JOB_HEAD = /^\n supersede:\n needs: \[private-repo-guard\]\n if: \$\{\{ github\.event_name == 'push' && github\.ref_name == github\.event\.repository\.default_branch \}\}\n runs-on: ubuntu-[0-9.]+\n timeout-minutes: \d+\n permissions:\n contents: read\n actions: write\n steps:\n/;
177
+
160
178
  const root = path.dirname(configPath);
161
179
  const workdir = path.resolve(root, config.workdir ?? ".");
162
180
  const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
@@ -167,6 +185,10 @@ if (!(resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.
167
185
  await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
168
186
  }
169
187
  validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
188
+ // One standard dev profile and panic = "unwind" in every app (templates/cargo-profiles.toml).
189
+ assertCargoProfileContract(root, config.app ?? path.basename(root));
190
+ // RightKit QA is mandatory for every app (also SwiftPM apps without a package.json): no QA lane refuses the release.
191
+ assertQaAdoption({ appRoot: root, app: config.app });
170
192
  const target = config.targets?.[opts.platform];
171
193
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
172
194
  const legalContract = config.legal
@@ -176,6 +198,16 @@ if (target.signed !== true) {
176
198
  fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
177
199
  }
178
200
  let macSigning; // validated macOS sign.prePackageFiles / sign.nestedCode (mac only)
201
+ let mediaSidecarConfig = null; // validated targets.<mac|win>.mediaSidecar (rightkit-ffmpeg decode sidecar)
202
+ if (target.mediaSidecar !== undefined) {
203
+ if (opts.platform !== "mac" && opts.platform !== "win") fail(`${config.app ?? "app"} ${opts.platform} does not support mediaSidecar (mac and win only)`);
204
+ try { mediaSidecarConfig = validateMediaSidecarConfig(target.mediaSidecar, { platform: opts.platform }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
205
+ }
206
+ let externalBinConfig = null; // validated targets.<mac|win>.externalBin (generic Tauri externalBin sidecars, #270)
207
+ if (target.externalBin !== undefined) {
208
+ if (opts.platform !== "mac" && opts.platform !== "win") fail(`${config.app ?? "app"} ${opts.platform} does not support externalBin (mac and win only)`);
209
+ try { externalBinConfig = validateExternalBinConfig(target.externalBin, { platform: opts.platform, reservedNames: mediaSidecarConfig?.tools ?? [] }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
210
+ }
179
211
  if (opts.platform === "win") {
180
212
  const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
181
213
  const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
@@ -197,7 +229,10 @@ if (opts.platform === "mac") {
197
229
  if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
198
230
  if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
199
231
  if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
200
- try { macSigning = validateMacSigningConfig(target.sign, { root }); } catch (error) { fail(`${config.app ?? "app"} mac ${error.message}`); }
232
+ const macSignConfig = mediaSidecarConfig || externalBinConfig
233
+ ? { ...target.sign, nestedCode: { ...(mediaSidecarConfig ? mediaSidecarNestedCode(mediaSidecarConfig) : {}), ...(externalBinConfig ? externalBinNestedCode(externalBinConfig) : {}), ...(target.sign.nestedCode ?? {}) } }
234
+ : target.sign;
235
+ try { macSigning = validateMacSigningConfig(macSignConfig, { root }); } catch (error) { fail(`${config.app ?? "app"} mac ${error.message}`); }
201
236
  if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
202
237
  if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
203
238
  if (target.notarize.staple === false && (target.notarize.app || /\.(dmg|pkg)$/i.test(target.notarize.file))) {
@@ -213,6 +248,10 @@ if (opts.platform === "mac") {
213
248
  } catch (error) { fail(`${config.app ?? "app"} mac targets.mac.dmg.spec: ${error.message}`); }
214
249
  }
215
250
  }
251
+ let androidTarget; // validated Android AAB target (android only; see android-release.mjs)
252
+ if (opts.platform === "android") {
253
+ try { androidTarget = validateAndroidTarget(target, { root }); } catch (error) { fail(`${config.app ?? "app"} android ${error.message}`); }
254
+ }
216
255
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
217
256
  /**
218
257
  * Assemble preflight inputs from the app's own files. Kept here (not in
@@ -315,6 +354,7 @@ try {
315
354
  { label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
316
355
  { label: "nativeAssembly.finalizer", command: nativeFinalizer },
317
356
  { label: "target.postSign", command: target.postSign },
357
+ { label: "target.gradle", command: target.gradle },
318
358
  ] });
319
359
  } catch (error) { fail(error.message); }
320
360
 
@@ -336,7 +376,7 @@ try {
336
376
  rust: config.releaseSwitches?.rust,
337
377
  features: config.releaseSwitches?.features ?? [],
338
378
  triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
339
- commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign],
379
+ commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign, target.gradle],
340
380
  });
341
381
  if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
342
382
  else treeText = closure.treeText;
@@ -345,7 +385,7 @@ try {
345
385
  console.log(`right-release: dry run: ${error.message}`);
346
386
  }
347
387
  // remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
348
- const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign].filter(Boolean).map((c) => commandTokens(c)) };
388
+ const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign, target.gradle].filter(Boolean).map((c) => commandTokens(c)) };
349
389
  if (releaseSwitchInventoryText != null) {
350
390
  assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
351
391
  } else if (treeText != null || closureInput.commands.length) {
@@ -359,7 +399,7 @@ try {
359
399
  const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
360
400
  const mode = "package";
361
401
  const command = target.package;
362
- if (!command && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
402
+ if (!command && opts.platform !== "android" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
363
403
  if (opts.upload && !target.publish && !target.upload) {
364
404
  fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
365
405
  }
@@ -396,8 +436,14 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
396
436
  if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
397
437
  } else if (opts.platform === "win") {
398
438
  const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
439
+ const sidecarPlan = await prepareMediaSidecar(root);
440
+ const externalPlan = await prepareExternalBin(root);
399
441
  await runCommand(target.prePackage, root);
400
442
  for (const file of rawFiles) await mustExist(file, `missing raw EXE signing artifact: ${file}`);
443
+ const sidecarPre = sidecarPlan && !opts.dryRun ? verifyStagedSidecars(sidecarPlan) : [];
444
+ const externalPre = externalPlan && !opts.dryRun ? verifyExternalBins(externalPlan) : [];
445
+ const sidecarFiles = [...sidecarPre, ...externalPre].map((file) => file.stagedPath);
446
+ assertNoQaControlBinaries("windows media sidecar and externalBin", sidecarFiles, { prePack: true });
401
447
  // Before signing and packaging: the raw EXE is uncompressed, so a QA control server is visible here.
402
448
  assertNoQaControlBinaries("windows raw EXE", rawFiles, { requireBinary: true, prePack: true });
403
449
  // The marker must reach its FINAL value before signing. Tauri rewrites it during
@@ -409,7 +455,15 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
409
455
  for (const receipt of bundleMarkerReceipts) {
410
456
  console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
411
457
  }
412
- await signWindows(rawFiles, "raw-exe", root);
458
+ await signWindows([...rawFiles, ...sidecarFiles], "raw-exe", root);
459
+ if (sidecarPlan && !opts.dryRun) {
460
+ const signed = Object.fromEntries(sidecarPre.map((file) => [file.tool, sidecarFileEvidence(file.stagedPath)]));
461
+ writeMediaSidecarReceipt(root, buildMediaSidecarReceipt(sidecarPlan, sidecarPre, signed));
462
+ }
463
+ if (externalPlan && !opts.dryRun) {
464
+ const signed = Object.fromEntries(externalPre.map((file) => [file.tool, sidecarFileEvidence(file.stagedPath)]));
465
+ writeExternalBinReceipt(root, buildExternalBinReceipt(externalPlan, externalPre, signed));
466
+ }
413
467
  if (nativeAssembly?.packageHook && !hasNativeTargetSpecificPrePackage(target, nativeAssembly.packageHook)) {
414
468
  await runCommand(nativeAssembly.packageHook, root);
415
469
  }
@@ -423,6 +477,8 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
423
477
  });
424
478
  }
425
479
  } else if (opts.platform === "mac") {
480
+ const sidecarPlan = await prepareMediaSidecar(root, { stage: runMacPhase("prepackage") });
481
+ const externalPlan = await prepareExternalBin(root, { stage: runMacPhase("prepackage") });
426
482
  if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
427
483
  // Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
428
484
  const signingEntries = macSigning.entries.map((entry) => ({ ...entry, file: resolveReleaseArtifactPath(root, entry.file) }));
@@ -436,9 +492,29 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
436
492
  }
437
493
  if (runMacPhase("sign")) {
438
494
  await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
495
+ if (sidecarPlan && !opts.dryRun) {
496
+ const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
497
+ if (!app) fail("mediaSidecar on macOS needs an .app entry in sign.prePackageFiles");
498
+ writeMediaSidecarReceipt(root, finishMacSidecar(sidecarPlan, app.file, root));
499
+ }
500
+ if (externalPlan && !opts.dryRun) {
501
+ const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
502
+ if (!app) fail("externalBin on macOS needs an .app entry in sign.prePackageFiles");
503
+ writeExternalBinReceipt(root, finishMacExternalBin(externalPlan, app.file, root));
504
+ }
439
505
  // Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
440
506
  if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
441
507
  }
508
+ } else if (opts.platform === "android") {
509
+ // Gradle bundleRelease with upload-key signing (secrets by file path only), then the in-process AAB scan, signer pin and receipt.
510
+ await runAndroidRelease({
511
+ root, app: config.app ?? path.basename(root), target: androidTarget, dryRun: opts.dryRun,
512
+ runGradle: (gradle, env) => runCommand(gradle, root, { env }),
513
+ resolveArtifact: (declared) => resolveReleaseArtifactPath(root, declared),
514
+ scanFiles: (label, files, scanOptions) => assertNoQaControlBinaries(label, files, scanOptions),
515
+ receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
516
+ allowContentOnlyDevtools: devtoolsAllowance.mode === "content-only",
517
+ });
442
518
  } else if (nativeAssembly) {
443
519
  if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
444
520
  if (nativeFinalizer) {
@@ -994,6 +1070,83 @@ function swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, in
994
1070
  return null;
995
1071
  }
996
1072
 
1073
+ // @rightkit/ci `sign-existing` lane (0.2.49, Orthic-Labs/rightkit#257): signs an already-qualified artifact of another run.
1074
+ // It is accepted only in its exact rendered shape: workflow_dispatch only; jobs private-repo-guard and sign-existing only;
1075
+ // the default-branch gate behind the private-repo guard; the protected environment; contents: read and actions: read
1076
+ // (workflow and job); exactly the three Apple Developer ID secrets, referenced inside the sign-existing job only; pinned
1077
+ // allowlisted actions; and no build, notarization or release step. Returns null when the shape matches, else the reason.
1078
+
1079
+ function signExistingRefusal(source) {
1080
+ const jobsAt = source.indexOf("\njobs:\n");
1081
+ if (jobsAt < 0) return "it declares no jobs";
1082
+ const head = source.slice(0, jobsAt + 1);
1083
+ const jobs = source.slice(jobsAt + 7);
1084
+ const onBlock = /^on:\n((?:[ \t]+[^\n]*\n|\n)*)/m.exec(head)?.[1] ?? "";
1085
+ const triggers = [...onBlock.matchAll(/^ ([A-Za-z_]+):/gm)].map((match) => match[1]);
1086
+ if (triggers.length !== 1 || triggers[0] !== "workflow_dispatch") return "it must be triggered by workflow_dispatch only";
1087
+ if (!/^permissions:\n contents: read\n actions: read\n(?! )/m.test(head)) return "workflow permissions must be exactly contents: read and actions: read";
1088
+ const names = [...jobs.matchAll(/^ ([A-Za-z0-9_-]+):[ \t]*$/gm)].map((match) => match[1]);
1089
+ if (names.join(",") !== "private-repo-guard,sign-existing") return `jobs must be exactly private-repo-guard and sign-existing, got ${names.join(", ") || "none"}`;
1090
+ const marker = "\n sign-existing:\n";
1091
+ const guard = jobs.slice(0, jobs.indexOf(marker) + 1);
1092
+ if (!guard.startsWith(SIGN_EXISTING_GUARD) || guard.length !== SIGN_EXISTING_GUARD.length + (guard.endsWith("\n\n") ? 1 : 0)) return "the private-repo-guard job is not the managed guard";
1093
+ const sign = jobs.slice(jobs.indexOf(marker) + marker.length);
1094
+ if (!SIGN_EXISTING_JOB_HEAD.test(sign)) return "the sign-existing job must need only private-repo-guard, run only for workflow_dispatch on the default branch in an environment on a macOS runner, with job permissions exactly contents: read and actions: read";
1095
+ const secrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
1096
+ const signSecrets = [...sign.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
1097
+ if (secrets.length !== signSecrets.length || new Set(secrets).size !== SIGN_EXISTING_SECRETS.size || secrets.some((name) => !SIGN_EXISTING_SECRETS.has(name))) return "it must reference exactly the three Apple Developer ID secrets, inside the sign-existing job";
1098
+ for (const [, action] of sign.matchAll(/^\s*(?:- )?uses: (\S+)/gm)) {
1099
+ const [name, pin] = action.split("@");
1100
+ if (!SIGN_EXISTING_ACTIONS.has(name) || !/^[0-9a-f]{40}$/.test(pin ?? "")) return `action ${action} is not an allowed commit-pinned action`;
1101
+ }
1102
+ for (const [, command] of source.matchAll(/right-release\s+([A-Za-z][A-Za-z-]*)/g)) {
1103
+ if (!SIGN_EXISTING_RIGHT_RELEASE_COMMANDS.has(command)) return `right-release ${command} is not allowed; the lane only signs an existing artifact`;
1104
+ }
1105
+ if (!/right-release sign-dev-artifact /.test(sign)) return "it must run right-release sign-dev-artifact";
1106
+ const build = SIGN_EXISTING_BUILD_RE.exec(sign);
1107
+ if (build) return `it must not build, notarize or release (found ${JSON.stringify(build[0])})`;
1108
+ return null;
1109
+ }
1110
+
1111
+ // The checks above are a denylist backstop. The authority is byte equality with what @rightkit/ci renders for this repository's
1112
+ // manifest: an extra step (for example one that copies a secret into `env:` and posts it somewhere, or one that interpolates
1113
+ // `${{ inputs.* }}` straight into `run:`) passes every pattern above but can never equal the managed render.
1114
+ // The renderer comes from this monorepo's own packages/ci when present, else from the repository's installed @rightkit/ci; with
1115
+ // neither the file is refused, because an unverifiable sign-existing.yml must not be accepted.
1116
+ async function loadRightKitCiRenderer(root) {
1117
+ const candidates = [pathToFileURL(fileURLToPath(new URL("../ci/sync.mjs", import.meta.url))).href];
1118
+ try {
1119
+ candidates.push(pathToFileURL(createRequire(path.join(root, "package.json")).resolve("@rightkit/ci/sync.mjs")).href);
1120
+ } catch {
1121
+ // not installed in the repository; the sibling candidate may still apply
1122
+ }
1123
+ for (const href of candidates) {
1124
+ try {
1125
+ if (href.startsWith("file:") && !existsSync(fileURLToPath(href))) continue;
1126
+ const loaded = await import(href);
1127
+ if (typeof loaded.renderLanes === "function") return loaded.renderLanes;
1128
+ } catch {
1129
+ // try the next candidate
1130
+ }
1131
+ }
1132
+ return null;
1133
+ }
1134
+
1135
+ async function signExistingRenderRefusal(root, manifest, source) {
1136
+ const renderLanes = await loadRightKitCiRenderer(root);
1137
+ if (!renderLanes) return "the @rightkit/ci renderer is not available (install @rightkit/ci as a devDependency), so the file cannot be compared with its managed render";
1138
+ let expected;
1139
+ try {
1140
+ expected = renderLanes(manifest, root).find((lane) => lane.filename === "sign-existing.yml")?.content;
1141
+ } catch (error) {
1142
+ return `the manifest does not render a sign-existing lane (${error.message})`;
1143
+ }
1144
+ if (typeof expected !== "string") return "the manifest does not render a sign-existing lane";
1145
+ if (source.replace(/\r\n?/g, "\n") !== expected.replace(/\r\n?/g, "\n")) return "the file is not byte-for-byte the @rightkit/ci render of this repository's .rightgit.json (run right-git sync); extra, changed or reordered steps are refused";
1146
+ return null;
1147
+ }
1148
+
1149
+
997
1150
  async function validateHostedWorkflows(root, appName, workflows, policy) {
998
1151
  if (policy !== "right-git-ci-only") {
999
1152
  fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
@@ -1007,15 +1160,15 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1007
1160
  }
1008
1161
  const lanes = manifest?.lanes;
1009
1162
  // Mirrors the `lanes` enum in @rightkit/ci manifest.schema.json.
1010
- const safeLanes = new Set(["ci", "qualification", "package-smoke", "release-candidate", "publish-npm", "publish-package-managers", "regenerate"]);
1163
+ const safeLanes = new Set(["ci", "swift-check", "qualification", "package-smoke", "release-candidate", "publish-npm", "publish-package-managers", "regenerate", "sign-existing"]);
1011
1164
  if (manifest?.schemaVersion !== 1
1012
1165
  || manifest?.compile !== "github-actions-only"
1013
1166
  || !Array.isArray(lanes)
1014
1167
  || lanes.length === 0
1015
- || !lanes.includes("ci")
1168
+ || !(lanes.includes("ci") || lanes.includes("swift-check"))
1016
1169
  || new Set(lanes).size !== lanes.length
1017
1170
  || lanes.some((lane) => !safeLanes.has(lane))) {
1018
- fail(`${appName} right-git-ci-only policy requires compile=github-actions-only plus unique .rightgit.json lanes drawn from [${[...safeLanes].map((lane) => `"${lane}"`).join(", ")}] and including "ci"`);
1171
+ fail(`${appName} right-git-ci-only policy requires compile=github-actions-only plus unique .rightgit.json lanes drawn from [${[...safeLanes].map((lane) => `"${lane}"`).join(", ")}] and including "ci" or "swift-check"`);
1019
1172
  }
1020
1173
  const expectedWorkflows = lanes.map((lane) => `${lane}.yml`).sort();
1021
1174
  if (JSON.stringify(workflows) !== JSON.stringify(expectedWorkflows)) {
@@ -1055,7 +1208,8 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1055
1208
  `${job}:\\n needs: \\[candidate\\]\\n if: \\$\\{\\{ startsWith\\(github\\.ref, 'refs\\/tags\\/v'\\)(?: \\|\\| \\(github\\.event_name == 'workflow_dispatch' && inputs\\.signed_qualification == true\\))? \\}\\}`,
1056
1209
  );
1057
1210
  const admittedSigningGate = (job) => new RegExp(
1058
- `${job}:\\n needs: \\[admission, candidate\\]\\n if: \\$\\{\\{ needs\\.admission\\.outputs\\.signed_qualification == 'true' \\}\\}`,
1211
+ // `, dev-qualification` (@rightkit/ci candidate.devQualification, #107) only adds a gate.
1212
+ `${job}:\\n needs: \\[admission, candidate(?:, dev-qualification)?\\]\\n if: \\$\\{\\{ needs\\.admission\\.outputs\\.signed_qualification == 'true' \\}\\}`,
1059
1213
  );
1060
1214
  const protectedWindowsSigning = workflow === "release-candidate.yml"
1061
1215
  && manifest?.candidate?.signWindows === true
@@ -1082,6 +1236,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1082
1236
  && /git push origin "HEAD:refs\/heads\/\$branch"\n/.test(regenerateSource)
1083
1237
  && !/git push(?![^\n]*"HEAD:refs\/heads\/\$branch")/.test(regenerateSource);
1084
1238
  const allowedRegeneratePermissions = new Set(["contents", "pull-requests"]);
1239
+ // @rightkit/ci `supersede` job (one CI run per repo, #338): `actions: write` is accepted only for the
1240
+ // job whose head matches the managed shape exactly (push to the default branch only, needs only
1241
+ // private-repo-guard, contents: read plus actions: write, no action `uses:`), and only in ci.yml when
1242
+ // the manifest has not opted into `concurrency.mainPushes: "queue"` (which renders no such job).
1243
+ const supersedeSpan = workflow === "ci.yml" ? jobSpan("supersede") : null;
1244
+ const supersedeSource = supersedeSpan ? source.slice(supersedeSpan.start, supersedeSpan.end) : "";
1245
+ const protectedSupersede = workflow === "ci.yml"
1246
+ && manifest?.concurrency?.mainPushes !== "queue"
1247
+ && SUPERSEDE_JOB_HEAD.test(supersedeSource)
1248
+ && !/^\s*-?\s*uses:/m.test(supersedeSource);
1085
1249
  const devMacSpan = workflow === "ci.yml" ? jobSpan("dev-mac-sign") : null;
1086
1250
  const devMacSource = devMacSpan ? source.slice(devMacSpan.start, devMacSpan.end) : "";
1087
1251
  // `mac: "notarize"` additionally allows the notary API key secrets; the
@@ -1092,6 +1256,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1092
1256
  const protectedDevMacSigning = workflow === "ci.yml"
1093
1257
  && (manifest?.devArtifact?.mac === "sign" || manifest?.devArtifact?.mac === "notarize")
1094
1258
  && /dev-mac-sign:\n(?: #[^\n]*\n)* needs: \[ci\]\n if: \$\{\{ github\.event_name == 'push' && github\.ref_name == github\.event\.repository\.default_branch \}\}\n environment: /.test(devMacSource);
1259
+ // @rightkit/ci `sign-existing` lane: any deviation from the exact managed shape is refused outright.
1260
+ const signExistingSpan = workflow === "sign-existing.yml" ? jobSpan("sign-existing") : null;
1261
+ let protectedSignExisting = false;
1262
+ if (workflow === "sign-existing.yml") {
1263
+ const reason = manifest?.signExisting && typeof manifest.signExisting === "object"
1264
+ ? (signExistingRefusal(source) ?? await signExistingRenderRefusal(root, manifest, source))
1265
+ : "the manifest declares no signExisting";
1266
+ if (reason !== null) fail(`${appName} right-git managed sign-existing.yml is not the protected sign-existing shape: ${reason}`);
1267
+ protectedSignExisting = true;
1268
+ }
1095
1269
  // @rightkit/ci `swift.privatePackages` credential: allowed only in the workflow/job pairs and on the
1096
1270
  // env lines listed with SWIFT_PRIVATE_JOBS; swiftPrivateSecretRefusal explains every other reference.
1097
1271
  const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
@@ -1115,12 +1289,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1115
1289
  const refusedSecrets = requestedSecrets
1116
1290
  .filter(({ name, index }) => !(protectedMacosSigning && inside(index, macosSpan) && allowedMacosSecrets.has(name))
1117
1291
  && !(protectedDevMacSigning && inside(index, devMacSpan) && allowedDevMacSecrets.has(name))
1292
+ && !(protectedSignExisting && inside(index, signExistingSpan) && SIGN_EXISTING_SECRETS.has(name))
1118
1293
  && !(protectedReleasePublish && inside(index, publishSpan) && allowedReleaseSecrets.has(name)))
1119
1294
  .map(({ name, index }) => swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, index }))
1120
1295
  .filter((reason) => reason !== null);
1121
1296
  if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
1122
1297
  || writePermissions.some(({ name, index }) => {
1123
1298
  if (protectedRegenerate && inside(index, regenerateSpan) && allowedRegeneratePermissions.has(name)) return false;
1299
+ if (protectedSupersede && inside(index, supersedeSpan) && name === "actions") return false;
1124
1300
  return name === "id-token"
1125
1301
  ? !(protectedWindowsSigning && inside(index, windowsSpan))
1126
1302
  : name !== "contents" || !protectedReleasePublish || !inside(index, publishSpan);
@@ -1163,6 +1339,99 @@ function expandEnv(value) {
1163
1339
  return String(value).replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? "");
1164
1340
  }
1165
1341
 
1342
+ // rightkit-ffmpeg decode sidecar (media-sidecar.mjs): plan from the catalog the app compiled against, write the LGPL
1343
+ // notices and the source offer, optionally run the app's stage command, and (before signing) check the staged files.
1344
+ async function prepareMediaSidecar(root, { stage = true } = {}) {
1345
+ if (!mediaSidecarConfig) return null;
1346
+ let plan;
1347
+ try {
1348
+ const loaded = loadSidecarCatalog({ config: mediaSidecarConfig, root, manifestPath: nativeLayout.manifestPath });
1349
+ plan = planMediaSidecar({ config: mediaSidecarConfig, ...loaded, root });
1350
+ } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
1351
+ console.log(`right-release: media sidecar ${plan.rows.map((row) => row.id).join(", ")}`);
1352
+ if (opts.dryRun) {
1353
+ console.log(`dry-run: would write ${plan.notices.length + 1} notice file(s) to ${mediaSidecarConfig.notices.dir} and expect ${plan.files.map((file) => path.basename(file.stagedPath)).join(", ")} in ${mediaSidecarConfig.stageDir}`);
1354
+ return plan;
1355
+ }
1356
+ if (stage) {
1357
+ try { writeMediaSidecarNotices(plan); } catch (error) { fail(`mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
1358
+ if (mediaSidecarConfig.stage) await runCommand(mediaSidecarConfig.stage, root);
1359
+ }
1360
+ return plan;
1361
+ }
1362
+
1363
+ function finishMacSidecar(plan, appPath, root) {
1364
+ let preSign;
1365
+ const signed = {};
1366
+ const checked = [];
1367
+ try { preSign = verifyStagedSidecars(plan); } catch (error) { fail(`mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
1368
+ for (const file of plan.files) {
1369
+ const inBundle = path.join(appPath, ...file.bundlePath.split("/"));
1370
+ if (!existsSync(inBundle)) fail(`mediaSidecar: ${file.tool} is not inside the built app at ${file.bundlePath}; list it under Tauri externalBin`);
1371
+ signed[file.tool] = sidecarFileEvidence(inBundle);
1372
+ checked.push({ file: inBundle, sha256: signed[file.tool].sha256 });
1373
+ }
1374
+ const nestedReceipt = path.resolve(path.dirname(macosReceiptPath(root, "signing")), "macos-signing-nested.json");
1375
+ let nested;
1376
+ try { nested = JSON.parse(readFileSync(nestedReceipt, "utf8")); } catch { fail(`mediaSidecar: missing nested macOS signing receipt ${nestedReceipt}`); }
1377
+ const problems = macSidecarSigningProblems(nested, checked);
1378
+ if (problems.length) fail(`mediaSidecar: sidecar signing evidence rejected: ${problems.join("; ")}`);
1379
+ const bundled = readdirSync(appPath, { recursive: true }).map(String);
1380
+ if (!bundled.some((entry) => path.basename(entry) === MEDIA_SIDECAR_OFFER_NAME)) {
1381
+ fail(`mediaSidecar: ${MEDIA_SIDECAR_OFFER_NAME} is not inside the built app; add ${mediaSidecarConfig.notices.dir} to the Tauri bundle resources`);
1382
+ }
1383
+ return buildMediaSidecarReceipt(plan, preSign, signed);
1384
+ }
1385
+
1386
+ // Generic Tauri externalBin sidecars (external-bin.mjs, #270): the app's own stage command writes <name>-<triple>[.exe].
1387
+ async function prepareExternalBin(root, { stage = true } = {}) {
1388
+ if (!externalBinConfig) return null;
1389
+ const plan = planExternalBin({ config: externalBinConfig, root });
1390
+ console.log(`right-release: externalBin ${plan.files.map((file) => path.basename(file.stagedPath)).join(", ")}`);
1391
+ if (opts.dryRun) {
1392
+ console.log(`dry-run: would expect ${plan.files.map((file) => path.basename(file.stagedPath)).join(", ")} in ${externalBinConfig.stageDir}`);
1393
+ return plan;
1394
+ }
1395
+ if (stage && externalBinConfig.stage) await runCommand(externalBinConfig.stage, root);
1396
+ return plan;
1397
+ }
1398
+
1399
+ function verifyExternalBins(plan) {
1400
+ try { return verifyStagedExternalBins(plan); } catch (error) { fail(`externalBin: [${error.code ?? "ERROR"}] ${error.message}`); }
1401
+ }
1402
+
1403
+ function finishMacExternalBin(plan, appPath, root) {
1404
+ const preSign = verifyExternalBins(plan);
1405
+ const signed = {};
1406
+ const checked = [];
1407
+ for (const file of plan.files) {
1408
+ const inBundle = path.join(appPath, ...file.bundlePath.split("/"));
1409
+ if (!existsSync(inBundle)) fail(`externalBin: ${file.tool} is not inside the built app at ${file.bundlePath}; list it under Tauri externalBin`);
1410
+ signed[file.tool] = sidecarFileEvidence(inBundle);
1411
+ checked.push({ file: inBundle, sha256: signed[file.tool].sha256 });
1412
+ }
1413
+ const nestedReceipt = path.resolve(path.dirname(macosReceiptPath(root, "signing")), "macos-signing-nested.json");
1414
+ let nested;
1415
+ try { nested = JSON.parse(readFileSync(nestedReceipt, "utf8")); } catch { fail(`externalBin: missing nested macOS signing receipt ${nestedReceipt}`); }
1416
+ const problems = macSidecarSigningProblems(nested, checked);
1417
+ if (problems.length) fail(`externalBin: signing evidence rejected: ${problems.join("; ")}`);
1418
+ return buildExternalBinReceipt(plan, preSign, signed);
1419
+ }
1420
+
1421
+ function writeExternalBinReceipt(root, receipt) {
1422
+ const file = path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), EXTERNAL_BIN_RECEIPT_NAME);
1423
+ mkdirSync(path.dirname(file), { recursive: true });
1424
+ writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
1425
+ console.log(`right-release: externalBin receipt ${file}`);
1426
+ }
1427
+
1428
+ function writeMediaSidecarReceipt(root, receipt) {
1429
+ const file = path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), MEDIA_SIDECAR_RECEIPT_NAME);
1430
+ mkdirSync(path.dirname(file), { recursive: true });
1431
+ writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
1432
+ console.log(`right-release: media sidecar receipt ${file}`);
1433
+ }
1434
+
1166
1435
  async function signWindows(files, phase, root) {
1167
1436
  const args = [SIGN_WINDOWS];
1168
1437
  const receipt = windowsReceiptPath(root, phase);
@@ -1180,13 +1449,13 @@ function windowsReceiptPath(root, phase) {
1180
1449
  return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
1181
1450
  }
1182
1451
 
1183
- // A signing step with per-file settings (entitlements, codesign options) goes to sign-macos.mjs as a manifest;
1452
+ // A signing step with per-file settings (entitlements, codesign options, a stable identifier) goes to sign-macos.mjs as a manifest;
1184
1453
  // a step with only defaults keeps the historical positional invocation.
1185
1454
  function macSignInvocation(items, receiptPath, extra = []) {
1186
1455
  if (opts.dryRun || !items.some(hasNonDefaultSettings)) return [SIGN_MACOS, ...extra, "--receipt", receiptPath, ...items.map((item) => item.file)];
1187
1456
  const manifest = path.resolve(path.dirname(receiptPath), `${path.basename(receiptPath, ".json")}.manifest.json`);
1188
1457
  mkdirSync(path.dirname(manifest), { recursive: true });
1189
- writeFileSync(manifest, `${JSON.stringify({ schema: 1, entries: items.map((item) => ({ file: item.file, ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options ?? [] })) }, null, 2)}\n`);
1458
+ writeFileSync(manifest, `${JSON.stringify({ schema: 1, entries: items.map((item) => ({ file: item.file, ...(item.identifier ? { identifier: item.identifier } : {}), ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options ?? [] })) }, null, 2)}\n`);
1190
1459
  return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
1191
1460
  }
1192
1461