@rightkit/release 0.6.4 → 0.6.5
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 -0
- package/admission-files.mjs +279 -0
- package/admission-host.mjs +403 -0
- package/admission.mjs +146 -0
- package/app-check.mjs +869 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +64 -25
- package/build-release.mjs +81 -21
- package/cli/right-release.mjs +37 -4
- package/deterministic-scans.mjs +21 -5
- package/inventory-check.mjs +12 -1
- package/ownership-dependency-allowlist.json +2 -0
- package/package.json +5 -3
- package/prebuild-gates.mjs +7 -3
- package/release.mjs +113 -58
- package/rightkit-versions.json +10 -8
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
// rightkit-gate [--base <ref>] [--plan] [--no-cache]
|
|
25
25
|
// run the gate (default base origin/main); --plan prints the scans and runs none;
|
|
26
26
|
// --no-cache (or RK_GATE_CACHE=0) reruns every scan
|
|
27
|
+
// rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
|
|
28
|
+
// the offline app check (../app-check.mjs): committed files only, no network, compile or
|
|
29
|
+
// broker, every finding in one run. Exit 0 pass, 1 findings, 2 could not run
|
|
27
30
|
// rightkit-gate install-hook [--force] write .githooks/pre-push and set core.hooksPath. core.hooksPath hides .git/hooks, so the
|
|
28
|
-
// pre-push hook runs the repository's own .git/hooks/pre-push first
|
|
29
|
-
// present in .git/hooks gets a
|
|
30
|
-
//
|
|
31
|
+
// pre-push hook runs the repository's own .git/hooks/pre-push first, then `rightkit-gate
|
|
32
|
+
// check --commit HEAD`, then the gate; every other hook present in .git/hooks gets a
|
|
33
|
+
// .githooks/<name> trampoline that runs it; the hook finds the gate binary in the root
|
|
34
|
+
// node_modules/.bin, then any workspace package's, then pnpm exec. Where @rightkit/release is
|
|
35
|
+
// installed it also writes the advisory ownership pre-commit step (`right-release hooks install`
|
|
36
|
+
// is an alias of this command)
|
|
31
37
|
//
|
|
32
38
|
// From the pre-push hook, git's ref lines arrive on stdin: the commit being pushed must be the checked-out HEAD (broker scans
|
|
33
39
|
// read the live checkout), and the remote's current commit is the diff base. To push an earlier commit of the local stack,
|
|
@@ -58,7 +64,7 @@ const text = (r) => String(r?.stdout ?? "");
|
|
|
58
64
|
const failed = (r) => !r || Boolean(r.error) || r.status !== 0;
|
|
59
65
|
const why = (r) => String(r?.error?.message ?? r?.stderr ?? "").trim().split(/\r?\n/).slice(-3).join(" | ");
|
|
60
66
|
|
|
61
|
-
const USAGE = `usage: rightkit-gate [--base <ref>] [--plan] [--no-cache] [--pre-push] | rightkit-gate install-hook [--force] | --version`;
|
|
67
|
+
const USAGE = `usage: rightkit-gate [--base <ref>] [--plan] [--no-cache] [--pre-push] | rightkit-gate install-hook [--force] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | --version`;
|
|
62
68
|
|
|
63
69
|
// ---- pure helpers (exported for tests) ----------------------------------------------------------------------
|
|
64
70
|
|
|
@@ -340,13 +346,15 @@ export function lockScope({ changed, cargoDir = ".", unitNames, showBase, readLo
|
|
|
340
346
|
/**
|
|
341
347
|
* The pre-push hook. git's ref lines arrive on stdin and the gate consumes them, so stdin is buffered once and fed to both the
|
|
342
348
|
* repository's own .git/hooks/pre-push (which core.hooksPath=.githooks would otherwise hide; it runs first and a failure stops the
|
|
343
|
-
* push) and the gate.
|
|
344
|
-
*
|
|
345
|
-
*
|
|
349
|
+
* push) and the gate. Between them runs the app check, `rightkit-gate check --commit HEAD`: seconds, committed files only, so a
|
|
350
|
+
* red tree stops the push before the gate's scans start; a push that only deletes refs has no content to check and skips it. The
|
|
351
|
+
* gate binary is the root node_modules/.bin/rightkit-gate, else any workspace package's (up to three levels down: tauri-app-next,
|
|
352
|
+
* apps/*, packages/*), else `pnpm exec rightkit-gate`, else node with a package-relative path when pnpm is absent; the line it
|
|
353
|
+
* chose is printed once. No absolute path is written: everything is resolved from git at run time.
|
|
346
354
|
*/
|
|
347
355
|
export function hookText(nodePath) {
|
|
348
356
|
return `#!/bin/sh
|
|
349
|
-
# Mandatory local gate before every push (rightkit-gate install-hook). Never bypass with --no-verify.
|
|
357
|
+
# Mandatory local check and gate before every push (rightkit-gate install-hook). Never bypass with --no-verify.
|
|
350
358
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
351
359
|
stdin=$(mktemp "\${TMPDIR:-/tmp}/rightkit-gate.XXXXXX") || exit 1
|
|
352
360
|
trap 'rm -f "$stdin"' EXIT
|
|
@@ -358,18 +366,27 @@ if [ -x "$own" ]; then
|
|
|
358
366
|
"$own" "$@" < "$stdin" || exit $?
|
|
359
367
|
fi
|
|
360
368
|
# The gate binary: the root install, any workspace package's install, then pnpm exec.
|
|
369
|
+
found=""
|
|
361
370
|
for bin in node_modules/.bin/rightkit-gate */node_modules/.bin/rightkit-gate */*/node_modules/.bin/rightkit-gate */*/*/node_modules/.bin/rightkit-gate; do
|
|
362
|
-
if [ -x "$bin" ]; then
|
|
363
|
-
echo "rightkit-gate: using $bin" >&2
|
|
364
|
-
("$bin" --base origin/main) < "$stdin"; exit $?
|
|
365
|
-
fi
|
|
371
|
+
if [ -x "$bin" ]; then found="$bin"; break; fi
|
|
366
372
|
done
|
|
367
|
-
if
|
|
373
|
+
if [ -n "$found" ]; then
|
|
374
|
+
echo "rightkit-gate: using $found" >&2
|
|
375
|
+
rightkit_gate() { "$found" "$@"; }
|
|
376
|
+
elif command -v pnpm >/dev/null 2>&1; then
|
|
368
377
|
echo "rightkit-gate: using pnpm exec rightkit-gate" >&2
|
|
369
|
-
(
|
|
378
|
+
rightkit_gate() { pnpm exec rightkit-gate "$@"; }
|
|
379
|
+
else
|
|
380
|
+
echo "rightkit-gate: using node ${nodePath}" >&2
|
|
381
|
+
rightkit_gate() { node "${nodePath}" "$@"; }
|
|
370
382
|
fi
|
|
371
|
-
|
|
372
|
-
|
|
383
|
+
# The check first, over the committed files of HEAD; a push that only deletes refs has nothing to check.
|
|
384
|
+
if [ -s "$stdin" ] && ! awk '$2 !~ /^0+$/ { found = 1 } END { exit !found }' "$stdin"; then
|
|
385
|
+
echo "rightkit-gate: only deletions are pushed; nothing to check" >&2
|
|
386
|
+
else
|
|
387
|
+
rightkit_gate check --commit HEAD < /dev/null >&2 || exit $?
|
|
388
|
+
fi
|
|
389
|
+
rightkit_gate --base origin/main < "$stdin"; exit $?
|
|
373
390
|
`;
|
|
374
391
|
}
|
|
375
392
|
|
|
@@ -780,7 +797,12 @@ function releaseProvider(o, root, git) {
|
|
|
780
797
|
return null;
|
|
781
798
|
}
|
|
782
799
|
|
|
783
|
-
|
|
800
|
+
// The advisory ownership pre-commit step (`right-release hooks install`, ownership-hooks.mjs) runs the pinned @rightkit/release, so
|
|
801
|
+
// it is written only where that is installed. Its own hook and the repository's .git/hooks/pre-commit both keep running.
|
|
802
|
+
const PINNED_CLI = "node_modules/@rightkit/release/cli/right-release.mjs";
|
|
803
|
+
const FOREIGN_PRE_COMMIT = "pre-commit.rightkit-foreign";
|
|
804
|
+
|
|
805
|
+
async function installHook(o, root, force) {
|
|
784
806
|
const git = (...a) => o.spawn("git", a, { cwd: root });
|
|
785
807
|
const hooksDir = path.join(root, ".githooks");
|
|
786
808
|
const hookPath = path.join(hooksDir, "pre-push");
|
|
@@ -797,15 +819,29 @@ function installHook(o, root, force) {
|
|
|
797
819
|
const gitDirResult = git("rev-parse", "--git-dir");
|
|
798
820
|
let present = [];
|
|
799
821
|
if (!failed(gitDirResult) && text(gitDirResult).trim()) { try { present = o.readdir(path.join(path.resolve(root, text(gitDirResult).trim()), "hooks")); } catch { present = []; } }
|
|
822
|
+
const gitHooksDir = !failed(gitDirResult) && text(gitDirResult).trim() ? path.join(path.resolve(root, text(gitDirResult).trim()), "hooks") : null;
|
|
800
823
|
const own = present.filter(chainableHook).sort();
|
|
801
|
-
|
|
824
|
+
// The ownership step takes .githooks/pre-commit. The repository's own pre-commit hook keeps running first, through a trampoline
|
|
825
|
+
// beside it, unless it is the ownership hook a plain `right-release hooks install` wrote (this one replaces that), whose own
|
|
826
|
+
// chained hook, pre-commit.rightkit-foreign, then runs instead. A name that is no plain hook name is reported, never chained.
|
|
827
|
+
const ownership = o.exists(path.join(root, PINNED_CLI)) ? await import("../ownership-hooks.mjs") : null;
|
|
828
|
+
const readHook = (name) => { try { return o.readFile(path.join(gitHooksDir, name)); } catch { return ""; } };
|
|
829
|
+
const chainName = !ownership ? null : own.includes("pre-commit") && !readHook("pre-commit").includes(ownership.HOOK_MARKER) ? "pre-commit" : present.includes(FOREIGN_PRE_COMMIT) ? FOREIGN_PRE_COMMIT : null;
|
|
830
|
+
const skipped = present.filter((name) => !name.endsWith(".sample") && !chainableHook(name) && !(ownership && name === FOREIGN_PRE_COMMIT)).sort();
|
|
802
831
|
if (skipped.length) o.err(`rightkit-gate: WARNING: not chaining .git/hooks/${skipped.join(", .git/hooks/")}: not a plain hook file name (letters, digits, - and _ only); run it from a hook of its own.`);
|
|
803
|
-
const trampolines = own.filter((name) => name !== "pre-push").map((name) => ({ name, file: path.join(hooksDir, name), wanted: trampolineText(name) }));
|
|
832
|
+
const trampolines = own.filter((name) => name !== "pre-push" && !(ownership && name === "pre-commit")).map((name) => ({ name, file: path.join(hooksDir, name), wanted: trampolineText(name), note: `runs .git/hooks/${name}` }));
|
|
833
|
+
if (ownership) {
|
|
834
|
+
trampolines.push({ name: "pre-commit", file: path.join(hooksDir, "pre-commit"), wanted: ownership.renderPreCommitHook(), ownership: true, replaces: [trampolineText("pre-commit")], marker: ownership.HOOK_MARKER, note: "the advisory ownership gate: right-release ownership --staged" });
|
|
835
|
+
if (chainName) trampolines.push({ name: FOREIGN_PRE_COMMIT, file: path.join(hooksDir, FOREIGN_PRE_COMMIT), wanted: trampolineText(chainName), note: `runs .git/hooks/${chainName}, before the ownership gate` });
|
|
836
|
+
}
|
|
804
837
|
for (const t of trampolines) {
|
|
805
838
|
t.exists = o.exists(t.file);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
839
|
+
const found = t.exists ? o.readFile(t.file).replace(/\r\n/g, "\n") : null;
|
|
840
|
+
t.current = found === t.wanted;
|
|
841
|
+
// Our own earlier artifacts are updated in place: a plain trampoline where the ownership hook now goes, and an older ownership hook.
|
|
842
|
+
const ours = found !== null && (t.current || t.replaces?.includes(found) || (t.marker && found.includes(t.marker)));
|
|
843
|
+
if (t.exists && !ours && !force) {
|
|
844
|
+
o.err(`rightkit-gate: .githooks/${t.name} already exists and differs; nothing changed. Review it, or run \`rightkit-gate install-hook --force\` to replace it with ${t.ownership ? "the ownership pre-commit hook" : `a trampoline to .git/hooks/${chainName ?? t.name}`}.`);
|
|
809
845
|
return 1;
|
|
810
846
|
}
|
|
811
847
|
}
|
|
@@ -825,7 +861,7 @@ function installHook(o, root, force) {
|
|
|
825
861
|
else put(hookPath, wanted, `${hasHook ? "replaced" : "wrote"} .githooks/pre-push`, ` (${nodePath} without pnpm)`);
|
|
826
862
|
for (const t of trampolines) {
|
|
827
863
|
if (t.current) done.push(`.githooks/${t.name} already current`);
|
|
828
|
-
else put(t.file, t.wanted, `${t.exists ? "replaced" : "wrote"} .githooks/${t.name}`, ` (
|
|
864
|
+
else put(t.file, t.wanted, `${t.exists ? "replaced" : "wrote"} .githooks/${t.name}`, ` (${t.note})`);
|
|
829
865
|
}
|
|
830
866
|
if (existingPath === ".githooks") done.push("core.hooksPath already .githooks");
|
|
831
867
|
else {
|
|
@@ -836,6 +872,7 @@ function installHook(o, root, force) {
|
|
|
836
872
|
for (const line of done) o.out(`rightkit-gate: ${line}`);
|
|
837
873
|
const chained = own.length ? own.map((name) => (name === "pre-push" ? "pre-push (run first, then the gate)" : name)) : [];
|
|
838
874
|
o.out(`rightkit-gate: chained hooks from .git/hooks: ${chained.length ? chained.join(", ") : "none"}${own.includes("pre-push") ? "" : "; the pre-push hook still runs .git/hooks/pre-push first if one appears later"}`);
|
|
875
|
+
if (!ownership) o.out(`rightkit-gate: no ownership pre-commit step: ${PINNED_CLI} is not installed here; install @rightkit/release, then run install-hook again`);
|
|
839
876
|
const files = [".githooks/pre-push", ...trampolines.map((t) => `.githooks/${t.name}`)];
|
|
840
877
|
o.out(`rightkit-gate: commit the hooks with executable mode (git add --chmod=+x ${files.join(" ")}); the pre-push hook finds rightkit-gate in node_modules/.bin of the repository root, else of any workspace package, else through \`pnpm exec\`.`);
|
|
841
878
|
const provider = releaseProvider(o, root, git);
|
|
@@ -865,14 +902,16 @@ export async function main(argv, io = {}) {
|
|
|
865
902
|
}
|
|
866
903
|
if (args.includes("--help") || args.includes("-h")) { o.out(USAGE); return 0; }
|
|
867
904
|
const sub = args[0] && !args[0].startsWith("-") ? args[0] : null;
|
|
868
|
-
if (sub && sub !== "install-hook") { o.err(`rightkit-gate: unknown command ${JSON.stringify(sub)}\n${USAGE}`); return 2; }
|
|
905
|
+
if (sub && sub !== "install-hook" && sub !== "check") { o.err(`rightkit-gate: unknown command ${JSON.stringify(sub)}\n${USAGE}`); return 2; }
|
|
906
|
+
// The app check has its own export and reads no gate state: it needs neither the base ref nor the pushed refs on stdin.
|
|
907
|
+
if (sub === "check") return (o.appCheck ?? (await import("../app-check.mjs")).main)(args.slice(1), { out: o.out, err: o.err, label: "check", cwd: o.cwd });
|
|
869
908
|
if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
|
|
870
909
|
|
|
871
910
|
const top = o.spawn("git", ["rev-parse", "--show-toplevel"], { cwd: o.cwd });
|
|
872
911
|
if (failed(top) || !text(top).trim()) { o.err("rightkit-gate: not inside a git repository"); return 2; }
|
|
873
912
|
const root = path.resolve(text(top).trim());
|
|
874
913
|
const git = (...a) => o.spawn("git", a, { cwd: root });
|
|
875
|
-
if (sub === "install-hook") return installHook(o, root, args.includes("--force"));
|
|
914
|
+
if (sub === "install-hook") return await installHook(o, root, args.includes("--force"));
|
|
876
915
|
|
|
877
916
|
const planOnly = args.includes("--plan");
|
|
878
917
|
let base = option("--base") ?? "origin/main";
|
package/build-release.mjs
CHANGED
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { preflightOwnershipScope } from "./ownership-scope.mjs";
|
|
22
22
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
23
|
-
import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord } from "./rightkit-registry-resolver.mjs";
|
|
23
|
+
import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord, writeSnapshot } from "./rightkit-registry-resolver.mjs";
|
|
24
|
+
import { formatAdmission, formatNotRun, runAdmission } from "./admission.mjs";
|
|
25
|
+
import { sccacheRefusal } from "./admission-host.mjs";
|
|
24
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
27
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
26
28
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
@@ -129,12 +131,15 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
129
131
|
if (platform !== "win" && platform !== "mac" && platform !== "android" && platform !== "ios") fail("--platform must be win, mac, android or ios");
|
|
130
132
|
if (resumeFrom && !["prepackage", "sign", "package", "post-sign", "harden"].includes(resumeFrom)) fail("--resume-from must be prepackage, sign, package, post-sign, or harden");
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
// Native real paths: Git reports the long form of a Windows short name (RUNNER~1) and the target of a symlinked temp root
|
|
135
|
+
// (macOS /var -> /private/var). The config path is resolved the same way the doctor resolves it (release.mjs), so every path
|
|
136
|
+
// derived from it (app root, build inputs, scan roots) compares equal to what Git reports; a non-canonical app root made the
|
|
137
|
+
// build-inputs qualification drop every input and the clean-source gate speak before the dirty-inputs row.
|
|
138
|
+
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
139
|
+
const configPath = nativeReal(path.resolve(configName));
|
|
133
140
|
const invocationRoot = path.dirname(configPath);
|
|
134
141
|
const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
|
|
135
142
|
assertPrimaryReleaseCheckout(repoRoot);
|
|
136
|
-
// Native real paths: Git reports the long form of a Windows short name (RUNNER~1).
|
|
137
|
-
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
138
143
|
const relativeConfig = path.relative(nativeReal(repoRoot), nativeReal(configPath)).replaceAll("\\", "/");
|
|
139
144
|
if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
|
|
140
145
|
const appRoot = path.dirname(configPath);
|
|
@@ -148,21 +153,19 @@ let heavySlot;
|
|
|
148
153
|
let interrupted;
|
|
149
154
|
let requestActiveRunStop = null;
|
|
150
155
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
156
|
+
// Admission (admission.mjs): every refusal the committed files, this host or the registries can decide is raised here, before the
|
|
157
|
+
// dry-run exit, the clean-source gate, the release lock, any state file, the install, a slot or the worker. `right-release doctor`
|
|
158
|
+
// runs the same rows with the same text, so the two cannot disagree (rightkit#946); a refused build leaves nothing behind.
|
|
159
|
+
const admitted = await admit();
|
|
160
|
+
|
|
161
|
+
// A dry-run is an inspection contract, not a partial build. It prints the admission rows and exits with their verdict, so it can
|
|
162
|
+
// never pass what the build refuses. It must finish before locks, cache directories, receipts, dependency installation, or any
|
|
153
163
|
// child build/sign process can exist.
|
|
154
164
|
if (dryRun) {
|
|
155
|
-
|
|
156
|
-
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
157
|
-
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
158
|
-
const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
|
|
159
|
-
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
160
|
-
const target = config.targets?.[platform];
|
|
161
|
-
// Android builds with target.gradle (validated by release.mjs), not a package command; iOS with targets.ios.iosPackage.
|
|
162
|
-
if (platform !== "android" && platform !== "ios" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
|
|
163
|
-
console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
|
|
165
|
+
console.log(`right-release build dry-run: ${admitted.config.app} ${admitted.config.version} ${platform}`);
|
|
164
166
|
console.log(`config: ${configPath}`);
|
|
165
|
-
console.log(`commit: ${commit}`);
|
|
167
|
+
console.log(`commit: ${admitted.commit}`);
|
|
168
|
+
for (const line of formatAdmission(admitted.result)) console.log(line);
|
|
166
169
|
process.exit(0);
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -396,7 +399,8 @@ try {
|
|
|
396
399
|
let rightKitResolution = null;
|
|
397
400
|
if (!legacySealed) {
|
|
398
401
|
try {
|
|
399
|
-
|
|
402
|
+
// A fresh build already asked the registries in admission(); the snapshot is bound to this release's state root below.
|
|
403
|
+
rightKitResolution = (!rightKitReuse && admitted.registry) || await prepareRightKitResolution({
|
|
400
404
|
root: appRoot,
|
|
401
405
|
extraLocks: rightKitLockInventory({ requiredInputs, cargoLockPaths: pinnedCargoLocks, nativeLayout, config, root: appRoot }),
|
|
402
406
|
versions: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-versions.json"), "utf8")),
|
|
@@ -414,6 +418,7 @@ try {
|
|
|
414
418
|
if (!(error instanceof RightKitRegistryError)) throw error;
|
|
415
419
|
fail(`${error.recoverable ? "RECOVERABLE " : ""}${error.message}`);
|
|
416
420
|
}
|
|
421
|
+
if (rightKitResolution === admitted.registry) writeSnapshot(rightKitSnapshotFile, rightKitResolution.snapshot);
|
|
417
422
|
for (const line of rightKitResolution.freshness.tasks) console.error(`[right-release] LANE TASK ${line}`);
|
|
418
423
|
for (const line of rightKitResolution.freshness.warnings) console.error(`[right-release] WARNING ${line}`);
|
|
419
424
|
console.error(`[right-release] rightkit-registry: ${rightKitResolution.summary}`);
|
|
@@ -552,6 +557,52 @@ try {
|
|
|
552
557
|
lock?.release();
|
|
553
558
|
}
|
|
554
559
|
|
|
560
|
+
// Read the config and run the admission rows (files, this host and, for a fresh build, the registries) with no side effect: no lock,
|
|
561
|
+
// state directory, cache directory, install or snapshot file. A finding is printed with its row and the process exits 1, no stack.
|
|
562
|
+
async function admit() {
|
|
563
|
+
let commit;
|
|
564
|
+
let config;
|
|
565
|
+
try {
|
|
566
|
+
commit = git(invocationRoot, ["rev-parse", "HEAD"]);
|
|
567
|
+
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
568
|
+
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
569
|
+
try { config = (await import(`${pathToFileURL(configPath).href}?commit=${commit}`)).default; } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error?.reported) process.exit(1);
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
// A fresh build asks the registries once, here; a resume or a re-run over a sealed release reuses its recorded snapshot instead.
|
|
575
|
+
const platformName = platform === "win" ? "windows" : platform;
|
|
576
|
+
const sealed = config?.app && config?.version
|
|
577
|
+
&& existsSync(path.join(repoRoot, ".right-release", "sealed", `${config.app}-${config.version}-${commit.slice(0, 8)}`, platformName, "release-manifest.json"));
|
|
578
|
+
const reuse = Boolean(resumeFrom) || Boolean(sealed);
|
|
579
|
+
const result = await runAdmission({
|
|
580
|
+
root: appRoot, repoRoot, configPath, config: config ?? {}, platform, commit, skipChecks,
|
|
581
|
+
tiers: reuse ? ["files", "host"] : ["files", "host", "network"],
|
|
582
|
+
registry: async (ctx) => {
|
|
583
|
+
const inputs = ctx.buildInputs();
|
|
584
|
+
if (!inputs.ok) return undefined; // the build-inputs row reports it
|
|
585
|
+
return prepareRightKitResolution({
|
|
586
|
+
root: appRoot,
|
|
587
|
+
extraLocks: rightKitLockInventory({ requiredInputs: inputs.value.requiredInputs, cargoLockPaths: inputs.value.cargoLockPaths, nativeLayout: inputs.value.nativeLayout, config, root: appRoot }),
|
|
588
|
+
versions: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-versions.json"), "utf8")),
|
|
589
|
+
ownership: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-ownership.json"), "utf8")),
|
|
590
|
+
releaseVersion: RELEASE_TOOL_VERSION,
|
|
591
|
+
// Build admission: refuse every pin behind the registry's latest. A dry run asks, but keeps no cache on disk.
|
|
592
|
+
admission: true,
|
|
593
|
+
...(dryRun ? { cacheDir: "" } : {}),
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
for (const finding of result.findings) console.error(`right-release build: ${finding.row}: ${finding.detail}`);
|
|
598
|
+
for (const line of formatNotRun(result)) console.error(`right-release build: ${line}`);
|
|
599
|
+
if (result.findings.length > 0) {
|
|
600
|
+
console.error(`right-release build: admission found ${result.findings.length} blocking problem(s); nothing was started`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
return { config, commit, result, registry: result.out.registry ?? null };
|
|
604
|
+
}
|
|
605
|
+
|
|
555
606
|
function hashInputs(files) {
|
|
556
607
|
const hashes = {};
|
|
557
608
|
for (const file of files) {
|
|
@@ -610,6 +661,14 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
610
661
|
current.add(role);
|
|
611
662
|
sources.set(source, current);
|
|
612
663
|
};
|
|
664
|
+
// Named refusals for a config the admission's seal-routes row already refused, so nothing here crashes on an undefined field.
|
|
665
|
+
for (const artifact of [...(target.installer?.artifacts ?? []), ...(target.updater?.artifacts ?? [])]) {
|
|
666
|
+
if (typeof artifact?.file !== "string" || !artifact.file) fail("installer and updater artifacts must each declare a file");
|
|
667
|
+
}
|
|
668
|
+
for (const artifact of target.updater?.artifacts ?? []) {
|
|
669
|
+
if (target.updater?.kind !== "appinstaller" && (typeof artifact.signature !== "string" || !artifact.signature)) fail(`updater artifact ${artifact.file} must declare the signature file sign-updater writes next to it`);
|
|
670
|
+
if (artifact.key === undefined && config.distribution?.provider !== "github-releases") fail(`updater artifact ${artifact.file} must declare the R2 route key (key); only github-releases names the route from the file`);
|
|
671
|
+
}
|
|
613
672
|
for (const artifact of target.installer?.artifacts ?? []) addSource(resolveArtifactPath(artifact.file), "installer");
|
|
614
673
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
615
674
|
addSource(resolveArtifactPath(artifact.file), "updater");
|
|
@@ -741,9 +800,9 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
741
800
|
// an unsound seal, not a portable release.
|
|
742
801
|
signing: signingIdentity ? { ...signingIdentity, receipts: (() => {
|
|
743
802
|
const present = signingIdentity.receiptInputs.filter((file) => existsSync(file));
|
|
744
|
-
if (present.length === 0)
|
|
803
|
+
if (present.length === 0) fail(`no signing receipts found to seal; expected at least ${signingIdentity.receiptInputs[0]}`);
|
|
745
804
|
if (platform === "mac") {
|
|
746
|
-
if (present.length !== signingIdentity.receiptInputs.length)
|
|
805
|
+
if (present.length !== signingIdentity.receiptInputs.length) fail("missing current-build macOS signing or notarization receipt");
|
|
747
806
|
validateNotarizationReceipt({ receiptPath: signingIdentity.receiptInputs[1], archive: resolveArtifactPath(target.notarize.file) });
|
|
748
807
|
}
|
|
749
808
|
return present.map((file) => ({ file, sha256: hashFile(file) }));
|
|
@@ -941,7 +1000,7 @@ function commandExists(name) {
|
|
|
941
1000
|
|
|
942
1001
|
function assertSccacheVersion(version) {
|
|
943
1002
|
const match = String(version ?? "").match(/sccache\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
944
|
-
if (!match || Number(match[1]) === 0 && Number(match[2]) < 15) fail(
|
|
1003
|
+
if (!match || Number(match[1]) === 0 && Number(match[2]) < 15) fail(sccacheRefusal(match ? match.slice(1).join(".") : null));
|
|
945
1004
|
}
|
|
946
1005
|
|
|
947
1006
|
function commandOutput(cmd, runArgs) {
|
|
@@ -1000,7 +1059,8 @@ function usage(code) {
|
|
|
1000
1059
|
|
|
1001
1060
|
function fail(message) {
|
|
1002
1061
|
console.error(`right-release build: ${message}`);
|
|
1003
|
-
|
|
1062
|
+
// `reported` marks an error whose text is already printed, so a caller that exits on it prints nothing twice.
|
|
1063
|
+
throw Object.assign(new Error(message), { reported: true });
|
|
1004
1064
|
}
|
|
1005
1065
|
|
|
1006
1066
|
function throwIfInterrupted() { if (interrupted) throw interrupted; }
|
package/cli/right-release.mjs
CHANGED
|
@@ -54,7 +54,17 @@ const commands = new Map([
|
|
|
54
54
|
|
|
55
55
|
const args = process.argv.slice(2);
|
|
56
56
|
const first = args[0];
|
|
57
|
+
// The commands that compile, package, sign, ship or inspect a release run the app check (../app-check.mjs, CHECKED_COMMANDS) in this
|
|
58
|
+
// process before they start a child. A red check prints its findings and exits 1; there is no flag that skips it.
|
|
59
|
+
async function requireGreenCheck() {
|
|
60
|
+
const { guardCommand } = await import("../app-check.mjs");
|
|
61
|
+
const refused = guardCommand({ args });
|
|
62
|
+
if (!refused) return;
|
|
63
|
+
for (const line of refused.lines) console.error(line);
|
|
64
|
+
process.exit(refused.exitCode);
|
|
65
|
+
}
|
|
57
66
|
if (first === "dev") {
|
|
67
|
+
await requireGreenCheck();
|
|
58
68
|
import("../development.mjs").then(({ main }) => main(args.slice(1))).catch((error) => {
|
|
59
69
|
console.error(`right-release dev: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
60
70
|
process.exitCode = 1;
|
|
@@ -81,6 +91,8 @@ if (!first || first === "-h" || first === "--help") {
|
|
|
81
91
|
process.exit(0);
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
await requireGreenCheck();
|
|
95
|
+
|
|
84
96
|
if (first === "--version" || first === "-v") {
|
|
85
97
|
run("release.mjs", ["--version"]);
|
|
86
98
|
} else if (first === "doctor") {
|
|
@@ -89,6 +101,18 @@ if (first === "--version" || first === "-v") {
|
|
|
89
101
|
} else {
|
|
90
102
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
91
103
|
}
|
|
104
|
+
} else if (first === "check") {
|
|
105
|
+
// The same offline check as `rightkit-gate check`; the repository is the git top-level of --config's folder, else the cwd.
|
|
106
|
+
const { appDirectory, main } = await import("../app-check.mjs");
|
|
107
|
+
process.exit(await main(args.slice(1), { label: "check", cwd: appDirectory(args, process.cwd()) }));
|
|
108
|
+
} else if (first === "hooks" && args[1] === "install") {
|
|
109
|
+
// One installer: `hooks install` is `rightkit-gate install-hook` (pre-push check and gate, ownership pre-commit step) run in the app.
|
|
110
|
+
const app = args.indexOf("--app");
|
|
111
|
+
if (app >= 0 && (!args[app + 1] || args[app + 1].startsWith("-"))) {
|
|
112
|
+
console.error("right-release hooks install: --app needs a directory");
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
run("bin/rightkit-gate.mjs", ["install-hook", ...(args.includes("--force") ? ["--force"] : [])], { cwd: app >= 0 ? path.resolve(args[app + 1]) : process.cwd() });
|
|
92
116
|
} else if (first === "hooks") {
|
|
93
117
|
run("ownership-hooks.mjs", args.slice(1));
|
|
94
118
|
} else if (first === "suite-doctor") {
|
|
@@ -170,9 +194,9 @@ if (first === "--version" || first === "-v") {
|
|
|
170
194
|
process.exit(2);
|
|
171
195
|
}
|
|
172
196
|
|
|
173
|
-
function run(script, scriptArgs) {
|
|
197
|
+
function run(script, scriptArgs, { cwd = process.cwd() } = {}) {
|
|
174
198
|
const child = spawn(process.execPath, [path.join(packageRoot, script), ...scriptArgs], {
|
|
175
|
-
cwd
|
|
199
|
+
cwd,
|
|
176
200
|
env: process.env,
|
|
177
201
|
stdio: "inherit",
|
|
178
202
|
windowsHide: true,
|
|
@@ -308,12 +332,21 @@ Commands:
|
|
|
308
332
|
apple profile renew [--config <file>] [--profile <file>] [--dry-run]
|
|
309
333
|
Renew macOS profile for current Keychain signer
|
|
310
334
|
signing-agent install|uninstall|status Manage invisible Aqua-session signing service
|
|
311
|
-
|
|
335
|
+
check [--repo <dir>] [--config <file>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
|
|
336
|
+
The offline app check, same as rightkit-gate check: committed files only, every finding in one run (exit 0 pass, 1 findings, 2 could not run).
|
|
337
|
+
Build, package, dev, test, web-build, swift-package, recipe-build, stage-sidecar, qa, upload, publish, publish-update, github, doctor,
|
|
338
|
+
sign-windows, sign-updater, sign-candidate, sign-dev-artifact, addon build and create-mac-updater run it first and start nothing when it is red; there is no flag that skips it.
|
|
339
|
+
Baseline: .rightkit/app-check-baseline.json (entries carry issue, added and sunset; the sunset is at most 90 days after added)
|
|
340
|
+
doctor [--platform mac|win] [--gates] [--inspect] [--strict]
|
|
341
|
+
Inspect one app's release config the way the build would refuse it; --gates also runs the build's pre-build source gates
|
|
342
|
+
--inspect lists admission findings (newer RightKit versions) as information instead of refusing them
|
|
343
|
+
--strict makes a row that could not run on this host (NOT RUN) a blocking problem; without it NOT RUN is printed and does not fail
|
|
344
|
+
--admission is still accepted and does nothing (admission is the default); it cannot be combined with --inspect
|
|
312
345
|
doctor --all Verify all Right Suite app release contracts
|
|
313
346
|
suite-doctor Verify all local Right Suite repositories
|
|
314
347
|
ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
|
|
315
348
|
ownership --staged [--app <dir>] [--json] Pre-commit gate: scan the staged index for block-tier findings
|
|
316
|
-
hooks install [--app <dir>]
|
|
349
|
+
hooks install [--app <dir>] [--force] Alias of rightkit-gate install-hook: the pre-push check and gate, and the advisory pre-commit ownership gate (pinned @rightkit/release)
|
|
317
350
|
ownership enroll [--root] [--name <device>] [--root-fingerprint <sha256>] [--app <dir>]
|
|
318
351
|
Enroll one native key or create a signed-device request
|
|
319
352
|
ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
|
package/deterministic-scans.mjs
CHANGED
|
@@ -205,10 +205,11 @@ const WORKSPACE_JS_FILE = /^(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|
|
|
|
205
205
|
const ESLINT_FILES = ["eslint.config.js", "eslint.config.mjs", ".eslintrc.json"];
|
|
206
206
|
const ROOT_LINT_FILES = [...ESLINT_FILES, "biome.json"];
|
|
207
207
|
// Everything scripts/generate-index.mjs reads or writes (its header and loadInventory): crate and package manifests and
|
|
208
|
-
// READMEs, the root Cargo.toml (workspace version), the versions and ownership data, the ownership-availability helper it imports, itself,
|
|
208
|
+
// READMEs, the root Cargo.toml (workspace version), the versions and ownership data, the ownership-availability helper it imports, itself,
|
|
209
|
+
// the port matrix generator it imports, and its outputs (the port matrix's MD and HTML are checked for consistency with its JSON).
|
|
209
210
|
// The Right Suite API contract and every projection scripts/gen-suite-api.mjs writes from it (the test-vector copy included).
|
|
210
211
|
const SUITE_API_INPUT = /^(contracts\/right-suite-api\.v1\.json|scripts\/gen-suite-api\.mjs|crates\/rightkit-license\/src\/suite_api_generated\.rs|packages\/app-update\/src\/suite-api\.generated\.ts|packages\/release\/suite-api\.generated\.mjs|crates\/rightkit-license\/test-vectors\/right-suite-api\.v1\.json)$/;
|
|
211
|
-
const GENERATOR_INPUT = /^(crates\/[^/]+\/(Cargo\.toml|README\.md)|packages\/[^/]+\/(package\.json|README\.md)|Cargo\.toml|packages\/release\/(rightkit-versions\.json|rightkit-ownership\.json|ownership-availability\.mjs)|scripts\/generate-index\.mjs|INDEX\.md|docs\/(ADOPTION|CAPABILITY-MAP)\.md)$/;
|
|
212
|
+
const GENERATOR_INPUT = /^(crates\/[^/]+\/(Cargo\.toml|README\.md)|packages\/[^/]+\/(package\.json|README\.md)|Cargo\.toml|packages\/release\/(rightkit-versions\.json|rightkit-ownership\.json|ownership-availability\.mjs)|scripts\/generate-(index|port-matrix)\.mjs|INDEX\.md|docs\/(ADOPTION|CAPABILITY-MAP|PORT-MATRIX)\.md|docs\/port-matrix\.(html|json))$/;
|
|
212
213
|
const PROBES = Object.freeze({
|
|
213
214
|
rustfmt: ["--version"], node: ["--version"], rustright: ["cargo", "--version"], pnpm: ["--version"],
|
|
214
215
|
actionlint: ["-version"], "right-git": ["--version"], swift: ["--version"],
|
|
@@ -358,10 +359,25 @@ export function planScans({
|
|
|
358
359
|
// ---- class A: content checks on the exported commit
|
|
359
360
|
// A vendor/ file keeps upstream formatting only when vendor/upstream-files.json records it and its
|
|
360
361
|
// bytes still match that upstream digest; any other vendor/ .rs (new, patched) is rustfmt-checked.
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
// A patched vendor/ file (rightkit#915) keeps upstream formatting only while its bytes match the
|
|
363
|
+
// reviewed `patched` record, which names its upstream base digest and the note describing the patch;
|
|
364
|
+
// reformatting it would bury the patch. Any edit changes the digest, so the file is checked until re-recorded.
|
|
365
|
+
let upstream = {}; let patched = {};
|
|
366
|
+
try {
|
|
367
|
+
const manifest = JSON.parse(readFile(path.join(contentRoot, "vendor/upstream-files.json")));
|
|
368
|
+
upstream = manifest.files ?? {}; patched = manifest.patched ?? {};
|
|
369
|
+
} catch { /* no manifest: nothing exempt */ }
|
|
370
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
363
371
|
const verbatimUpstream = (f) => f.startsWith("vendor/") && typeof upstream[f] === "string" && bytesDigest(f) === upstream[f];
|
|
364
|
-
const
|
|
372
|
+
const recordedPatch = (f) => f.startsWith("vendor/") && DIGEST.test(patched[f]?.base ?? "") && typeof patched[f]?.note === "string"
|
|
373
|
+
&& patched[f].note.startsWith("vendor/") && DIGEST.test(patched[f]?.sha256 ?? "") && bytesDigest(f) === patched[f].sha256;
|
|
374
|
+
// Generated Rust is not hand-formatted: bindgen output, or rustfmt's own `@generated` marker in the first five lines.
|
|
375
|
+
const generatedRust = (f) => {
|
|
376
|
+
let head = "";
|
|
377
|
+
try { head = readFile(path.join(contentRoot, f)).split("\n", 5).join("\n"); } catch { return false; }
|
|
378
|
+
return /@generated\b|automatically generated by rust-bindgen\b/.test(head);
|
|
379
|
+
};
|
|
380
|
+
const rustFiles = files.filter((f) => f.endsWith(".rs") && !verbatimUpstream(f) && !recordedPatch(f) && !generatedRust(f));
|
|
365
381
|
const byEdition = new Map();
|
|
366
382
|
for (const f of rustFiles) {
|
|
367
383
|
let dir = path.dirname(path.join(contentRoot, f));
|
package/inventory-check.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
generateAdoption,
|
|
17
17
|
generateLocal,
|
|
18
18
|
} from "../../scripts/generate-index.mjs";
|
|
19
|
+
import { PORT_MATRIX_OUTPUTS, generatePortMatrix } from "../../scripts/generate-port-matrix.mjs";
|
|
19
20
|
|
|
20
21
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
export const DEFAULT_REPO_ROOT = resolve(HERE, "..", "..");
|
|
@@ -781,6 +782,16 @@ function checkGenerated({ repoRoot, appsRoot, apps, findings }) {
|
|
|
781
782
|
for (const rel of driftOf(repoRoot, adoption)) {
|
|
782
783
|
findings.push(issue("generated", rel, "generated-doc-drift", "committed adoption report differs from the app repos; run generate-index.mjs"));
|
|
783
784
|
}
|
|
785
|
+
// The port matrix reads the app repos like the adoption report. Its stamp, app directories and HEADs, and per-app check counts
|
|
786
|
+
// are carried over from the committed docs/port-matrix.json, so only a change in the data itself reads as drift.
|
|
787
|
+
try {
|
|
788
|
+
const matrix = generatePortMatrix({ root: repoRoot, workspace: appsRoot, carry: true });
|
|
789
|
+
for (const rel of driftOf(repoRoot, matrix.outputs)) {
|
|
790
|
+
findings.push(issue("generated", rel, "generated-doc-drift", "committed port matrix differs from the app repos; run generate-index.mjs"));
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
findings.push(issue("generated", PORT_MATRIX_OUTPUTS[2], "generated-doc-drift", `the port matrix could not be regenerated: ${error.message}`));
|
|
794
|
+
}
|
|
784
795
|
return { local, adoption: Object.values(adoption)[0] };
|
|
785
796
|
}
|
|
786
797
|
|
|
@@ -842,7 +853,7 @@ export async function runInventoryCheck(options = {}) {
|
|
|
842
853
|
account: { crates: account.crates, npm: account.npm },
|
|
843
854
|
thirdParty: { declarations: thirdParty.declarations, uniquePackages: thirdParty.uniquePackages, lockDuplicates: thirdParty.lockDuplicates },
|
|
844
855
|
apps: { rows: appRows },
|
|
845
|
-
generated: { checked: ["INDEX.md", "docs/CAPABILITY-MAP.md", "docs/ADOPTION.md"] },
|
|
856
|
+
generated: { checked: ["INDEX.md", "docs/CAPABILITY-MAP.md", "docs/ADOPTION.md", ...PORT_MATRIX_OUTPUTS] },
|
|
846
857
|
hygiene: { checked: records.length },
|
|
847
858
|
},
|
|
848
859
|
};
|
|
@@ -399,10 +399,12 @@
|
|
|
399
399
|
"cargo:base64",
|
|
400
400
|
"cargo:block2",
|
|
401
401
|
"cargo:chrono",
|
|
402
|
+
"cargo:clap",
|
|
402
403
|
"cargo:dirs",
|
|
403
404
|
"cargo:futures",
|
|
404
405
|
"cargo:getrandom",
|
|
405
406
|
"cargo:image",
|
|
407
|
+
"cargo:libc",
|
|
406
408
|
"cargo:objc2",
|
|
407
409
|
"cargo:objc2-foundation",
|
|
408
410
|
"cargo:reqwest",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"LICENSE-MIT",
|
|
34
34
|
"LICENSE-APACHE",
|
|
35
35
|
"!*.test.mjs",
|
|
36
|
-
"!
|
|
36
|
+
"!cli/*.test.mjs",
|
|
37
|
+
"!standalone-clone-evidence.json",
|
|
38
|
+
"!refusals.json"
|
|
37
39
|
],
|
|
38
40
|
"sideEffects": false,
|
|
39
41
|
"publishConfig": {
|
|
@@ -49,7 +51,7 @@
|
|
|
49
51
|
"smol-toml": "1.9.1"
|
|
50
52
|
},
|
|
51
53
|
"scripts": {
|
|
52
|
-
"test": "node --import ./test-support/broker-guard.mjs --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
54
|
+
"test": "node --import ./test-support/broker-guard.mjs --test --test-concurrency=1 --test-force-exit *.test.mjs cli/*.test.mjs",
|
|
53
55
|
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
54
56
|
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
55
57
|
"doctor:all": "node --test right-suite-contract.test.mjs",
|
package/prebuild-gates.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Source-level release gates that run before a build starts. The build (release.mjs) and `doctor --gates` call the
|
|
2
2
|
// same function, so the doctor can never pass a config the build refuses. Gate order is part of the contract.
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
6
|
import { assertReleaseDependencies } from "./release-dependencies.mjs";
|
|
@@ -145,11 +145,15 @@ export function evaluatePrebuildGates({ root, config, target, platform, iosSpec,
|
|
|
145
145
|
let sourceRoots;
|
|
146
146
|
let skipRoots;
|
|
147
147
|
let repoTop;
|
|
148
|
+
let appRootReal;
|
|
149
|
+
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
148
150
|
if (releaseSwitchInventoryText != null) {
|
|
149
151
|
gate("source-switch-roots", ["closure-commands", ...(strict ? ["feature-closure"] : [])], () => {
|
|
150
152
|
if (platform === "ios") {
|
|
151
153
|
repoTop = git(root, ["rev-parse", "--show-toplevel"]);
|
|
152
|
-
|
|
154
|
+
// Git reports the canonical path; a caller's root may be a symlink or a Windows short name (hosted temp roots).
|
|
155
|
+
appRootReal = nativeReal(root);
|
|
156
|
+
const swiftSide = (iosOwnershipScope({ repoRoot: repoTop, appRoot: appRootReal, target: config.targets?.ios }) ?? []).filter((dir) => dir !== ".rightkit").map((dir) => path.join(repoTop, dir));
|
|
153
157
|
sourceRoots = [...new Set([...swiftSide, ...localCrateDirsFromTree(treeText, repoTop)])].sort();
|
|
154
158
|
console.log(`right-release: source switch scan (ios): ${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}`);
|
|
155
159
|
} else {
|
|
@@ -162,7 +166,7 @@ export function evaluatePrebuildGates({ root, config, target, platform, iosSpec,
|
|
|
162
166
|
if (platform === "ios") {
|
|
163
167
|
gate("source-switch-prepare", ["source-switch-roots"], () => {
|
|
164
168
|
for (const step of config.targets?.ios?.iosPackage?.prepare ?? []) {
|
|
165
|
-
const cwd = path.resolve(
|
|
169
|
+
const cwd = path.resolve(appRootReal, step?.cwd ?? ".");
|
|
166
170
|
if (!sourceRoots.some((dir) => cwd === dir || cwd.startsWith(dir + path.sep))) throw new Error(`source switch inventory: targets.ios.iosPackage.prepare runs in ${path.relative(repoTop, cwd) || "."}, outside the iOS scan roots (${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}); list that directory in targets.ios.ownershipScope`);
|
|
167
171
|
}
|
|
168
172
|
});
|