@rightkit/release 0.2.165 → 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 +298 -5
  24. package/disk-policy.json +7 -5
  25. package/disk-policy.mjs +105 -16
  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 +281 -13
  67. package/rightkit-ownership.json +537 -5
  68. package/rightkit-sync.mjs +26 -0
  69. package/rightkit-versions.json +109 -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");
@@ -87,6 +93,7 @@ const SWIFT_PRIVATE_PARALLEL_DEV_MAC = {
87
93
  };
88
94
  const SWIFT_PRIVATE_JOBS = new Map([
89
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]])],
90
97
  ["qualification.yml", new Map([["qualification", SWIFT_PRIVATE_ANY_PROFILE]])],
91
98
  ["release-candidate.yml", new Map([["candidate", SWIFT_PRIVATE_ANY_PROFILE]])],
92
99
  ["regenerate.yml", new Map([["regenerate", SWIFT_PRIVATE_ANY_PROFILE]])],
@@ -158,6 +165,16 @@ const config = (await import(pathToFileURL(configPath))).default;
158
165
  if (!config) usage(2, `config did not export default: ${configPath}`);
159
166
  if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<missing>"} (expected 1)`);
160
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
+
161
178
  const root = path.dirname(configPath);
162
179
  const workdir = path.resolve(root, config.workdir ?? ".");
163
180
  const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
@@ -168,6 +185,10 @@ if (!(resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.
168
185
  await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
169
186
  }
170
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 });
171
192
  const target = config.targets?.[opts.platform];
172
193
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
173
194
  const legalContract = config.legal
@@ -177,6 +198,16 @@ if (target.signed !== true) {
177
198
  fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
178
199
  }
179
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
+ }
180
211
  if (opts.platform === "win") {
181
212
  const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
182
213
  const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
@@ -198,7 +229,10 @@ if (opts.platform === "mac") {
198
229
  if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
199
230
  if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
200
231
  if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
201
- 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}`); }
202
236
  if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
203
237
  if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
204
238
  if (target.notarize.staple === false && (target.notarize.app || /\.(dmg|pkg)$/i.test(target.notarize.file))) {
@@ -214,6 +248,10 @@ if (opts.platform === "mac") {
214
248
  } catch (error) { fail(`${config.app ?? "app"} mac targets.mac.dmg.spec: ${error.message}`); }
215
249
  }
216
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
+ }
217
255
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
218
256
  /**
219
257
  * Assemble preflight inputs from the app's own files. Kept here (not in
@@ -316,6 +354,7 @@ try {
316
354
  { label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
317
355
  { label: "nativeAssembly.finalizer", command: nativeFinalizer },
318
356
  { label: "target.postSign", command: target.postSign },
357
+ { label: "target.gradle", command: target.gradle },
319
358
  ] });
320
359
  } catch (error) { fail(error.message); }
321
360
 
@@ -337,7 +376,7 @@ try {
337
376
  rust: config.releaseSwitches?.rust,
338
377
  features: config.releaseSwitches?.features ?? [],
339
378
  triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
340
- commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign],
379
+ commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign, target.gradle],
341
380
  });
342
381
  if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
343
382
  else treeText = closure.treeText;
@@ -346,7 +385,7 @@ try {
346
385
  console.log(`right-release: dry run: ${error.message}`);
347
386
  }
348
387
  // remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
349
- 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)) };
350
389
  if (releaseSwitchInventoryText != null) {
351
390
  assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
352
391
  } else if (treeText != null || closureInput.commands.length) {
@@ -360,7 +399,7 @@ try {
360
399
  const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
361
400
  const mode = "package";
362
401
  const command = target.package;
363
- 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`);
364
403
  if (opts.upload && !target.publish && !target.upload) {
365
404
  fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
366
405
  }
@@ -397,8 +436,14 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
397
436
  if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
398
437
  } else if (opts.platform === "win") {
399
438
  const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
439
+ const sidecarPlan = await prepareMediaSidecar(root);
440
+ const externalPlan = await prepareExternalBin(root);
400
441
  await runCommand(target.prePackage, root);
401
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 });
402
447
  // Before signing and packaging: the raw EXE is uncompressed, so a QA control server is visible here.
403
448
  assertNoQaControlBinaries("windows raw EXE", rawFiles, { requireBinary: true, prePack: true });
404
449
  // The marker must reach its FINAL value before signing. Tauri rewrites it during
@@ -410,7 +455,15 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
410
455
  for (const receipt of bundleMarkerReceipts) {
411
456
  console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
412
457
  }
413
- 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
+ }
414
467
  if (nativeAssembly?.packageHook && !hasNativeTargetSpecificPrePackage(target, nativeAssembly.packageHook)) {
415
468
  await runCommand(nativeAssembly.packageHook, root);
416
469
  }
@@ -424,6 +477,8 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
424
477
  });
425
478
  }
426
479
  } else if (opts.platform === "mac") {
480
+ const sidecarPlan = await prepareMediaSidecar(root, { stage: runMacPhase("prepackage") });
481
+ const externalPlan = await prepareExternalBin(root, { stage: runMacPhase("prepackage") });
427
482
  if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
428
483
  // Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
429
484
  const signingEntries = macSigning.entries.map((entry) => ({ ...entry, file: resolveReleaseArtifactPath(root, entry.file) }));
@@ -437,9 +492,29 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
437
492
  }
438
493
  if (runMacPhase("sign")) {
439
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
+ }
440
505
  // Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
441
506
  if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
