@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.
- package/README.md +121 -4
- package/addon-command.mjs +6 -13
- package/android-aab-scan.mjs +265 -0
- package/android-dev.mjs +111 -0
- package/android-ndk.mjs +77 -0
- package/android-play-upload.mjs +262 -0
- package/android-release.mjs +344 -0
- package/android-test-fixtures.mjs +79 -0
- package/apple-profile-create.mjs +363 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/build-release.mjs +73 -13
- package/cargo-guard.mjs +2 -0
- package/cargo-profile-contract.mjs +183 -0
- package/cargo-target.mjs +8 -0
- package/cli/right-release.mjs +14 -4
- package/committed-source.mjs +1 -1
- package/dev-artifact-refresh.mjs +176 -0
- package/dev-artifact-support.mjs +61 -0
- package/development-signing.mjs +71 -4
- package/development.mjs +78 -46
- package/direct-bootstrap.mjs +26 -41
- package/disk-command.mjs +298 -5
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +105 -16
- package/disk-stable-root.mjs +19 -16
- package/engine-locator.mjs +116 -0
- package/external-bin.mjs +133 -0
- package/github-release.mjs +2 -2
- package/github-updater-release.mjs +154 -0
- package/hardeningscan.mjs +1 -1
- package/in-place-install.mjs +56 -0
- package/install-app-windows.mjs +63 -0
- package/install-app.mjs +53 -23
- package/local-publish.mjs +11 -1
- package/mac-signing-matrix.mjs +23 -11
- package/managed-cargo.mjs +6 -0
- package/media-sidecar-catalog.mjs +45 -0
- package/media-sidecar.mjs +306 -0
- package/ownership-approval-command.mjs +10 -44
- package/ownership-approval.mjs +3 -42
- package/ownership-catalog-config.json +2 -1
- package/ownership-catalog.mjs +4 -2
- package/ownership-command.mjs +7 -9
- package/ownership-dependency-allowlist.json +25 -1
- package/ownership-evidence.mjs +8 -5
- package/ownership-git-inventory.mjs +41 -7
- package/ownership-lockfiles.mjs +91 -8
- package/ownership-mandatory-set.json +36 -16
- package/ownership-report.mjs +5 -6
- package/ownership-scan-worker.mjs +2 -2
- package/ownership-verdict.mjs +10 -7
- package/ownership.mjs +47 -29
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/publication-artifacts.mjs +34 -0
- package/publish-update.mjs +72 -11
- package/qa-adoption-exemptions.json +5 -0
- package/qa-build-features.mjs +7 -0
- package/qa-contract.mjs +182 -0
- package/qa-control-scan.mjs +3 -3
- package/release-qa-command.mjs +38 -12
- package/release-qa-installed.mjs +136 -0
- package/release-qa-receipt.mjs +23 -2
- package/release.mjs +281 -13
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +26 -0
- package/rightkit-versions.json +109 -62
- package/runtime-budget-classes.json +28 -0
- package/runtime-budgets.mjs +4 -1
- package/sign-candidate.mjs +161 -0
- package/sign-dev-artifact.mjs +198 -0
- package/stage-binary.mjs +60 -0
- package/swap-helper-bundle.mjs +105 -0
- package/templates/cargo-profiles.toml +28 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|