@rightkit/release 0.6.8 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/local-publish.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// all-features check, packaging, gate, publish and registry readback, and writes its own evidence file. Crates
|
|
14
14
|
// of one dependency layer publish concurrently (bounded); layer N+1 starts only after every layer-N crate is
|
|
15
15
|
// visible in the registry index with the verified checksum.
|
|
16
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
16
17
|
import crypto from "node:crypto";
|
|
17
18
|
import fs from "node:fs";
|
|
18
19
|
import os from "node:os";
|
|
@@ -28,6 +29,7 @@ import { refuseHostUpload } from "./host-upload-refusal.mjs";
|
|
|
28
29
|
import { pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
|
|
29
30
|
import { recordPublished, stageSourceVersions, workspaceFeedOptions } from "./rightkit-sync.mjs";
|
|
30
31
|
import { prepareSwapHelper, SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
|
|
32
|
+
import { requireDoorRun, gateStateDir } from "./gate-stages.mjs";
|
|
31
33
|
|
|
32
34
|
export const LOCAL_OPERATOR = "agent (local-build grant)";
|
|
33
35
|
export const LOCAL_HOST_POLICY_REASON = "Adrian 2026-10-14 local-build grant (.rightkit-local-build.json); Windows covered by the next CI run";
|
|
@@ -46,6 +48,12 @@ export const DEFAULT_CONCURRENCY = 4;
|
|
|
46
48
|
const MAX_CONCURRENCY = 8;
|
|
47
49
|
const CRATE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
48
50
|
const JOBS_SUBCOMMANDS = new Set(["test", "clippy", "check", "package"]);
|
|
51
|
+
// The argv of the broker jobs a unit's compile and package stages launch. Built here, once, for the stages themselves and for
|
|
52
|
+
// stage-commands.mjs, which derives what a qualification's signed receipts must show (the hosted admission and the gate share it).
|
|
53
|
+
export const qualifyFmtArgs = (unit) => ["fmt", "-p", unit, "--check"];
|
|
54
|
+
export const npmPackArgs = (packDir, scratch) => ["--dir", packDir, "pack", "--pack-destination", scratch];
|
|
55
|
+
export const cargoPackageArgs = (unit, qualifyOnly) => ["package", "--locked", ...(qualifyOnly ? [] : ["--no-verify"]), "-p", unit];
|
|
56
|
+
export const CARGO_METADATA_ARGS = Object.freeze(["metadata", "--offline", "--format-version", "1", "--no-deps"]);
|
|
49
57
|
export function withCargoJobs(args, jobs) {
|
|
50
58
|
return jobs !== undefined && JOBS_SUBCOMMANDS.has(args[0])
|
|
51
59
|
? [args[0], "--jobs", String(jobs), ...args.slice(1)] : args;
|
|
@@ -121,8 +129,8 @@ export function parseLocalPublishArgs(kind, argv, cwd = process.cwd()) {
|
|
|
121
129
|
if (arg === "--local") local = true;
|
|
122
130
|
else if (arg === flag) unit = argv[++i];
|
|
123
131
|
else if (arg === "--workspace") workspace = argv[++i];
|
|
124
|
-
else if (
|
|
125
|
-
else if (
|
|
132
|
+
else if (arg === "--qualify-only") qualifyOnly = true;
|
|
133
|
+
else if (arg === "--bundle") bundle = argv[++i];
|
|
126
134
|
// Same meaning as publish.yml's allow_ignored: this unit may have ignored tests; the count is recorded.
|
|
127
135
|
else if (arg === "--allow-ignored") allowIgnored = true;
|
|
128
136
|
else if (kind === "cargo" && arg === "--crates") crates = crateList(argv[++i], "--crates");
|
|
@@ -365,31 +373,26 @@ export function localTestPlan({ kind, unit, dir, config, manifest, batch = false
|
|
|
365
373
|
|
|
366
374
|
/**
|
|
367
375
|
* The clippy invocations that replace each crate's own `cargo clippy -p <crate> --no-deps --all-targets
|
|
368
|
-
* <featureSets[0]> -- -D warnings`: same flags and lint level, so the same lints on the same code.
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* unify extra features into a selected crate and each is linted with exactly the features it was before.
|
|
373
|
-
* 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.
|
|
374
380
|
*/
|
|
375
381
|
export function batchClippyInvocations(layer, { featuresOf = () => [], featureSetsOf = (unit) => [featuresOf(unit)], assetGatedOf = () => false } = {}) {
|
|
376
|
-
const
|
|
382
|
+
const invocations = [];
|
|
377
383
|
for (const unit of layer) {
|
|
378
384
|
// Every tested feature set of a crate is linted, in order; identical sets of one crate collapse to one run.
|
|
379
385
|
const sets = featureSetsOf(unit) ?? [[]];
|
|
380
386
|
const unique = sets.filter((set, index) => sets.findIndex((other) => JSON.stringify(other ?? []) === JSON.stringify(set ?? [])) === index);
|
|
381
387
|
for (const set of unique) {
|
|
382
388
|
const features = set ?? [];
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
});
|
|
387
393
|
}
|
|
388
394
|
}
|
|
389
|
-
return
|
|
390
|
-
crates, assetGated,
|
|
391
|
-
args: ["clippy", ...crates.flatMap((unit) => ["-p", unit]), "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"],
|
|
392
|
-
}));
|
|
395
|
+
return invocations;
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
@@ -414,6 +417,19 @@ function assertLocalHost(deps, workspaceRoot, kind) {
|
|
|
414
417
|
|
|
415
418
|
// Host uploads are retired: only --qualify-only (no upload) runs. The upload engine below is kept for direct calls in
|
|
416
419
|
// tests; no CLI path reaches it (see host-upload-refusal.mjs).
|
|
420
|
+
/** One door: a publish engine entry (the CLI's `publish <kind> --local`, or a direct call of the engine) runs only inside a
|
|
421
|
+
* live rightkit-gate door run for the workspace's HEAD (RK_GATE_RUN; gate-stages requireDoorRun). The unit stages
|
|
422
|
+
* (compileLocalUnit and its successors) never pass through here: they are the gate's own path and carry their own door. */
|
|
423
|
+
export function requireEngineDoor(parsed, { env = process.env, git = defaultGit, command = "right-release publish --local" } = {}) {
|
|
424
|
+
const root = parsed.workspaceRoot;
|
|
425
|
+
const common = git(["rev-parse", "--git-common-dir"], root);
|
|
426
|
+
const commit = git(["rev-parse", "HEAD"], root);
|
|
427
|
+
if (!common || !/^[0-9a-f]{40}$/.test(commit ?? "")) refuse(`${command} runs only inside a rightkit-gate door run of a git checkout (no HEAD or common dir in ${root})`);
|
|
428
|
+
const denial = requireDoorRun({ env, commonDir: path.resolve(root, common), commit, command });
|
|
429
|
+
if (denial) refuse(denial);
|
|
430
|
+
return { commit, commonDir: path.resolve(root, common) };
|
|
431
|
+
}
|
|
432
|
+
|
|
417
433
|
export async function runLocalPublish(kind, argv, deps = {}) {
|
|
418
434
|
const parsed = parseLocalPublishArgs(kind, argv, deps.cwd);
|
|
419
435
|
if (!parsed.qualifyOnly) refuseHostUpload(`right-release publish ${kind} --local`);
|
|
@@ -422,10 +438,101 @@ export async function runLocalPublish(kind, argv, deps = {}) {
|
|
|
422
438
|
|
|
423
439
|
export async function runLocalPublishUploadEngine(kind, argv, deps = {}) {
|
|
424
440
|
const parsed = parseLocalPublishArgs(kind, argv, deps.cwd);
|
|
441
|
+
(deps.requireDoor ?? requireEngineDoor)(parsed, { env: deps.env ?? process.env, command: `right-release publish ${kind} --local` });
|
|
425
442
|
if (parsed.batch) return runLocalPublishBatch(parsed, deps);
|
|
426
443
|
return publishUnit(parsed, deps, null);
|
|
427
444
|
}
|
|
428
445
|
|
|
446
|
+
// Stage state never leaves this process: persisted receipts are reports, not authority to skip unit tests.
|
|
447
|
+
const unitStages = new WeakMap();
|
|
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);
|
|
483
|
+
if (!/^[0-9a-f]{40}$/.test(commit ?? "") || !/^[0-9a-f]{40}$/.test(tree ?? "") || !common) refuse("cannot bind unit stage to HEAD and tree");
|
|
484
|
+
if (identity && (identity.commit !== commit || identity.tree !== tree)) refuse("unit stage HEAD or tree changed");
|
|
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);
|
|
487
|
+
if (dirty === null || dirty !== "") refuse("unit stage requires a clean tracked checkout");
|
|
488
|
+
const commonDir = path.resolve(source ? source.checkout : root, common);
|
|
489
|
+
const denial = requireDoorRun({ env, commonDir, commit, command: `unit ${stage}` });
|
|
490
|
+
if (denial) refuse(denial);
|
|
491
|
+
let run;
|
|
492
|
+
try { run = JSON.parse(fs.readFileSync(path.join(gateStateDir(commonDir), "runs", `${env.RK_GATE_RUN}.json`), "utf8")); } catch { refuse("cannot read the unit door run"); }
|
|
493
|
+
if (run.stage !== stage || run.tree !== tree) refuse(`unit ${stage} needs its own live door run for this tree`);
|
|
494
|
+
return { commit, tree };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function compileLocalUnit(kind, argv, { env = process.env, source = null } = {}) {
|
|
498
|
+
const parsed = parseLocalPublishArgs(kind, argv);
|
|
499
|
+
if (!parsed.qualifyOnly || parsed.batch) refuse("unit stages require one --qualify-only unit and a new bundle");
|
|
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);
|
|
502
|
+
try {
|
|
503
|
+
const step = await runner.next();
|
|
504
|
+
if (step.done || step.value?.stage !== "compile") refuse("unit compile did not complete");
|
|
505
|
+
assertUnitDoor(parsed.workspaceRoot, "compile", env, identity, source);
|
|
506
|
+
const state = Object.freeze({});
|
|
507
|
+
unitStages.set(state, { runner, parsed, identity, source, stage: "compile", compileRequests: [...(step.value.brokerRequests ?? [])] });
|
|
508
|
+
return state;
|
|
509
|
+
} catch (error) { await runner.return(); throw error; }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function advanceLocalUnit(state, expected, stage, env) {
|
|
513
|
+
const entry = unitStages.get(state);
|
|
514
|
+
if (!entry || entry.stage !== expected) refuse(`unit ${stage} requires this process's completed ${expected} stage`);
|
|
515
|
+
entry.stage = "running";
|
|
516
|
+
try {
|
|
517
|
+
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity, entry.source);
|
|
518
|
+
const step = await entry.runner.next(env);
|
|
519
|
+
if (stage === "package" ? step.done || step.value?.stage !== "package" : !step.done || !step.value?.qualifiedOnly) refuse(`unit ${stage} did not complete`);
|
|
520
|
+
assertUnitDoor(entry.parsed.workspaceRoot, stage, env, entry.identity, entry.source);
|
|
521
|
+
entry.stage = stage;
|
|
522
|
+
if (stage === "qualify") unitStages.delete(state);
|
|
523
|
+
return step.value;
|
|
524
|
+
} catch (error) { unitStages.delete(state); await entry.runner.return(); throw error; }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export const packageLocalUnit = (state, { env = process.env } = {}) => advanceLocalUnit(state, "compile", "package", env);
|
|
528
|
+
export const qualifyLocalUnit = (state, { env = process.env } = {}) => advanceLocalUnit(state, "package", "qualify", env);
|
|
529
|
+
/** The broker request ids of this process's completed compile stage (the package stage's ride on its result). */
|
|
530
|
+
export const localUnitCompileRequests = (state) => [...(unitStages.get(state)?.compileRequests ?? [])];
|
|
531
|
+
export async function closeLocalUnit(state) {
|
|
532
|
+
const entry = unitStages.get(state);
|
|
533
|
+
if (entry) { unitStages.delete(state); await entry.runner.return(); }
|
|
534
|
+
}
|
|
535
|
+
|
|
429
536
|
/**
|
|
430
537
|
* Qualify and publish ONE unit. `batch` (cargo batch mode only) is `{ head, tree, receipt, commands }`: the
|
|
431
538
|
* batch-level fmt, secret scan and clippy already ran at `head`/`tree`, so this unit skips its own clippy and
|
|
@@ -433,11 +540,31 @@ export async function runLocalPublishUploadEngine(kind, argv, deps = {}) {
|
|
|
433
540
|
* publish and the registry readback) runs for this unit exactly as in single mode.
|
|
434
541
|
*/
|
|
435
542
|
async function publishUnit(parsed, deps, batch) {
|
|
543
|
+
const runner = publishUnitStages(parsed, deps, batch);
|
|
544
|
+
let result;
|
|
545
|
+
do { result = await runner.next(); } while (!result.done);
|
|
546
|
+
return result.value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function* publishUnitStages(parsed, deps, batch) {
|
|
436
550
|
const log = deps.log ?? ((line) => process.stdout.write(`${line}\n`));
|
|
437
551
|
const { kind, unit, allowIgnored, workspaceRoot } = parsed;
|
|
438
552
|
const git = deps.git ?? defaultGit;
|
|
439
553
|
const readJson = deps.readJson ?? defaultReadJson;
|
|
440
|
-
|
|
554
|
+
// Every broker job a stage launches prints `rustright: request <id>`; the unit gate fetches those jobs' signed receipts, so the ids
|
|
555
|
+
// are collected here per stage and handed over at each stage yield (compile, then package). Nothing about what runs changes.
|
|
556
|
+
const brokerRequests = [];
|
|
557
|
+
const noteBrokerRequests = (result) => { for (const [, id] of `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`.matchAll(/^rustright: request ([0-9a-f-]{36})/gm)) if (!brokerRequests.includes(id)) brokerRequests.push(id); return result; };
|
|
558
|
+
const launch = deps.run ?? (batch ? defaultRunAsync : defaultRun);
|
|
559
|
+
// Every pnpm step goes through the one door for build and test launches (build-launch.mjs): a rightkit-pnpm broker job on a
|
|
560
|
+
// managed host (refused, never run directly, when the broker is unreachable), direct on a host without one. A run headed to release
|
|
561
|
+
// then also has the signed receipt each stage owes (stage-commands.mjs). Other commands (cargo is already a broker job) are unchanged.
|
|
562
|
+
const run = (command, args, options) => {
|
|
563
|
+
const target = command === "pnpm" ? buildLaunch(command, args, { env: options?.env ?? process.env, platform: deps.platform ?? process.platform, exists: fs.existsSync }) : { command, args };
|
|
564
|
+
const result = launch(target.command, target.args, options);
|
|
565
|
+
return typeof result?.then === "function" ? result.then(noteBrokerRequests) : noteBrokerRequests(result);
|
|
566
|
+
};
|
|
567
|
+
|
|
441
568
|
|
|
442
569
|
// 1. Preconditions, fail closed.
|
|
443
570
|
const platform = assertLocalHost(deps, workspaceRoot, kind);
|
|
@@ -465,9 +592,24 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
465
592
|
const tree = git(["rev-parse", "HEAD^{tree}"], workspaceRoot);
|
|
466
593
|
if (tree !== batch.tree) refuse(`${unit}: HEAD tree ${tree} differs from the batch tree ${batch.tree}`);
|
|
467
594
|
}
|
|
468
|
-
const
|
|
595
|
+
const feedPath = "packages/release/rightkit-versions.json";
|
|
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] : [])];
|
|
600
|
+
const dirty = git(["status", "--porcelain", "--", ...guardedPaths], workspaceRoot);
|
|
469
601
|
if (dirty === null) refuse(`cannot read git status for ${dir}`);
|
|
470
602
|
if (dirty !== "") refuse(`tree is not clean for ${dir} or the gate config (${GATE_CONFIG_FILES.join(", ")}); commit first:\n${dirty}`);
|
|
603
|
+
if (parsed.qualifyOnly && kind === "npm") {
|
|
604
|
+
const feedFile = path.join(workspaceRoot, feedPath);
|
|
605
|
+
if (unit === "@rightkit/release" && !fs.existsSync(feedFile)) refuse(`${unit}: committed ${feedPath} is required before packing`);
|
|
606
|
+
if (fs.existsSync(feedFile)) {
|
|
607
|
+
const committedFeed = git(["show", `HEAD:${feedPath}`], workspaceRoot);
|
|
608
|
+
if (committedFeed === null || committedFeed !== fs.readFileSync(feedFile, "utf8").trim()) refuse(`${feedPath} must match the committed HEAD before qualification`);
|
|
609
|
+
const feed = readJson(feedFile) ?? refuse(`cannot read ${feedPath}`);
|
|
610
|
+
if (feed.stagedNpm?.[unit] !== version && feed.registry?.npm?.[unit] !== version) refuse(`${unit}@${version}: stage the version in ${feedPath} and commit it before qualification`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
471
613
|
if (await (deps.registryHas ?? defaultRegistryHas)({ kind, name: unit, version })) refuse(`${unit}@${version} is already on the ${kind === "npm" ? "npm" : "crates.io"} registry; bump the version`);
|
|
472
614
|
log(`[right-release] local publish ${kind}:${unit}@${version} at ${head.slice(0, 12)} (local-build grant, ${LOCAL_HOST_POLICIES[platform]} policy)`);
|
|
473
615
|
|
|
@@ -484,10 +626,10 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
484
626
|
if (consumerDirty === null) refuse(`cannot read git status for the consumer rows of ${unit}`);
|
|
485
627
|
if (consumerDirty !== "") refuse(`tree is not clean for the consumer rows of ${unit}; commit first:\n${consumerDirty}`);
|
|
486
628
|
}
|
|
487
|
-
if (parsed.qualifyOnly) {
|
|
629
|
+
if (parsed.qualifyOnly && kind === "cargo") {
|
|
488
630
|
const violations = (deps.scanSecrets ?? scanCrateForSecrets)(unitPath);
|
|
489
631
|
if (violations.length) refuse(`qualification secret scan rejected: ${violations.join(", ")}`);
|
|
490
|
-
plan.steps.unshift({ command: "cargo", args:
|
|
632
|
+
plan.steps.unshift({ command: "cargo", args: qualifyFmtArgs(unit), test: false });
|
|
491
633
|
}
|
|
492
634
|
const modules = await (deps.loadWorkspaceModules ?? defaultLoadWorkspaceModules)(workspaceRoot);
|
|
493
635
|
const { ResultScanner, TEST_COMMAND, judgeUnit } = modules.results;
|
|
@@ -496,11 +638,14 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
496
638
|
// written reason in ci/gate.config.json (rust.<crate>.allowIgnored). The count stays in the evidence.
|
|
497
639
|
const configReason = kind === "cargo" && typeof gateConfig?.rust?.[unit]?.allowIgnored === "string" ? gateConfig.rust[unit].allowIgnored.trim() : "";
|
|
498
640
|
const ignoredAllowed = Boolean(allowIgnored || parsed.allowIgnoredUnits?.includes(unit) || configReason);
|
|
499
|
-
const steps = parsed.qualifyOnly ? [{ command: `scanCrateForSecrets ${dir}`, ok: true }] : [];
|
|
641
|
+
const steps = parsed.qualifyOnly && kind === "cargo" ? [{ command: `scanCrateForSecrets ${dir}`, ok: true }] : [];
|
|
500
642
|
for (const step of plan.steps) {
|
|
501
643
|
const commandText = `${step.command} ${step.args.join(" ")}`;
|
|
502
644
|
log(`[right-release] ${plan.id}: ${commandText}`);
|
|
503
|
-
|
|
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 });
|
|
504
649
|
const row = { command: commandText, ok: false, isTest: step.test && TEST_COMMAND.test(commandText) };
|
|
505
650
|
if (result.error || result.status !== 0) {
|
|
506
651
|
steps.push(row);
|
|
@@ -519,40 +664,55 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
519
664
|
if (verdict.status === "fail" || steps.some((s) => s.failed > 0)) refuse(`${plan.id} not qualified: ${verdict.problems.join("; ") || "failed tests reported"}`);
|
|
520
665
|
if (!(verdict.executed > 0)) refuse(`${plan.id} executed no test`);
|
|
521
666
|
log(`[right-release] ${plan.id}: ${verdict.executed} passed, 0 failed, ${verdict.skipped} skipped, ${verdict.ignored} ignored${isAssetGated ? " (asset-gated: RIGHTKIT_REQUIRE_ASSETS=0, skips recorded)" : ""}`);
|
|
667
|
+
const packageEnv = yield { stage: "compile", kind, name: unit, version, commit: head, brokerRequests: brokerRequests.splice(0) };
|
|
668
|
+
if (packageEnv) Object.assign(env, testEnv({ env: packageEnv }, isAssetGated));
|
|
522
669
|
|
|
523
670
|
// 3. Pack the artifact and hash it.
|
|
524
|
-
|
|
671
|
+
// An npm pack is a broker job whose --dir and --pack-destination the adapter accepts only under the host's scratch root, so it
|
|
672
|
+
// packs there (the workspace rule for run output) rather than in the OS temp directory.
|
|
673
|
+
const scratchRoot = deps.tmpdir ?? (kind === "npm" ? (platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch") : os.tmpdir());
|
|
674
|
+
const scratch = fs.mkdtempSync(path.join(scratchRoot, "rk-local-publish-"));
|
|
525
675
|
const cleanup = () => fs.rmSync(scratch, { recursive: true, force: true });
|
|
526
676
|
try {
|
|
527
677
|
let artifactFile; const packCommands = [];
|
|
678
|
+
let packDir = dir;
|
|
528
679
|
if (kind === "npm" && unit === SWAP_HELPER_PACKAGE) {
|
|
680
|
+
if (parsed.qualifyOnly) {
|
|
681
|
+
// Signing writes its binary and receipt. Keep both in the pack copy, never the source checkout.
|
|
682
|
+
packDir = path.join(scratch, "package-source");
|
|
683
|
+
fs.cpSync(unitPath, packDir, { recursive: true, filter: (source) => path.basename(source) !== "node_modules" });
|
|
684
|
+
}
|
|
529
685
|
// #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed).
|
|
530
|
-
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: unitPath, platform, run, env, log }); }
|
|
686
|
+
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: parsed.qualifyOnly ? packDir : unitPath, platform, run, env, log }); }
|
|
531
687
|
catch (error) { refuse(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
532
688
|
packCommands.push(`rustright cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
|
|
533
689
|
}
|
|
534
690
|
if (kind === "npm") {
|
|
535
691
|
// The feed a tarball ships must list the version it ships with (#319): stage it before packing.
|
|
536
692
|
const feedFile = path.join(workspaceRoot, "packages", "release", "rightkit-versions.json");
|
|
537
|
-
if (fs.existsSync(feedFile)) await withVersionsLock(() => (deps.stageSourceVersions ?? stageSourceVersions)([{ registry: "npm", name: unit, version }], { file: feedFile, root: workspaceRoot, log }));
|
|
538
|
-
|
|
693
|
+
if (!parsed.qualifyOnly && fs.existsSync(feedFile)) await withVersionsLock(() => (deps.stageSourceVersions ?? stageSourceVersions)([{ registry: "npm", name: unit, version }], { file: feedFile, root: workspaceRoot, log }));
|
|
694
|
+
// The tarball goes to an empty `pack` directory inside the scratch directory: the adapter takes --pack-destination only as a
|
|
695
|
+
// child of a scratch directory (never the scratch directory itself) and refuses one that already holds a tarball.
|
|
696
|
+
const packOut = path.join(scratch, "pack");
|
|
697
|
+
fs.mkdirSync(packOut);
|
|
698
|
+
const args = npmPackArgs(packDir, packOut);
|
|
539
699
|
packCommands.push(`pnpm ${args.join(" ")}`);
|
|
540
700
|
const packed = await run("pnpm", args, { cwd: workspaceRoot, env });
|
|
541
701
|
if (packed.error || packed.status !== 0) refuse(`pnpm pack failed for ${unit}\n${tail(`${packed.stdout ?? ""}\n${packed.stderr ?? ""}`)}`);
|
|
542
|
-
artifactFile = path.join(
|
|
702
|
+
artifactFile = path.join(packOut, tarballName(unit, version));
|
|
543
703
|
if (!fs.existsSync(artifactFile)) refuse(`pnpm pack did not produce ${path.basename(artifactFile)}`);
|
|
544
704
|
const checked = modules.checkNpmTarball({ file: artifactFile, name: unit, version, dir, spawn: deps.spawnSync ?? spawnSync });
|
|
545
705
|
if (checked.problems.length) refuse(`${unit}: packed tarball refused:\n - ${checked.problems.join("\n - ")}`);
|
|
546
706
|
} else {
|
|
547
707
|
// A combined publish verifies once in `cargo publish`; detached qualification verifies here before
|
|
548
708
|
// its evidence leaves this host, so the publishing host can reuse that exact archive's proof.
|
|
549
|
-
const packageArgs =
|
|
709
|
+
const packageArgs = cargoPackageArgs(unit, parsed.qualifyOnly);
|
|
550
710
|
packCommands.push(`cargo ${packageArgs.join(" ")}`);
|
|
551
|
-
const packed = await run("cargo", withCargoJobs(packageArgs, parsed.jobs), { cwd:
|
|
711
|
+
const packed = await run("cargo", withCargoJobs(packageArgs, parsed.jobs), { cwd: unitPath, env });
|
|
552
712
|
if (packed.error || packed.status !== 0) refuse(`cargo package failed for ${unit}\n${tail(`${packed.stdout ?? ""}\n${packed.stderr ?? ""}`)}`);
|
|
553
713
|
if (parsed.qualifyOnly) steps.push({ command: `cargo ${packageArgs.join(" ")}`, ok: true });
|
|
554
|
-
const metadataArgs = [
|
|
555
|
-
const meta = await runCargoReadOnly(metadataArgs, { cwd:
|
|
714
|
+
const metadataArgs = [...CARGO_METADATA_ARGS];
|
|
715
|
+
const meta = await runCargoReadOnly(metadataArgs, { cwd: unitPath, env }, (_command, args, settings) => run("cargo", args, settings),
|
|
556
716
|
(failed) => log(`[right-release] offline Cargo metadata missed cache; retrying online once\n${failed.stdout ?? ""}${failed.stderr ? `\n${failed.stderr}` : ""}`));
|
|
557
717
|
const actualMetadataArgs = meta.usedOfflineFallback ? metadataArgs.filter((arg) => arg !== "--offline") : metadataArgs;
|
|
558
718
|
packCommands.push(`cargo ${actualMetadataArgs.join(" ")}`);
|
|
@@ -565,6 +725,16 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
565
725
|
if (!fs.existsSync(artifactFile)) refuse(`cargo package did not produce ${artifactFile}`);
|
|
566
726
|
}
|
|
567
727
|
const artifactSha256 = sha256(fs.readFileSync(artifactFile));
|
|
728
|
+
if (parsed.qualifyOnly && (git(["rev-parse", "HEAD"], workspaceRoot) !== head || git(["status", "--porcelain", "--", ...guardedPaths], workspaceRoot) !== "")) refuse("qualification inputs changed while preparing the artifact; no bundle was created");
|
|
729
|
+
if (parsed.qualifyOnly) {
|
|
730
|
+
fs.mkdirSync(parsed.bundleDir, { recursive: false });
|
|
731
|
+
const bundledArtifact = path.join(parsed.bundleDir, path.basename(artifactFile));
|
|
732
|
+
fs.copyFileSync(artifactFile, bundledArtifact, fs.constants.COPYFILE_EXCL);
|
|
733
|
+
if (sha256(fs.readFileSync(bundledArtifact)) !== artifactSha256) refuse("qualification archive changed while copying");
|
|
734
|
+
artifactFile = bundledArtifact;
|
|
735
|
+
yield { stage: "package", kind, name: unit, version, commit: head, artifactFile, artifactSha256, brokerRequests: brokerRequests.splice(0) };
|
|
736
|
+
if (sha256(fs.readFileSync(artifactFile)) !== artifactSha256) refuse("qualification archive changed after packaging");
|
|
737
|
+
}
|
|
568
738
|
|
|
569
739
|
// 4. Evidence in the normal schema, then the EXISTING gate.
|
|
570
740
|
const commands = [...plan.steps.map((s) => `${s.command} ${s.args.join(" ")}`), ...packCommands, ...(batch ? batch.commands : [])];
|
|
@@ -583,23 +753,21 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
583
753
|
steps: steps.map(({ command, ok, executed, failed, ignored, skipped, binaries }) => ({ command, ok, ...(executed === undefined ? {} : { executed, failed, ignored, skipped, binaries }) })),
|
|
584
754
|
}],
|
|
585
755
|
};
|
|
586
|
-
|
|
756
|
+
// A qualification writes its evidence beside the bytes, in the bundle: the tracked release-evidence/gate file is the record of a
|
|
757
|
+
// host publish, and rewriting it would dirty the checkout the unit door guards (the first `--to release --unit` run stopped
|
|
758
|
+
// there, 2026-10-27). Never overwrite a bundle's evidence.
|
|
759
|
+
const evidencePath = parsed.qualifyOnly ? path.join(parsed.bundleDir, "gate.json") : path.resolve(defaultEvidencePath(workspaceRoot, kind, unit));
|
|
587
760
|
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
|
|
588
|
-
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n
|
|
761
|
+
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`, { flag: parsed.qualifyOnly ? "wx" : "w" });
|
|
589
762
|
log(`[right-release] wrote ${evidencePath} (artifact sha256 ${artifactSha256})`);
|
|
590
|
-
const gateOptions = { kind, name: unit, version, packagePath: unitPath, evidencePath };
|
|
763
|
+
const gateOptions = { kind, name: unit, version, packagePath: unitPath, evidencePath, artifactBytes: fs.readFileSync(artifactFile) };
|
|
591
764
|
(deps.enforceGate ?? enforcePublicationGate)(gateOptions, { write: (line) => log(line.replace(/\n$/, "")) });
|
|
592
765
|
|
|
593
766
|
if (parsed.qualifyOnly) {
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
fs.copyFileSync(artifactFile, bundledArtifact, fs.constants.COPYFILE_EXCL);
|
|
599
|
-
if (sha256(fs.readFileSync(bundledArtifact)) !== artifactSha256) refuse("qualification archive changed while copying");
|
|
600
|
-
fs.copyFileSync(evidencePath, bundledEvidence, fs.constants.COPYFILE_EXCL);
|
|
601
|
-
log(`[right-release] qualified ${unit}@${version}; bundle ${parsed.bundleDir}; publish cargo --crate ${unit} --from-evidence ${bundledEvidence}`);
|
|
602
|
-
return { kind, name: unit, version, commit: head, artifactSha256, evidencePath: bundledEvidence, artifactFile: bundledArtifact, qualifiedOnly: true };
|
|
767
|
+
// Bytes and evidence stay together outside the checkout.
|
|
768
|
+
const bundledEvidence = evidencePath;
|
|
769
|
+
log(`[right-release] qualified ${unit}@${version}; bundle ${parsed.bundleDir}${kind === "cargo" ? `; publish cargo --crate ${unit} --from-evidence ${bundledEvidence}` : ""}`);
|
|
770
|
+
return { kind, name: unit, version, commit: head, artifactSha256, evidencePath: bundledEvidence, artifactFile, qualifiedOnly: true };
|
|
603
771
|
}
|
|
604
772
|
|
|
605
773
|
// 5. The EXISTING publish path.
|
|
@@ -666,11 +834,11 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
666
834
|
if (!head || !/^[0-9a-f]{40}$/.test(head)) refuse("cannot resolve HEAD");
|
|
667
835
|
const tree = git(["rev-parse", "HEAD^{tree}"], workspaceRoot);
|
|
668
836
|
if (!tree || !/^[0-9a-f]{40}$/.test(tree)) refuse("cannot resolve the HEAD tree");
|
|
669
|
-
// Every batch crate
|
|
670
|
-
// match the commit the batch checks are bound to, before the first layer publishes (0.4.1 review A3, A4).
|
|
671
|
-
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);
|
|
672
840
|
if (dirty === null) refuse("cannot read git status for the batch");
|
|
673
|
-
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}`);
|
|
674
842
|
const registryHas = deps.registryHas ?? defaultRegistryHas;
|
|
675
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);
|
|
676
844
|
if (published.length) refuse(`already on the crates.io registry: ${published.join(", ")}; bump the version or remove them from the batch`);
|
|
@@ -680,15 +848,18 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
680
848
|
const env = testEnv(deps, false);
|
|
681
849
|
const stamp = { commit: head, tree };
|
|
682
850
|
const checks = []; const commands = [];
|
|
683
|
-
|
|
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]) => {
|
|
684
853
|
const text = `${command} ${args.join(" ")}`;
|
|
685
854
|
log(`[right-release] batch ${name}: ${text}`);
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
}
|
|
688
859
|
checks.push({ name, command: text, ok: true, ...stamp, ...(crateNames ? { crates: crateNames } : {}) });
|
|
689
860
|
commands.push(text);
|
|
690
861
|
};
|
|
691
|
-
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)));
|
|
692
863
|
log("[right-release] batch secret-scan: crate directories of the batch");
|
|
693
864
|
const scan = deps.scanSecrets ?? scanCrateForSecrets;
|
|
694
865
|
const violations = crates.flatMap((unit) => scan(path.join(workspaceRoot, "crates", unit)).map((file) => `${unit}: ${file}`));
|
|
@@ -699,7 +870,7 @@ async function runLocalPublishBatch(parsed, deps) {
|
|
|
699
870
|
const featureSetsOf = (unit) => gateConfig?.rust?.[unit]?.featureSets ?? [[]];
|
|
700
871
|
for (const layer of layers) {
|
|
701
872
|
for (const invocation of batchClippyInvocations(layer, { featureSetsOf, assetGatedOf: isAssetGated })) {
|
|
702
|
-
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)));
|
|
703
874
|
}
|
|
704
875
|
}
|
|
705
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,
|