@rightkit/release 0.6.8 → 0.6.11
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 +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/ownership-evidence.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { acquireCommittedSource, assertCommittedCheckoutClean, committedSourceDa
|
|
|
8
8
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
9
9
|
import { runningPolicyBundle } from "./ownership-policy-bundle.mjs";
|
|
10
10
|
import { canonicalRepositoryIdentity, loadTrustedExternal, resolveKeyLogTrust, sha256, validateOwnershipFloor, verifyRepositoryRegistration } from "./ownership-approval.mjs";
|
|
11
|
+
import { isReleaseSource } from "./release-source-brand.mjs";
|
|
11
12
|
|
|
12
13
|
const digestFile = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
13
14
|
|
|
@@ -32,8 +33,22 @@ function createEvidence(repoRoot, {
|
|
|
32
33
|
requireCleanCheckout = false,
|
|
33
34
|
allowPendingAdvisory = false,
|
|
34
35
|
policyBundleProvider = runningPolicyBundle,
|
|
36
|
+
// A door source split (release-source-identity.mjs `resolveReleaseSource`, item 7 row 7.04): the evidence binds the SELECTED commit's
|
|
37
|
+
// objects, read from the checkout's object database, and never the checkout's HEAD or status. null (and a checkout-mode source) is today's behaviour.
|
|
38
|
+
releaseSource = null,
|
|
35
39
|
} = {}, readAuthority = readCommittedKeyAuthority, scanCommitted = checkCommittedApp) {
|
|
40
|
+
if (releaseSource !== null && !isReleaseSource(releaseSource)) throw new Error("ownership evidence: the release source was not resolved by resolveReleaseSource");
|
|
41
|
+
const snapshotSource = releaseSource?.mode === "snapshot" ? releaseSource : null;
|
|
36
42
|
const root = path.resolve(repoRoot);
|
|
43
|
+
// Real paths on both sides: Git's root is resolved (macOS /private/var), callers may pass /var or /tmp.
|
|
44
|
+
const realPath = (p) => { try { return realpathSync.native(p); } catch { return path.resolve(p); } };
|
|
45
|
+
if (snapshotSource && realPath(root) !== realPath(snapshotSource.checkout)) throw new Error(`ownership evidence: in a door source split the git root is the run's checkout ${snapshotSource.checkout}, not ${root}`);
|
|
46
|
+
// The caller names its app root in the snapshot (its cwd); the committed scan places it by its path inside the repository, which is the
|
|
47
|
+
// same relative path in the checkout. Nothing is read from either: the scan reads committed objects.
|
|
48
|
+
if (snapshotSource) {
|
|
49
|
+
const relative = path.relative(realPath(snapshotSource.snapshot), realPath(scanRoot));
|
|
50
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) scanRoot = path.join(snapshotSource.checkout, relative);
|
|
51
|
+
}
|
|
37
52
|
const scope = requestedScope ? [...new Set(requestedScope)].sort() : null;
|
|
38
53
|
const manifestBytes = readFileSync(manifestPath);
|
|
39
54
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
@@ -48,8 +63,14 @@ function createEvidence(repoRoot, {
|
|
|
48
63
|
// a useful local backlog scan. It cannot assert a protected source binding or verify
|
|
49
64
|
// approval signatures, so this result is deliberately advisory.
|
|
50
65
|
const useCommittedSource = committed && (!enrollmentPending || Boolean(trustState.trust?.rootFingerprint));
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
// In a split the evidence is the selected commit's committed source or nothing: the advisory fallback would scan a mutable tree and
|
|
67
|
+
// read HEAD, which are exactly the reads a split replaces. Fail closed.
|
|
68
|
+
if (snapshotSource && !useCommittedSource) throw new Error("ownership evidence in a door source split needs the committed-source scan; this host's trust enrollment cannot provide it (RK_SOURCE_SPLIT)");
|
|
69
|
+
// The clean-checkout condition guards "bytes scanned (committed blobs) = bytes built (working tree)". A split builds a verified snapshot
|
|
70
|
+
// of the commit instead, so the checkout's cleanliness is irrelevant there (GC:24, GC:156 item 1).
|
|
71
|
+
if (useCommittedSource && requireCleanCheckout && !snapshotSource) assertCommittedCheckoutClean(root);
|
|
72
|
+
const selected = snapshotSource ? { commit: snapshotSource.commit, expectedTree: snapshotSource.tree } : {};
|
|
73
|
+
const source = useCommittedSource ? acquireCommittedSource(root, { ...selected, requiredPaths: [...(trustState.trust?.provenance ?? []).flatMap((record) => record.paths ?? []), ...(trustState.trust?.rootFingerprint && !trustState.trust?.keyAuthority ? [".rightkit/ownership-keys.json"] : [])] }) : null;
|
|
53
74
|
let keyLogEvidence = null;
|
|
54
75
|
if (trustState.trust?.keyAuthority) {
|
|
55
76
|
const resolved = readAuthority(trustState.trust);
|
|
@@ -59,7 +80,7 @@ function createEvidence(repoRoot, {
|
|
|
59
80
|
keyLogEvidence = { keyLogDigest: resolved.digest, keyCheckpoint: resolved.keyCheckpoint, rootFingerprint: resolved.rootFingerprint, keyAuthority: resolved.authority };
|
|
60
81
|
if (!resolved.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
|
|
61
82
|
} else if (trustState.trust?.rootFingerprint) {
|
|
62
|
-
const keySource = source ?? acquireCommittedSource(root, { requiredPaths: [".rightkit/ownership-keys.json"] });
|
|
83
|
+
const keySource = source ?? acquireCommittedSource(root, { ...selected, requiredPaths: [".rightkit/ownership-keys.json"] });
|
|
63
84
|
const sourceData = committedSourceDataForInternalUse(keySource);
|
|
64
85
|
if (!sourceData) throw new Error("protected ownership key log requires authenticated committed source");
|
|
65
86
|
const entry = sourceData.entries.find((item) => item.rel === ".rightkit/ownership-keys.json");
|
|
@@ -70,7 +91,8 @@ function createEvidence(repoRoot, {
|
|
|
70
91
|
keyLogEvidence = { keyLogDigest: resolved.keyLogDigest, keyCheckpoint: resolved.checkpoint, rootFingerprint: resolved.trust.rootFingerprint };
|
|
71
92
|
if (!resolved.trust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
|
|
72
93
|
}
|
|
73
|
-
if (useCommittedSource && requireCleanCheckout) assertCommittedCheckoutClean(root, { expectedCommit: source.commit, expectedTree: source.tree });
|
|
94
|
+
if (useCommittedSource && requireCleanCheckout && !snapshotSource) assertCommittedCheckoutClean(root, { expectedCommit: source.commit, expectedTree: source.tree });
|
|
95
|
+
if (snapshotSource && (source.commit !== snapshotSource.commit || source.tree !== snapshotSource.tree)) throw new Error("ownership evidence: the acquired committed source is not the selected commit's");
|
|
74
96
|
const commit = useCommittedSource ? source.commit : git(root, ["rev-parse", "HEAD"], gitRunner);
|
|
75
97
|
if (!commit) throw new Error("ownership evidence: scan source did not produce a HEAD commit");
|
|
76
98
|
const tree = useCommittedSource ? source.tree : git(root, ["rev-parse", `${commit}^{tree}`], gitRunner);
|
|
@@ -92,8 +114,7 @@ function createEvidence(repoRoot, {
|
|
|
92
114
|
? validateOwnershipFloor(trustState.trust, { scannerVersion, catalogVersion, catalogDigest, ...(policyBundleDigest !== undefined ? { policyBundleDigest } : {}) })
|
|
93
115
|
: { ok: true, problems: [], warnings: ["protected ownership floor is unavailable: no protected trust registry is enrolled on this host (warning only)"] };
|
|
94
116
|
floor.warnings = [...(floor.warnings ?? []), ...(bundleProblem ? [bundleProblem] : [])];
|
|
95
|
-
|
|
96
|
-
const real = (p) => { try { return realpathSync.native(p); } catch { return path.resolve(p); } };
|
|
117
|
+
const real = realPath;
|
|
97
118
|
const requestedRoot = path.relative(real(root), real(scanRoot)).replaceAll("\\", "/") || ".";
|
|
98
119
|
if (requestedRoot.startsWith("../") || requestedRoot === ".." || path.isAbsolute(requestedRoot)) throw new Error("ownership evidence scan root must be inside repository");
|
|
99
120
|
const evidenceDiagnostics = [];
|
|
@@ -199,13 +220,13 @@ function createEvidence(repoRoot, {
|
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
/** Create scan evidence bound to committed source, catalog, and signed exceptions. */
|
|
202
|
-
export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
|
|
203
|
-
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
|
|
223
|
+
export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
|
|
224
|
+
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true, releaseSource });
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
/** Alias spelling makes the immutable-source contract explicit at adapter boundaries. */
|
|
207
|
-
export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
|
|
208
|
-
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true });
|
|
228
|
+
export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
|
|
229
|
+
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, releaseSource });
|
|
209
230
|
}
|
|
210
231
|
|
|
211
232
|
// Injection seam is deliberately named for tests and is not used by release commands.
|
|
@@ -228,12 +249,15 @@ export function runProtectedLocalOwnershipCheck(repoRoot, scanRoot = repoRoot) {
|
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
/** Re-scan exact source and compare every sealed binding before release upload. */
|
|
231
|
-
export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null) {
|
|
232
|
-
return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope), scope);
|
|
252
|
+
export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null, releaseSource = null) {
|
|
253
|
+
return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope, releaseSource), scope);
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
export function verifyOwnershipEvidenceForTest(expected, repoRoot, options) {
|
|
236
|
-
|
|
257
|
+
// Re-scan the way the evidence was made: committed-git-tree evidence (always the case in a door source split) through the
|
|
258
|
+
// committed-source scan, as verifyOwnershipEvidence does in production.
|
|
259
|
+
const rescan = expected?.source === "committed-git-tree" ? createCommittedOwnershipEvidenceForTest : createOwnershipEvidenceForTest;
|
|
260
|
+
return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => rescan(repoRoot, options), options.scope ?? null);
|
|
237
261
|
}
|
|
238
262
|
|
|
239
263
|
function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent, scope = null) {
|
package/ownership-hooks.mjs
CHANGED
|
@@ -1,41 +1,22 @@
|
|
|
1
|
-
// right-release hooks
|
|
1
|
+
// right-release hooks: the advisory pre-commit ownership gate is retired (docs/GATE-CONTRACT.md: "Commit and push run no gate and no
|
|
2
|
+
// local hook checks"). Nothing here writes a hook any more.
|
|
2
3
|
//
|
|
3
|
-
// right-release hooks install [--app <dir>]
|
|
4
|
+
// right-release hooks install [--app <dir>] (`right-release hooks install` is `rightkit-gate install-hook`: it retires the hook)
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
import {
|
|
6
|
+
// An earlier release wrote `.git/hooks/pre-commit` (or the one in core.hooksPath) running `right-release ownership --staged`, and
|
|
7
|
+
// kept a foreign pre-commit as pre-commit.rightkit-foreign. Retirement removes the hook by its own marker only and puts the foreign
|
|
8
|
+
// pre-commit back. `right-release ownership --staged` stays a manual command; broker admission and the release gate remain
|
|
9
|
+
// authoritative.
|
|
10
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { spawnSync } from "./background-process.mjs";
|
|
14
|
+
import { HOOK_MARKER, retireHooksIn } from "./hook-retirement.mjs";
|
|
13
15
|
import { repositoryRootFor } from "./ownership-staged.mjs";
|
|
14
16
|
|
|
15
|
-
export
|
|
16
|
-
const FOREIGN = "pre-commit.rightkit-foreign";
|
|
17
|
-
const PINNED_CLI = path.join("node_modules", "@rightkit", "release", "cli", "right-release.mjs");
|
|
17
|
+
export { HOOK_MARKER };
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
return [
|
|
21
|
-
"#!/bin/sh",
|
|
22
|
-
`# ${HOOK_MARKER}: advisory ownership gate installed by \`right-release hooks install\`.`,
|
|
23
|
-
"# Block-tier findings without a valid exception fail here; --no-verify bypasses this gate.",
|
|
24
|
-
"# Authoritative enforcement remains in broker admission and the release gate.",
|
|
25
|
-
"ROOT=\"$(git rev-parse --show-toplevel)\" || exit 1",
|
|
26
|
-
"HOOKS_DIR=\"$(dirname \"$0\")\"",
|
|
27
|
-
`if [ -x "$HOOKS_DIR/${FOREIGN}" ]; then`,
|
|
28
|
-
` "$HOOKS_DIR/${FOREIGN}" "$@" || exit $?`,
|
|
29
|
-
"fi",
|
|
30
|
-
`GATE="$ROOT/${PINNED_CLI.split(path.sep).join("/")}"`,
|
|
31
|
-
"if [ ! -f \"$GATE\" ]; then",
|
|
32
|
-
" echo \"[rightkit] @rightkit/release is not installed in this repository; add it as a devDependency, then re-run: right-release hooks install\" >&2",
|
|
33
|
-
" exit 1",
|
|
34
|
-
"fi",
|
|
35
|
-
"exec node \"$GATE\" ownership --staged",
|
|
36
|
-
"",
|
|
37
|
-
].join("\n");
|
|
38
|
-
}
|
|
19
|
+
const FS_OPS = Object.freeze({ readdir: readdirSync, readFile: (file) => readFileSync(file, "utf8"), lstat: lstatSync, unlink: unlinkSync, rename: renameSync, rmdir: rmdirSync });
|
|
39
20
|
|
|
40
21
|
function git(cwd, args) {
|
|
41
22
|
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))) });
|
|
@@ -52,46 +33,26 @@ export function hooksDirectoryFor(root) {
|
|
|
52
33
|
return path.resolve(root, common);
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
export function installPreCommitHook(repoDir) {
|
|
36
|
+
/** Remove the ownership pre-commit hook an earlier release wrote, restoring the foreign pre-commit it displaced. { status: "retired" | "restored" | "absent", file } */
|
|
37
|
+
export function retirePreCommitHook(repoDir) {
|
|
59
38
|
const root = repositoryRootFor(repoDir);
|
|
60
|
-
const pinned = path.join(root, PINNED_CLI);
|
|
61
|
-
if (!existsSync(pinned)) throw new Error(`@rightkit/release is not installed at ${pinned}; add it as a devDependency and run pnpm install first`);
|
|
62
39
|
const hooksDir = hooksDirectoryFor(root);
|
|
40
|
+
const result = retireHooksIn(hooksDir, FS_OPS, { scripts: false });
|
|
63
41
|
const file = path.join(hooksDir, "pre-commit");
|
|
64
|
-
|
|
65
|
-
const body = renderPreCommitHook();
|
|
66
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
67
|
-
let status = "installed";
|
|
68
|
-
if (lstatSync(file, { throwIfNoEntry: false })) {
|
|
69
|
-
const current = readFileSync(file, "utf8");
|
|
70
|
-
if (current === body) return { status: "unchanged", file };
|
|
71
|
-
if (current.includes(HOOK_MARKER)) status = "updated";
|
|
72
|
-
else {
|
|
73
|
-
if (existsSync(path.join(hooksDir, FOREIGN))) throw new Error(`${FOREIGN} already exists; resolve the chained hook manually before installing`);
|
|
74
|
-
renameSync(file, foreign);
|
|
75
|
-
status = "chained";
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
79
|
-
writeFileSync(temporary, body, { mode: 0o755 });
|
|
80
|
-
chmodSync(temporary, 0o755);
|
|
81
|
-
renameSync(temporary, file);
|
|
82
|
-
return { status, file };
|
|
42
|
+
return { status: result.restored.length ? "restored" : result.removed.includes("pre-commit") ? "retired" : "absent", file };
|
|
83
43
|
}
|
|
84
44
|
|
|
85
45
|
export function main(args, { print = (line) => console.log(line) } = {}) {
|
|
86
46
|
const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
87
47
|
if (args[0] !== "install") {
|
|
88
|
-
print("right-release hooks install [--app <dir>]");
|
|
48
|
+
print("right-release hooks install [--app <dir>] (retires the ownership pre-commit hook; no hook is written)");
|
|
89
49
|
return args.includes("--help") ? 0 : 2;
|
|
90
50
|
}
|
|
91
51
|
try {
|
|
92
|
-
const result =
|
|
93
|
-
print(`[right-release] pre-commit ownership gate ${result.status}: ${result.file}`);
|
|
94
|
-
if (result.status === "
|
|
52
|
+
const result = retirePreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
|
|
53
|
+
print(`[right-release] pre-commit ownership gate retired (${result.status}): ${result.file}`);
|
|
54
|
+
if (result.status === "restored") print("[right-release] the previous pre-commit hook is back in place");
|
|
55
|
+
print("[right-release] commit runs no local hook check; `right-release ownership --staged` is still available by hand");
|
|
95
56
|
return 0;
|
|
96
57
|
} catch (error) {
|
|
97
58
|
print(`[right-release] hooks install FAILED: ${error.message}`);
|
package/ownership-lockfiles.mjs
CHANGED
|
@@ -322,9 +322,22 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
322
322
|
if (bytes && !contentDigests.some((item) => item.path === rel)) contentDigests.push({ path: rel, digest: sha256(bytes) });
|
|
323
323
|
return bytes;
|
|
324
324
|
};
|
|
325
|
+
// Directories the repository declares as gate fixtures (ci/gate.config.json `fixtures`: consumer recipes, vendored crates,
|
|
326
|
+
// spikes) are samples and harnesses, never the app's code: the gate builds none of them as units, and neither are they scanned here.
|
|
327
|
+
let fixtures = [];
|
|
328
|
+
try {
|
|
329
|
+
const rel = "ci/gate.config.json";
|
|
330
|
+
const config = inventory.snapshotFiles?.get(`${inventory.pathPrefix ?? ""}${rel}`)?.bytes ?? files.get(rel)?.bytes
|
|
331
|
+
?? (!inventory.committed && existsSync(path.resolve(appRoot, rel)) ? readFileSync(path.resolve(appRoot, rel)) : undefined);
|
|
332
|
+
if (config) fixtures = (JSON.parse(config.toString("utf8")).fixtures ?? []).map((d) => String(d).replace(/\/+$/, ""));
|
|
333
|
+
} catch { fixtures = []; }
|
|
334
|
+
const underFixture = (rel) => fixtures.some((d) => rel === d || rel.startsWith(`${d}/`));
|
|
325
335
|
for (const manifest of manifests) {
|
|
326
336
|
const base = path.posix.basename(manifest.rel), ecosystem = base === "Cargo.toml" ? "cargo" : base === "package.json" ? "npm" : null;
|
|
327
337
|
if (!ecosystem || !manifest.bytes) continue;
|
|
338
|
+
if (underFixture(manifest.rel)) continue;
|
|
339
|
+
// A Cargo manifest with its own [workspace] table is a workspace root: its lock sits beside it and Cargo never looks higher.
|
|
340
|
+
const workspaceRoot = ecosystem === "cargo" && /^\s*\[workspace\]\s*$/m.test(manifest.bytes.toString("utf8"));
|
|
328
341
|
const names = ecosystem === "cargo" ? ["Cargo.lock"] : OWNERSHIP_LOCKFILES.slice(1);
|
|
329
342
|
let dir = path.posix.dirname(manifest.rel), found = false;
|
|
330
343
|
try {
|
|
@@ -337,7 +350,7 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
337
350
|
}
|
|
338
351
|
found = true; break;
|
|
339
352
|
}
|
|
340
|
-
if (dir === ".") break;
|
|
353
|
+
if (dir === "." || workspaceRoot) break;
|
|
341
354
|
dir = path.posix.dirname(dir);
|
|
342
355
|
}
|
|
343
356
|
if (!found) diagnostic(manifest.rel, "lockfile-missing", ecosystem);
|
|
@@ -17,10 +17,11 @@ const CATALOG_FILE = "rightkit-ownership.json";
|
|
|
17
17
|
const REGISTRY_FILE = "ownership-repositories.json";
|
|
18
18
|
// Step 4: the release QA requirement floor is protected like the catalog. Dropping a required check changes the digest.
|
|
19
19
|
export const QA_FLOOR_FILE = "release-qa-floor.json";
|
|
20
|
-
// Modules that make or carry enforcement decisions. Their import closure is pinned.
|
|
20
|
+
// Modules that make or carry enforcement decisions. Their import closure is pinned. ownership-hooks.mjs left the list with the
|
|
21
|
+
// advisory pre-commit hook it used to write (GATE-CONTRACT: commit runs no local hook check): it retires that hook and decides nothing.
|
|
21
22
|
export const POLICY_BUNDLE_ENTRYPOINTS = Object.freeze([
|
|
22
23
|
"ownership-scan-worker.mjs", "ownership-evidence.mjs", "ownership-verdict.mjs", "ownership-admission.mjs",
|
|
23
|
-
"ownership-staged.mjs", "ownership-
|
|
24
|
+
"ownership-staged.mjs", "ownership-key-trust.mjs", "release-qa-receipt.mjs",
|
|
24
25
|
]);
|
|
25
26
|
// Data files are pinned through their own digests, not as source files.
|
|
26
27
|
const DATA_FILES = new Set([CATALOG_FILE, REGISTRY_FILE, ALLOWLIST_FILE, QA_FLOOR_FILE]);
|
package/ownership-staged.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Ownership gate for the STAGED tree (index content), never the working tree: the manual command `right-release ownership --staged`.
|
|
2
|
+
// No git hook runs it any more (docs/GATE-CONTRACT.md: commit runs no local hook check).
|
|
2
3
|
// Authoritative enforcement stays in broker admission and the release gate.
|
|
3
4
|
//
|
|
4
5
|
// right-release ownership --staged [--app <dir>] [--json]
|
package/ownership.mjs
CHANGED
|
@@ -382,6 +382,9 @@ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDi
|
|
|
382
382
|
const refs = lock.package.filter((p) => p.name === owner).flatMap((p) => p.dependencies ?? []);
|
|
383
383
|
candidates = candidates.filter((p) => refs.some((ref) => ref === `${d.name} ${p.version}` || ref.startsWith(`${d.name} ${p.version} (`)));
|
|
384
384
|
}
|
|
385
|
+
// A crate may declare two versions of one package under different names (windows 0.61 and 0.62 lanes): the declared
|
|
386
|
+
// requirement picks the resolved one, by Cargo's default caret rule (or an exact `=` pin).
|
|
387
|
+
if (candidates.length > 1 && typeof d.requirement === "string") candidates = candidates.filter((p) => cargoRequirementMatches(d.requirement, p.version));
|
|
385
388
|
if (candidates.length !== 1 || typeof candidates[0].version !== "string") throw new Error(`Cargo.lock resolution missing or ambiguous: ${d.name}`);
|
|
386
389
|
d.resolvedVersion = candidates[0].version;
|
|
387
390
|
}
|
|
@@ -917,3 +920,24 @@ export function checkCommittedApp(appRoot, manifest, options = {}) {
|
|
|
917
920
|
export function checkCommittedAppForTest(appRoot, manifest, options = {}) {
|
|
918
921
|
return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource, options.readAuthorityForTest);
|
|
919
922
|
}
|
|
923
|
+
|
|
924
|
+
/** Cargo's default requirement (`0.61`, `^1.2`, `=0.3.4`, `>=2.10.1, <3`) against a resolved version; anything else matches. */
|
|
925
|
+
function cargoRequirementMatches(requirement, version) {
|
|
926
|
+
const parse = (v) => String(v).split("+")[0].split("-")[0].split(".").map((n) => Number(n));
|
|
927
|
+
const [x, y = 0, z = 0] = parse(version);
|
|
928
|
+
const cmp = (a, b) => a[0] - b[0] || (a[1] ?? 0) - (b[1] ?? 0) || (a[2] ?? 0) - (b[2] ?? 0);
|
|
929
|
+
return requirement.split(",").map((r) => r.trim()).filter(Boolean).every((r) => {
|
|
930
|
+
const m = /^(=|\^|>=|<=|>|<|~)?\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(r);
|
|
931
|
+
if (!m) return true;
|
|
932
|
+
const op = m[1] ?? "^"; const want = [Number(m[2]), m[3] === undefined ? undefined : Number(m[3]), m[4] === undefined ? undefined : Number(m[4])];
|
|
933
|
+
const v = [x, y, z]; const c = cmp(v, want);
|
|
934
|
+
if (op === "=") return want.every((n, i) => n === undefined || n === v[i]);
|
|
935
|
+
if (op === ">=") return c >= 0; if (op === ">") return c > 0; if (op === "<=") return c <= 0; if (op === "<") return c < 0;
|
|
936
|
+
if (op === "~") return c >= 0 && v[0] === want[0] && (want[1] === undefined || v[1] === want[1]);
|
|
937
|
+
if (c < 0) return false;
|
|
938
|
+
if (want[0] > 0) return v[0] === want[0];
|
|
939
|
+
if (want[1] === undefined) return v[0] === 0;
|
|
940
|
+
if (want[1] > 0) return v[0] === 0 && v[1] === want[1];
|
|
941
|
+
return v[0] === 0 && v[1] === 0 && (want[2] === undefined || v[2] === want[2]);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
@@ -8,7 +8,9 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
11
|
+
import { ArtifactPathError, SPLIT_TARGET_UNRESOLVED_CODE, isSplitArtifactEnv } from "./release-artifact-path.mjs";
|
|
11
12
|
import { defaultManagedRoots, isUnderManagedRoot } from "./managed-target-stage.mjs";
|
|
13
|
+
import { NativeHelpersError, nativeHelperNestedCode, validateNativeHelpers } from "./native-helpers.mjs";
|
|
12
14
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
13
15
|
import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
|
|
14
16
|
import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
|
|
@@ -78,13 +80,13 @@ const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config",
|
|
|
78
80
|
* release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
|
|
79
81
|
* the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
|
|
80
82
|
*/
|
|
81
|
-
function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs }) {
|
|
83
|
+
function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs, verifySidecars, nativeHelpers, boundTargets }) {
|
|
82
84
|
const steps = [
|
|
83
85
|
...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
|
|
84
86
|
{ phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
|
|
85
87
|
...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
|
|
86
88
|
];
|
|
87
|
-
const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}) };
|
|
89
|
+
const plan = { version: 1, steps, ...(boundTargets?.length ? { boundTargets } : {}), ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
88
90
|
return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -113,6 +115,9 @@ function targetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, m
|
|
|
113
115
|
function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, label, managedRoots, staged }) {
|
|
114
116
|
const explicit = spec.cargoTargetRoot;
|
|
115
117
|
if (explicit !== undefined && explicit !== true && explicit !== false && typeof explicit !== "string") fail(`${label}.cargoTargetRoot must be true or an app-relative Cargo.toml path`);
|
|
118
|
+
// A door split (row 7.04c, R-T1/R-T3): the snapshot has no target directory, so every declared target path is the bound target's, absolute, and
|
|
119
|
+
// nothing is copied (the #924 bridge plans zero staged entries): the build output is read and signed in place.
|
|
120
|
+
if (isSplitArtifactEnv(env)) return splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label });
|
|
116
121
|
const envDir = typeof env.CARGO_TARGET_DIR === "string" && env.CARGO_TARGET_DIR.trim() !== "";
|
|
117
122
|
if (explicit === false || (!explicit && !envDir)) return (declared) => declared;
|
|
118
123
|
const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
|
|
@@ -140,9 +145,34 @@ function buildTargetRootMapper({ spec, appRoot, cwd, env, resolveTargetRoot, lab
|
|
|
140
145
|
};
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
function
|
|
148
|
+
function splitTargetRootMapper({ spec, appRoot, cwd, resolveTargetRoot, label }) {
|
|
149
|
+
const explicit = spec.cargoTargetRoot;
|
|
150
|
+
const manifestRel = typeof explicit === "string" ? relativeInside(explicit, `${label}.cargoTargetRoot`) : path.posix.join(cwd === "." ? "" : cwd, "src-tauri", "Cargo.toml");
|
|
151
|
+
const manifest = path.resolve(appRoot, manifestRel);
|
|
152
|
+
// An app that declares no recipe target and has no src-tauri manifest has nothing to bind; a declared one that is missing is refused below.
|
|
153
|
+
if (explicit !== true && typeof explicit !== "string" && !existsSync(manifest)) return (declared) => declared;
|
|
154
|
+
let targetDir;
|
|
155
|
+
try { targetDir = resolveTargetRoot(manifest); } catch (error) {
|
|
156
|
+
throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: cannot resolve the Cargo target directory of ${manifest} (${error.message}); a split never falls back to a snapshot-relative target/`);
|
|
157
|
+
}
|
|
158
|
+
const relativeToApp = path.relative(appRoot, targetDir);
|
|
159
|
+
if (relativeToApp === "" || (!relativeToApp.startsWith("..") && !path.isAbsolute(relativeToApp))) {
|
|
160
|
+
throw new ArtifactPathError(SPLIT_TARGET_UNRESOLVED_CODE, `${label}: the Cargo target directory ${targetDir} is inside the snapshot; a split binds it outside (CARGO_TARGET_DIR, row 7.09)`);
|
|
161
|
+
}
|
|
162
|
+
const prefix = path.posix.join(path.posix.dirname(manifestRel).replace(/^\.$/, ""), "target");
|
|
163
|
+
const mapper = (declared) => {
|
|
164
|
+
const normalized = String(declared).replaceAll("\\", "/");
|
|
165
|
+
if (normalized !== prefix && !normalized.startsWith(`${prefix}/`)) return declared;
|
|
166
|
+
if (normalized.split("/").includes("..")) fail(`${label}: ${declared} must not contain ..`);
|
|
167
|
+
return path.resolve(targetDir, normalized.slice(prefix.length + 1));
|
|
168
|
+
};
|
|
169
|
+
mapper.bound = [targetDir];
|
|
170
|
+
return mapper;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function winPrePackage({ extras, cwd, appRoot, triple, jobs, timeoutMs, cleanup, stability, stage, boundTargets }) {
|
|
144
174
|
const args = ["exec", "tauri", "build", "--no-bundle", "--target", triple, ...configArgs(extras, appRoot, cwd), ...buildCargoArgs(extras, jobs)];
|
|
145
|
-
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs });
|
|
175
|
+
return recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stability, stage, timeoutMs, boundTargets });
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
/** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
|
|
@@ -192,14 +222,16 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
192
222
|
return {
|
|
193
223
|
...rest,
|
|
194
224
|
signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
|
|
195
|
-
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage }),
|
|
225
|
+
prePackage: winPrePackage({ extras, cwd, appRoot: plan.appRoot, triple: plan.triple, jobs, timeoutMs: timeout, cleanup, stability: { files: [rawExe] }, stage: prePackageStage, boundTargets: mapPath.bound }),
|
|
196
226
|
package: bundleCommand,
|
|
197
227
|
sign: { ...(target.sign ?? {}), prePackageFiles: [rawExe], files: [installerFile] },
|
|
198
228
|
recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
|
|
199
229
|
};
|
|
200
230
|
}
|
|
201
231
|
|
|
202
|
-
const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater"]);
|
|
232
|
+
const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater", "stageSidecars", "nativeHelpers"]);
|
|
233
|
+
const STAGE_SIDECARS_KEYS = new Set(["config", "receipt"]);
|
|
234
|
+
const DEFAULT_SIDECAR_RECEIPT = "src-tauri/binaries/stage-sidecar.json";
|
|
203
235
|
const MAC_TRIPLES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
|
|
204
236
|
const MAC_CLEANUP_KEYS = new Set(["staleApps", "staleDmgVolumes", "lsclean"]);
|
|
205
237
|
|
|
@@ -221,7 +253,26 @@ function macCleanupSpec(value) {
|
|
|
221
253
|
* `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
|
|
222
254
|
* it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
|
|
223
255
|
*/
|
|
224
|
-
|
|
256
|
+
/**
|
|
257
|
+
* `stageSidecars: true | { config?, receipt? }`: stage the release config's declarative `sidecars` for the build's triple through this
|
|
258
|
+
* package's own `right-release stage-sidecar --target <triple>` as the first prepare step. For `universal-apple-darwin` stage-sidecar
|
|
259
|
+
* builds each slice (aarch64 and x86_64), `lipo -create`s them into `<name>-universal-apple-darwin` (the name Tauri's universal
|
|
260
|
+
* build expects) and refuses a result without both archs; recipe-build then re-checks every externalBin in the receipt
|
|
261
|
+
* (`receipt`, default src-tauri/binaries/stage-sidecar.json) with `lipo -archs` (exactly arm64 + x86_64) before the Tauri build.
|
|
262
|
+
*/
|
|
263
|
+
function stageSidecarsSpec(value) {
|
|
264
|
+
if (value === undefined || value === false) return undefined;
|
|
265
|
+
if (value !== true && !isObject(value)) fail("targets.mac.macPackage.stageSidecars must be true or { config?, receipt? }");
|
|
266
|
+
const spec = value === true ? {} : value;
|
|
267
|
+
const unknownKeys = Object.keys(spec).filter((key) => !STAGE_SIDECARS_KEYS.has(key));
|
|
268
|
+
if (unknownKeys.length) fail(`targets.mac.macPackage.stageSidecars has unknown key(s): ${unknownKeys.join(", ")} (allowed: ${[...STAGE_SIDECARS_KEYS].join(", ")})`);
|
|
269
|
+
return {
|
|
270
|
+
...(spec.config === undefined ? {} : { config: relativeInside(spec.config, "targets.mac.macPackage.stageSidecars.config") }),
|
|
271
|
+
receipt: spec.receipt === undefined ? DEFAULT_SIDECAR_RECEIPT : relativeInside(spec.receipt, "targets.mac.macPackage.stageSidecars.receipt"),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function expandMacPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
|
|
225
276
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
226
277
|
const spec = target.macPackage;
|
|
227
278
|
if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
|
|
@@ -247,6 +298,25 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
247
298
|
}
|
|
248
299
|
const override = { bundle: { createUpdaterArtifacts: false, macOS: { signingIdentity: null } } };
|
|
249
300
|
const extras = buildExtras(spec, "targets.mac.macPackage", { postBuild: true });
|
|
301
|
+
const sidecars = stageSidecarsSpec(spec.stageSidecars);
|
|
302
|
+
if (sidecars) {
|
|
303
|
+
// First prepare step: stage the declared sidecars for this triple (universal: both slices, lipo, <name>-universal-apple-darwin).
|
|
304
|
+
extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
|
|
305
|
+
}
|
|
306
|
+
let helpers;
|
|
307
|
+
if (spec.nativeHelpers !== undefined) {
|
|
308
|
+
try { helpers = validateNativeHelpers(spec.nativeHelpers, { label: "targets.mac.macPackage.nativeHelpers" }); } catch (error) {
|
|
309
|
+
if (error instanceof NativeHelpersError) fail(error.message);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
// A universal app may not carry a thin helper: it would compile, sign and notarize and then fail on half the Macs.
|
|
313
|
+
const thin = triple === "universal-apple-darwin" ? helpers.filter((helper) => !helper.universal) : [];
|
|
314
|
+
if (thin.length) fail(`targets.mac.macPackage.nativeHelpers: ${thin.map((helper) => helper.name).join(", ")} must declare universal: true (this build's triple is universal-apple-darwin)`);
|
|
315
|
+
if (helpers.some((helper) => helper.kind === "app") && (typeof config?.version !== "string" || !/^\d+(\.\d+){0,2}$/.test(config.version))) fail("targets.mac.macPackage.nativeHelpers: a kind \"app\" helper needs the release config version (1-3 numeric parts) for its Info.plist");
|
|
316
|
+
const collisions = Object.keys(nativeHelperNestedCode(helpers)).filter((key) => isObject(target.sign?.nestedCode) && Object.hasOwn(target.sign.nestedCode, key));
|
|
317
|
+
if (collisions.length) fail(`targets.mac.macPackage.nativeHelpers: sign.nestedCode must not redeclare ${collisions.join(", ")}; declare entitlements on the helper entry`);
|
|
318
|
+
if (isObject(target.sign?.entitlements) ? helpers.some((helper) => Object.hasOwn(target.sign.entitlements, helper.embed)) : false) fail("targets.mac.macPackage.nativeHelpers: sign.entitlements must not redeclare a helper's embed path; declare entitlements on the helper entry");
|
|
319
|
+
}
|
|
250
320
|
const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", ...configArgs(extras, appRoot, cwd), "--config", JSON.stringify(override)];
|
|
251
321
|
if (spec.features?.length) args.push("--features", spec.features.join(","));
|
|
252
322
|
args.push(...profileArgs(extras));
|
|
@@ -270,7 +340,10 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
270
340
|
// An entry inside another staged entry (a file within the .app) is covered by copying the outer one.
|
|
271
341
|
const macEntries = [...mapPath.staged.values()].filter((entry) => ![...mapPath.staged.values()].some((outer) => outer !== entry && entry.to.startsWith(`${outer.to}/`)));
|
|
272
342
|
const macStage = macEntries.length ? { post: macEntries.map((entry) => ({ ...entry, fresh: true })), resolve: mapPath.resolve() } : undefined;
|
|
273
|
-
const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage;
|
|
343
|
+
const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage || helpers;
|
|
344
|
+
const forceArch = helpers && triple !== "universal-apple-darwin" ? (triple === "aarch64-apple-darwin" ? "arm64" : "x86_64") : undefined;
|
|
345
|
+
const nativeHelpersPlan = helpers ? { app, helpers, ...(config?.version ? { version: config.version } : {}), ...(forceArch ? { forceArch } : {}), configuration: "release" } : undefined;
|
|
346
|
+
const verifySidecars = sidecars && triple === "universal-apple-darwin" ? { receipt: sidecars.receipt, target: triple } : undefined;
|
|
274
347
|
const { macPackage, recipe: _ignoredMac, ...rest } = target;
|
|
275
348
|
// The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
|
|
276
349
|
const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
|
|
@@ -283,8 +356,9 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
283
356
|
return {
|
|
284
357
|
...rest,
|
|
285
358
|
...postSign,
|
|
286
|
-
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
287
|
-
|
|
359
|
+
prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage, verifySidecars, nativeHelpers: nativeHelpersPlan, boundTargets: mapPath.bound }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
|
|
360
|
+
// Helpers are signed by release.mjs inside-out through sign.nestedCode (deepest path first, hardened runtime, each helper's own entitlements), then the app.
|
|
361
|
+
sign: { ...(target.sign ?? {}), ...(helpers ? { nestedCode: { ...nativeHelperNestedCode(helpers), ...(target.sign?.nestedCode ?? {}) } } : {}), prePackageFiles },
|
|
288
362
|
notarize: { ...target.notarize, app, appBeforePackage: true },
|
|
289
363
|
dmg: isObject(target.dmg) ? target.dmg : {},
|
|
290
364
|
recipe: { kind: "macPackage", universal: triple === "universal-apple-darwin" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
"!*.test.mjs",
|
|
79
79
|
"!cli/*.test.mjs",
|
|
80
80
|
"!standalone-clone-evidence.json",
|
|
81
|
-
"!refusals.json"
|
|
81
|
+
"!refusals.json",
|
|
82
|
+
"!provenance-real-receipt.fixture.json"
|
|
82
83
|
],
|
|
83
84
|
"sideEffects": false,
|
|
84
85
|
"publishConfig": {
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"directory": "packages/release"
|
|
92
93
|
},
|
|
93
94
|
"dependencies": {
|
|
94
|
-
"@rightkit/outdated": "0.
|
|
95
|
+
"@rightkit/outdated": "0.3.0",
|
|
95
96
|
"smol-toml": "1.9.1"
|
|
96
97
|
},
|
|
97
98
|
"scripts": {
|
package/pipeline-admission.mjs
CHANGED
|
@@ -239,13 +239,13 @@ function admitRuntime(bin, rest, options, label) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
|
-
* `rightkit-gate install-hook`:
|
|
243
|
-
* It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is run by
|
|
244
|
-
*
|
|
242
|
+
* `rightkit-gate install-hook`: retires the git hooks an earlier release wrote and registers the agent hook, so an app's `prepare` script may run it.
|
|
243
|
+
* It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is never run by a release
|
|
244
|
+
* config; only the installer and the version probe are admitted.
|
|
245
245
|
*/
|
|
246
246
|
function admitRightKitGate(args, label) {
|
|
247
247
|
if (args.length === 1 && (args[0] === "install-hook" || args[0] === "--version")) return admit(`rightkit-gate-${args[0].replace(/^--/, "")}`, "owner");
|
|
248
|
-
return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself
|
|
248
|
+
return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself is run through the door, never by a release config`);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
function admitRightRelease(args, label) {
|