@rightkit/release 0.3.0 → 0.4.0
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 +25 -4
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +11 -2
- package/codesign-environment.mjs +2 -2
- package/content-digest.mjs +209 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/github-updater-release.mjs +70 -7
- package/in-place-install.mjs +17 -14
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +21 -40
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +92 -37
- package/managed-cargo.mjs +8 -0
- package/mirror-root-artifact.mjs +5 -1
- package/native-cargo-layout.mjs +47 -4
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +1 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +49 -7
- package/package-recipe-targets.mjs +41 -1
- package/package.json +1 -1
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +15 -4
- package/publish-cargo.mjs +109 -22
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +45 -17
- package/release.mjs +69 -17
- package/rightkit-ownership.json +1 -1
- package/rightkit-versions.json +379 -189
- package/runtime-budgets.mjs +120 -16
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +160 -32
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
|
@@ -8,6 +8,8 @@ import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "
|
|
|
8
8
|
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, TRUST_SYNC_RETIRED, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
9
9
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
10
10
|
import { buildMacInvocation, hasTerminal } from "./ownership-key-elevation.mjs";
|
|
11
|
+
import { mergeRepositoriesFromInstalledRelease, prepareTrustUpdate, REPOSITORY_REGISTRY_MALFORMED_CODE } from "./ownership-key-trust.mjs";
|
|
12
|
+
import { summarizeTrustChange } from "./ownership-trust-summary.mjs";
|
|
11
13
|
|
|
12
14
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
const LOG = ".rightkit/ownership-keys.json";
|
|
@@ -355,14 +357,24 @@ export async function main(args, deps = {}) {
|
|
|
355
357
|
if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
|
|
356
358
|
try {
|
|
357
359
|
if (!platform) throw new Error(`ownership key management unsupported platform: ${process.platform}`);
|
|
360
|
+
const refresh = args[0] === "trust-refresh";
|
|
358
361
|
// Delegate BEFORE taking the command lock: the desktop-session child needs that same lock.
|
|
359
|
-
|
|
362
|
+
// trust-refresh signs nothing, so it never touches the native key helper.
|
|
363
|
+
const helper = deps.helper ?? (refresh ? null : helperFor(run));
|
|
360
364
|
const lockPath = deps.lockPath ?? path.join(os.tmpdir(), "rightkit-key-command.lock");
|
|
361
365
|
let lockFd;
|
|
362
366
|
lockFd = acquireCommandLock(lockPath);
|
|
363
367
|
try {
|
|
364
368
|
commandTemp = mkdtempSync(path.join(os.tmpdir(), "rightkit-key-command-"));
|
|
365
369
|
const authority = resolveAuthority(root, run, deps.authority);
|
|
370
|
+
if (refresh) {
|
|
371
|
+
// Key-neutral: no key log event, no metadata commit, so no minimize hook. A dry run changes nothing at all,
|
|
372
|
+
// so it also skips the fetch and fast-forward that the real ceremony performs.
|
|
373
|
+
const dryRun = args.includes("--dry-run");
|
|
374
|
+
const transport = dryRun ? null : syncBeforeCeremony(root, run);
|
|
375
|
+
const headSnapshot = dryRun ? null : git(run, root, ["rev-parse", "HEAD"]);
|
|
376
|
+
return await trustRefresh(args, { ...deps, run, print, root, authority, transport, headSnapshot });
|
|
377
|
+
}
|
|
366
378
|
const hook = detectMinimizeHook(root, run, deps);
|
|
367
379
|
const transport = syncBeforeCeremony(root, run);
|
|
368
380
|
const headSnapshot = git(run, root, ["rev-parse", "HEAD"]);
|
|
@@ -370,7 +382,7 @@ export async function main(args, deps = {}) {
|
|
|
370
382
|
if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
371
383
|
if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
372
384
|
if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
373
|
-
throw new Error("expected enroll, approve-key,
|
|
385
|
+
throw new Error("expected enroll, approve-key, revoke-key, or trust-refresh (trust-sync is retired)");
|
|
374
386
|
} finally {
|
|
375
387
|
closeSync(lockFd);
|
|
376
388
|
rmSync(lockPath, { force: true });
|
|
@@ -571,6 +583,61 @@ async function revoke(args, d) {
|
|
|
571
583
|
print(`Ownership key revoked: ${keyId}; active keys: ${verifyKeyLog(next, { rootFingerprint: pin }).keys.length}`);
|
|
572
584
|
return 0;
|
|
573
585
|
}
|
|
586
|
+
const repositoryNames = (list) => list.map((identity) => String(identity).replace(/^https:\/\/github\.com\//i, "")).join(", ");
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Key-neutral elevated trust update. Builds the same request an approve-key checkpoint update sends (current committed
|
|
590
|
+
* key log, the host's pinned root fingerprint, the protected authority) so performProtectedTrustUpdate runs its
|
|
591
|
+
* additions-only registration merge and floor raise from the INSTALLED release. It adds no key event, writes nothing
|
|
592
|
+
* under .rightkit/ and creates no commit. Elevation, the pinned request digest and the installed-sibling-script rule
|
|
593
|
+
* are the unchanged elevate() path.
|
|
594
|
+
*/
|
|
595
|
+
async function trustRefresh(args, d) {
|
|
596
|
+
const { print, root } = d;
|
|
597
|
+
const dryRun = args.includes("--dry-run");
|
|
598
|
+
const rest = args.slice(1);
|
|
599
|
+
const unexpected = rest.filter((arg, i) => !(arg === "--dry-run" || arg === "--app" || rest[i - 1] === "--app"));
|
|
600
|
+
if (unexpected.length) throw new Error(`trust-refresh accepts only --dry-run and --app <dir>; unexpected: ${unexpected.join(" ")}`);
|
|
601
|
+
assertSafeMetadataParents(root, LOG);
|
|
602
|
+
const hostTrust = (d.trustLoader ?? trustState)();
|
|
603
|
+
const existing = hostTrust?.trust;
|
|
604
|
+
if (!existing?.rootFingerprint) throw new Error("this host is not enrolled: protected ownership trust has no pinned root. trust-refresh never creates trust; run `right-release ownership enroll` first");
|
|
605
|
+
const log = readLog(root);
|
|
606
|
+
if (!log.events.length) throw new Error("no root key log exists in this checkout");
|
|
607
|
+
// Verifies the local log against the pinned root, the protected authority, and (unless injected) the committed HEAD blob.
|
|
608
|
+
currentLogTrust(root, log, hostTrust, d.authority, Boolean(d.resolveTrust));
|
|
609
|
+
const pin = existing.rootFingerprint;
|
|
610
|
+
const requestBytes = Buffer.from(json({ schema: 1, log, rootFingerprint: pin, baseline: null, authority: d.authority }));
|
|
611
|
+
const registry = d.installedRegistry ? { registry: d.installedRegistry } : {};
|
|
612
|
+
const merge = mergeRepositoriesFromInstalledRelease(prepareTrustUpdate(existing, JSON.parse(requestBytes.toString("utf8"))), registry);
|
|
613
|
+
const unusable = merge.warnings.find((item) => item.code === REPOSITORY_REGISTRY_MALFORMED_CODE);
|
|
614
|
+
if (unusable) throw new Error(`the installed release's repository registry is missing or malformed (${unusable.message}); refusing to refresh trust from it`);
|
|
615
|
+
const summary = summarizeTrustChange({ existing, requestBytes, ...registry });
|
|
616
|
+
const before = new Set((existing.repositories ?? []).map((entry) => entry.repositoryIdentity));
|
|
617
|
+
const added = merge.trust.repositories.map((entry) => entry.repositoryIdentity).filter((identity) => !before.has(identity));
|
|
618
|
+
print(added.length
|
|
619
|
+
? `trust-refresh would add ${added.length} repositor${added.length === 1 ? "y" : "ies"} to protected trust: ${repositoryNames(added)}`
|
|
620
|
+
: "trust-refresh would add no repository registrations");
|
|
621
|
+
for (const line of summary.lines) print(` ${line}`);
|
|
622
|
+
if (!summary.changed) { print("Protected ownership trust is already current; nothing to elevate."); return 0; }
|
|
623
|
+
if (dryRun) { print("Dry run: nothing was elevated or written."); return 0; }
|
|
624
|
+
if (git(d.run, root, ["rev-parse", "HEAD"]) !== d.headSnapshot) throw new Error("repository HEAD changed during ownership ceremony");
|
|
625
|
+
const request = makeTrustRequest(log, pin, null, d.authority);
|
|
626
|
+
try {
|
|
627
|
+
if (request.digest !== summary.payloadDigest) throw new Error("trust request bytes differ from the summarized request; refusing to elevate");
|
|
628
|
+
try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (platform === "macos" && !hasTerminal()) error.message += ". macOS elevation needs a human: run `right-release ownership trust-refresh` at a terminal for the sudo prompt, or approve the administrator dialog on the desktop; an unattended agent session without a tty cannot pass it";
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
} finally { await import("node:fs").then(({ unlinkSync }) => unlinkSync(request.file)).catch(() => {}); }
|
|
634
|
+
postVerify(root, log, d);
|
|
635
|
+
const present = ((d.trustLoader ?? trustState)().trust?.repositories ?? []).map((entry) => entry.repositoryIdentity);
|
|
636
|
+
const missing = added.filter((identity) => !present.includes(identity));
|
|
637
|
+
if (missing.length) throw new Error(`elevated update finished but protected trust still lacks: ${repositoryNames(missing)}`);
|
|
638
|
+
print(`Protected trust now registers ${present.length} repositories: ${repositoryNames(present)}`);
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
574
641
|
async function signWithHelper(run, helper, log, operation, subject, signer) {
|
|
575
642
|
const draft = createKeyEvent(log, { operation, subject, signerKeyId: signer.keyId });
|
|
576
643
|
const input = temp("draft"), output = temp("draft-signed");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// right-release ownership-mandatory: Table 1 mandatory-owner-set check (plan task 5.1).
|
|
3
3
|
//
|
|
4
|
+
// This evaluates declarations only; it does not establish active target/features, lock resolution or production adoption.
|
|
4
5
|
// A registered app must declare every Table 1 owner (rows M1-M23) at or above the required version,
|
|
5
6
|
// from the registry. Report mode (default) prints per-app findings and exits 0; --enforce exits 1
|
|
6
7
|
// on any missing owner, below-required version, or path/git/patch override ("not-on-registry").
|
|
@@ -25,14 +26,51 @@ const SKIP_DIRS = new Set(["node_modules", "target", ".git", "rk-snapshot", "ven
|
|
|
25
26
|
const MAX_MANIFESTS = 2000;
|
|
26
27
|
|
|
27
28
|
// ---- versions -------------------------------------------------------------------------------
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Proven minimum of a Cargo/npm requirement: a version every satisfying version is guaranteed >=, or null when
|
|
31
|
+
* none can be proven (upper-only `<9`, `*`, `latest`, malformed, or a `||` branch with no proven minimum).
|
|
32
|
+
* Space/comma comparators AND (proven minimum = max of lower contributions; `<`/`<=` contribute none); `||`
|
|
33
|
+
* branches union (proven minimum = min of branch minima, null if any branch is unprovable). Hyphen ranges
|
|
34
|
+
* contribute their lower end. `>`, `>=`, `=`, `^`, `~`, bare & wildcard partials contribute their version
|
|
35
|
+
* (missing/`x`/`*` components read as 0), so `>1.2.3` proves 1.2.3 (sound, never above the true floor).
|
|
36
|
+
*/
|
|
29
37
|
export function lowerBound(spec) {
|
|
30
38
|
if (typeof spec !== "string") return null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
39
|
+
const s = spec.trim().replace(/^npm:(?:@[^@/]+\/)?[^@]+@/, "").trim();
|
|
40
|
+
if (!s || /^(\*|latest|x)$/i.test(s)) return null;
|
|
41
|
+
const part = (v) => (v === undefined || /[xX*]/.test(v) ? 0 : Number(v));
|
|
42
|
+
const versionOf = (text) => {
|
|
43
|
+
const m = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text);
|
|
44
|
+
if (m?.[4] && m[4].split(".").some((id) => !id || (/^\d+$/.test(id) && id.length > 1 && id.startsWith("0")))) return null;
|
|
45
|
+
return m ? { major: Number(m[1]), minor: part(m[2]), patch: part(m[3]), pre: m[4] ?? null } : null;
|
|
46
|
+
};
|
|
47
|
+
const branchBound = (branch) => {
|
|
48
|
+
const raw = branch.split(/[\s,]+/).filter(Boolean);
|
|
49
|
+
const tokens = [];
|
|
50
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
51
|
+
if (/^(>=|<=|>|<|=|\^|~)$/.test(raw[i]) && i + 1 < raw.length) { tokens.push(raw[i] + raw[i + 1]); i += 1; }
|
|
52
|
+
else tokens.push(raw[i]);
|
|
53
|
+
}
|
|
54
|
+
if (!tokens.length) return null;
|
|
55
|
+
const lowers = [];
|
|
56
|
+
let skipUpperEnd = false;
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (token === "-") { skipUpperEnd = true; continue; }
|
|
59
|
+
if (skipUpperEnd) { skipUpperEnd = false; continue; }
|
|
60
|
+
if (/^(\*|x|latest)$/i.test(token)) continue;
|
|
61
|
+
const m = /^(>=|<=|>|<|=|\^|~)?(.+)$/.exec(token);
|
|
62
|
+
const version = m && versionOf(m[2]);
|
|
63
|
+
if (!version) return null;
|
|
64
|
+
if (m[1] === "<" || m[1] === "<=") continue;
|
|
65
|
+
lowers.push(version);
|
|
66
|
+
}
|
|
67
|
+
return lowers.length ? lowers.reduce((best, v) => (compareVersions(v, best) > 0 ? v : best)) : null;
|
|
68
|
+
};
|
|
69
|
+
const branches = s.split("||").map((b) => b.trim());
|
|
70
|
+
if (branches.some((b) => !b)) return null;
|
|
71
|
+
const bounds = branches.map(branchBound);
|
|
72
|
+
if (bounds.some((b) => b === null)) return null;
|
|
73
|
+
return bounds.reduce((best, v) => (compareVersions(v, best) < 0 ? v : best));
|
|
36
74
|
}
|
|
37
75
|
export const parseVersion = lowerBound;
|
|
38
76
|
export function compareVersions(a, b) {
|
|
@@ -40,7 +78,19 @@ export function compareVersions(a, b) {
|
|
|
40
78
|
if (a.pre === b.pre) return 0;
|
|
41
79
|
if (a.pre === null) return 1;
|
|
42
80
|
if (b.pre === null) return -1;
|
|
43
|
-
|
|
81
|
+
const left = a.pre.split("."), right = b.pre.split(".");
|
|
82
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
83
|
+
if (left[i] === undefined) return -1;
|
|
84
|
+
if (right[i] === undefined) return 1;
|
|
85
|
+
if (left[i] === right[i]) continue;
|
|
86
|
+
const an = /^\d+$/.test(left[i]), bn = /^\d+$/.test(right[i]);
|
|
87
|
+
if (an && bn) {
|
|
88
|
+
const av = BigInt(left[i]), bv = BigInt(right[i]);
|
|
89
|
+
if (av !== bv) return av < bv ? -1 : 1;
|
|
90
|
+
} else if (an !== bn) return an ? -1 : 1;
|
|
91
|
+
else return left[i] < right[i] ? -1 : 1;
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
44
94
|
}
|
|
45
95
|
const show = (v) => `${v.major}.${v.minor}.${v.patch}${v.pre ? `-${v.pre}` : ""}`;
|
|
46
96
|
|
|
@@ -75,21 +125,21 @@ export function readManifest(file, root) {
|
|
|
75
125
|
try { doc = parseToml(text); } catch (error) { return { rel, facts, error: `unparseable Cargo.toml: ${error.message}` }; }
|
|
76
126
|
workspaceDeps = doc.workspace?.dependencies;
|
|
77
127
|
const wsTable = workspaceDeps ?? {};
|
|
78
|
-
const sections = (table, dev) => {
|
|
128
|
+
const sections = (table, dev, target = null) => {
|
|
79
129
|
for (const [section, isDev] of [["dependencies", dev], ["build-dependencies", dev], ["dev-dependencies", true]]) {
|
|
80
|
-
for (const [key, raw] of Object.entries(table?.[section] ?? {})) add(key, raw, isDev);
|
|
130
|
+
for (const [key, raw] of Object.entries(table?.[section] ?? {})) add(key, raw, isDev, target);
|
|
81
131
|
}
|
|
82
132
|
};
|
|
83
|
-
const add = (key, raw, dev) => {
|
|
133
|
+
const add = (key, raw, dev, target) => {
|
|
84
134
|
let value = raw;
|
|
85
135
|
if (value && typeof value === "object" && value.workspace === true && wsTable[key] !== undefined) value = { ...(typeof wsTable[key] === "object" ? wsTable[key] : { version: wsTable[key] }), ...value };
|
|
86
136
|
const name = typeof value === "object" && typeof value.package === "string" ? value.package : key;
|
|
87
137
|
const spec = typeof value === "string" ? value : value?.version;
|
|
88
138
|
const unresolved = raw && typeof raw === "object" && raw.workspace === true && wsTable[key] === undefined;
|
|
89
|
-
facts.push({ owner: name, ecosystem: "cargo", kind: isNonRegistry(value) ? "override" : "registry", dev, spec: spec ?? null, via: isNonRegistry(value) ? ("path" in value ? "path dependency" : "git dependency") : "dependency", file: rel, ...(unresolved ? { inheritsWorkspace: key } : {}) });
|
|
139
|
+
facts.push({ target, optional: Boolean(value?.optional), owner: name, ecosystem: "cargo", kind: isNonRegistry(value) ? "override" : "registry", dev, spec: spec ?? null, via: isNonRegistry(value) ? ("path" in value ? "path dependency" : "git dependency") : "dependency", file: rel, ...(unresolved ? { inheritsWorkspace: key } : {}) });
|
|
90
140
|
};
|
|
91
141
|
sections(doc, false);
|
|
92
|
-
for (const target of Object.
|
|
142
|
+
for (const [target, table] of Object.entries(doc.target ?? {})) sections(table, false, target);
|
|
93
143
|
for (const [key, raw] of Object.entries(wsTable)) {
|
|
94
144
|
// workspace.dependencies only counts through members that opt in (above); record path/git overrides here.
|
|
95
145
|
if (isNonRegistry(raw)) facts.push({ owner: typeof raw.package === "string" ? raw.package : key, ecosystem: "cargo", kind: "override", dev: false, spec: raw.version ?? null, via: "workspace.dependencies path/git", file: rel, declarationOnly: true });
|
|
@@ -107,7 +157,7 @@ export function readManifest(file, root) {
|
|
|
107
157
|
const alias = typeof spec === "string" ? /^npm:((?:@[^@/]+\/)?[^@]+)@/.exec(spec) : null;
|
|
108
158
|
const owner = alias ? alias[1] : name;
|
|
109
159
|
const local = typeof spec === "string" && /^(workspace:|file:|link:|portal:|git\+|git:|github:|https?:)/.test(spec);
|
|
110
|
-
facts.push({ owner, ecosystem: "npm", kind: local ? "override" : "registry", dev, spec: typeof spec === "string" ? spec : null, via: local ? `${spec.split(":")[0]}: specifier` : "dependency", file: rel });
|
|
160
|
+
facts.push({ declarationSection: section, optional: section === "optionalDependencies", peer: section === "peerDependencies", owner, ecosystem: "npm", kind: local ? "override" : "registry", dev, spec: typeof spec === "string" ? spec : null, via: local ? `${spec.split(":")[0]}: specifier` : "dependency", file: rel });
|
|
111
161
|
}
|
|
112
162
|
}
|
|
113
163
|
for (const table of [doc.pnpm?.overrides, doc.overrides, doc.resolutions]) {
|
|
@@ -164,7 +214,7 @@ function judge(req, facts) {
|
|
|
164
214
|
const counted = mine.filter((f) => f.kind === "registry" && (req.allowDev || !f.dev));
|
|
165
215
|
const devOnly = !counted.length && mine.some((f) => f.kind === "registry" && f.dev);
|
|
166
216
|
const min = lowerBound(req.min);
|
|
167
|
-
const base = { owner: req.name, ecosystem: req.ecosystem, rows: [...new Set(req.rows)], required: req.min };
|
|
217
|
+
const base = { evidenceKind: "declaration", qualification: "unverified", activeTarget: null, lockResolution: null, productionUse: null, declarations: mine.map((f) => ({ file: f.file, spec: f.spec, dev: f.dev, target: f.target ?? null, optional: f.optional ?? false, peer: f.peer ?? false })), owner: req.name, ecosystem: req.ecosystem, rows: [...new Set(req.rows)], required: req.min };
|
|
168
218
|
const bounds = counted.map((f) => ({ f, v: lowerBound(f.spec) }));
|
|
169
219
|
const best = bounds.filter((b) => b.v).sort((a, b) => compareVersions(b.v, a.v))[0];
|
|
170
220
|
const declaredAt = best ? show(best.v) : null;
|
|
@@ -218,7 +268,7 @@ export function checkApp(root, { data = loadData(), profile, amendments = [] } =
|
|
|
218
268
|
resolveWorkspaceInheritance(manifests);
|
|
219
269
|
for (const m of manifests) if (m.error) problems.push(`${m.rel}: ${m.error}`);
|
|
220
270
|
const facts = manifests.flatMap((m) => m.facts);
|
|
221
|
-
const owners = [...requirements(applicable).values()].map((req) => judge(req, facts));
|
|
271
|
+
const owners = [...requirements(applicable).values()].map((req) => { const result = judge(req, facts); return { ...result, declarationStatus: result.status }; });
|
|
222
272
|
// A profile that is unknown, marked unratified, or has no rows checks nothing; an app may not select it, so it fails rather than passing empty.
|
|
223
273
|
const profileStatus = !data.profiles[active] ? "unknown-profile" : (data.profiles[active].ratified === false || !data.rows.some((r) => r.profile === active)) ? "unratified" : null;
|
|
224
274
|
if (profileStatus) {
|
|
@@ -236,7 +286,7 @@ export function checkApps(roots, options = {}) {
|
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
export function renderText(report) {
|
|
239
|
-
const out = [`ownership-mandatory (${report.mode}) — Table 1
|
|
289
|
+
const out = [`ownership-mandatory (${report.mode}) — Table 1 owner declarations (active-target, lock & production qualification unverified)`];
|
|
240
290
|
for (const app of report.apps) {
|
|
241
291
|
const count = (s) => app.owners.filter((o) => o.status === s).length;
|
|
242
292
|
out.push("", `${app.app} [${app.profile}] ${app.scanned ? `ok=${count("ok")} missing=${count("missing")} below-required=${count("below-required")} not-on-registry=${count("not-on-registry")}` : "UNSCANNED"}`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{ "repositoryIdentity": "https://github.com/orthic-labs/legion", "roots": ["."], "visibility": "public" },
|
|
16
16
|
{ "repositoryIdentity": "https://github.com/bogusyogi/workright", "roots": ["."], "visibility": "private" },
|
|
17
17
|
{ "repositoryIdentity": "https://github.com/orthic-labs/sellright", "roots": ["."], "visibility": "public" },
|
|
18
|
-
{ "repositoryIdentity": "https://github.com/
|
|
18
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/pulse", "roots": ["."], "visibility": "private" },
|
|
19
19
|
{ "repositoryIdentity": "https://github.com/bogusyogi/swiftright", "roots": ["."], "visibility": "private" }
|
|
20
20
|
]
|
|
21
21
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Ownership scan scope for a target that builds from only part of its repository (iOS).
|
|
2
|
+
// A scope is a sorted, de-duplicated list of repository-relative directory roots. Only committed files under those roots
|
|
3
|
+
// are inventoried; the exceptions file under `.rightkit` is always read. Sealed ownership evidence records the scope and
|
|
4
|
+
// upload-time verification re-derives it from the config, so the scope cannot be narrowed after the build.
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const OWNERSHIP_SCOPE_ALWAYS = ".rightkit";
|
|
9
|
+
|
|
10
|
+
/** Normalize and validate repository-relative scope roots; throws a clear error for absolute, escaping, or missing roots. */
|
|
11
|
+
export function normalizeScopeRoots(roots, { repoRoot, label = "ownershipScope" } = {}) {
|
|
12
|
+
if (!Array.isArray(roots)) throw new Error(`${label} must be an array of repository-relative directories`);
|
|
13
|
+
const out = new Set();
|
|
14
|
+
for (const raw of roots) {
|
|
15
|
+
if (typeof raw !== "string" || !raw.trim()) throw new Error(`${label} entries must be non-empty strings`);
|
|
16
|
+
const slashed = raw.replaceAll("\\", "/");
|
|
17
|
+
if (path.posix.isAbsolute(slashed) || path.win32.isAbsolute(raw) || /^[A-Za-z]:/.test(slashed)) throw new Error(`${label} entry ${raw} must be repository-relative, not absolute`);
|
|
18
|
+
const normal = path.posix.normalize(slashed).replace(/\/+$/, "");
|
|
19
|
+
if (normal === ".." || normal.startsWith("../")) throw new Error(`${label} entry ${raw} escapes the repository`);
|
|
20
|
+
if (normal === "." || normal === "") throw new Error(`${label} entry ${raw} is the whole repository; omit the scope instead`);
|
|
21
|
+
if (repoRoot) {
|
|
22
|
+
const absolute = path.join(repoRoot, ...normal.split("/"));
|
|
23
|
+
let directory = false;
|
|
24
|
+
try { directory = existsSync(absolute) && statSync(absolute).isDirectory(); } catch { directory = false; }
|
|
25
|
+
if (!directory) throw new Error(`${label} entry ${raw} is not an existing directory in the repository`);
|
|
26
|
+
}
|
|
27
|
+
out.add(normal);
|
|
28
|
+
}
|
|
29
|
+
return [...out];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Local SwiftPM package directories named by `path:` inside the `packages:` section of an XcodeGen project.yml. */
|
|
33
|
+
export function projectYmlLocalPackagePaths(text) {
|
|
34
|
+
const lines = String(text).split(/\r?\n/);
|
|
35
|
+
const paths = [];
|
|
36
|
+
let inPackages = false;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (/^\s*(#.*)?$/.test(line)) continue;
|
|
39
|
+
if (/^\S/.test(line)) { inPackages = /^packages:\s*(#.*)?$/.test(line); continue; }
|
|
40
|
+
if (!inPackages) continue;
|
|
41
|
+
const match = /(?:^|[\s{,])path:\s*(?:"([^"]*)"|'([^']*)'|([^\s,}#]+))/.exec(line);
|
|
42
|
+
if (match) paths.push(match[1] ?? match[2] ?? match[3]);
|
|
43
|
+
}
|
|
44
|
+
return paths;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan scope of an iOS target: the Xcode project directory, each local SwiftPM package its project.yml references,
|
|
49
|
+
* `.rightkit` (exceptions), the native Rust assembly (`nativeAssembly.workspaceRoot` and the directory of its cargo manifest,
|
|
50
|
+
* which is compiled into the IPA), and `targets.ios.ownershipScope` additions. Accepts the raw or the expanded target.
|
|
51
|
+
* The scan itself closes this scope under local path dependencies of the in-scope manifests (see ownership.mjs).
|
|
52
|
+
* A native assembly rooted at the repository root is ignored (it would make the scope the whole repository).
|
|
53
|
+
* Returns null when the target has no iosPackage (nothing to scope from).
|
|
54
|
+
*/
|
|
55
|
+
export function iosOwnershipScope({ repoRoot, appRoot, target, nativeAssembly }) {
|
|
56
|
+
const spec = target?.recipe?.kind === "iosPackage" ? target.recipe.spec : target?.iosPackage;
|
|
57
|
+
if (!spec || typeof spec !== "object") return null;
|
|
58
|
+
const toRepo = (absolute) => {
|
|
59
|
+
const relative = path.relative(repoRoot, absolute).split(path.sep).join("/");
|
|
60
|
+
if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) throw new Error(`ios ownership scope: ${absolute} is outside the repository`);
|
|
61
|
+
return relative || ".";
|
|
62
|
+
};
|
|
63
|
+
const specFile = spec.projectSpec ?? spec.project;
|
|
64
|
+
if (typeof specFile !== "string") throw new Error("ios ownership scope: iosPackage.project or projectSpec is required");
|
|
65
|
+
const specDir = path.resolve(appRoot, path.posix.dirname(specFile.replaceAll("\\", "/")));
|
|
66
|
+
const roots = [];
|
|
67
|
+
const projectDir = toRepo(specDir);
|
|
68
|
+
if (projectDir === ".") throw new Error("ios ownership scope: the Xcode project sits at the repository root, so there is no narrower scope; declare the project in a subdirectory");
|
|
69
|
+
roots.push(projectDir);
|
|
70
|
+
if (spec.projectSpec) {
|
|
71
|
+
const ymlPath = path.resolve(appRoot, spec.projectSpec);
|
|
72
|
+
if (existsSync(ymlPath)) {
|
|
73
|
+
for (const local of projectYmlLocalPackagePaths(readFileSync(ymlPath, "utf8"))) roots.push(toRepo(path.resolve(specDir, local)));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const native = nativeAssembly && typeof nativeAssembly === "object" ? nativeAssembly : null;
|
|
77
|
+
const nativeDirs = [native?.workspaceRoot ?? native?.cargoWorkspaceRoot, (native?.cargoManifest ?? native?.manifest) ? path.dirname(native.cargoManifest ?? native.manifest) : undefined].filter((value) => typeof value === "string" && value);
|
|
78
|
+
for (const dir of nativeDirs) {
|
|
79
|
+
const nativeRoot = toRepo(path.resolve(appRoot, dir));
|
|
80
|
+
// A native assembly at the repository root is the app's own (usually desktop) workspace; folding it in would widen the
|
|
81
|
+
// scope to the whole repository, so it adds nothing. The scan's path-dependency closure still follows the in-scope manifests.
|
|
82
|
+
if (nativeRoot !== ".") roots.push(nativeRoot);
|
|
83
|
+
}
|
|
84
|
+
const extra = target.ownershipScope;
|
|
85
|
+
if (extra !== undefined) roots.push(...normalizeScopeRoots(extra, { repoRoot, label: "targets.ios.ownershipScope" }));
|
|
86
|
+
const checked = normalizeScopeRoots(roots.filter((root) => root !== OWNERSHIP_SCOPE_ALWAYS), { repoRoot, label: "ios ownership scope" });
|
|
87
|
+
return [...new Set([...checked, OWNERSHIP_SCOPE_ALWAYS])].sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Repository-relative file or directory under any scope root (null scope = everything). */
|
|
91
|
+
export function inOwnershipScope(scope, rel) {
|
|
92
|
+
if (!scope) return true;
|
|
93
|
+
return scope.some((root) => rel === root || rel.startsWith(`${root}/`));
|
|
94
|
+
}
|
package/ownership.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { acquireCommittedSource, committedSourceDataForInternalUse } from "./committed-source.mjs";
|
|
13
13
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
14
14
|
import { gitListedInventory } from "./ownership-git-inventory.mjs";
|
|
15
|
+
import { inOwnershipScope } from "./ownership-scope.mjs";
|
|
15
16
|
import os from "node:os";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
|
|
@@ -240,14 +241,40 @@ function fileInventory(appRoot, trust, pathPrefix = "", repositoryRoot = appRoot
|
|
|
240
241
|
return listedCoverage(inventory);
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
|
|
244
|
+
// A scoped scan also reads the workspace/lockfile authorities that sit in an ancestor directory of a scope root
|
|
245
|
+
// (a root Cargo.toml / Cargo.lock / lockfile resolves a member crate's inherited fields and lock graph), so a member
|
|
246
|
+
// of a larger workspace is judged against the same lockfile as in a whole-repository scan.
|
|
247
|
+
function inScopeOrAuthority(scope, rel) {
|
|
248
|
+
if (inOwnershipScope(scope, rel)) return true;
|
|
249
|
+
const dir = path.posix.dirname(rel);
|
|
250
|
+
return ["Cargo.toml", "Cargo.lock", "package.json", "pnpm-workspace.yaml", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel))
|
|
251
|
+
&& scope.some((root) => dir === "." || root === dir || root.startsWith(`${dir}/`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Directories named by local path dependencies of in-scope manifests that are not yet in scope. A target that is not a
|
|
255
|
+
// directory of the committed tree is left to resolveLocalPath, which records a blocking coverage gap for it.
|
|
256
|
+
function localDependencyRoots(deps, scope, entries) {
|
|
257
|
+
const roots = new Set();
|
|
258
|
+
for (const dep of deps) {
|
|
259
|
+
if (!dep.local || typeof dep.source !== "string" || !dep.source.startsWith("path:")) continue;
|
|
260
|
+
const value = dep.source.slice(5);
|
|
261
|
+
if (!value || value.startsWith("workspace:") || path.posix.isAbsolute(value) || path.win32.isAbsolute(value) || value.includes("\\")) continue;
|
|
262
|
+
const relative = path.posix.normalize(path.posix.join(path.posix.dirname(dep.file), value)).replace(/\/$/, "");
|
|
263
|
+
if (relative === "." || relative === ".." || relative.startsWith("../") || inOwnershipScope(scope, relative)) continue;
|
|
264
|
+
if (entries.some((entry) => entry.rel.startsWith(`${relative}/`))) roots.add(relative);
|
|
265
|
+
}
|
|
266
|
+
return [...roots];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function committedFileInventory(appRoot, trust, pathPrefix, committed, scope = null) {
|
|
244
270
|
const prefix = pathPrefix ? `${pathPrefix.replace(/\/$/, "")}/` : "";
|
|
245
271
|
const repositoryFiles = committed.entries.filter((entry) => entry.type === "blob").map((entry) => ({ rel: entry.rel, size: entry.size, bytes: entry.bytes, mode: entry.mode }));
|
|
246
|
-
const files = [], coverage = committed.coverage.filter((entry) => !prefix || entry.path.startsWith(prefix)).map((entry) => ({ ...entry, path: prefix ? entry.path.slice(prefix.length) : entry.path }));
|
|
272
|
+
const files = [], coverage = committed.coverage.filter((entry) => (!prefix || entry.path.startsWith(prefix)) && inScopeOrAuthority(scope, entry.path)).map((entry) => ({ ...entry, path: prefix ? entry.path.slice(prefix.length) : entry.path }));
|
|
247
273
|
const protectedPaths = new Set((trust?.provenance ?? []).flatMap((record) => record.paths ?? []));
|
|
248
274
|
const reportedOpaque = new Set(), reportedNodeModules = new Set();
|
|
249
275
|
for (const entry of committed.entries) {
|
|
250
276
|
if (prefix && !entry.rel.startsWith(prefix)) continue;
|
|
277
|
+
if (!inScopeOrAuthority(scope, entry.rel)) continue;
|
|
251
278
|
const rel = prefix ? entry.rel.slice(prefix.length) : entry.rel;
|
|
252
279
|
if (!rel) continue;
|
|
253
280
|
if (entry.mode === "120000") { coverage.push({ path: rel, status: "unscanned", reason: "committed-symlink" }); continue; }
|
|
@@ -834,14 +861,28 @@ function checkCommitted(appRoot, manifest, options, acquire, readAuthority = rea
|
|
|
834
861
|
keyCheckpoint = resolved.checkpoint;
|
|
835
862
|
}
|
|
836
863
|
const location = { root: committed.repositoryRoot, prefix: pathPrefix, pathBase: "repository" };
|
|
837
|
-
const
|
|
838
|
-
if (location.prefix && !committed.entries.some((entry) => entry.rel.startsWith(location.prefix))) {
|
|
839
|
-
inventory.coverage.push({ path: ".", status: "unscanned", reason: "registered-root-missing-from-committed-tree" });
|
|
840
|
-
}
|
|
864
|
+
const declaredScope = options.scope ?? null;
|
|
841
865
|
const committedEnrollment = trust?.rootFingerprint
|
|
842
866
|
? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
|
|
843
867
|
: trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined;
|
|
844
|
-
|
|
868
|
+
// The scope is closed under local path dependencies: a path dependency of an in-scope manifest that lands in an
|
|
869
|
+
// out-of-scope directory adds that directory to the scope and the scan repeats, so compiled-in code is never skipped.
|
|
870
|
+
let scope = declaredScope ? [...declaredScope] : null;
|
|
871
|
+
let inventory, scanned;
|
|
872
|
+
for (let round = 0; ; round += 1) {
|
|
873
|
+
inventory = committedFileInventory(appRoot, trust, location.prefix, committed, scope);
|
|
874
|
+
if (location.prefix && !committed.entries.some((entry) => entry.rel.startsWith(location.prefix))) {
|
|
875
|
+
inventory.coverage.push({ path: ".", status: "unscanned", reason: "registered-root-missing-from-committed-tree" });
|
|
876
|
+
}
|
|
877
|
+
scanned = scanInternal(appRoot, manifest, { inventory, trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix, allowlistPolicy: dependencyAllowlistPolicy(trust, repositoryIdentity, { pending: enrollmentPending(trust, committedEnrollment), shipped: options.shippedAllowlist }) });
|
|
878
|
+
if (!scope) break;
|
|
879
|
+
const added = localDependencyRoots(scanned.deps, scope, committed.entries);
|
|
880
|
+
if (!added.length) break;
|
|
881
|
+
if (round >= 64) throw new Error("committed ownership: scan scope did not converge under local path dependencies");
|
|
882
|
+
scope = [...new Set([...scope, ...added])].sort();
|
|
883
|
+
}
|
|
884
|
+
// Provenance and lockfile diagnostics are computed beside the inventory; a scoped scan reports only gaps under its scope.
|
|
885
|
+
if (scope) scanned.coverage = scanned.coverage.filter((entry) => typeof entry.path !== "string" || entry.path === "." || inScopeOrAuthority(scope, entry.path));
|
|
845
886
|
const exceptionPath = [location.prefix.replace(/\/$/, ""), EXCEPTIONS_FILE.split(path.sep).join("/")].filter(Boolean).join("/");
|
|
846
887
|
const exceptionObject = committed.entries.find((entry) => entry.rel === exceptionPath);
|
|
847
888
|
const exceptionEntry = exceptionObject?.bytes;
|
|
@@ -856,6 +897,7 @@ function checkCommitted(appRoot, manifest, options, acquire, readAuthority = rea
|
|
|
856
897
|
scannedAt: new Date().toISOString(), catalogDigest: catalogDigest(manifest), catalogVersion: manifest.version ?? String(manifest.schema ?? "1"),
|
|
857
898
|
scannerVersion: SCANNER_VERSION, contentDigests: scanned.contentDigests, coverage: scanned.coverage,
|
|
858
899
|
pathBase: location.pathBase, pathPrefix: location.prefix, provenance: scanned.provenance, exceptionDigest,
|
|
900
|
+
...(scope ? { ownershipScopeClosed: scope } : {}),
|
|
859
901
|
source: "committed-git-tree", ...(keyLogDigest ? { keyLogDigest, keyCheckpoint, rootFingerprint: trust.rootFingerprint, ...(keyAuthorityBinding ? { keyAuthority: keyAuthorityBinding } : {}) } : {}),
|
|
860
902
|
};
|
|
861
903
|
const result = evaluate(scanned.findings, exceptions, { ...options, manifest, trust, trustEnrollment: trust?.rootFingerprint
|
|
@@ -9,6 +9,8 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
11
11
|
import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
|
|
12
|
+
import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
|
|
13
|
+
import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
|
|
12
14
|
|
|
13
15
|
export class PackageRecipeTargetError extends Error {
|
|
14
16
|
constructor(message) { super(message); this.name = "PackageRecipeTargetError"; this.code = "RK_PACKAGE_RECIPE_TARGET"; }
|
|
@@ -249,11 +251,49 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
|
|
|
249
251
|
};
|
|
250
252
|
}
|
|
251
253
|
|
|
254
|
+
const IOS_TARGET_KEYS = new Set(["signed", "signingContract", "iosPackage", "artifacts", "hardening", "hardeningAllowances", "qaControlScan", "publishBlocked", "distribution", "ownershipScope", "cargoManifest", "cargoTarget", "recipe"]);
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* `targets.ios.iosPackage` (bogusyogi/rightkit#349) is validated strictly and kept as `recipe: { kind: "iosPackage", spec }`.
|
|
258
|
+
* Unlike macPackage there is no hook to expand into: xcodebuild archive and export run in ios-package-recipe.mjs (macOS only),
|
|
259
|
+
* which release.mjs calls like the Android lane. The target may carry only the keys in IOS_TARGET_KEYS: a hand-written
|
|
260
|
+
* prePackage / package / sign / notarize / gradle script, or an App Store Connect credential, is refused.
|
|
261
|
+
* `artifacts` must list the .ipa so the IPA is checked and sealed. `distribution` (the App Store Connect target of an app that
|
|
262
|
+
* also ships desktop) is validated here exactly as the upload validates it, so a credential in it fails the build.
|
|
263
|
+
*/
|
|
264
|
+
export function expandIosPackageTarget(target, { appRoot: _appRoot } = {}) {
|
|
265
|
+
if (!isObject(target) || target.iosPackage === undefined) return withoutRecipe(target);
|
|
266
|
+
const unknown = Object.keys(target).filter((key) => !IOS_TARGET_KEYS.has(key));
|
|
267
|
+
if (unknown.length) fail(`targets.ios has unknown key(s): ${unknown.join(", ")} (allowed: ${[...IOS_TARGET_KEYS].filter((key) => key !== "recipe").join(", ")}); the iOS recipe replaces prePackage, package, sign and notarize scripts, and App Store Connect credentials never belong in a target`);
|
|
268
|
+
if (target.distribution !== undefined) {
|
|
269
|
+
try { validateIosDistribution(target.distribution); } catch (error) {
|
|
270
|
+
if (error instanceof IosUploadError) fail(`targets.ios.${error.message.replace(/^\[[A-Z0-9_]+\] /, "")}`);
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (target.ownershipScope !== undefined && (!Array.isArray(target.ownershipScope) || target.ownershipScope.some((entry) => typeof entry !== "string" || !entry.trim()))) fail("targets.ios.ownershipScope must be an array of repository-relative directory strings");
|
|
275
|
+
// The Rust the app links (for the feature-closure scan): an app-relative Cargo.toml and its iPhone triple.
|
|
276
|
+
if (target.cargoManifest !== undefined && (typeof target.cargoManifest !== "string" || !target.cargoManifest.endsWith("Cargo.toml"))) fail("targets.ios.cargoManifest must be an app-relative path to a Cargo.toml");
|
|
277
|
+
if (target.cargoTarget !== undefined && !/^aarch64-apple-ios(-sim)?$/.test(target.cargoTarget)) fail("targets.ios.cargoTarget must be aarch64-apple-ios or aarch64-apple-ios-sim");
|
|
278
|
+
if (target.signed !== true) fail("targets.ios must declare signed: true");
|
|
279
|
+
if (target.signingContract !== undefined && target.signingContract !== IOS_SIGNING_CONTRACT) fail(`targets.ios.iosPackage signs with Xcode automatic signing; signingContract must be ${IOS_SIGNING_CONTRACT}, got ${target.signingContract}`);
|
|
280
|
+
let spec;
|
|
281
|
+
try { spec = validateIosPackageConfig(target.iosPackage); } catch (error) {
|
|
282
|
+
if (error instanceof IosPackageError) fail(`targets.ios.${error.message}`);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
const declared = (target.artifacts ?? []).map((artifact) => String(typeof artifact === "string" ? artifact : artifact?.file).replaceAll("\\", "/").replace(/^\.\//, ""));
|
|
286
|
+
if (!declared.includes(spec.ipa)) fail("targets.ios.artifacts must list iosPackage.ipa so the IPA is checked and sealed");
|
|
287
|
+
const { iosPackage, recipe: _ignored, ...rest } = target;
|
|
288
|
+
return { ...rest, signingContract: IOS_SIGNING_CONTRACT, recipe: { kind: "iosPackage", spec } };
|
|
289
|
+
}
|
|
290
|
+
|
|
252
291
|
/** Expand whichever declarative package key the target carries. */
|
|
253
292
|
export function expandPackageRecipeTarget(target, { platform, appRoot, config, env, resolveTargetRoot }) {
|
|
254
293
|
const injected = { ...(env ? { env } : {}), ...(resolveTargetRoot ? { resolveTargetRoot } : {}) };
|
|
255
294
|
if (platform === "win") return expandWinPackageTarget(target, { appRoot, config, ...injected });
|
|
256
295
|
if (platform === "mac") return expandMacPackageTarget(target, { appRoot, config, ...injected });
|
|
257
|
-
if (
|
|
296
|
+
if (platform === "ios") return expandIosPackageTarget(target, { appRoot });
|
|
297
|
+
if (target?.winPackage || target?.macPackage || target?.iosPackage) fail(`winPackage / macPackage / iosPackage are not valid on ${platform}`);
|
|
258
298
|
return target;
|
|
259
299
|
}
|
package/package.json
CHANGED
package/preflight.mjs
CHANGED
|
@@ -192,15 +192,22 @@ export function collectPreflight({
|
|
|
192
192
|
isBrokerManagedHost = brokerManagedHost,
|
|
193
193
|
ownership = true,
|
|
194
194
|
ownershipEvidenceFactory = createOwnershipEvidence,
|
|
195
|
+
ownershipScope = null,
|
|
195
196
|
runtimeBudgetAdoption = appRuntimeBudgetAdoptionCheck,
|
|
196
197
|
freeDisk = freeDiskGb,
|
|
197
198
|
projectType,
|
|
199
|
+
// false for a native iOS app with no Cargo host: no target bridge, Rust compiler cache or Rust budget adoption applies.
|
|
200
|
+
rust = true,
|
|
201
|
+
// false skips only the target bridge and the sccache check (a native iOS app that pins a Cargo.lock has no broker target).
|
|
202
|
+
bridge = true,
|
|
198
203
|
msix = false,
|
|
199
204
|
findSdkTool = findWindowsSdkTool,
|
|
200
205
|
} = {}) {
|
|
201
206
|
const checks = [];
|
|
202
207
|
const windows = platform === "win" || platform === "win32";
|
|
203
208
|
const swift = isSwiftProject(projectType);
|
|
209
|
+
const noRust = swift || !rust;
|
|
210
|
+
const noBridge = noRust || !bridge;
|
|
204
211
|
|
|
205
212
|
if (swift) {
|
|
206
213
|
const swiftVersion = versionProbe("swift");
|
|
@@ -211,7 +218,7 @@ export function collectPreflight({
|
|
|
211
218
|
);
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
if (appRoot && !
|
|
221
|
+
if (appRoot && !noBridge) {
|
|
215
222
|
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
216
223
|
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
217
224
|
let entry = null;
|
|
@@ -273,7 +280,7 @@ export function collectPreflight({
|
|
|
273
280
|
else if (msix) checks.push(fail("signtool", "msix target configured but no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
274
281
|
else checks.push(warn("signtool", "no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
275
282
|
|
|
276
|
-
const vendorsOpenssl = !
|
|
283
|
+
const vendorsOpenssl = !noRust && cargoLockPaths.some((lockPath) => graphVendorsOpenssl(lockPath));
|
|
277
284
|
if (vendorsOpenssl) {
|
|
278
285
|
const perl = resolveWindowsPerl({ env });
|
|
279
286
|
checks.push(
|
|
@@ -288,8 +295,8 @@ export function collectPreflight({
|
|
|
288
295
|
}
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
if (
|
|
292
|
-
// No Rust compiler cache applies to SwiftPM targets.
|
|
298
|
+
if (noBridge) {
|
|
299
|
+
// No Rust compiler cache applies to SwiftPM targets or to a native iOS app without a Cargo host.
|
|
293
300
|
} else if (isBrokerManagedHost(env)) {
|
|
294
301
|
checks.push(ok("compiler-cache", "broker-managed; compiler-cache selection and status are recorded in build receipts"));
|
|
295
302
|
} else {
|
|
@@ -321,7 +328,7 @@ export function collectPreflight({
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
// Held-resource crates are proven at adoption: an app that links one needs its own installed measurement.
|
|
324
|
-
if (appRoot && !
|
|
331
|
+
if (appRoot && !noRust) {
|
|
325
332
|
const adoption = runtimeBudgetAdoption({ appRoot, app, version, cargoLockPaths });
|
|
326
333
|
if (adoption) checks.push(adoption);
|
|
327
334
|
}
|
|
@@ -329,7 +336,7 @@ export function collectPreflight({
|
|
|
329
336
|
if (repoRoot && ownership) {
|
|
330
337
|
// An app living inside the RightKit workspace repository is scoped to its own directory.
|
|
331
338
|
const workspace = existsSync(path.join(repoRoot, "tools", "rightkit", "packages", "release"));
|
|
332
|
-
checks.push(ownershipCheck(repoRoot, { scanRoot: workspace && appRoot ? appRoot : repoRoot, evidenceFactory: ownershipEvidenceFactory }));
|
|
339
|
+
checks.push(ownershipCheck(repoRoot, { scanRoot: workspace && appRoot ? appRoot : repoRoot, scope: ownershipScope, evidenceFactory: ownershipEvidenceFactory }));
|
|
333
340
|
}
|
|
334
341
|
|
|
335
342
|
return checks;
|
|
@@ -338,7 +345,7 @@ export function collectPreflight({
|
|
|
338
345
|
/** Release-blocking: the app keeps no unexcepted copy of a RightKit-owned capability. */
|
|
339
346
|
export function ownershipCheck(repoRoot, options = {}) {
|
|
340
347
|
let checked;
|
|
341
|
-
try { checked = (options.evidenceFactory ?? createOwnershipEvidence)(repoRoot, options.scanRoot ?? repoRoot); }
|
|
348
|
+
try { checked = (options.evidenceFactory ?? createOwnershipEvidence)(repoRoot, options.scanRoot ?? repoRoot, options.scope ?? null); }
|
|
342
349
|
catch (error) { return fail("ownership", `ownership evidence failed: ${error.message}`, "restore protected RightKit ownership trust and catalog"); }
|
|
343
350
|
const { result, evidence, floor } = checked;
|
|
344
351
|
if (checked.ok) {
|
package/project-type.mjs
CHANGED
|
@@ -29,7 +29,9 @@ export function resolveProjectType({ appRoot, config = {}, target = {}, exists =
|
|
|
29
29
|
export const isSwiftProject = (projectType) => projectType === "swiftpm";
|
|
30
30
|
|
|
31
31
|
/** Executables the build itself needs (the config's own preflight.executables are appended by callers). */
|
|
32
|
-
export function requiredBuildExecutables({ projectType, packageManager = "pnpm", usesPackageManager = true } = {}) {
|
|
32
|
+
export function requiredBuildExecutables({ projectType, packageManager = "pnpm", usesPackageManager = true, usesCargo = true } = {}) {
|
|
33
|
+
// A native iOS app with no Cargo host: git, plus the package manager only when the app has a package.json (xcodebuild/xcodegen are the recipe's own preflight).
|
|
34
|
+
if (!usesCargo && !isSwiftProject(projectType)) return ["git", ...(usesPackageManager && packageManager ? [packageManager] : [])];
|
|
33
35
|
if (isSwiftProject(projectType)) return ["git", "swift", ...(usesPackageManager && packageManager ? [packageManager] : [])];
|
|
34
36
|
return ["git", packageManager, "cargo", "rustc"];
|
|
35
37
|
}
|