@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/snapshot.mjs
CHANGED
|
@@ -363,15 +363,66 @@ export function dependencyDirs(entries, platform = process.platform) {
|
|
|
363
363
|
* carries every later checkout edit into the build, with bytes that may still equal the commit's). Hard links inside the
|
|
364
364
|
* dependency directories are pnpm's own store links; their bytes are bound by the environment digest instead. Never follows a link
|
|
365
365
|
* to enter it. Returns a message naming the path, or null.
|
|
366
|
+
*
|
|
367
|
+
* The stage-output ledger (item 5 row 5.09d; signed/item7-sage-decisions-2.md Q1). Given `ledger` and/or `admitNew`, the walk does not
|
|
368
|
+
* refuse the first path the commit lacks: it collects every such path as a candidate entry and admits it only when ALL hold:
|
|
369
|
+
* - it is untracked (not in `entries`) and ignored by the COMMIT'S OWN ignore rules, judged by the neutral oracle (neutralIgnored:
|
|
370
|
+
* `git check-ignore --no-index` against an empty bare GIT_DIR with core.excludesFile at the null device, so neither the checkout's
|
|
371
|
+
* info/exclude nor any global excludes file widens what is ignored; probe P4);
|
|
372
|
+
* - it is a regular file or a directory, never a link of any kind, and a file has nlink 1;
|
|
373
|
+
* - no path segment is `target`, `.right-release`, `.git` or `node_modules` (outside the pnpm roots, which are not candidates),
|
|
374
|
+
* and it is not named `.gitignore` (an untracked ignore file would make the oracle ignore whatever it names).
|
|
375
|
+
* `ledger` (an array of `{ path, type, size, sha256, execBit }` recorded by an earlier verify) must still match exactly, byte for byte,
|
|
376
|
+
* and none of its entries may be missing (an absent directory that only parents ledger files is implied by them). `admitNew` (end of a
|
|
377
|
+
* stage only) additionally admits candidates the ledger does not hold. Without `admitNew`, a candidate outside the ledger is refused,
|
|
378
|
+
* so the start of a stage stays as strict as the sync. In this mode the result is `{ problem, admitted }`, `admitted` being every
|
|
379
|
+
* admitted entry (ledger and new), sorted by path; otherwise it is the message or null as before.
|
|
366
380
|
*/
|
|
367
|
-
export function snapshotProblem(
|
|
381
|
+
export function snapshotProblem(options) {
|
|
382
|
+
const r = inspectSnapshot(options);
|
|
383
|
+
return options.ledger !== undefined || options.admitNew ? r : r.problem;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const RESERVED_SEGMENTS = new Set(["target", ".right-release", ".git", "node_modules"]);
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Which of `paths` (relative, `/`-separated, existing under `dir`) the snapshot's own ignore rules ignore, and nothing else: the
|
|
390
|
+
* sealed runner (snapshot-git.mjs) gives an empty info/exclude and an empty private file for core.excludesFile (never a null device,
|
|
391
|
+
* so Windows needs no `NUL`), and `--no-index` so only the work tree's `.gitignore` files count. Returns a Set of the ignored paths as given.
|
|
392
|
+
*/
|
|
393
|
+
export function neutralIgnored(dir, paths) {
|
|
394
|
+
if (!paths.length) return new Set();
|
|
395
|
+
return withSealedGit({ snapshot: dir }, (git) => {
|
|
396
|
+
const r = git.run(["check-ignore", "--no-index", "-z", "--stdin"], { input: `${paths.join("\0")}\0` });
|
|
397
|
+
if (r.error) throw new SnapshotError("error", `git check-ignore: ${r.error.message}`);
|
|
398
|
+
// Exit 0: some path is ignored; 1: none is; anything else is git failing, which must never read as "not ignored" or "ignored".
|
|
399
|
+
if (r.status !== 0 && r.status !== 1) throw new SnapshotError("error", `git check-ignore: ${String(r.stderr).trim() || `exit ${r.status}`}`);
|
|
400
|
+
return new Set(String(r.stdout ?? "").split("\0").filter(Boolean));
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** sha256 of a file, read in chunks (a staged sidecar can be large). */
|
|
405
|
+
function sha256File(file) {
|
|
406
|
+
const h = crypto.createHash("sha256");
|
|
407
|
+
const fd = fs.openSync(file, "r");
|
|
408
|
+
try {
|
|
409
|
+
const buf = Buffer.allocUnsafe(1 << 20);
|
|
410
|
+
for (let n; (n = fs.readSync(fd, buf, 0, buf.length, null)) > 0;) h.update(buf.subarray(0, n));
|
|
411
|
+
} finally { fs.closeSync(fd); }
|
|
412
|
+
return h.digest("hex");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function inspectSnapshot({ dir, entries, checkout, platform = process.platform, originUrl, ledger, admitNew = false }) {
|
|
416
|
+
const problem = (message) => ({ problem: message, admitted: [] });
|
|
417
|
+
const ledgerMode = ledger !== undefined || admitNew === true;
|
|
368
418
|
const fold = foldFor(platform);
|
|
419
|
+
const candidates = [];
|
|
369
420
|
const byKey = new Map(entries.map((e) => [fold(e.path), e]));
|
|
370
421
|
const dirs = new Set();
|
|
371
422
|
for (const e of entries) { const parts = e.path.split("/"); for (let i = 1; i < parts.length; i++) dirs.add(fold(parts.slice(0, i).join("/"))); }
|
|
372
423
|
const deps = dependencyDirs(entries, platform);
|
|
373
424
|
let realDir; let realCheckout = checkout;
|
|
374
|
-
try { realDir = fs.realpathSync.native(dir); } catch (e) { return `${dir} cannot be resolved: ${e.message}
|
|
425
|
+
try { realDir = fs.realpathSync.native(dir); } catch (e) { return problem(`${dir} cannot be resolved: ${e.message}`); }
|
|
375
426
|
try { realCheckout = fs.realpathSync.native(checkout); } catch { /* the path as given */ }
|
|
376
427
|
const linkProblem = (rel, p) => {
|
|
377
428
|
let real;
|
|
@@ -416,6 +467,38 @@ export function snapshotProblem({ dir, entries, checkout, platform = process.pla
|
|
|
416
467
|
}
|
|
417
468
|
return null;
|
|
418
469
|
};
|
|
470
|
+
// Ledger mode: a path the commit lacks is a candidate. Everything about it that does not need git is decided here; whether the
|
|
471
|
+
// commit's own rules ignore it is decided for all candidates at once afterwards (neutralIgnored).
|
|
472
|
+
const reserved = (rel) => {
|
|
473
|
+
const parts = rel.split("/").map(fold);
|
|
474
|
+
const bad = parts.find((x) => RESERVED_SEGMENTS.has(x));
|
|
475
|
+
if (bad !== undefined) return `${rel} is under ${JSON.stringify(bad)}, a reserved path no stage output may occupy (target/ is the checkout's output directory, .right-release holds right-release's, .git is the gate's config-only file, node_modules is allowed only as the pnpm roots' installed dependencies)`;
|
|
476
|
+
if (fold(parts.at(-1)) === ".gitignore") return `${rel} is an ignore file the commit does not have: an untracked .gitignore would decide what counts as ignored`;
|
|
477
|
+
return null;
|
|
478
|
+
};
|
|
479
|
+
const candidate = (p, rel) => {
|
|
480
|
+
const bad = reserved(rel);
|
|
481
|
+
if (bad) return bad;
|
|
482
|
+
const s = lstat(p);
|
|
483
|
+
if (!s) return `${rel} vanished while it was checked`;
|
|
484
|
+
if (s.isSymbolicLink()) return `${rel} is a link: a link is never admitted to the stage-output ledger`;
|
|
485
|
+
if (!s.isFile()) return `${rel} is not a regular file or a directory`;
|
|
486
|
+
if (s.nlink > 1n) return `${rel} has ${s.nlink} hard links: a checkout edit made through another name for it would reach the build`;
|
|
487
|
+
candidates.push({ path: rel, type: "file", size: Number(s.size), sha256: sha256File(p), execBit: platform !== "win32" && (Number(s.mode) & 0o111) !== 0 });
|
|
488
|
+
return null;
|
|
489
|
+
};
|
|
490
|
+
const candidateDir = (p, rel) => {
|
|
491
|
+
const bad = reserved(rel);
|
|
492
|
+
if (bad) return bad;
|
|
493
|
+
candidates.push({ path: rel, type: "dir", size: 0, sha256: null, execBit: false });
|
|
494
|
+
for (const e of fs.readdirSync(p, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
495
|
+
const r = `${rel}/${e.name}`;
|
|
496
|
+
const q = path.join(p, e.name);
|
|
497
|
+
const found = e.isSymbolicLink() ? `${r} is a link: a link is never admitted to the stage-output ledger` : e.isDirectory() ? candidateDir(q, r) : candidate(q, r);
|
|
498
|
+
if (found) return found;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
};
|
|
419
502
|
const walk = (d, rel, inDeps) => {
|
|
420
503
|
const names = fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
421
504
|
for (const e of names) {
|
|
@@ -426,19 +509,42 @@ export function snapshotProblem({ dir, entries, checkout, platform = process.pla
|
|
|
426
509
|
if (e.isSymbolicLink()) {
|
|
427
510
|
// The landing is named first when it is the worse news (into the checkout, out of the snapshot); a link the commit lacks is refused either way.
|
|
428
511
|
found = linkProblem(r, p);
|
|
429
|
-
if (!found && !inDeps) found = !byKey.has(key) ? `${r} is not in the commit's tree (a link the commit does not have)` : fileProblem(r, p, byKey.get(key));
|
|
512
|
+
if (!found && !inDeps) found = !byKey.has(key) ? (ledgerMode ? `${r} is a link the commit does not have: a link is never admitted to the stage-output ledger` : `${r} is not in the commit's tree (a link the commit does not have)`) : fileProblem(r, p, byKey.get(key));
|
|
430
513
|
} else if (e.isDirectory()) {
|
|
431
514
|
if (inDeps) found = walk(p, r, true);
|
|
432
515
|
else if (deps.has(key)) found = walk(p, r, true);
|
|
433
516
|
else if (dirs.has(key)) found = walk(p, r, false);
|
|
434
517
|
else if (key === ".git") found = gitConfigProblem(r, p);
|
|
435
|
-
else found = `${r} is not in the commit's tree (a directory the commit does not have)`;
|
|
436
|
-
} else if (!inDeps) found = !byKey.has(key) ? `${r} is not in the commit's tree` : fileProblem(r, p, byKey.get(key));
|
|
518
|
+
else found = ledgerMode ? candidateDir(p, r) : `${r} is not in the commit's tree (a directory the commit does not have)`;
|
|
519
|
+
} else if (!inDeps) found = !byKey.has(key) ? (ledgerMode ? candidate(p, r) : `${r} is not in the commit's tree`) : fileProblem(r, p, byKey.get(key));
|
|
437
520
|
if (found) return found;
|
|
438
521
|
}
|
|
439
522
|
return null;
|
|
440
523
|
};
|
|
441
|
-
|
|
524
|
+
const found = walk(dir, "", false);
|
|
525
|
+
if (found || !ledgerMode) return { problem: found, admitted: [] };
|
|
526
|
+
|
|
527
|
+
// The ledger's entries, keyed like the walk's, must all be here and byte-identical; anything else the walk met is new.
|
|
528
|
+
const held = new Map((ledger ?? []).map((e) => [fold(e.path), e]));
|
|
529
|
+
const seen = new Map(candidates.map((c) => [fold(c.path), c]));
|
|
530
|
+
for (const [key, want] of held) {
|
|
531
|
+
const have = seen.get(key);
|
|
532
|
+
if (!have) return problem(`${want.path} is a ledger entry (recorded at the end of an earlier stage) and is missing`);
|
|
533
|
+
if (have.type !== want.type || have.size !== want.size || have.sha256 !== want.sha256 || have.execBit !== want.execBit) {
|
|
534
|
+
return problem(`${want.path} is a ledger entry and changed since it was recorded (${want.type} ${want.size} bytes sha256 ${want.sha256}${want.execBit ? " executable" : ""} was recorded; ${have.type} ${have.size} bytes sha256 ${have.sha256}${have.execBit ? " executable" : ""} is here)`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// A directory that only parents ledger entries is implied by them; any other candidate must be admitted as new.
|
|
538
|
+
const parents = new Set();
|
|
539
|
+
for (const e of held.values()) { const parts = e.path.split("/"); for (let i = 1; i < parts.length; i++) parents.add(fold(parts.slice(0, i).join("/"))); }
|
|
540
|
+
for (const c of candidates) {
|
|
541
|
+
const key = fold(c.path);
|
|
542
|
+
if (!held.has(key) && !(c.type === "dir" && parents.has(key)) && !admitNew) return problem(`${c.path} is not in the commit's tree and not in the stage-output ledger (new stage outputs are admitted only at the end of a stage)`);
|
|
543
|
+
}
|
|
544
|
+
// Every candidate, ledger or new, must be ignored by the commit's own rules, as the neutral oracle judges them.
|
|
545
|
+
const ignored = neutralIgnored(dir, candidates.map((c) => c.path));
|
|
546
|
+
for (const c of candidates) if (!ignored.has(c.path)) return problem(`${c.path} is not in the commit's tree and is not ignored by the commit's own ignore rules (the checkout's info/exclude and global excludes do not count)`);
|
|
547
|
+
return { problem: null, admitted: [...candidates].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) };
|
|
442
548
|
}
|
|
443
549
|
|
|
444
550
|
function blobId(content, objectFormat) {
|
|
@@ -775,8 +881,14 @@ export function syncSnapshot({ checkout, rev = "HEAD", root = snapshotRoot(), ma
|
|
|
775
881
|
* digest (the caller supplies it: the digest is the gate's, bin/rightkit-gate.mjs classAEnvironmentDigest); a difference is a
|
|
776
882
|
* mismatch, so dependency bytes changed or planted mid-stage refuse at the end verify. Callers run this at the start and again at
|
|
777
883
|
* the end of a stage (the stage-level calls are item 7, 7.08). `managedRoot` and `device` as for syncSnapshot.
|
|
884
|
+
*
|
|
885
|
+
* Stage-output ledger (row 5.09d, snapshotProblem): `ledger` is the array of entries an earlier verify returned (the start of a stage
|
|
886
|
+
* passes the whole ledger and no `admitNew`: nothing new, nothing changed, nothing missing); `admitNew: true` (the END of a stage
|
|
887
|
+
* only) also admits the ignored, untracked, regular-file-or-directory outputs the stage's children wrote. Either option makes the
|
|
888
|
+
* result carry `ledger: [{ path, type, size, sha256, execBit }]`, every admitted entry. The tree id is still the commit's tree:
|
|
889
|
+
* an ignored output does not change it. With neither option nothing beyond the commit (and 5.09's allowances) is accepted.
|
|
778
890
|
*/
|
|
779
|
-
export function verifySnapshot({ checkout, rev = "HEAD", root = snapshotRoot(), dir, managedRoot = hostManagedRootFor(root), device, environment } = {}) {
|
|
891
|
+
export function verifySnapshot({ checkout, rev = "HEAD", root = snapshotRoot(), dir, managedRoot = hostManagedRootFor(root), device, environment, ledger, admitNew } = {}) {
|
|
780
892
|
const repo = repositoryOf(checkout);
|
|
781
893
|
const { commit, tree } = resolveCommit(repo.checkout, rev);
|
|
782
894
|
if (managedRoot) requireManagedRoot(managedRoot, { device });
|
|
@@ -785,8 +897,9 @@ export function verifySnapshot({ checkout, rev = "HEAD", root = snapshotRoot(),
|
|
|
785
897
|
if (path.resolve(path.dirname(at)) === path.resolve(root)) requirePrivateRoot(root, lstat(root));
|
|
786
898
|
if (!lstat(at)?.isDirectory()) throw new SnapshotError("mismatch", `no snapshot directory at ${at}`);
|
|
787
899
|
// The seeded tree id cannot see an ignored file or follow a link, so the directory itself is walked first (E6, §1 exact snapshot).
|
|
788
|
-
const
|
|
789
|
-
|
|
900
|
+
const inspected = snapshotProblem({ dir: at, entries: treeEntries(repo.checkout, commit), checkout: repo.checkout, ledger, admitNew });
|
|
901
|
+
const extra = ledger !== undefined || admitNew ? inspected.problem : inspected;
|
|
902
|
+
if (extra) throw new SnapshotError("mismatch", `the snapshot at ${at} is not exactly commit ${commit}${ledger !== undefined || admitNew ? " plus its stage-output ledger" : ""}: ${extra}`);
|
|
790
903
|
const actual = snapshotTree({ checkout: repo.checkout, dir: at, baseTree: tree, repo });
|
|
791
904
|
if (actual !== tree) throw new SnapshotError("mismatch", `the snapshot at ${at} is tree ${actual}, not tree ${tree} of commit ${commit}`);
|
|
792
905
|
if (environment !== undefined) {
|
|
@@ -796,7 +909,7 @@ export function verifySnapshot({ checkout, rev = "HEAD", root = snapshotRoot(),
|
|
|
796
909
|
catch (e) { throw new SnapshotError("error", `the environment digest of the snapshot at ${at} cannot be computed: ${e.message}`); }
|
|
797
910
|
if (now !== environment.expected) throw new SnapshotError("mismatch", `the installed environment of the snapshot at ${at} changed: env=${now}, recorded env=${environment.expected}`);
|
|
798
911
|
}
|
|
799
|
-
return { schema: SNAPSHOT_SCHEMA, path: at, repoKey: repo.key, commit, tree, ...(environment !== undefined ? { env: environment.expected } : {}) };
|
|
912
|
+
return { schema: SNAPSHOT_SCHEMA, path: at, repoKey: repo.key, commit, tree, ...(environment !== undefined ? { env: environment.expected } : {}), ...(ledger !== undefined || admitNew ? { ledger: inspected.admitted } : {}) };
|
|
800
913
|
}
|
|
801
914
|
|
|
802
915
|
/**
|
package/source-split.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The right-release source split (docs/GATE-CONTRACT.md "Commit-only build"; item 7 rows 7.03 to 7.05).
|
|
2
|
+
//
|
|
3
|
+
// Under the commit-only build the gate selects a commit, syncs it into a gate-owned snapshot and runs every stage from there. A
|
|
4
|
+
// right-release child the door starts for a stage (dev, test, stage-sidecar) therefore runs with its cwd inside the SNAPSHOT: its
|
|
5
|
+
// sources, its config and every command it spawns resolve there. What stays with the CHECKOUT is the git context the door run was
|
|
6
|
+
// made in (the run file, the primary-checkout rule, the commit's object database) and every output or cache right-release owns
|
|
7
|
+
// (`.right-release/...`).
|
|
8
|
+
//
|
|
9
|
+
// The split is never taken from a flag or from the child's own idea of the repository. The door writes it into its live run file
|
|
10
|
+
// (gate-stages.mjs startRun `source`) and names the checkout in RK_GATE_CHECKOUT; this module re-reads the run file, requires the
|
|
11
|
+
// run alive, and requires the process to sit inside the snapshot that run names. Anything inconsistent throws (fail closed): a
|
|
12
|
+
// child told it is split that cannot prove it never builds from where it stands.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { spawnSync } from "./background-process.mjs";
|
|
16
|
+
import { CHECKOUT_ENV, readDoorRun } from "./gate-stages.mjs";
|
|
17
|
+
|
|
18
|
+
export { CHECKOUT_ENV };
|
|
19
|
+
export const SOURCE_SPLIT_CODE = "RK_SOURCE_SPLIT";
|
|
20
|
+
export const SOURCE_SPLIT_UNSUPPORTED = "RK_SOURCE_SPLIT_UNSUPPORTED";
|
|
21
|
+
|
|
22
|
+
export class SourceSplitError extends Error {
|
|
23
|
+
constructor(message, code = SOURCE_SPLIT_CODE) { super(message); this.name = "SourceSplitError"; this.code = code; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const real = (p) => { try { return fs.realpathSync.native(p); } catch { return path.resolve(p); } };
|
|
27
|
+
const fold = (p) => (process.platform === "win32" || process.platform === "darwin" ? p.toLowerCase() : p);
|
|
28
|
+
const contains = (parent, child) => { const rel = path.relative(fold(real(parent)), fold(real(child))); return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The split this process runs under, or null when RK_GATE_CHECKOUT is not set (the checkout route: hosted CI, or no door).
|
|
32
|
+
* Throws SourceSplitError when it is set and anything about it cannot be proven. `run` runs git (tests inject a double).
|
|
33
|
+
*/
|
|
34
|
+
export function readSourceSplit({ env = process.env, cwd = process.cwd(), run = spawnSync, readFile, alive } = {}) {
|
|
35
|
+
const named = env[CHECKOUT_ENV];
|
|
36
|
+
if (named === undefined || named === "") return null;
|
|
37
|
+
if (!path.isAbsolute(named)) throw new SourceSplitError(`${CHECKOUT_ENV} must be an absolute path (got ${JSON.stringify(named)})`);
|
|
38
|
+
const common = run("git", ["rev-parse", "--git-common-dir"], { cwd: named, encoding: "utf8", windowsHide: true });
|
|
39
|
+
if (common.status !== 0 || !String(common.stdout).trim()) throw new SourceSplitError(`${CHECKOUT_ENV} ${named} is not a git checkout`);
|
|
40
|
+
const commonDir = path.resolve(named, String(common.stdout).trim());
|
|
41
|
+
const found = readDoorRun({ env, commonDir, ...(readFile ? { readFile } : {}), ...(alive ? { alive } : {}) });
|
|
42
|
+
if (found.problem) throw new SourceSplitError(`no live door run ${found.problem === "ended" ? "(it has ended)" : "(RK_GATE_RUN names none)"} under ${named}; a source split exists only inside one`);
|
|
43
|
+
const source = found.run.source;
|
|
44
|
+
if (!source || typeof source !== "object") throw new SourceSplitError("the live door run carries no source split");
|
|
45
|
+
for (const key of ["checkout", "snapshot"]) if (typeof source[key] !== "string" || !path.isAbsolute(source[key])) throw new SourceSplitError(`the door run's source.${key} is not an absolute path`);
|
|
46
|
+
for (const key of ["commit", "tree"]) if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(source[key] ?? "")) throw new SourceSplitError(`the door run's source.${key} is not an object id`);
|
|
47
|
+
if (typeof source.appDir !== "string" || source.appDir.startsWith("/") || source.appDir.split("/").includes("..") || source.appDir.includes("\\")) throw new SourceSplitError("the door run's source.appDir is not a contained relative path");
|
|
48
|
+
if (real(source.checkout) !== real(named)) throw new SourceSplitError(`the door run was made in ${source.checkout}, not in ${named}`);
|
|
49
|
+
if (found.run.commit !== source.commit) throw new SourceSplitError("the door run's commit and its source split's commit differ");
|
|
50
|
+
const appRoot = source.appDir === "." ? source.snapshot : path.join(source.snapshot, ...source.appDir.split("/"));
|
|
51
|
+
if (!contains(source.snapshot, cwd)) throw new SourceSplitError(`this process runs in ${cwd}, outside the snapshot ${source.snapshot} its door run names; a split child runs only inside the snapshot`);
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
checkout: source.checkout, snapshot: source.snapshot, commit: source.commit, tree: source.tree, appDir: source.appDir, commonDir,
|
|
54
|
+
appRoot, checkoutAppRoot: source.appDir === "." ? source.checkout : path.join(source.checkout, ...source.appDir.split("/")),
|
|
55
|
+
stage: found.run.stage,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether `file` lies inside the split's snapshot. */
|
|
60
|
+
export const inSnapshot = (split, file) => contains(split.snapshot, file);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The git context a door-checked step needs: `{ commonDir, commit }` of the live run for a split process, else of the git
|
|
64
|
+
* repository at `cwd` (HEAD), else null. door.mjs, web-build and the right-release CLI ask the same question, so they share it.
|
|
65
|
+
*/
|
|
66
|
+
export function doorContextFor({ cwd = process.cwd(), env = process.env, run = spawnSync } = {}) {
|
|
67
|
+
const split = readSourceSplit({ env, cwd, run });
|
|
68
|
+
if (split) return { commonDir: split.commonDir, commit: split.commit, split };
|
|
69
|
+
const git = (args) => { const r = run("git", args, { cwd, encoding: "utf8" }); return r.status === 0 ? String(r.stdout).trim() : ""; };
|
|
70
|
+
const common = git(["rev-parse", "--git-common-dir"]);
|
|
71
|
+
if (!common) return null;
|
|
72
|
+
return { commonDir: path.resolve(cwd, common), commit: git(["rev-parse", "HEAD"]), split: null };
|
|
73
|
+
}
|
package/stage-commands.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// a signed receipt of its own job; a missing command is a refusal that names it.
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { CARGO_METADATA_ARGS, cargoPackageArgs, localTestPlan, npmPackArgs, qualifyFmtArgs } from "./local-publish.mjs";
|
|
7
|
-
import { SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
|
|
7
|
+
import { SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_METADATA_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
|
|
8
8
|
|
|
9
9
|
/** Stands for the one argument that is a fresh absolute scratch path (`pnpm pack --pack-destination <mkdtemp>`). */
|
|
10
10
|
export const ANY_ABSOLUTE_PATH = Object.freeze({ anyAbsolutePath: true });
|
|
@@ -27,7 +27,7 @@ export function expectedStageCommands({ kind, unit, config, manifest, platform }
|
|
|
27
27
|
if (kind === "npm") {
|
|
28
28
|
if (unit === SWAP_HELPER_PACKAGE) {
|
|
29
29
|
// The swap helper builds and signs a binary, then packs from a scratch copy of its directory.
|
|
30
|
-
pack.push(CARGO(SWAP_HELPER_CARGO_ARGS), CARGO(
|
|
30
|
+
pack.push(CARGO(SWAP_HELPER_CARGO_ARGS), CARGO(SWAP_HELPER_METADATA_ARGS), PNPM(npmPackArgs(ANY_ABSOLUTE_PATH, ANY_ABSOLUTE_PATH)));
|
|
31
31
|
} else pack.push(PNPM(npmPackArgs(dir, ANY_ABSOLUTE_PATH)));
|
|
32
32
|
} else {
|
|
33
33
|
compile.unshift(CARGO(qualifyFmtArgs(unit)));
|
package/stage-ledger.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// The stage-output ledger (docs/GATE-CONTRACT.md "Commit-only build"; item 7 decision 2, question 1, rows 7.05/7.05b/7.07/7.08).
|
|
2
|
+
//
|
|
3
|
+
// A real build writes into the directory it builds from: Tauri's `gen/schemas`, staged sidecars, a frontend `dist`. The snapshot is
|
|
4
|
+
// exactly the commit's tree (plus the gate's installed dependencies) at the start of every stage; what a stage's own children write
|
|
5
|
+
// during it may remain at the stage's end only as LEDGER ENTRIES, each of which must be:
|
|
6
|
+
// 1. not tracked (not in `ls-tree <commit>`) and ignored by the commit's own ignore rules, judged with an empty bare GIT_DIR and a null
|
|
7
|
+
// core.excludesFile, so neither the checkout's `info/exclude` nor a global excludes file ever widens what is ignored;
|
|
8
|
+
// 2. a regular file (a directory only as the parent of one), never a link, and with exactly one hard link;
|
|
9
|
+
// 3. never under a reserved path: a `target` segment, `.right-release`, `.git`, or a `node_modules` outside the gate's own dependency roots;
|
|
10
|
+
// 4. recorded by THIS walk, never by a child: the ledger lives in the door's memory and in the door receipt under the checkout's git
|
|
11
|
+
// common directory, never inside the snapshot (GC:162).
|
|
12
|
+
// This module only walks, classifies and compares. The exact-snapshot verify is snapshot.mjs verifySnapshot, which takes the ledger.
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawnSync } from "./background-process.mjs";
|
|
18
|
+
|
|
19
|
+
export const LEDGER_RESERVED_SEGMENTS = Object.freeze(["target", ".right-release", ".git", "node_modules"]);
|
|
20
|
+
export const NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
21
|
+
|
|
22
|
+
const fold = (platform = process.platform) => (platform === "darwin" || platform === "win32" ? (s) => s.toLowerCase() : (s) => s);
|
|
23
|
+
|
|
24
|
+
/** The comparable form of an entry: what the next stage's start verify must find unchanged. */
|
|
25
|
+
export const entryKey = (e) => `${e.path}\0${e.type}\0${e.size}\0${e.sha256}\0${e.execBit ? 1 : 0}`;
|
|
26
|
+
|
|
27
|
+
/** A clean env for the git calls below: nothing of the caller's GIT_* and no global or system configuration. */
|
|
28
|
+
function gitEnv(env = process.env) {
|
|
29
|
+
const clean = Object.fromEntries(Object.entries(env).filter(([k]) => !/^GIT_/i.test(k)));
|
|
30
|
+
return { ...clean, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: NULL_DEVICE, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Which of `paths` (posix, relative to `dir`) the commit's own ignore rules ignore: `git check-ignore --no-index` run with an empty
|
|
35
|
+
* bare GIT_DIR, the snapshot as the work tree and a null core.excludesFile, so only the snapshot's own `.gitignore` files apply.
|
|
36
|
+
* Throws when git cannot answer (the caller fails closed).
|
|
37
|
+
*/
|
|
38
|
+
export function neutralIgnored({ dir, paths, run = spawnSync, env = process.env }) {
|
|
39
|
+
if (!paths.length) return new Set();
|
|
40
|
+
const bare = fs.mkdtempSync(path.join(os.tmpdir(), "rk-ignore-oracle-"));
|
|
41
|
+
try {
|
|
42
|
+
const made = run("git", ["init", "--bare", "-q", "--template=", bare], { encoding: "utf8", env: gitEnv(env), windowsHide: true });
|
|
43
|
+
if (made.error || made.status !== 0) throw new Error(`cannot create the empty git directory for the ignore check: ${made.error?.message ?? String(made.stderr).trim()}`);
|
|
44
|
+
const r = run("git", [`--git-dir=${bare}`, `--work-tree=${dir}`, "-c", `core.excludesFile=${NULL_DEVICE}`, "check-ignore", "--no-index", "-z", "--stdin"], {
|
|
45
|
+
cwd: dir, input: `${paths.join("\0")}\0`, encoding: "utf8", env: gitEnv(env), windowsHide: true, maxBuffer: 1 << 28,
|
|
46
|
+
});
|
|
47
|
+
// 0: some path is ignored; 1: none is; anything else is git failing.
|
|
48
|
+
if (r.error || (r.status !== 0 && r.status !== 1)) throw new Error(`git check-ignore failed (${r.error?.message ?? `exit ${r.status}: ${String(r.stderr).trim()}`})`);
|
|
49
|
+
return new Set(String(r.stdout ?? "").split("\0").filter(Boolean));
|
|
50
|
+
} finally { fs.rmSync(bare, { recursive: true, force: true }); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Walks `dir` and returns what it holds beyond the commit: `{ entries, problems }`. `tracked` is the set of every path in
|
|
55
|
+
* `ls-tree <commit>` (posix); `dependencyRoots` the `node_modules` directories the gate installed under 5.09a (posix, relative; their
|
|
56
|
+
* bytes are bound by the class A environment digest, not here). `entries` are admissible ledger entries; `problems` name every write
|
|
57
|
+
* that cannot be one (not ignored, a link, a hard link, reserved, an empty directory, a `.git` holding more than the gate's config).
|
|
58
|
+
*/
|
|
59
|
+
export function walkSnapshot({ dir, tracked, dependencyRoots = new Set(), run = spawnSync, env = process.env, platform = process.platform }) {
|
|
60
|
+
const f = fold(platform);
|
|
61
|
+
const trackedFolded = new Set([...tracked].map(f));
|
|
62
|
+
const trackedDirs = new Set();
|
|
63
|
+
for (const p of tracked) { const parts = f(p).split("/"); for (let i = 1; i < parts.length; i++) trackedDirs.add(parts.slice(0, i).join("/")); }
|
|
64
|
+
const deps = new Set([...dependencyRoots].map(f));
|
|
65
|
+
const candidates = []; const problems = [];
|
|
66
|
+
const reserved = (rel) => rel.split("/").find((seg) => LEDGER_RESERVED_SEGMENTS.includes(f(seg)));
|
|
67
|
+
const walk = (abs, rel) => {
|
|
68
|
+
let names;
|
|
69
|
+
try { names = fs.readdirSync(abs, { withFileTypes: true }); } catch (e) { problems.push(`${rel || "."} cannot be read: ${e.message}`); return; }
|
|
70
|
+
names.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
71
|
+
let any = false;
|
|
72
|
+
for (const d of names) {
|
|
73
|
+
const r = rel ? `${rel}/${d.name}` : d.name;
|
|
74
|
+
const key = f(r);
|
|
75
|
+
const p = path.join(abs, d.name);
|
|
76
|
+
if (deps.has(key)) continue;
|
|
77
|
+
any = true;
|
|
78
|
+
if (key === ".git" && rel === "") {
|
|
79
|
+
// 5.09b: only the gate's config-only origin file; its exact bytes are item 5's to verify.
|
|
80
|
+
let inner = [];
|
|
81
|
+
try { inner = fs.readdirSync(p); } catch { /* reported below */ }
|
|
82
|
+
if (inner.length !== 1 || inner[0] !== "config") problems.push(`${r} holds more than the gate's config-only file (.git is reserved)`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let st;
|
|
86
|
+
try { st = fs.lstatSync(p); } catch (e) { problems.push(`${r} cannot be read: ${e.message}`); continue; }
|
|
87
|
+
if (st.isSymbolicLink()) { if (!trackedFolded.has(key)) problems.push(`${r} is a link the commit does not have (a stage output is never a link)`); continue; }
|
|
88
|
+
if (st.isDirectory()) {
|
|
89
|
+
if (trackedDirs.has(key)) { walk(p, r); continue; }
|
|
90
|
+
const bad = reserved(r);
|
|
91
|
+
if (bad) { problems.push(`${r} is under the reserved path segment ${JSON.stringify(bad)} (a stage output is never written there)`); continue; }
|
|
92
|
+
const before = candidates.length + problems.length;
|
|
93
|
+
walk(p, r);
|
|
94
|
+
if (candidates.length + problems.length === before) problems.push(`${r} is an empty directory the commit does not have`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!st.isFile()) { problems.push(`${r} is neither a regular file nor a directory`); continue; }
|
|
98
|
+
if (trackedFolded.has(key)) continue; // a tracked file: its bytes are verifySnapshot's
|
|
99
|
+
const bad = reserved(r);
|
|
100
|
+
if (bad) { problems.push(`${r} is under the reserved path segment ${JSON.stringify(bad)} (a stage output is never written there)`); continue; }
|
|
101
|
+
if (st.nlink > 1) { problems.push(`${r} has ${st.nlink} hard links (a stage output has exactly one)`); continue; }
|
|
102
|
+
candidates.push({ rel: r, abs: p, size: st.size, execBit: platform !== "win32" && (st.mode & 0o111) !== 0 });
|
|
103
|
+
}
|
|
104
|
+
return any;
|
|
105
|
+
};
|
|
106
|
+
walk(dir, "");
|
|
107
|
+
let ignored;
|
|
108
|
+
try { ignored = neutralIgnored({ dir, paths: candidates.map((c) => c.rel), run, env }); }
|
|
109
|
+
catch (e) { return { entries: [], problems: [...problems, `the ignore rules of the commit could not be evaluated: ${e.message}`] }; }
|
|
110
|
+
const ignoredFolded = new Set([...ignored].map(f));
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const c of candidates) {
|
|
113
|
+
// A split package stage writes a declared root output (a DMG, an updater tarball) into the snapshot as a generated entry (row 7.04c R-T2 rule 5): it must be
|
|
114
|
+
// ignored by the commit itself, so the message names the declared fields such an output comes from and the fix.
|
|
115
|
+
if (!ignoredFolded.has(f(c.rel))) { problems.push(`${c.rel} is not ignored by the commit's own ignore rules (the checkout's info/exclude and global excludes never count); if the release config declares it (updater.artifacts[].file, installer.artifacts[].file, notarize.file, artifacts[] or a postSign output), add ${c.rel} to the commit's .gitignore`); continue; }
|
|
116
|
+
let bytes;
|
|
117
|
+
try { bytes = fs.readFileSync(c.abs); } catch (e) { problems.push(`${c.rel} cannot be read: ${e.message}`); continue; }
|
|
118
|
+
entries.push({ path: c.rel, type: "file", size: bytes.length, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), execBit: c.execBit });
|
|
119
|
+
}
|
|
120
|
+
entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
121
|
+
return { entries, problems };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What changed between two entry lists: `{ added, changed, removed }` as paths. */
|
|
125
|
+
export function diffEntries(before, after) {
|
|
126
|
+
const b = new Map(before.map((e) => [e.path, e])); const a = new Map(after.map((e) => [e.path, e]));
|
|
127
|
+
return {
|
|
128
|
+
added: [...a.keys()].filter((p) => !b.has(p)),
|
|
129
|
+
changed: [...a.keys()].filter((p) => b.has(p) && entryKey(b.get(p)) !== entryKey(a.get(p))),
|
|
130
|
+
removed: [...b.keys()].filter((p) => !a.has(p)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const sameEntries = (x, y) => { const d = diffEntries(x, y); return !d.added.length && !d.changed.length && !d.removed.length; };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The attribution diff of a staging step: every path the step added must be one it reported (or its receipt), and every path it
|
|
138
|
+
* reported must exist. Anything else is an unattributed write. `reported` and `receipt` are snapshot-relative posix paths.
|
|
139
|
+
*/
|
|
140
|
+
export function attributionProblems({ before, after, reported, receipt }) {
|
|
141
|
+
const d = diffEntries(before, after);
|
|
142
|
+
const allowed = new Set([...reported, ...(receipt ? [receipt] : [])]);
|
|
143
|
+
const problems = [];
|
|
144
|
+
for (const p of [...d.added, ...d.changed]) if (!allowed.has(p)) problems.push(`${p} was written by the staging step but it did not report it`);
|
|
145
|
+
for (const p of d.removed) problems.push(`${p} was removed by the staging step`);
|
|
146
|
+
const present = new Set(after.map((e) => e.path));
|
|
147
|
+
for (const p of allowed) if (!present.has(p)) problems.push(`${p} was reported by the staging step but is not in the snapshot as a ledger entry`);
|
|
148
|
+
return problems;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Every `path` string a staging receipt or its `--json` result carries (files, their notices), app-relative. */
|
|
152
|
+
export function reportedPaths(result) {
|
|
153
|
+
const out = new Set();
|
|
154
|
+
const visit = (v) => {
|
|
155
|
+
if (Array.isArray(v)) { v.forEach(visit); return; }
|
|
156
|
+
if (v && typeof v === "object") { for (const [k, x] of Object.entries(v)) { if (k === "path" && typeof x === "string") out.add(x.replaceAll("\\", "/")); else visit(x); } }
|
|
157
|
+
};
|
|
158
|
+
visit(result?.files ?? []);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The cross-check with `.rightkit/compile-inputs.json` (decision 2, Q1): for each declared input that covers ledger entries, the digest
|
|
164
|
+
* the compile receipt names must equal the gate's own digest of those bytes (compile-inputs.mjs judgeCompileInputs, the broker's
|
|
165
|
+
* algorithm). Returns problems; `judge` is judgeCompileInputs.
|
|
166
|
+
*/
|
|
167
|
+
export function declaredInputProblems({ snapshot, entries, receiptDigests, judge }) {
|
|
168
|
+
const judged = judge(snapshot);
|
|
169
|
+
const problems = judged.findings.map((x) => `declared compile input: ${x}`);
|
|
170
|
+
const named = new Map((receiptDigests ?? []).map((d) => [d.path, d.sha256]));
|
|
171
|
+
for (const input of judged.inputs) {
|
|
172
|
+
const covers = entries.some((e) => e.path === input.path || e.path.startsWith(`${input.path}/`));
|
|
173
|
+
if (!covers) continue;
|
|
174
|
+
if (!named.has(input.path)) problems.push(`declared compile input ${input.path} covers ledger entries but the receipt carries no digest for it`);
|
|
175
|
+
else if (named.get(input.path) !== input.sha256) problems.push(`declared compile input ${input.path}: the receipt's digest ${named.get(input.path)} differs from the gate's digest ${input.sha256} of the ledger bytes`);
|
|
176
|
+
}
|
|
177
|
+
return problems;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Whether the entries the snapshot verify admitted (snapshot.mjs verifySnapshot / snapshot-check.mjs verifySnapshotDir with a ledger,
|
|
182
|
+
* row 5.09d) agree with the entries this gate's own walk recorded. The verify's `admitted` lists FILES and the DIRECTORIES that parent them
|
|
183
|
+
* (`type: "dir"`); the walk records files only (a directory is implied by its files). They agree when the admitted files are exactly the
|
|
184
|
+
* walk's files (same path, size, digest and exec bit when the verify names them) and every admitted directory parents one of them.
|
|
185
|
+
* Returns a message naming the first disagreement, or null.
|
|
186
|
+
*/
|
|
187
|
+
export function admittedMismatch(admitted, entries) {
|
|
188
|
+
const files = admitted.filter((e) => (e.type ?? "file") !== "dir");
|
|
189
|
+
const dirs = admitted.filter((e) => e.type === "dir");
|
|
190
|
+
const mine = new Map(entries.map((e) => [e.path, e])); const theirs = new Map(files.map((e) => [e.path, e]));
|
|
191
|
+
for (const p of mine.keys()) if (!theirs.has(p)) return `${p} is in the door's walk but the verify did not admit it`;
|
|
192
|
+
for (const p of theirs.keys()) if (!mine.has(p)) return `${p} was admitted by the verify but the door's walk did not record it`;
|
|
193
|
+
for (const [p, e] of theirs) {
|
|
194
|
+
const w = mine.get(p);
|
|
195
|
+
if (e.sha256 !== undefined && (e.sha256 !== w.sha256 || e.size !== w.size || Boolean(e.execBit) !== Boolean(w.execBit))) return `${p} differs between the verify's entry and the door's walk`;
|
|
196
|
+
}
|
|
197
|
+
const parents = new Set();
|
|
198
|
+
for (const p of mine.keys()) { const parts = p.split("/"); for (let i = 1; i < parts.length; i++) parents.add(parts.slice(0, i).join("/")); }
|
|
199
|
+
for (const d of dirs) if (!parents.has(d.path)) return `the directory ${d.path} was admitted by the verify but parents no ledger entry`;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The receipt form of the ledger: no absolute path, no bytes. */
|
|
204
|
+
export const receiptLedger = (entries) => entries.map((e) => ({ path: e.path, kind: e.kind, type: e.type, size: e.size, sha256: e.sha256, execBit: e.execBit, producer: e.producer }));
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
8
8
|
import { planSidecarStaging, resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
|
|
9
|
+
import { readSourceSplit } from "./source-split.mjs";
|
|
9
10
|
|
|
10
11
|
export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
11
12
|
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false };
|
|
@@ -50,6 +51,9 @@ async function main() {
|
|
|
50
51
|
const opts = parseStageSidecarArgs(process.argv.slice(2));
|
|
51
52
|
const configPath = path.resolve(opts.config);
|
|
52
53
|
const appRoot = path.dirname(configPath);
|
|
54
|
+
// Inside the door's source split the config and every declared source are the snapshot's; the caches and build trees right-release
|
|
55
|
+
// owns stay in the checkout's app directory (stage-sidecar.mjs `cacheRoot`).
|
|
56
|
+
const split = readSourceSplit({ cwd: appRoot });
|
|
53
57
|
const config = await importReleaseConfig(configPath, { lenient: true });
|
|
54
58
|
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
55
59
|
if (opts.check) {
|
|
@@ -61,7 +65,7 @@ async function main() {
|
|
|
61
65
|
}
|
|
62
66
|
const target = resolveTargetTriple({ target: opts.target, cwd: appRoot });
|
|
63
67
|
// Warnings (a never-run row, a catalog row that is not staged) go to stderr; stdout stays the staged-file list or the --json document.
|
|
64
|
-
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, deps: { warn: (line) => console.error(line) } });
|
|
68
|
+
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, ...(split ? { cacheRoot: split.checkoutAppRoot } : {}), deps: { warn: (line) => console.error(line) } });
|
|
65
69
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
66
70
|
else {
|
|
67
71
|
for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
|