@rightkit/release 0.6.10 → 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 +7 -5
- package/admission-files.mjs +3 -0
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +13 -9
- package/bin/rightkit-gate.mjs +762 -345
- package/broker-pins.mjs +152 -0
- package/build-release.mjs +80 -27
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +98 -26
- package/cli/right-release.mjs +28 -4
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +196 -21
- package/development.mjs +13 -4
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +10 -0
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-stages.mjs +76 -10
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/inventory-check.mjs +11 -8
- package/ios-upload.mjs +20 -15
- package/local-publish.mjs +77 -41
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/mirror-root-artifact.mjs +18 -5
- 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 +35 -6
- package/package.json +1 -1
- package/pipeline-admission.mjs +4 -4
- package/preflight.mjs +27 -4
- package/publish-cargo.mjs +4 -3
- package/recipe-build.mjs +38 -14
- package/release-artifact-path.mjs +341 -0
- 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 +44 -17
- package/rightkit-versions.json +62 -42
- 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 +12 -6
- package/snapshot-request.mjs +207 -5
- package/snapshot.mjs +123 -10
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +2 -2
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +100 -20
- package/swap-helper-bundle.mjs +5 -2
- package/unit-gate.mjs +45 -3
- package/web-build.mjs +6 -4
- package/win-package-recipe.mjs +20 -7
package/init-command.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export function initTemplate(root, options) {
|
|
|
253
253
|
|
|
254
254
|
/** What a person does after init: the steps no command can do for them, in order. */
|
|
255
255
|
export const NEXT_STEPS = Object.freeze([
|
|
256
|
-
"pnpm install (installs the pinned packages and, through `prepare`, the
|
|
256
|
+
"pnpm install (installs the pinned packages and, through `prepare`, the agent hook; no git hook)",
|
|
257
257
|
"right-release doctor (the build's own checks for this app, as rows)",
|
|
258
258
|
"set the app's public legal host in legal-snapshot.config.json, replace legal/PRODUCT_SCHEDULE.md and legal/PRIVACY_NOTICE.md with reviewed text, then `pnpm exec legal-snapshot`",
|
|
259
259
|
"register the repository on each host: add it to packages/release/ownership-repositories.json in RightKit (and its byte copy under packages/build-control-policy), then run `right-release ownership trust-refresh` on the Mac and the Dell (a person with administrator rights; an agent cannot)",
|
package/inventory-check.mjs
CHANGED
|
@@ -621,8 +621,8 @@ async function checkThirdParty({ repoRoot, records, live, findings }) {
|
|
|
621
621
|
declarations.get(key).wheres.add(where);
|
|
622
622
|
};
|
|
623
623
|
|
|
624
|
-
|
|
625
|
-
const cargoSources = [
|
|
624
|
+
// Every crate is its own Cargo workspace (rightkit#1285): there is no root manifest, each crate declares its own requirements.
|
|
625
|
+
const cargoSources = [];
|
|
626
626
|
for (const record of records.filter((r) => r.ecosystem === "cargo")) {
|
|
627
627
|
cargoSources.push({ where: relative(repoRoot, join(record.dir, "Cargo.toml")), manifest: record.manifest });
|
|
628
628
|
}
|
|
@@ -673,11 +673,14 @@ async function checkThirdParty({ repoRoot, records, live, findings }) {
|
|
|
673
673
|
});
|
|
674
674
|
|
|
675
675
|
const lockDuplicates = [];
|
|
676
|
-
|
|
677
|
-
|
|
676
|
+
// One lock per crate workspace: look for duplicate resolutions in each crate's own crates/<x>/Cargo.lock.
|
|
677
|
+
for (const record of records.filter((r) => r.ecosystem === "cargo").sort((a, b) => a.dir.localeCompare(b.dir))) {
|
|
678
|
+
const lockFile = relative(repoRoot, join(record.dir, "Cargo.lock"));
|
|
679
|
+
const lockText = await readText(join(repoRoot, lockFile));
|
|
680
|
+
if (lockText === null) continue;
|
|
678
681
|
for (const dup of duplicateCargoVersions(lockText)) {
|
|
679
|
-
lockDuplicates.push(dup);
|
|
680
|
-
findings.push(issue("thirdParty", `crate ${dup.name}`, "duplicate-lock-versions",
|
|
682
|
+
lockDuplicates.push({ ...dup, lock: lockFile });
|
|
683
|
+
findings.push(issue("thirdParty", `crate ${dup.name} (${lockFile})`, "duplicate-lock-versions", `${lockFile} resolves ${dup.versions.join(", ")}`));
|
|
681
684
|
}
|
|
682
685
|
}
|
|
683
686
|
return { declarations: declarations.size, uniquePackages: unique, lockDuplicates };
|
|
@@ -914,8 +917,8 @@ export function renderMarkdown(report) {
|
|
|
914
917
|
section("account");
|
|
915
918
|
lines.push(`## ${SECTIONS.thirdParty}`, "", `Declarations checked: ${report.sections.thirdParty.declarations}. Unique external packages: ${report.sections.thirdParty.uniquePackages}.`, "");
|
|
916
919
|
section("thirdParty");
|
|
917
|
-
lines.push("Multiple versions in
|
|
918
|
-
for (const dup of report.sections.thirdParty.lockDuplicates) lines.push(`- \`${dup.name}\`: ${dup.versions.join(", ")}`);
|
|
920
|
+
lines.push("Multiple versions in a crate's own Cargo.lock (one per crate workspace):", "");
|
|
921
|
+
for (const dup of report.sections.thirdParty.lockDuplicates) lines.push(`- \`${dup.name}\` in \`${dup.lock}\`: ${dup.versions.join(", ")}`);
|
|
919
922
|
if (!report.sections.thirdParty.lockDuplicates.length) lines.push("- none");
|
|
920
923
|
lines.push("");
|
|
921
924
|
lines.push(`## ${SECTIONS.apps}`, "", "| App | Present | Duplicate copies in lock files |", "| --- | --- | ---: |");
|
package/ios-upload.mjs
CHANGED
|
@@ -33,8 +33,8 @@ import { IOS_SIGNING_CONTRACT } from "./ios-package-recipe.mjs";
|
|
|
33
33
|
import { ASC_API, createAscClient, createTokenProvider, distributeToTestFlight, normalizeGroupSelection, TestFlightError, testflightPlan } from "./ios-testflight.mjs";
|
|
34
34
|
import { iosOwnershipScope } from "./ownership-scope.mjs";
|
|
35
35
|
import { assertArtifactUnchanged, digestBytes, verifyPublicationRelease } from "./publication-artifacts.mjs";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
36
|
+
import { assertBindsSource, assertInsideSource, assertReleaseSource, gitRootOf, isSnapshotSource, resolveReleaseSource } from "./release-source-identity.mjs";
|
|
37
|
+
import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
38
38
|
import { targetDistribution } from "./target-distribution.mjs";
|
|
39
39
|
|
|
40
40
|
export const IOS_PROVIDER = "app-store-connect";
|
|
@@ -129,28 +129,33 @@ export function iosUploadPlan({ distribution, ipa, receipt }) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// ---- authorisation -----------------------------------------------------------------------------------------------
|
|
132
|
-
const git = (cwd, args) => {
|
|
133
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
134
|
-
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
|
|
135
|
-
return result.stdout.trim();
|
|
136
|
-
};
|
|
137
132
|
|
|
138
133
|
/** Local checks only; no network, no secret. The sealed release is the only input: build-free by construction. */
|
|
139
|
-
export async function authorizeIosUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd) }) {
|
|
134
|
+
export async function authorizeIosUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd), env = process.env }) {
|
|
140
135
|
if (!RELEASE_ID.test(releaseId ?? "")) refuse("IOS_UPLOAD_RELEASE_REQUIRED", "--release <sealed-id> is required");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
136
|
+
// Source identity (release-source-identity.mjs, item 7 row 7.04). Outside a door run: the checkout's HEAD, primary worktree and a clean
|
|
137
|
+
// status, as always. Inside one: the selected commit and its verified snapshot, with HEAD, status and branch of the checkout unread.
|
|
138
|
+
const release = resolveReleaseSource({ cwd, env });
|
|
139
|
+
assertReleaseSource(release, { commandId: "right-release upload", env });
|
|
140
|
+
const split = isSnapshotSource(release);
|
|
141
|
+
const repoRoot = gitRootOf(release);
|
|
142
|
+
const commit = release.commit;
|
|
145
143
|
const configPath = path.resolve(cwd, configName);
|
|
144
|
+
assertInsideSource(release, configPath, "the release config");
|
|
146
145
|
if (!existsSync(configPath)) refuse("IOS_UPLOAD_CONFIG_INVALID", `release config is required: ${configName}`);
|
|
147
146
|
// A legacy config is a module that runs on import (a JSON config is data): loaded only after the tree is proven clean.
|
|
148
147
|
const config = await importReleaseConfig(configPath, { query: `upload=${Date.now()}` });
|
|
149
148
|
const distribution = validateIosDistribution(targetDistribution(config, "ios"));
|
|
150
149
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "ios");
|
|
151
|
-
|
|
150
|
+
// The sealed release, its signing receipts and the state live where the build wrote them (the checkout's `.right-release`); the scope of
|
|
151
|
+
// the ownership scan is read from the snapshot's own config and project, which are the commit's.
|
|
152
|
+
const appRoot = split ? release.checkoutAppRoot : path.dirname(configPath);
|
|
153
|
+
const scopeRepoRoot = split ? release.snapshot : repoRoot;
|
|
154
|
+
const scopeAppRoot = path.dirname(configPath);
|
|
152
155
|
// The IPA is verified by a streamed hash and never held in memory (streamRoles); the parts are read as byte ranges at upload time.
|
|
153
|
-
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot, commit, app: config.app, version: config.version, platform: "ios", configPath, releaseId, streamRoles: ["ipa"], ownershipScope: iosOwnershipScope({ repoRoot, appRoot, target: config.targets?.ios, nativeAssembly: config.nativeAssembly }) }
|
|
156
|
+
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot, commit, app: config.app, version: config.version, platform: "ios", configPath, releaseId, streamRoles: ["ipa"], ownershipScope: iosOwnershipScope({ repoRoot: scopeRepoRoot, appRoot: scopeAppRoot, target: config.targets?.ios, nativeAssembly: config.nativeAssembly }) },
|
|
157
|
+
split ? { verifyOwnership: (expected, root, scanRoot, scope) => verifyOwnershipEvidence(expected, root, scanRoot, scope, release) } : {});
|
|
158
|
+
if (split) assertBindsSource(release, sealed.manifest, "the sealed release");
|
|
154
159
|
const entries = sealed.manifest.files.filter((entry) => entry.role === "ipa");
|
|
155
160
|
if (entries.length !== 1) refuse("IOS_IPA_MISSING", "the sealed release must hold exactly one ipa entry");
|
|
156
161
|
const ipa = sealed.artifacts.get(entries[0].name);
|
|
@@ -221,7 +226,7 @@ export async function runIosUpload({
|
|
|
221
226
|
acquireLock = acquireFileLock, journalDir, sleep, monotonic, now = () => new Date(),
|
|
222
227
|
}) {
|
|
223
228
|
refuseIosAscEnv(env);
|
|
224
|
-
const auth = await authorize({ releaseId, configName, cwd });
|
|
229
|
+
const auth = await authorize({ releaseId, configName, cwd, env });
|
|
225
230
|
const ipaEvidence = { name: auth.entry.name, sha256: auth.entry.sha256, sizeBytes: auth.entry.sizeBytes };
|
|
226
231
|
const plan = iosUploadPlan({ distribution: auth.distribution, ipa: ipaEvidence, receipt: auth.receipt });
|
|
227
232
|
const stateRoot = path.join(auth.repoRoot, ".right-release", "state", releaseId, "ios");
|
package/local-publish.mjs
CHANGED
|
@@ -373,31 +373,26 @@ export function localTestPlan({ kind, unit, dir, config, manifest, batch = false
|
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
375
|
* The clippy invocations that replace each crate's own `cargo clippy -p <crate> --no-deps --all-targets
|
|
376
|
-
* <featureSets[0]> -- -D warnings`: same flags and lint level, so the same lints on the same code.
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* unify extra features into a selected crate and each is linted with exactly the features it was before.
|
|
381
|
-
* Crates with a different RIGHTKIT_REQUIRE_ASSETS (asset-gated) stay apart so the environment is unchanged.
|
|
376
|
+
* <featureSets[0]> -- -D warnings`: same flags and lint level, so the same lints on the same code. Every crate is its own
|
|
377
|
+
* Cargo workspace with its own Cargo.lock (rightkit#1285), so cargo runs in that crate's directory and one invocation never
|
|
378
|
+
* names two crates: one `-p <crate>` run per crate and feature set, in layer order. Crates with a different
|
|
379
|
+
* RIGHTKIT_REQUIRE_ASSETS (asset-gated) keep their own environment.
|
|
382
380
|
*/
|
|
383
381
|
export function batchClippyInvocations(layer, { featuresOf = () => [], featureSetsOf = (unit) => [featuresOf(unit)], assetGatedOf = () => false } = {}) {
|
|
384
|
-
const
|
|
382
|
+
const invocations = [];
|
|
385
383
|
for (const unit of layer) {
|
|
386
384
|
// Every tested feature set of a crate is linted, in order; identical sets of one crate collapse to one run.
|
|
387
385
|
const sets = featureSetsOf(unit) ?? [[]];
|
|
388
386
|
const unique = sets.filter((set, index) => sets.findIndex((other) => JSON.stringify(other ?? []) === JSON.stringify(set ?? [])) === index);
|
|
389
387
|
for (const set of unique) {
|
|
390
388
|
const features = set ?? [];
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
invocations.push({
|
|
390
|
+
crates: [unit], assetGated: assetGatedOf(unit),
|
|
391
|
+
args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"],
|
|
392
|
+
});
|
|
395
393
|
}
|
|
396
394
|
}
|
|
397
|
-
return
|
|
398
|
-
crates, assetGated,
|
|
399
|
-
args: ["clippy", ...crates.flatMap((unit) => ["-p", unit]), "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"],
|
|
400
|
-
}));
|
|
395
|
+
return invocations;
|
|
401
396
|
}
|
|
402
397
|
|
|
403
398
|
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
@@ -450,15 +445,47 @@ export async function runLocalPublishUploadEngine(kind, argv, deps = {}) {
|
|
|
450
445
|
|
|
451
446
|
// Stage state never leaves this process: persisted receipts are reports, not authority to skip unit tests.
|
|
452
447
|
const unitStages = new WeakMap();
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A unit stage built from a door SNAPSHOT (item 7 row 7.06a): `source` is { checkout, commit, tree } of the commit the door selected,
|
|
451
|
+
* and `--workspace` is the gate-owned snapshot of it. Identity is that commit's immutable objects, read from the CHECKOUT's object
|
|
452
|
+
* database, plus the door's start and end verification of the snapshot; it is never the checkout's HEAD or status. The git calls of
|
|
453
|
+
* the unit flow are mapped: HEAD and its tree are the selection, `status` is clean by the door's verify, `show HEAD:<path>` and
|
|
454
|
+
* `ls-files --error-unmatch` read the selected commit, and everything else runs in the checkout.
|
|
455
|
+
*/
|
|
456
|
+
export function sourceGit(source, git = defaultGit) {
|
|
457
|
+
return (args, cwd) => {
|
|
458
|
+
const [a, b] = args;
|
|
459
|
+
if (a === "rev-parse" && b === "HEAD") return source.commit;
|
|
460
|
+
if (a === "rev-parse" && b === "HEAD^{tree}") return source.tree;
|
|
461
|
+
if (a === "status") return "";
|
|
462
|
+
if (a === "show" && /^HEAD:/.test(b ?? "")) return git(["show", `${source.commit}:${b.slice(5)}`], source.checkout);
|
|
463
|
+
if (a === "ls-files" && b === "--error-unmatch") { const files = args.slice(args.indexOf("--") + 1); for (const f of files) if (git(["cat-file", "-e", `${source.commit}:${f}`], source.checkout) === null) return null; return files.join("\n"); }
|
|
464
|
+
return git(args, source.checkout);
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The local-build grant read from the SELECTED COMMIT (`git show <commit>:.rightkit-local-build.json`), its repository checked against the checkout's origin. */
|
|
469
|
+
export function localBuildGrantedAt(source, git = defaultGit) {
|
|
470
|
+
try {
|
|
471
|
+
const grant = JSON.parse(git(["show", `${source.commit}:${LOCAL_BUILD_FILE}`], source.checkout) ?? "null");
|
|
472
|
+
if (grant?.allow !== "build-test-publish" || typeof grant.repository !== "string") return false;
|
|
473
|
+
const origin = git(["config", "--get", "remote.origin.url"], source.checkout) ?? "";
|
|
474
|
+
const repo = /github\.com[:/]([^/]+\/[^/.\s]+?)(?:\.git)?\/?$/i.exec(origin)?.[1];
|
|
475
|
+
return !!repo && repo.toLowerCase() === grant.repository.toLowerCase();
|
|
476
|
+
} catch { return false; }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function assertUnitDoor(root, stage, env, identity, source = null) {
|
|
480
|
+
const commit = source ? source.commit : defaultGit(["rev-parse", "HEAD"], root);
|
|
481
|
+
const tree = source ? source.tree : defaultGit(["rev-parse", "HEAD^{tree}"], root);
|
|
482
|
+
const common = defaultGit(["rev-parse", "--git-common-dir"], source ? source.checkout : root);
|
|
457
483
|
if (!/^[0-9a-f]{40}$/.test(commit ?? "") || !/^[0-9a-f]{40}$/.test(tree ?? "") || !common) refuse("cannot bind unit stage to HEAD and tree");
|
|
458
484
|
if (identity && (identity.commit !== commit || identity.tree !== tree)) refuse("unit stage HEAD or tree changed");
|
|
459
|
-
|
|
485
|
+
// The snapshot route has no checkout cleanliness to ask: the door verifies the snapshot against the commit at each stage's start and end.
|
|
486
|
+
const dirty = source ? "" : defaultGit(["status", "--porcelain", "--untracked-files=no"], root);
|
|
460
487
|
if (dirty === null || dirty !== "") refuse("unit stage requires a clean tracked checkout");
|
|
461
|
-
const commonDir = path.resolve(root, common);
|
|
488
|
+
const commonDir = path.resolve(source ? source.checkout : root, common);
|
|
462
489
|
const denial = requireDoorRun({ env, commonDir, commit, command: `unit ${stage}` });
|
|
463
490
|
if (denial) refuse(denial);
|
|
464
491
|
let run;
|
|
@@ -467,17 +494,17 @@ function assertUnitDoor(root, stage, env, identity) {
|
|
|
467
494
|
return { commit, tree };
|
|
468
495
|
}
|
|
469
496
|
|
|
470
|
-
export async function compileLocalUnit(kind, argv, { env = process.env } = {}) {
|
|
497
|
+
export async function compileLocalUnit(kind, argv, { env = process.env, source = null } = {}) {
|
|
471
498
|
const parsed = parseLocalPublishArgs(kind, argv);
|
|
472
499
|
if (!parsed.qualifyOnly || parsed.batch) refuse("unit stages require one --qualify-only unit and a new bundle");
|
|
473
|
-
const identity = assertUnitDoor(parsed.workspaceRoot, "compile", env);
|
|
474
|
-
const runner = publishUnitStages(parsed, { env }, null);
|
|
500
|
+
const identity = assertUnitDoor(parsed.workspaceRoot, "compile", env, undefined, source);
|
|
501
|
+
const runner = publishUnitStages(parsed, source ? { env, git: sourceGit(source), localBuildGranted: () => localBuildGrantedAt(source) } : { env }, null);
|
|
475
502
|
try {
|
|
476
503
|
const step = await runner.next();
|
|
477
504
|
if (step.done || step.value?.stage !== "compile") refuse("unit compile did not complete");
|
|
478
|
-
assertUnitDoor(parsed.workspaceRoot, "compile", env, identity);
|
|
505
|
+
assertUnitDoor(parsed.workspaceRoot, "compile", env, identity, source);
|
|
479
506
|
const state = Object.freeze({});
|
|
480
|
-
unitStages.set(state, { runner, parsed, identity, stage: "compile", compileRequests: [...(step.value.brokerRequests ?? [])] });
|
|
507
|
+
unitStages.set(state, { runner, parsed, identity, source, stage: "compile", compileRequests: [...(step.value.brokerRequests ?? [])] });
|
|
481
508
|
return state;
|
|
482
509
|
} catch (error) { await runner.return(); throw error; }
|
|
483
510
|
}
|
|
@@ -487,10 +514,10 @@ async function advanceLocalUnit(state, expected, stage, env) {
|
|
|
487
514
|
if (!entry || entry.stage !== expected) refuse(`unit ${stage} requires this process's completed ${expected} stage`);
|
|
488
515
|
entry.stage = "running";
|
|
489
516
|
try {
|
|
490
|
-
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity);
|
|
517
|
+
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity, entry.source);
|
|
491
518
|
const step = await entry.runner.next(env);
|
|
492
519
|
if (stage === "package" ? step.done || step.value?.stage !== "package" : !step.done || !step.value?.qualifiedOnly) refuse(`unit ${stage} did not complete`);
|
|
493
|
-
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity);
|
|
520
|
+
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity, entry.source);
|
|
494
521
|
entry.stage = stage;
|
|
495
522
|
if (stage === "qualify") unitStages.delete(state);
|
|
496
523
|
return step.value;
|
|
@@ -566,7 +593,10 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
566
593
|
if (tree !== batch.tree) refuse(`${unit}: HEAD tree ${tree} differs from the batch tree ${batch.tree}`);
|
|
567
594
|
}
|
|
568
595
|
const feedPath = "packages/release/rightkit-versions.json";
|
|
569
|
-
|
|
596
|
+
// `dir` holds the crate's own Cargo.toml and Cargo.lock (each crate is its own workspace, #1285); a sibling crate it path-depends on
|
|
597
|
+
// shapes what is built, so its directory must match the commit too.
|
|
598
|
+
const siblingDirs = kind === "cargo" ? pathDependencyDirs(unitPath).map((sibling) => path.relative(workspaceRoot, sibling).split(path.sep).join("/")).filter((sibling) => sibling && !sibling.startsWith("..")) : [];
|
|
599
|
+
const guardedPaths = [dir, ...siblingDirs, ...GATE_CONFIG_FILES, ...(parsed.qualifyOnly && kind === "npm" ? [feedPath] : [])];
|
|
570
600
|
const dirty = git(["status", "--porcelain", "--", ...guardedPaths], workspaceRoot);
|
|
571
601
|
if (dirty === null) refuse(`cannot read git status for ${dir}`);
|
|
572
602
|
if (dirty !== "") refuse(`tree is not clean for ${dir} or the gate config (${GATE_CONFIG_FILES.join(", ")}); commit first:\n${dirty}`);
|
|
@@ -612,7 +642,10 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
612
642
|
for (const step of plan.steps) {
|
|
613
643
|
const commandText = `${step.command} ${step.args.join(" ")}`;
|
|
614
644
|
log(`[right-release] ${plan.id}: ${commandText}`);
|
|
615
|
-
|
|
645
|
+
// A crate's own steps run in its Cargo workspace (its own Cargo.lock and target directory, #1285); a consumer row names its
|
|
646
|
+
// separate workspace by --manifest-path relative to the repository root.
|
|
647
|
+
const stepCwd = kind === "cargo" && step.command === "cargo" && !step.args.includes("--manifest-path") ? unitPath : workspaceRoot;
|
|
648
|
+
const result = await run(step.command, step.command === "cargo" ? withCargoJobs(step.args, parsed.jobs) : step.args, { cwd: stepCwd, env });
|
|
616
649
|
const row = { command: commandText, ok: false, isTest: step.test && TEST_COMMAND.test(commandText) };
|
|
617
650
|
if (result.error || result.status !== 0) {
|
|
618
651
|
steps.push(row);
|
|
@@ -675,11 +708,11 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
675
708
|
// its evidence leaves this host, so the publishing host can reuse that exact archive's proof.
|
|
676
709
|
const packageArgs = cargoPackageArgs(unit, parsed.qualifyOnly);
|
|
677
710
|
packCommands.push(`cargo ${packageArgs.join(" ")}`);
|
|
678
|
-
const packed = await run("cargo", withCargoJobs(packageArgs, parsed.jobs), { cwd:
|
|
711
|
+
const packed = await run("cargo", withCargoJobs(packageArgs, parsed.jobs), { cwd: unitPath, env });
|
|
679
712
|
if (packed.error || packed.status !== 0) refuse(`cargo package failed for ${unit}\n${tail(`${packed.stdout ?? ""}\n${packed.stderr ?? ""}`)}`);
|
|
680
713
|
if (parsed.qualifyOnly) steps.push({ command: `cargo ${packageArgs.join(" ")}`, ok: true });
|
|
681
714
|
const metadataArgs = [...CARGO_METADATA_ARGS];
|
|
682
|
-
const meta = await runCargoReadOnly(metadataArgs, { cwd:
|
|
715
|
+
const meta = await runCargoReadOnly(metadataArgs, { cwd: unitPath, env }, (_command, args, settings) => run("cargo", args, settings),
|
|
683
716
|
(failed) => log(`[right-release] offline Cargo metadata missed cache; retrying online once\n${failed.stdout ?? ""}${failed.stderr ? `\n${failed.stderr}` : ""}`));
|
|
684
717
|
const actualMetadataArgs = meta.usedOfflineFallback ? metadataArgs.filter((arg) => arg !== "--offline") : metadataArgs;
|
|
685
718
|
packCommands.push(`cargo ${actualMetadataArgs.join(" ")}`);
|
|
@@ -801,11 +834,11 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
801
834
|
if (!head || !/^[0-9a-f]{40}$/.test(head)) refuse("cannot resolve HEAD");
|
|
802
835
|
const tree = git(["rev-parse", "HEAD^{tree}"], workspaceRoot);
|
|
803
836
|
if (!tree || !/^[0-9a-f]{40}$/.test(tree)) refuse("cannot resolve the HEAD tree");
|
|
804
|
-
// Every batch crate
|
|
805
|
-
// match the commit the batch checks are bound to, before the first layer publishes (0.4.1 review A3, A4).
|
|
806
|
-
const dirty = git(["status", "--porcelain", "--", ...crates.map((unit) => `crates/${unit}`),
|
|
837
|
+
// Every batch crate (its own Cargo.toml and Cargo.lock live in its directory, #1285), the gate config and EVERY crate's
|
|
838
|
+
// consumer directories must match the commit the batch checks are bound to, before the first layer publishes (0.4.1 review A3, A4).
|
|
839
|
+
const dirty = git(["status", "--porcelain", "--", ...crates.map((unit) => `crates/${unit}`), ...GATE_CONFIG_FILES, ...batchConsumerDirs], workspaceRoot);
|
|
807
840
|
if (dirty === null) refuse("cannot read git status for the batch");
|
|
808
|
-
if (dirty !== "") refuse(`tree is not clean for the batch crates, the
|
|
841
|
+
if (dirty !== "") refuse(`tree is not clean for the batch crates, the gate config or a consumer directory; commit first:\n${dirty}`);
|
|
809
842
|
const registryHas = deps.registryHas ?? defaultRegistryHas;
|
|
810
843
|
const published = (await Promise.all(crates.map(async (unit) => ((await registryHas({ kind: "cargo", name: unit, version: versions.get(unit) })) ? `${unit}@${versions.get(unit)}` : null)))).filter(Boolean);
|
|
811
844
|
if (published.length) refuse(`already on the crates.io registry: ${published.join(", ")}; bump the version or remove them from the batch`);
|
|
@@ -815,15 +848,18 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
815
848
|
const env = testEnv(deps, false);
|
|
816
849
|
const stamp = { commit: head, tree };
|
|
817
850
|
const checks = []; const commands = [];
|
|
818
|
-
|
|
851
|
+
// One recorded check; `cwds` are the crate workspaces it runs in (each crate is its own Cargo workspace, #1285), once each.
|
|
852
|
+
const batchCheck = async (name, command, args, crateNames, runEnv = env, cwds = [workspaceRoot]) => {
|
|
819
853
|
const text = `${command} ${args.join(" ")}`;
|
|
820
854
|
log(`[right-release] batch ${name}: ${text}`);
|
|
821
|
-
|
|
822
|
-
|
|
855
|
+
for (const cwd of cwds) {
|
|
856
|
+
const result = await run(command, args, { cwd, env: runEnv });
|
|
857
|
+
if (result.error || result.status !== 0) refuse(`batch ${name} failed (${result.error?.message ?? `exit ${result.status}`}): ${text}${cwd === workspaceRoot ? "" : ` (in ${path.relative(workspaceRoot, cwd)})`}\n${tail(`${result.stdout ?? ""}\n${result.stderr ?? ""}`)}`);
|
|
858
|
+
}
|
|
823
859
|
checks.push({ name, command: text, ok: true, ...stamp, ...(crateNames ? { crates: crateNames } : {}) });
|
|
824
860
|
commands.push(text);
|
|
825
861
|
};
|
|
826
|
-
await batchCheck("fmt", "cargo", ["fmt", "--all", "--check"]);
|
|
862
|
+
await batchCheck("fmt", "cargo", ["fmt", "--all", "--check"], undefined, env, crates.map((unit) => path.join(workspaceRoot, "crates", unit)));
|
|
827
863
|
log("[right-release] batch secret-scan: crate directories of the batch");
|
|
828
864
|
const scan = deps.scanSecrets ?? scanCrateForSecrets;
|
|
829
865
|
const violations = crates.flatMap((unit) => scan(path.join(workspaceRoot, "crates", unit)).map((file) => `${unit}: ${file}`));
|
|
@@ -834,7 +870,7 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
834
870
|
const featureSetsOf = (unit) => gateConfig?.rust?.[unit]?.featureSets ?? [[]];
|
|
835
871
|
for (const layer of layers) {
|
|
836
872
|
for (const invocation of batchClippyInvocations(layer, { featureSetsOf, assetGatedOf: isAssetGated })) {
|
|
837
|
-
await batchCheck("clippy", "cargo", invocation.args, invocation.crates, testEnv(deps, invocation.assetGated));
|
|
873
|
+
await batchCheck("clippy", "cargo", invocation.args, invocation.crates, testEnv(deps, invocation.assetGated), invocation.crates.map((unit) => path.join(workspaceRoot, "crates", unit)));
|
|
838
874
|
}
|
|
839
875
|
}
|
|
840
876
|
const receipt = { id: `batch-${head.slice(0, 12)}-${sha256(JSON.stringify(layers)).slice(0, 8)}`, ...stamp, crates, checks };
|
package/local-qualify.mjs
CHANGED
|
@@ -370,7 +370,7 @@ function brokerIdentity(deps) {
|
|
|
370
370
|
return { name: "rustright", client: String(client).replaceAll("\\", "/"), sha256: sha256((deps.readFile ?? fs.readFileSync)(client)) };
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim
|
|
373
|
+
// Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim.
|
|
374
374
|
function detectCargoCommand(deps) {
|
|
375
375
|
const which = (command) => {
|
|
376
376
|
const result = (deps.run ?? spawnSync)((deps.platform ?? process.platform) === "win32" ? "where" : "which", [command], { encoding: "utf8", windowsHide: true });
|
|
@@ -406,14 +406,16 @@ async function localScanRecord({ root, commit, crates, jobs, scratch, deps, log
|
|
|
406
406
|
const repo = path.join(exportRoot, "rightkit");
|
|
407
407
|
const tracked = String((deps.git ?? defaultGit)(["ls-files", "-z"], root) ?? "").split("\0").filter(Boolean);
|
|
408
408
|
const unitDirs = crates.map((name) => `crates/${name}/`);
|
|
409
|
-
//
|
|
409
|
+
// Repository-level scans (workflows, YAML) plus every scan of the units, each in its own crate workspace (lock, deny, audit); other crates and npm packages stay out of scope.
|
|
410
410
|
const files = tracked.filter((file) => (!file.startsWith("crates/") && !file.startsWith("packages/")) || unitDirs.some((dir) => file.startsWith(dir)));
|
|
411
411
|
const crateFeatures = (deps.readJson ?? defaultReadJson)(path.join(repo, "ci", "gate.config.json"))?.rust ?? {};
|
|
412
412
|
const plan = (deps.planScans ?? planScans)({ changed: files, contentRoot: repo, liveRoot: root, includeTests: false, localCompile: true, cargoCommand: detectCargoCommand(deps), crateFeatures, readBytes: (file) => fs.readFileSync(file) });
|
|
413
413
|
const observedTools = modules.observeToolVersions();
|
|
414
414
|
log(`[local-qualify] scans: ${plan.scans.length} planned over ${files.length} files (class A, then class B through the broker)`);
|
|
415
415
|
const result = await (deps.runScans ?? runScans)(plan, { classes: ["A", "B"], jobs: Math.max(1, Number(jobs ?? 4) || 4), onResult: (row, progress) => log(`[local-qualify] scan [${progress.done}/${progress.total}] ${row.status.padEnd(7)} ${row.id}`) });
|
|
416
|
-
|
|
416
|
+
// Every unit is bound to its own crate workspace lock (#1285): crates/<x>/Cargo.lock, no root lock.
|
|
417
|
+
const cargoLocks = Object.fromEntries([...crates].sort().map((name) => [name, { path: `crates/${name}/Cargo.lock`, sha256: sha256(fs.readFileSync(path.join(root, "crates", name, "Cargo.lock"))) }]));
|
|
418
|
+
return modules.buildLocalScanRecord({ result, planned: plan.scans, sourceCommit: commit, headCommit: commit, cargoLocks, observedTools, units: crates, files: files.length });
|
|
417
419
|
} finally { fs.rmSync(exportRoot, { recursive: true, force: true }); }
|
|
418
420
|
}
|
|
419
421
|
|
package/local-runtime.mjs
CHANGED
|
@@ -163,8 +163,9 @@ export async function measureRuntimeCrate(options, deps = {}) {
|
|
|
163
163
|
const bindings = {};
|
|
164
164
|
for (const full of [...lsTree([`crates/${crate}/src`]), `crates/${crate}/Cargo.toml`]) bindings[full] = sha256(blob(full));
|
|
165
165
|
Object.assign(bindings, owner.bindHarnessMetadata(crate, prescription, blob));
|
|
166
|
-
|
|
167
|
-
const {
|
|
166
|
+
// Each crate is its own Cargo workspace with its own Cargo.lock (#1285): the same context files scripts/ci/runtime-qualify.mjs binds.
|
|
167
|
+
for (const full of lsTree([`crates/${crate}/Cargo.toml`, `crates/${crate}/Cargo.lock`, "rust-toolchain.toml", ".cargo", `crates/${crate}/.cargo`, `crates/${crate}/benches`, "crates/rightkit-child-process/src", "crates/rightkit-child-process/Cargo.toml"])) bindings[full] = sha256(blob(full));
|
|
168
|
+
const { specs, problems: scopeProblems = [] } = (deps.measuredPathspecs ?? H.budgets.measuredPathspecs)(crateDir, { base: root });
|
|
168
169
|
if (scopeProblems.length) throw new Error(`cannot bind the measured dependency scope: ${scopeProblems.join("; ")}`);
|
|
169
170
|
for (const full of lsTree(specs)) bindings[full] = sha256(blob(full));
|
|
170
171
|
const helpers = Object.fromEntries(H.helperPaths().map((name) => [name, sha256(blob(name))]));
|
|
@@ -174,9 +175,9 @@ export async function measureRuntimeCrate(options, deps = {}) {
|
|
|
174
175
|
const socket = discoverBrokerSocket(deps.env ?? process.env, { platform });
|
|
175
176
|
if (socket) brokerEnv.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
|
|
176
177
|
const run = deps.run ?? ((command, args, settings) => spawnCollect(command, args, { maxBuffer: 64 * 1024 * 1024, ...settings }));
|
|
177
|
-
const invoke = (args, settings) => run(cargoCommand[0], [...cargoCommand.slice(1), ...args], { cwd:
|
|
178
|
+
const invoke = (args, settings) => run(cargoCommand[0], [...cargoCommand.slice(1), ...args], { cwd: crateDir, windowsHide: true, ...settings });
|
|
178
179
|
const toolchainOf = async () => {
|
|
179
|
-
const rustc = await run(cargoCommand[0] === "rustright" ? "rustright" : "rustc", cargoCommand[0] === "rustright" ? ["rustc", "-Vv"] : ["-Vv"], { cwd:
|
|
180
|
+
const rustc = await run(cargoCommand[0] === "rustright" ? "rustright" : "rustc", cargoCommand[0] === "rustright" ? ["rustc", "-Vv"] : ["-Vv"], { cwd: crateDir, env: { ...baseEnv, ...brokerEnv }, windowsHide: true, timeout: 60_000 });
|
|
180
181
|
const cargo = await invoke(["-V"], { env: { ...baseEnv, ...brokerEnv }, timeout: 60_000 });
|
|
181
182
|
if (rustc.error || rustc.status !== 0 || cargo.error || cargo.status !== 0) throw new Error("cannot read the toolchain through the broker");
|
|
182
183
|
return { rustc: String(rustc.stdout).trim(), cargo: String(cargo.stdout).trim() };
|
package/mac-package-recipe.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { buildDmg } from "./dmg-build.mjs";
|
|
|
18
18
|
import { spawnSync } from "./background-process.mjs";
|
|
19
19
|
import { signMacosEntries } from "./mac-sign-core.mjs";
|
|
20
20
|
import { mainExecutableName } from "./mac-bundle-signing.mjs";
|
|
21
|
+
import { resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
|
|
21
22
|
import { notarizeMacosArtifact } from "./macos-notarization.mjs";
|
|
22
23
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
23
24
|
import { runMacCodesignCommands, signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
@@ -160,10 +161,12 @@ const defaultInspect = (cmd, args) => {
|
|
|
160
161
|
* signUpdater(files, options), receiptPath, log(event).
|
|
161
162
|
*/
|
|
162
163
|
export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} }) {
|
|
164
|
+
let artifactPaths = null;
|
|
163
165
|
const { recipe, app: declaredApp, dmgFile } = validateMacPackageTarget(target);
|
|
164
166
|
const {
|
|
165
167
|
platform = process.platform, env = process.env, fs = nodeFs,
|
|
166
|
-
|
|
168
|
+
// A door split (row 7.04c, R-T2) reads a declared target path in the bound Cargo target, never the snapshot's or the checkout's; outside a split this is path.resolve.
|
|
169
|
+
resolveArtifact = (root, p) => { artifactPaths ??= splitArtifactContext({ cwd: root, env, target }); return artifactPaths.mode === "split" ? resolveArtifactPath(p, artifactPaths) : path.resolve(root, p); },
|
|
167
170
|
resolveIdentity = resolveMacosDeveloperIdIdentity,
|
|
168
171
|
signOne = (codesignArgs) => runMacCodesignCommands([codesignArgs], { env })[0],
|
|
169
172
|
inspect = defaultInspect, evidence, hashFile,
|
package/mirror-root-artifact.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSy
|
|
|
11
11
|
import { execFileSync } from "./background-process.mjs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { importReleaseConfig } from "./release-config.mjs";
|
|
14
|
+
import { ArtifactPathError, isSplitArtifactEnv, loadSplitArtifactContext, resolveArtifactPath } from "./release-artifact-path.mjs";
|
|
14
15
|
|
|
15
16
|
const args = parseArgs(process.argv.slice(2));
|
|
16
17
|
const usage = "Usage: right-release mirror-root-artifact (--file <artifact> --package-root <dir> [--name <filename>] | --config <file> --platform mac|win [--package-root <dir>])";
|
|
@@ -52,15 +53,27 @@ if (args.config) {
|
|
|
52
53
|
|
|
53
54
|
const packageRoot = realpathSync(path.resolve(rootArg));
|
|
54
55
|
const managedOutput = path.resolve(process.env.RIGHT_RELEASE_ARTIFACT_DIR);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// A door split (row 7.04c): the package root is the snapshot, which has no git directory; the repositories the output must stay out of are the
|
|
57
|
+
// checkout and the snapshot, and a target-prefixed source is the bound Cargo target's (read in place, never the checkout's).
|
|
58
|
+
const splitMirror = isSplitArtifactEnv(process.env);
|
|
59
|
+
const artifactPaths = await loadSplitArtifactContext({ cwd: packageRoot });
|
|
60
|
+
const repoRoots = splitMirror ? [path.resolve(process.env.RK_GATE_CHECKOUT), packageRoot] : [path.resolve(git(packageRoot, "rev-parse", "--show-toplevel"))];
|
|
61
|
+
for (const repoRoot of repoRoots) {
|
|
62
|
+
if (inside(managedOutput, repoRoot)) {
|
|
63
|
+
console.error(`right-release mirror-root-artifact: managed output must be outside repository: ${managedOutput}`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
59
66
|
}
|
|
60
67
|
const safeName = (name) => name === path.basename(name) && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(name);
|
|
61
68
|
|
|
62
69
|
for (const entry of entries) {
|
|
63
|
-
|
|
70
|
+
let source;
|
|
71
|
+
try { source = splitMirror ? resolveArtifactPath(entry.file, artifactPaths) : path.resolve(packageRoot, entry.file); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!(error instanceof ArtifactPathError)) throw error;
|
|
74
|
+
console.error(`right-release mirror-root-artifact: ${error.message}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
64
77
|
const name = entry.name || path.basename(source);
|
|
65
78
|
if (!safeName(name)) {
|
|
66
79
|
console.error(`right-release mirror-root-artifact: unsafe artifact name: ${name}`);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// app-specific product domain logic and presentation only.
|
|
5
5
|
// Non-product code stays in an app only under an approved, expiring entry of .rightkit/ownership-exceptions.json that names the
|
|
6
6
|
// RightKit issue that moves it. These two rules run wherever the app check runs (`rightkit-gate check`, `right-release check`, the
|
|
7
|
-
//
|
|
7
|
+
// start of every checked right-release command and RightKit's own preflight):
|
|
8
8
|
//
|
|
9
9
|
// mandatory-foundation OW-05 an app whose profile requires a foundation owner the app does not declare as a dependency (missing,
|
|
10
10
|
// or dev-only), or that selects an unknown or unratified profile. An app is a repository whose root
|
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) {
|