@rightkit/release 0.2.165 → 0.2.173
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 +16 -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 +93 -47
- 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/install-tauri-devid.mjs +172 -0
- package/inventory-check.mjs +1 -1
- package/local-publish.mjs +11 -1
- package/mac-package-recipe.mjs +304 -0
- 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-recipe-targets.mjs +95 -0
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/project-type.mjs +1 -1
- 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 +285 -14
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +27 -1
- package/rightkit-versions.json +111 -63
- 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/stage-sidecar-command.mjs +41 -0
- package/stage-sidecar.mjs +424 -0
- package/swap-helper-bundle.mjs +105 -0
- package/swift-mac-dmg.mjs +2 -1
- package/templates/cargo-profiles.toml +28 -0
- package/upload-release.mjs +11 -1
- package/win-package-recipe.mjs +321 -0
package/release-qa-receipt.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// key held in the protected credential store (Keychain / protected file / CI secret) — not app-writable JSON.
|
|
11
11
|
// The floor, this verifier, and the receipt schema are pinned in the protected policy bundle.
|
|
12
12
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
13
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { spawnSync } from "./background-process.mjs";
|
|
@@ -50,6 +50,22 @@ export function qaReceiptPath({ repoRoot, releaseId, platform, stage }) {
|
|
|
50
50
|
return path.join(repoRoot, ".right-release", "qa", releaseId, platformDirOf(platform), `${stage}.receipt.json`);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export const QA_CARRIED_RECEIPT_ENV = "RIGHT_RELEASE_QA_RECEIPT";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Receipt carried from the job that ran source QA to the signing job (Orthic-Labs/rightkit#183).
|
|
57
|
+
* `RIGHT_RELEASE_QA_RECEIPT` names the receipt file, or a directory holding `source.receipt.json`.
|
|
58
|
+
* Returns the file path when it exists, else null. The carried file is verified exactly like a local one
|
|
59
|
+
* (MAC, commit, tree, lockfile closure, config, floor, bundle), so carrying adds no trust.
|
|
60
|
+
*/
|
|
61
|
+
export function carriedSourceReceiptPath(env = process.env) {
|
|
62
|
+
const value = String(env?.[QA_CARRIED_RECEIPT_ENV] ?? "").trim();
|
|
63
|
+
if (!value) return null;
|
|
64
|
+
const resolved = path.resolve(value);
|
|
65
|
+
const file = existsSync(resolved) && statSync(resolved).isDirectory() ? path.join(resolved, "source.receipt.json") : resolved;
|
|
66
|
+
return existsSync(file) ? file : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
export function loadQaFloor(dir = here) {
|
|
54
70
|
const file = path.join(dir, QA_FLOOR_FILE);
|
|
55
71
|
let floor;
|
|
@@ -247,10 +263,15 @@ function loadKeyOrRefuse(opts, command) {
|
|
|
247
263
|
*/
|
|
248
264
|
export function assertSourceQaAdmission({ repoRoot, releaseId, platform, configName, releaseConfigSha256, ...opts }) {
|
|
249
265
|
const command = qaCommandFor({ stage: "source", platform, releaseId, configName });
|
|
250
|
-
|
|
266
|
+
// A receipt carried from the QA job (RIGHT_RELEASE_QA_RECEIPT) wins over the local path; the signing job has no local one.
|
|
267
|
+
const carried = carriedSourceReceiptPath(opts.env ?? process.env);
|
|
268
|
+
const receipt = readQaReceipt(carried ?? qaReceiptPath({ repoRoot, releaseId, platform, stage: "source" }));
|
|
251
269
|
if (!receipt) throw new QaAdmissionError(`no source QA receipt for ${releaseId} (${platform}); a candidate cannot be built or signed without passing source QA`, { command });
|
|
252
270
|
const verdict = verifyQaReceipt(receipt, { key: loadKeyOrRefuse(opts, command), stage: "source", repoRoot, releaseConfigSha256, floorDir: opts.floorDir, gitRunner: opts.gitRunner, policyBundleDigest: opts.policyBundleDigest, source: opts.source, closure: opts.closure });
|
|
253
271
|
if (!verdict.ok) throw new QaAdmissionError(`source QA receipt rejected: ${verdict.reason}`, { command });
|
|
272
|
+
if (carried && (receipt.releaseId !== releaseId || receipt.platform !== platform)) {
|
|
273
|
+
throw new QaAdmissionError(`source QA receipt rejected: carried receipt is for ${receipt.releaseId} (${receipt.platform}), not ${releaseId} (${platform})`, { command });
|
|
274
|
+
}
|
|
254
275
|
return { digest: receipt.digest, floorDigest: receipt.floor.digest };
|
|
255
276
|
}
|
|
256
277
|
|
package/release.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
|
|
2
3
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
9
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
8
10
|
import { validateRightKitCargoContract } from "./cargo-contract.mjs";
|
|
9
|
-
import {
|
|
11
|
+
import { assertCargoProfileContract } from "./cargo-profile-contract.mjs";
|
|
12
|
+
import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
|
|
10
13
|
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
11
14
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
12
15
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
@@ -28,6 +31,9 @@ import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
|
28
31
|
import { terminateProcessTree } from "./heavy-command.mjs";
|
|
29
32
|
import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
30
33
|
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
34
|
+
import { MEDIA_SIDECAR_OFFER_NAME, MEDIA_SIDECAR_RECEIPT_NAME, buildMediaSidecarReceipt, macSidecarSigningProblems, mediaSidecarNestedCode, planMediaSidecar, sidecarFileEvidence, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices } from "./media-sidecar.mjs";
|
|
35
|
+
import { loadSidecarCatalog } from "./media-sidecar-catalog.mjs";
|
|
36
|
+
import { EXTERNAL_BIN_RECEIPT_NAME, buildExternalBinReceipt, externalBinNestedCode, planExternalBin, validateExternalBinConfig, verifyStagedExternalBins } from "./external-bin.mjs";
|
|
31
37
|
import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
32
38
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
33
39
|
import { fileEvidence } from "./macos-file-evidence.mjs";
|
|
@@ -42,6 +48,7 @@ import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs
|
|
|
42
48
|
import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
|
|
43
49
|
import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
|
|
44
50
|
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
51
|
+
import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
|
|
45
52
|
|
|
46
53
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
47
54
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -87,6 +94,7 @@ const SWIFT_PRIVATE_PARALLEL_DEV_MAC = {
|
|
|
87
94
|
};
|
|
88
95
|
const SWIFT_PRIVATE_JOBS = new Map([
|
|
89
96
|
["ci.yml", new Map([["ci", SWIFT_PRIVATE_ANY_PROFILE], ["dev-artifact-mac", SWIFT_PRIVATE_PARALLEL_DEV_MAC], ["test", SWIFT_PRIVATE_NODE_PROFILE]])],
|
|
97
|
+
["swift-check.yml", new Map([["swift-check", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
90
98
|
["qualification.yml", new Map([["qualification", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
91
99
|
["release-candidate.yml", new Map([["candidate", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
92
100
|
["regenerate.yml", new Map([["regenerate", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
@@ -158,6 +166,16 @@ const config = (await import(pathToFileURL(configPath))).default;
|
|
|
158
166
|
if (!config) usage(2, `config did not export default: ${configPath}`);
|
|
159
167
|
if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<missing>"} (expected 1)`);
|
|
160
168
|
|
|
169
|
+
// Hosted-workflow policy patterns: defined before the top-level await below uses them.
|
|
170
|
+
const SIGN_EXISTING_SECRETS = new Set(["APPLE_CERTIFICATE_BASE64", "APPLE_CERTIFICATE_PASSWORD", "APPLE_KEYCHAIN_PASSWORD"]);
|
|
171
|
+
const SIGN_EXISTING_ACTIONS = new Set(["actions/checkout", "actions/setup-node", "pnpm/action-setup", "actions/download-artifact", "actions/upload-artifact"]);
|
|
172
|
+
const SIGN_EXISTING_RIGHT_RELEASE_COMMANDS = new Set(["ownership", "sign-dev-artifact"]);
|
|
173
|
+
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/;
|
|
174
|
+
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";
|
|
175
|
+
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/;
|
|
176
|
+
// Head of the managed `supersede` job (the span starts at the preceding newline, before the job key line).
|
|
177
|
+
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/;
|
|
178
|
+
|
|
161
179
|
const root = path.dirname(configPath);
|
|
162
180
|
const workdir = path.resolve(root, config.workdir ?? ".");
|
|
163
181
|
const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
|
|
@@ -168,7 +186,13 @@ if (!(resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.
|
|
|
168
186
|
await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
|
|
169
187
|
}
|
|
170
188
|
validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
|
|
171
|
-
|
|
189
|
+
// One standard dev profile and panic = "unwind" in every app (templates/cargo-profiles.toml).
|
|
190
|
+
assertCargoProfileContract(root, config.app ?? path.basename(root));
|
|
191
|
+
// RightKit QA is mandatory for every app (also SwiftPM apps without a package.json): no QA lane refuses the release.
|
|
192
|
+
assertQaAdoption({ appRoot: root, app: config.app });
|
|
193
|
+
let target = config.targets?.[opts.platform];
|
|
194
|
+
// Declarative winPackage / macPackage (#345) expand to the same hook shape every gate below already checks.
|
|
195
|
+
try { target = expandPackageRecipeTarget(target, { platform: opts.platform, appRoot: root, config }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
|
|
172
196
|
if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
|
|
173
197
|
const legalContract = config.legal
|
|
174
198
|
? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
|
|
@@ -177,6 +201,16 @@ if (target.signed !== true) {
|
|
|
177
201
|
fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
|
|
178
202
|
}
|
|
179
203
|
let macSigning; // validated macOS sign.prePackageFiles / sign.nestedCode (mac only)
|
|
204
|
+
let mediaSidecarConfig = null; // validated targets.<mac|win>.mediaSidecar (rightkit-ffmpeg decode sidecar)
|
|
205
|
+
if (target.mediaSidecar !== undefined) {
|
|
206
|
+
if (opts.platform !== "mac" && opts.platform !== "win") fail(`${config.app ?? "app"} ${opts.platform} does not support mediaSidecar (mac and win only)`);
|
|
207
|
+
try { mediaSidecarConfig = validateMediaSidecarConfig(target.mediaSidecar, { platform: opts.platform }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
|
|
208
|
+
}
|
|
209
|
+
let externalBinConfig = null; // validated targets.<mac|win>.externalBin (generic Tauri externalBin sidecars, #270)
|
|
210
|
+
if (target.externalBin !== undefined) {
|
|
211
|
+
if (opts.platform !== "mac" && opts.platform !== "win") fail(`${config.app ?? "app"} ${opts.platform} does not support externalBin (mac and win only)`);
|
|
212
|
+
try { externalBinConfig = validateExternalBinConfig(target.externalBin, { platform: opts.platform, reservedNames: mediaSidecarConfig?.tools ?? [] }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
|
|
213
|
+
}
|
|
180
214
|
if (opts.platform === "win") {
|
|
181
215
|
const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
|
|
182
216
|
const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
|
|
@@ -198,7 +232,10 @@ if (opts.platform === "mac") {
|
|
|
198
232
|
if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
|
|
199
233
|
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
|
|
200
234
|
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
|
|
201
|
-
|
|
235
|
+
const macSignConfig = mediaSidecarConfig || externalBinConfig
|
|
236
|
+
? { ...target.sign, nestedCode: { ...(mediaSidecarConfig ? mediaSidecarNestedCode(mediaSidecarConfig) : {}), ...(externalBinConfig ? externalBinNestedCode(externalBinConfig) : {}), ...(target.sign.nestedCode ?? {}) } }
|
|
237
|
+
: target.sign;
|
|
238
|
+
try { macSigning = validateMacSigningConfig(macSignConfig, { root }); } catch (error) { fail(`${config.app ?? "app"} mac ${error.message}`); }
|
|
202
239
|
if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
|
|
203
240
|
if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
|
|
204
241
|
if (target.notarize.staple === false && (target.notarize.app || /\.(dmg|pkg)$/i.test(target.notarize.file))) {
|
|
@@ -214,6 +251,10 @@ if (opts.platform === "mac") {
|
|
|
214
251
|
} catch (error) { fail(`${config.app ?? "app"} mac targets.mac.dmg.spec: ${error.message}`); }
|
|
215
252
|
}
|
|
216
253
|
}
|
|
254
|
+
let androidTarget; // validated Android AAB target (android only; see android-release.mjs)
|
|
255
|
+
if (opts.platform === "android") {
|
|
256
|
+
try { androidTarget = validateAndroidTarget(target, { root }); } catch (error) { fail(`${config.app ?? "app"} android ${error.message}`); }
|
|
257
|
+
}
|
|
217
258
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
218
259
|
/**
|
|
219
260
|
* Assemble preflight inputs from the app's own files. Kept here (not in
|
|
@@ -316,6 +357,7 @@ try {
|
|
|
316
357
|
{ label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
|
|
317
358
|
{ label: "nativeAssembly.finalizer", command: nativeFinalizer },
|
|
318
359
|
{ label: "target.postSign", command: target.postSign },
|
|
360
|
+
{ label: "target.gradle", command: target.gradle },
|
|
319
361
|
] });
|
|
320
362
|
} catch (error) { fail(error.message); }
|
|
321
363
|
|
|
@@ -337,7 +379,7 @@ try {
|
|
|
337
379
|
rust: config.releaseSwitches?.rust,
|
|
338
380
|
features: config.releaseSwitches?.features ?? [],
|
|
339
381
|
triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
340
|
-
commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign],
|
|
382
|
+
commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign, target.gradle],
|
|
341
383
|
});
|
|
342
384
|
if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
|
|
343
385
|
else treeText = closure.treeText;
|
|
@@ -346,7 +388,7 @@ try {
|
|
|
346
388
|
console.log(`right-release: dry run: ${error.message}`);
|
|
347
389
|
}
|
|
348
390
|
// 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)) };
|
|
391
|
+
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
392
|
if (releaseSwitchInventoryText != null) {
|
|
351
393
|
assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
|
|
352
394
|
} else if (treeText != null || closureInput.commands.length) {
|
|
@@ -360,7 +402,7 @@ try {
|
|
|
360
402
|
const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
|
|
361
403
|
const mode = "package";
|
|
362
404
|
const command = target.package;
|
|
363
|
-
if (!command && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
|
|
405
|
+
if (!command && opts.platform !== "android" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
|
|
364
406
|
if (opts.upload && !target.publish && !target.upload) {
|
|
365
407
|
fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
|
|
366
408
|
}
|
|
@@ -397,8 +439,14 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
397
439
|
if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
|
|
398
440
|
} else if (opts.platform === "win") {
|
|
399
441
|
const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
442
|
+
const sidecarPlan = await prepareMediaSidecar(root);
|
|
443
|
+
const externalPlan = await prepareExternalBin(root);
|
|
400
444
|
await runCommand(target.prePackage, root);
|
|
401
445
|
for (const file of rawFiles) await mustExist(file, `missing raw EXE signing artifact: ${file}`);
|
|
446
|
+
const sidecarPre = sidecarPlan && !opts.dryRun ? verifyStagedSidecars(sidecarPlan) : [];
|
|
447
|
+
const externalPre = externalPlan && !opts.dryRun ? verifyExternalBins(externalPlan) : [];
|
|
448
|
+
const sidecarFiles = [...sidecarPre, ...externalPre].map((file) => file.stagedPath);
|
|
449
|
+
assertNoQaControlBinaries("windows media sidecar and externalBin", sidecarFiles, { prePack: true });
|
|
402
450
|
// Before signing and packaging: the raw EXE is uncompressed, so a QA control server is visible here.
|
|
403
451
|
assertNoQaControlBinaries("windows raw EXE", rawFiles, { requireBinary: true, prePack: true });
|
|
404
452
|
// The marker must reach its FINAL value before signing. Tauri rewrites it during
|
|
@@ -410,7 +458,15 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
410
458
|
for (const receipt of bundleMarkerReceipts) {
|
|
411
459
|
console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
|
|
412
460
|
}
|
|
413
|
-
await signWindows(rawFiles, "raw-exe", root);
|
|
461
|
+
await signWindows([...rawFiles, ...sidecarFiles], "raw-exe", root);
|
|
462
|
+
if (sidecarPlan && !opts.dryRun) {
|
|
463
|
+
const signed = Object.fromEntries(sidecarPre.map((file) => [file.tool, sidecarFileEvidence(file.stagedPath)]));
|
|
464
|
+
writeMediaSidecarReceipt(root, buildMediaSidecarReceipt(sidecarPlan, sidecarPre, signed));
|
|
465
|
+
}
|
|
466
|
+
if (externalPlan && !opts.dryRun) {
|
|
467
|
+
const signed = Object.fromEntries(externalPre.map((file) => [file.tool, sidecarFileEvidence(file.stagedPath)]));
|
|
468
|
+
writeExternalBinReceipt(root, buildExternalBinReceipt(externalPlan, externalPre, signed));
|
|
469
|
+
}
|
|
414
470
|
if (nativeAssembly?.packageHook && !hasNativeTargetSpecificPrePackage(target, nativeAssembly.packageHook)) {
|
|
415
471
|
await runCommand(nativeAssembly.packageHook, root);
|
|
416
472
|
}
|
|
@@ -424,6 +480,8 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
424
480
|
});
|
|
425
481
|
}
|
|
426
482
|
} else if (opts.platform === "mac") {
|
|
483
|
+
const sidecarPlan = await prepareMediaSidecar(root, { stage: runMacPhase("prepackage") });
|
|
484
|
+
const externalPlan = await prepareExternalBin(root, { stage: runMacPhase("prepackage") });
|
|
427
485
|
if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
|
|
428
486
|
// Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
|
|
429
487
|
const signingEntries = macSigning.entries.map((entry) => ({ ...entry, file: resolveReleaseArtifactPath(root, entry.file) }));
|
|
@@ -437,9 +495,29 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
437
495
|
}
|
|
438
496
|
if (runMacPhase("sign")) {
|
|
439
497
|
await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
|
|
498
|
+
if (sidecarPlan && !opts.dryRun) {
|
|
499
|
+
const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
|
|
500
|
+
if (!app) fail("mediaSidecar on macOS needs an .app entry in sign.prePackageFiles");
|
|
501
|
+
writeMediaSidecarReceipt(root, finishMacSidecar(sidecarPlan, app.file, root));
|
|
502
|
+
}
|
|
503
|
+
if (externalPlan && !opts.dryRun) {
|
|
504
|
+
const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
|
|
505
|
+
if (!app) fail("externalBin on macOS needs an .app entry in sign.prePackageFiles");
|
|
506
|
+
writeExternalBinReceipt(root, finishMacExternalBin(externalPlan, app.file, root));
|
|
507
|
+
}
|
|
440
508
|
// Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
|
|
441
509
|
if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
|
|
442
510
|
}
|
|
511
|
+
} else if (opts.platform === "android") {
|
|
512
|
+
// Gradle bundleRelease with upload-key signing (secrets by file path only), then the in-process AAB scan, signer pin and receipt.
|
|
513
|
+
await runAndroidRelease({
|
|
514
|
+
root, app: config.app ?? path.basename(root), target: androidTarget, dryRun: opts.dryRun,
|
|
515
|
+
runGradle: (gradle, env) => runCommand(gradle, root, { env }),
|
|
516
|
+
resolveArtifact: (declared) => resolveReleaseArtifactPath(root, declared),
|
|
517
|
+
scanFiles: (label, files, scanOptions) => assertNoQaControlBinaries(label, files, scanOptions),
|
|
518
|
+
receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
|
|
519
|
+
allowContentOnlyDevtools: devtoolsAllowance.mode === "content-only",
|
|
520
|
+
});
|
|
443
521
|
} else if (nativeAssembly) {
|
|
444
522
|
if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
|
|
445
523
|
if (nativeFinalizer) {
|
|
@@ -995,6 +1073,83 @@ function swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, in
|
|
|
995
1073
|
return null;
|
|
996
1074
|
}
|
|
997
1075
|
|
|
1076
|
+
// @rightkit/ci `sign-existing` lane (0.2.49, Orthic-Labs/rightkit#257): signs an already-qualified artifact of another run.
|
|
1077
|
+
// It is accepted only in its exact rendered shape: workflow_dispatch only; jobs private-repo-guard and sign-existing only;
|
|
1078
|
+
// the default-branch gate behind the private-repo guard; the protected environment; contents: read and actions: read
|
|
1079
|
+
// (workflow and job); exactly the three Apple Developer ID secrets, referenced inside the sign-existing job only; pinned
|
|
1080
|
+
// allowlisted actions; and no build, notarization or release step. Returns null when the shape matches, else the reason.
|
|
1081
|
+
|
|
1082
|
+
function signExistingRefusal(source) {
|
|
1083
|
+
const jobsAt = source.indexOf("\njobs:\n");
|
|
1084
|
+
if (jobsAt < 0) return "it declares no jobs";
|
|
1085
|
+
const head = source.slice(0, jobsAt + 1);
|
|
1086
|
+
const jobs = source.slice(jobsAt + 7);
|
|
1087
|
+
const onBlock = /^on:\n((?:[ \t]+[^\n]*\n|\n)*)/m.exec(head)?.[1] ?? "";
|
|
1088
|
+
const triggers = [...onBlock.matchAll(/^ ([A-Za-z_]+):/gm)].map((match) => match[1]);
|
|
1089
|
+
if (triggers.length !== 1 || triggers[0] !== "workflow_dispatch") return "it must be triggered by workflow_dispatch only";
|
|
1090
|
+
if (!/^permissions:\n contents: read\n actions: read\n(?! )/m.test(head)) return "workflow permissions must be exactly contents: read and actions: read";
|
|
1091
|
+
const names = [...jobs.matchAll(/^ ([A-Za-z0-9_-]+):[ \t]*$/gm)].map((match) => match[1]);
|
|
1092
|
+
if (names.join(",") !== "private-repo-guard,sign-existing") return `jobs must be exactly private-repo-guard and sign-existing, got ${names.join(", ") || "none"}`;
|
|
1093
|
+
const marker = "\n sign-existing:\n";
|
|
1094
|
+
const guard = jobs.slice(0, jobs.indexOf(marker) + 1);
|
|
1095
|
+
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";
|
|
1096
|
+
const sign = jobs.slice(jobs.indexOf(marker) + marker.length);
|
|
1097
|
+
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";
|
|
1098
|
+
const secrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
|
|
1099
|
+
const signSecrets = [...sign.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
|
|
1100
|
+
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";
|
|
1101
|
+
for (const [, action] of sign.matchAll(/^\s*(?:- )?uses: (\S+)/gm)) {
|
|
1102
|
+
const [name, pin] = action.split("@");
|
|
1103
|
+
if (!SIGN_EXISTING_ACTIONS.has(name) || !/^[0-9a-f]{40}$/.test(pin ?? "")) return `action ${action} is not an allowed commit-pinned action`;
|
|
1104
|
+
}
|
|
1105
|
+
for (const [, command] of source.matchAll(/right-release\s+([A-Za-z][A-Za-z-]*)/g)) {
|
|
1106
|
+
if (!SIGN_EXISTING_RIGHT_RELEASE_COMMANDS.has(command)) return `right-release ${command} is not allowed; the lane only signs an existing artifact`;
|
|
1107
|
+
}
|
|
1108
|
+
if (!/right-release sign-dev-artifact /.test(sign)) return "it must run right-release sign-dev-artifact";
|
|
1109
|
+
const build = SIGN_EXISTING_BUILD_RE.exec(sign);
|
|
1110
|
+
if (build) return `it must not build, notarize or release (found ${JSON.stringify(build[0])})`;
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// The checks above are a denylist backstop. The authority is byte equality with what @rightkit/ci renders for this repository's
|
|
1115
|
+
// manifest: an extra step (for example one that copies a secret into `env:` and posts it somewhere, or one that interpolates
|
|
1116
|
+
// `${{ inputs.* }}` straight into `run:`) passes every pattern above but can never equal the managed render.
|
|
1117
|
+
// The renderer comes from this monorepo's own packages/ci when present, else from the repository's installed @rightkit/ci; with
|
|
1118
|
+
// neither the file is refused, because an unverifiable sign-existing.yml must not be accepted.
|
|
1119
|
+
async function loadRightKitCiRenderer(root) {
|
|
1120
|
+
const candidates = [pathToFileURL(fileURLToPath(new URL("../ci/sync.mjs", import.meta.url))).href];
|
|
1121
|
+
try {
|
|
1122
|
+
candidates.push(pathToFileURL(createRequire(path.join(root, "package.json")).resolve("@rightkit/ci/sync.mjs")).href);
|
|
1123
|
+
} catch {
|
|
1124
|
+
// not installed in the repository; the sibling candidate may still apply
|
|
1125
|
+
}
|
|
1126
|
+
for (const href of candidates) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (href.startsWith("file:") && !existsSync(fileURLToPath(href))) continue;
|
|
1129
|
+
const loaded = await import(href);
|
|
1130
|
+
if (typeof loaded.renderLanes === "function") return loaded.renderLanes;
|
|
1131
|
+
} catch {
|
|
1132
|
+
// try the next candidate
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function signExistingRenderRefusal(root, manifest, source) {
|
|
1139
|
+
const renderLanes = await loadRightKitCiRenderer(root);
|
|
1140
|
+
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";
|
|
1141
|
+
let expected;
|
|
1142
|
+
try {
|
|
1143
|
+
expected = renderLanes(manifest, root).find((lane) => lane.filename === "sign-existing.yml")?.content;
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
return `the manifest does not render a sign-existing lane (${error.message})`;
|
|
1146
|
+
}
|
|
1147
|
+
if (typeof expected !== "string") return "the manifest does not render a sign-existing lane";
|
|
1148
|
+
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";
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
|
|
998
1153
|
async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
999
1154
|
if (policy !== "right-git-ci-only") {
|
|
1000
1155
|
fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
@@ -1008,15 +1163,15 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1008
1163
|
}
|
|
1009
1164
|
const lanes = manifest?.lanes;
|
|
1010
1165
|
// 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"]);
|
|
1166
|
+
const safeLanes = new Set(["ci", "swift-check", "qualification", "package-smoke", "release-candidate", "publish-npm", "publish-package-managers", "regenerate", "sign-existing"]);
|
|
1012
1167
|
if (manifest?.schemaVersion !== 1
|
|
1013
1168
|
|| manifest?.compile !== "github-actions-only"
|
|
1014
1169
|
|| !Array.isArray(lanes)
|
|
1015
1170
|
|| lanes.length === 0
|
|
1016
|
-
|| !lanes.includes("ci")
|
|
1171
|
+
|| !(lanes.includes("ci") || lanes.includes("swift-check"))
|
|
1017
1172
|
|| new Set(lanes).size !== lanes.length
|
|
1018
1173
|
|| 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"`);
|
|
1174
|
+
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
1175
|
}
|
|
1021
1176
|
const expectedWorkflows = lanes.map((lane) => `${lane}.yml`).sort();
|
|
1022
1177
|
if (JSON.stringify(workflows) !== JSON.stringify(expectedWorkflows)) {
|
|
@@ -1056,7 +1211,8 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1056
1211
|
`${job}:\\n needs: \\[candidate\\]\\n if: \\$\\{\\{ startsWith\\(github\\.ref, 'refs\\/tags\\/v'\\)(?: \\|\\| \\(github\\.event_name == 'workflow_dispatch' && inputs\\.signed_qualification == true\\))? \\}\\}`,
|
|
1057
1212
|
);
|
|
1058
1213
|
const admittedSigningGate = (job) => new RegExp(
|
|
1059
|
-
|
|
1214
|
+
// `, dev-qualification` (@rightkit/ci candidate.devQualification, #107) only adds a gate.
|
|
1215
|
+
`${job}:\\n needs: \\[admission, candidate(?:, dev-qualification)?\\]\\n if: \\$\\{\\{ needs\\.admission\\.outputs\\.signed_qualification == 'true' \\}\\}`,
|
|
1060
1216
|
);
|
|
1061
1217
|
const protectedWindowsSigning = workflow === "release-candidate.yml"
|
|
1062
1218
|
&& manifest?.candidate?.signWindows === true
|
|
@@ -1083,6 +1239,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1083
1239
|
&& /git push origin "HEAD:refs\/heads\/\$branch"\n/.test(regenerateSource)
|
|
1084
1240
|
&& !/git push(?![^\n]*"HEAD:refs\/heads\/\$branch")/.test(regenerateSource);
|
|
1085
1241
|
const allowedRegeneratePermissions = new Set(["contents", "pull-requests"]);
|
|
1242
|
+
// @rightkit/ci `supersede` job (one CI run per repo, #338): `actions: write` is accepted only for the
|
|
1243
|
+
// job whose head matches the managed shape exactly (push to the default branch only, needs only
|
|
1244
|
+
// private-repo-guard, contents: read plus actions: write, no action `uses:`), and only in ci.yml when
|
|
1245
|
+
// the manifest has not opted into `concurrency.mainPushes: "queue"` (which renders no such job).
|
|
1246
|
+
const supersedeSpan = workflow === "ci.yml" ? jobSpan("supersede") : null;
|
|
1247
|
+
const supersedeSource = supersedeSpan ? source.slice(supersedeSpan.start, supersedeSpan.end) : "";
|
|
1248
|
+
const protectedSupersede = workflow === "ci.yml"
|
|
1249
|
+
&& manifest?.concurrency?.mainPushes !== "queue"
|
|
1250
|
+
&& SUPERSEDE_JOB_HEAD.test(supersedeSource)
|
|
1251
|
+
&& !/^\s*-?\s*uses:/m.test(supersedeSource);
|
|
1086
1252
|
const devMacSpan = workflow === "ci.yml" ? jobSpan("dev-mac-sign") : null;
|
|
1087
1253
|
const devMacSource = devMacSpan ? source.slice(devMacSpan.start, devMacSpan.end) : "";
|
|
1088
1254
|
// `mac: "notarize"` additionally allows the notary API key secrets; the
|
|
@@ -1093,6 +1259,16 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1093
1259
|
const protectedDevMacSigning = workflow === "ci.yml"
|
|
1094
1260
|
&& (manifest?.devArtifact?.mac === "sign" || manifest?.devArtifact?.mac === "notarize")
|
|
1095
1261
|
&& /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);
|
|
1262
|
+
// @rightkit/ci `sign-existing` lane: any deviation from the exact managed shape is refused outright.
|
|
1263
|
+
const signExistingSpan = workflow === "sign-existing.yml" ? jobSpan("sign-existing") : null;
|
|
1264
|
+
let protectedSignExisting = false;
|
|
1265
|
+
if (workflow === "sign-existing.yml") {
|
|
1266
|
+
const reason = manifest?.signExisting && typeof manifest.signExisting === "object"
|
|
1267
|
+
? (signExistingRefusal(source) ?? await signExistingRenderRefusal(root, manifest, source))
|
|
1268
|
+
: "the manifest declares no signExisting";
|
|
1269
|
+
if (reason !== null) fail(`${appName} right-git managed sign-existing.yml is not the protected sign-existing shape: ${reason}`);
|
|
1270
|
+
protectedSignExisting = true;
|
|
1271
|
+
}
|
|
1096
1272
|
// @rightkit/ci `swift.privatePackages` credential: allowed only in the workflow/job pairs and on the
|
|
1097
1273
|
// env lines listed with SWIFT_PRIVATE_JOBS; swiftPrivateSecretRefusal explains every other reference.
|
|
1098
1274
|
const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
|
|
@@ -1116,12 +1292,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1116
1292
|
const refusedSecrets = requestedSecrets
|
|
1117
1293
|
.filter(({ name, index }) => !(protectedMacosSigning && inside(index, macosSpan) && allowedMacosSecrets.has(name))
|
|
1118
1294
|
&& !(protectedDevMacSigning && inside(index, devMacSpan) && allowedDevMacSecrets.has(name))
|
|
1295
|
+
&& !(protectedSignExisting && inside(index, signExistingSpan) && SIGN_EXISTING_SECRETS.has(name))
|
|
1119
1296
|
&& !(protectedReleasePublish && inside(index, publishSpan) && allowedReleaseSecrets.has(name)))
|
|
1120
1297
|
.map(({ name, index }) => swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, index }))
|
|
1121
1298
|
.filter((reason) => reason !== null);
|
|
1122
1299
|
if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
|
|
1123
1300
|
|| writePermissions.some(({ name, index }) => {
|
|
1124
1301
|
if (protectedRegenerate && inside(index, regenerateSpan) && allowedRegeneratePermissions.has(name)) return false;
|
|
1302
|
+
if (protectedSupersede && inside(index, supersedeSpan) && name === "actions") return false;
|
|
1125
1303
|
return name === "id-token"
|
|
1126
1304
|
? !(protectedWindowsSigning && inside(index, windowsSpan))
|
|
1127
1305
|
: name !== "contents" || !protectedReleasePublish || !inside(index, publishSpan);
|
|
@@ -1164,6 +1342,99 @@ function expandEnv(value) {
|
|
|
1164
1342
|
return String(value).replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? "");
|
|
1165
1343
|
}
|
|
1166
1344
|
|
|
1345
|
+
// rightkit-ffmpeg decode sidecar (media-sidecar.mjs): plan from the catalog the app compiled against, write the LGPL
|
|
1346
|
+
// notices and the source offer, optionally run the app's stage command, and (before signing) check the staged files.
|
|
1347
|
+
async function prepareMediaSidecar(root, { stage = true } = {}) {
|
|
1348
|
+
if (!mediaSidecarConfig) return null;
|
|
1349
|
+
let plan;
|
|
1350
|
+
try {
|
|
1351
|
+
const loaded = loadSidecarCatalog({ config: mediaSidecarConfig, root, manifestPath: nativeLayout.manifestPath });
|
|
1352
|
+
plan = planMediaSidecar({ config: mediaSidecarConfig, ...loaded, root });
|
|
1353
|
+
} catch (error) { fail(`${config.app ?? "app"} ${opts.platform} mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
|
|
1354
|
+
console.log(`right-release: media sidecar ${plan.rows.map((row) => row.id).join(", ")}`);
|
|
1355
|
+
if (opts.dryRun) {
|
|
1356
|
+
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}`);
|
|
1357
|
+
return plan;
|
|
1358
|
+
}
|
|
1359
|
+
if (stage) {
|
|
1360
|
+
try { writeMediaSidecarNotices(plan); } catch (error) { fail(`mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
|
|
1361
|
+
if (mediaSidecarConfig.stage) await runCommand(mediaSidecarConfig.stage, root);
|
|
1362
|
+
}
|
|
1363
|
+
return plan;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function finishMacSidecar(plan, appPath, root) {
|
|
1367
|
+
let preSign;
|
|
1368
|
+
const signed = {};
|
|
1369
|
+
const checked = [];
|
|
1370
|
+
try { preSign = verifyStagedSidecars(plan); } catch (error) { fail(`mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`); }
|
|
1371
|
+
for (const file of plan.files) {
|
|
1372
|
+
const inBundle = path.join(appPath, ...file.bundlePath.split("/"));
|
|
1373
|
+
if (!existsSync(inBundle)) fail(`mediaSidecar: ${file.tool} is not inside the built app at ${file.bundlePath}; list it under Tauri externalBin`);
|
|
1374
|
+
signed[file.tool] = sidecarFileEvidence(inBundle);
|
|
1375
|
+
checked.push({ file: inBundle, sha256: signed[file.tool].sha256 });
|
|
1376
|
+
}
|
|
1377
|
+
const nestedReceipt = path.resolve(path.dirname(macosReceiptPath(root, "signing")), "macos-signing-nested.json");
|
|
1378
|
+
let nested;
|
|
1379
|
+
try { nested = JSON.parse(readFileSync(nestedReceipt, "utf8")); } catch { fail(`mediaSidecar: missing nested macOS signing receipt ${nestedReceipt}`); }
|
|
1380
|
+
const problems = macSidecarSigningProblems(nested, checked);
|
|
1381
|
+
if (problems.length) fail(`mediaSidecar: sidecar signing evidence rejected: ${problems.join("; ")}`);
|
|
1382
|
+
const bundled = readdirSync(appPath, { recursive: true }).map(String);
|
|
1383
|
+
if (!bundled.some((entry) => path.basename(entry) === MEDIA_SIDECAR_OFFER_NAME)) {
|
|
1384
|
+
fail(`mediaSidecar: ${MEDIA_SIDECAR_OFFER_NAME} is not inside the built app; add ${mediaSidecarConfig.notices.dir} to the Tauri bundle resources`);
|
|
1385
|
+
}
|
|
1386
|
+
return buildMediaSidecarReceipt(plan, preSign, signed);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Generic Tauri externalBin sidecars (external-bin.mjs, #270): the app's own stage command writes <name>-<triple>[.exe].
|
|
1390
|
+
async function prepareExternalBin(root, { stage = true } = {}) {
|
|
1391
|
+
if (!externalBinConfig) return null;
|
|
1392
|
+
const plan = planExternalBin({ config: externalBinConfig, root });
|
|
1393
|
+
console.log(`right-release: externalBin ${plan.files.map((file) => path.basename(file.stagedPath)).join(", ")}`);
|
|
1394
|
+
if (opts.dryRun) {
|
|
1395
|
+
console.log(`dry-run: would expect ${plan.files.map((file) => path.basename(file.stagedPath)).join(", ")} in ${externalBinConfig.stageDir}`);
|
|
1396
|
+
return plan;
|
|
1397
|
+
}
|
|
1398
|
+
if (stage && externalBinConfig.stage) await runCommand(externalBinConfig.stage, root);
|
|
1399
|
+
return plan;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function verifyExternalBins(plan) {
|
|
1403
|
+
try { return verifyStagedExternalBins(plan); } catch (error) { fail(`externalBin: [${error.code ?? "ERROR"}] ${error.message}`); }
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function finishMacExternalBin(plan, appPath, root) {
|
|
1407
|
+
const preSign = verifyExternalBins(plan);
|
|
1408
|
+
const signed = {};
|
|
1409
|
+
const checked = [];
|
|
1410
|
+
for (const file of plan.files) {
|
|
1411
|
+
const inBundle = path.join(appPath, ...file.bundlePath.split("/"));
|
|
1412
|
+
if (!existsSync(inBundle)) fail(`externalBin: ${file.tool} is not inside the built app at ${file.bundlePath}; list it under Tauri externalBin`);
|
|
1413
|
+
signed[file.tool] = sidecarFileEvidence(inBundle);
|
|
1414
|
+
checked.push({ file: inBundle, sha256: signed[file.tool].sha256 });
|
|
1415
|
+
}
|
|
1416
|
+
const nestedReceipt = path.resolve(path.dirname(macosReceiptPath(root, "signing")), "macos-signing-nested.json");
|
|
1417
|
+
let nested;
|
|
1418
|
+
try { nested = JSON.parse(readFileSync(nestedReceipt, "utf8")); } catch { fail(`externalBin: missing nested macOS signing receipt ${nestedReceipt}`); }
|
|
1419
|
+
const problems = macSidecarSigningProblems(nested, checked);
|
|
1420
|
+
if (problems.length) fail(`externalBin: signing evidence rejected: ${problems.join("; ")}`);
|
|
1421
|
+
return buildExternalBinReceipt(plan, preSign, signed);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function writeExternalBinReceipt(root, receipt) {
|
|
1425
|
+
const file = path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), EXTERNAL_BIN_RECEIPT_NAME);
|
|
1426
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
1427
|
+
writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1428
|
+
console.log(`right-release: externalBin receipt ${file}`);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function writeMediaSidecarReceipt(root, receipt) {
|
|
1432
|
+
const file = path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), MEDIA_SIDECAR_RECEIPT_NAME);
|
|
1433
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
1434
|
+
writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1435
|
+
console.log(`right-release: media sidecar receipt ${file}`);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1167
1438
|
async function signWindows(files, phase, root) {
|
|
1168
1439
|
const args = [SIGN_WINDOWS];
|
|
1169
1440
|
const receipt = windowsReceiptPath(root, phase);
|
|
@@ -1181,13 +1452,13 @@ function windowsReceiptPath(root, phase) {
|
|
|
1181
1452
|
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
1182
1453
|
}
|
|
1183
1454
|
|
|
1184
|
-
// A signing step with per-file settings (entitlements, codesign options) goes to sign-macos.mjs as a manifest;
|
|
1455
|
+
// A signing step with per-file settings (entitlements, codesign options, a stable identifier) goes to sign-macos.mjs as a manifest;
|
|
1185
1456
|
// a step with only defaults keeps the historical positional invocation.
|
|
1186
1457
|
function macSignInvocation(items, receiptPath, extra = []) {
|
|
1187
1458
|
if (opts.dryRun || !items.some(hasNonDefaultSettings)) return [SIGN_MACOS, ...extra, "--receipt", receiptPath, ...items.map((item) => item.file)];
|
|
1188
1459
|
const manifest = path.resolve(path.dirname(receiptPath), `${path.basename(receiptPath, ".json")}.manifest.json`);
|
|
1189
1460
|
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`);
|
|
1461
|
+
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
1462
|
return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
|
|
1192
1463
|
}
|
|
1193
1464
|
|