@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.
- 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 +315 -13
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +141 -22
- 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 +283 -14
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +26 -0
- package/rightkit-versions.json +110 -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");
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|