442
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
+ });
443
518
  } else if (nativeAssembly) {
444
519
  if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
445
520
  if (nativeFinalizer) {
@@ -995,6 +1070,83 @@ function swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, in
995
1070
  return null;
996
1071
  }
997
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
+
998
1150
  async function validateHostedWorkflows(root, appName, workflows, policy) {
999
1151
  if (policy !== "right-git-ci-only") {
1000
1152
  fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
@@ -1008,15 +1160,15 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1008
1160
  }
1009
1161
  const lanes = manifest?.lanes;
1010
1162
  // Mirrors the `lanes` enum in @rightkit/ci manifest.schema.json.
1011
- 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"]);
1012
1164
  if (manifest?.schemaVersion !== 1
1013
1165
  || manifest?.compile !== "github-actions-only"
1014
1166
  || !Array.isArray(lanes)
1015
1167
  || lanes.length === 0
1016
- || !lanes.includes("ci")
1168
+ || !(lanes.includes("ci") || lanes.includes("swift-check"))
1017
1169
  || new Set(lanes).size !== lanes.length
1018
1170
  || lanes.some((lane) => !safeLanes.has(lane))) {
1019
- 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"`);
1020
1172
  }
1021
1173
  const expectedWorkflows = lanes.map((lane) => `${lane}.yml`).sort();
1022
1174
  if (JSON.stringify(workflows) !== JSON.stringify(expectedWorkflows)) {
@@ -1056,7 +1208,8 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1056
1208
  `${job}:\\n needs: \\[candidate\\]\\n if: \\$\\{\\{ startsWith\\(github\\.ref, 'refs\\/tags\\/v'\\)(?: \\|\\| \\(github\\.event_name == 'workflow_dispatch' && inputs\\.signed_qualification == true\\))? \\}\\}`,
1057
1209
  );
1058
1210
  const admittedSigningGate = (job) => new RegExp(
1059
- `${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' \\}\\}`,
1060
1213
  );
1061
1214
  const protectedWindowsSigning = workflow === "release-candidate.yml"
1062
1215
  && manifest?.candidate?.signWindows === true
@@ -1083,6 +1236,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1083
1236
  && /git push origin "HEAD:refs\/heads\/\$branch"\n/.test(regenerateSource)
1084
1237
  && !/git push(?![^\n]*"HEAD:refs\/heads\/\$branch")/.test(regenerateSource);
1085
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);
1086
1249
  const devMacSpan = workflow === "ci.yml" ? jobSpan("dev-mac-sign") : null;
1087
1250
  const devMacSource = devMacSpan ? source.slice(devMacSpan.start, devMacSpan.end) : "";
1088
1251
  // `mac: "notarize"` additionally allows the notary API key secrets; the
@@ -1093,6 +1256,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1093
1256
  const protectedDevMacSigning = workflow === "ci.yml"
1094
1257
  && (manifest?.devArtifact?.mac === "sign" || manifest?.devArtifact?.mac === "notarize")
1095
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
+ }
1096
1269
  // @rightkit/ci `swift.privatePackages` credential: allowed only in the workflow/job pairs and on the
1097
1270
  // env lines listed with SWIFT_PRIVATE_JOBS; swiftPrivateSecretRefusal explains every other reference.
1098
1271
  const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
@@ -1116,12 +1289,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
1116
1289
  const refusedSecrets = requestedSecrets
1117
1290
  .filter(({ name, index }) => !(protectedMacosSigning && inside(index, macosSpan) && allowedMacosSecrets.has(name))
1118
1291
  && !(protectedDevMacSigning && inside(index, devMacSpan) && allowedDevMacSecrets.has(name))
1292
+ && !(protectedSignExisting && inside(index, signExistingSpan) && SIGN_EXISTING_SECRETS.has(name))
1119
1293
  && !(protectedReleasePublish && inside(index, publishSpan) && allowedReleaseSecrets.has(name)))
1120
1294
  .map(({ name, index }) => swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, index }))
1121
1295
  .filter((reason) => reason !== null);
1122
1296
  if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
1123
1297
  || writePermissions.some(({ name, index }) => {
1124
1298
  if (protectedRegenerate && inside(index, regenerateSpan) && allowedRegeneratePermissions.has(name)) return false;
1299
+ if (protectedSupersede && inside(index, supersedeSpan) && name === "actions") return false;
1125
1300
  return name === "id-token"
1126
1301
  ? !(protectedWindowsSigning && inside(index, windowsSpan))
1127
1302
  : name !== "contents" || !protectedReleasePublish || !inside(index, publishSpan);
@@ -1164,6 +1339,99 @@ function expandEnv(value) {
1164
1339
  return String(value).replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? "");
1165
1340
  }
1166
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
+
1167
1435
  async function signWindows(files, phase, root) {
1168
1436
  const args = [SIGN_WINDOWS];
1169
1437
  const receipt = windowsReceiptPath(root, phase);
@@ -1181,13 +1449,13 @@ function windowsReceiptPath(root, phase) {
1181
1449
  return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
1182
1450
  }
1183
1451
 
1184
- // 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;
1185
1453
  // a step with only defaults keeps the historical positional invocation.
1186
1454
  function macSignInvocation(items, receiptPath, extra = []) {
1187
1455
  if (opts.dryRun || !items.some(hasNonDefaultSettings)) return [SIGN_MACOS, ...extra, "--receipt", receiptPath, ...items.map((item) => item.file)];
1188
1456
  const manifest = path.resolve(path.dirname(receiptPath), `${path.basename(receiptPath, ".json")}.manifest.json`);
1189
1457
  mkdirSync(path.dirname(manifest), { recursive: true });
1190
- 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`);
1191
1459
  return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
1192
1460
  }
1193
1461