@rightkit/release 0.4.0 → 0.4.1
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 +49 -6
- package/adoption-evidence.mjs +650 -0
- package/apple-profile-create.mjs +113 -10
- package/bin/darwin/rightkit-swap +0 -0
- package/binary-license-notices.mjs +161 -0
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +10 -0
- package/local-publish.mjs +133 -19
- package/ownership-dependency-allowlist.json +2 -0
- package/ownership-mandatory-set.json +117 -67
- package/ownership-mandatory-set.mjs +75 -5
- package/package.json +1 -1
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/production-mode.mjs +42 -0
- package/publication-gate.mjs +83 -32
- package/publish-cargo.mjs +11 -6
- package/publish-swift.mjs +12 -3
- package/qa-control-scan.mjs +4 -0
- package/release-config.mjs +330 -0
- package/release-switch-scan.mjs +44 -5
- package/release.mjs +14 -4
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +132 -88
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +128 -31
- package/stage-sidecar.mjs +47 -8
- package/swap-helper-bundle.mjs +7 -3
- package/bin/darwin/rightkit-swap.sign.json +0 -6
package/runtime-budgets.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import path from "node:path";
|
|
|
59
59
|
import { fileURLToPath } from "node:url";
|
|
60
60
|
import { spawnSync } from "./background-process.mjs";
|
|
61
61
|
import { parse } from "smol-toml";
|
|
62
|
+
import { RUNTIME_BUDGET_CRATES } from "./runtime-budget-crates.mjs";
|
|
62
63
|
import { singleHostOfPolicy } from "./publication-gate.mjs";
|
|
63
64
|
import { findWorkspaceRoot, lockClosure, pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
|
|
64
65
|
|
|
@@ -68,18 +69,7 @@ export const RUNTIME_BUDGET_FILE = "runtime-budget.json";
|
|
|
68
69
|
export const RUNTIME_BUDGET_EVIDENCE_DIR = "runtime-budget.evidence";
|
|
69
70
|
export const RUNTIME_BUDGET_SCHEMA = 1;
|
|
70
71
|
|
|
71
|
-
export
|
|
72
|
-
"rightkit-fsindex",
|
|
73
|
-
"rightkit-file-watch",
|
|
74
|
-
"rightkit-job-queue",
|
|
75
|
-
"rightkit-daemon",
|
|
76
|
-
"rightkit-cache",
|
|
77
|
-
"rightkit-sync",
|
|
78
|
-
"rightkit-logs",
|
|
79
|
-
"rightkit-tauri-shell",
|
|
80
|
-
"rightkit-qa",
|
|
81
|
-
"rightkit-send",
|
|
82
|
-
]);
|
|
72
|
+
export { RUNTIME_BUDGET_CRATES };
|
|
83
73
|
|
|
84
74
|
export const BUDGET_FIGURES = Object.freeze(["residentMemoryBytes", "idleCpuPercent", "wakeupsPerSecond", "diskOpsPerSecond"]);
|
|
85
75
|
const FIGURE_LABEL = Object.freeze({
|
|
@@ -260,17 +250,86 @@ const posix = (p) => p.split(path.sep).join("/");
|
|
|
260
250
|
/**
|
|
261
251
|
* The git pathspecs (relative to the workspace root) whose change makes a measurement stale: the
|
|
262
252
|
* crate's src, benches, Cargo.toml (features) and build.rs, plus src, Cargo.toml and build.rs of every
|
|
263
|
-
* workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs }.
|
|
253
|
+
* workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs, problems }.
|
|
254
|
+
* Callers must refuse incomplete metadata rather than using a partial dependency scope.
|
|
264
255
|
*/
|
|
265
256
|
export function measuredPathspecs(crateDir, deps = {}) {
|
|
266
|
-
const
|
|
257
|
+
const { exists = existsSync, readFile = readFileSync } = deps;
|
|
258
|
+
const problems = [];
|
|
259
|
+
// Workspace discovery deliberately tolerates bad ancestor manifests elsewhere. Freshness cannot:
|
|
260
|
+
// a swallowed read/parse failure could hide inherited dependencies and shrink the measured graph.
|
|
261
|
+
const checkedRead = (file, encoding) => {
|
|
262
|
+
try {
|
|
263
|
+
const contents = readFile(file, encoding);
|
|
264
|
+
if (path.basename(file) === "Cargo.toml") parse(contents);
|
|
265
|
+
return contents;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
problems.push(`cannot read dependency metadata ${file}: ${error.message}`);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const checked = { exists, readFile: checkedRead };
|
|
272
|
+
let root = path.resolve(crateDir);
|
|
267
273
|
const specs = [];
|
|
268
274
|
const add = (dir, names) => { for (const name of names) specs.push(posix(path.relative(root, path.join(dir, name)))); };
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
275
|
+
try {
|
|
276
|
+
root = findWorkspaceRoot(crateDir, checked) ?? root;
|
|
277
|
+
add(path.resolve(crateDir), CRATE_PATHS);
|
|
278
|
+
const dependencies = pathDependencyDirs(crateDir, checked);
|
|
279
|
+
const workspace = exists(path.join(root, "Cargo.toml")) ? parse(checkedRead(path.join(root, "Cargo.toml"), "utf8")).workspace : undefined;
|
|
280
|
+
for (const dir of [path.resolve(crateDir), ...dependencies]) {
|
|
281
|
+
const manifestPath = path.join(dir, "Cargo.toml");
|
|
282
|
+
if (!exists(manifestPath)) {
|
|
283
|
+
// A manifest-free standalone path has no Cargo graph. A declared path dependency must exist.
|
|
284
|
+
if (dir !== path.resolve(crateDir)) problems.push(`dependency manifest is missing: ${manifestPath}`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const manifest = parse(checkedRead(manifestPath, "utf8"));
|
|
288
|
+
if (!nonEmpty(manifest.package?.name)) problems.push(`dependency manifest has no package.name: ${manifestPath}`);
|
|
289
|
+
const holders = [manifest, ...Object.values(manifest.target ?? {})];
|
|
290
|
+
for (const holder of holders) {
|
|
291
|
+
for (const key of ["dependencies", "build-dependencies", ...(dir === path.resolve(crateDir) ? ["dev-dependencies"] : [])]) {
|
|
292
|
+
for (const [name, spec] of Object.entries(holder?.[key] ?? {})) {
|
|
293
|
+
if (spec && typeof spec === "object" && spec.workspace === true && workspace?.dependencies?.[name] === undefined) {
|
|
294
|
+
problems.push(`workspace dependency ${name} cannot be resolved for ${manifestPath}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const dir of dependencies) add(dir, DEPENDENCY_PATHS);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!problems.length) problems.push(`cannot resolve measured dependency paths for ${crateDir}: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
return { root, specs: [...new Set(specs)], problems: [...new Set(problems)] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// lockClosure compares reachable entries, but its permissive parser can return an empty/partial graph.
|
|
308
|
+
// Require the root and every reachable edge before allowing equality to establish freshness.
|
|
309
|
+
function completeLockClosure(contents, name) {
|
|
310
|
+
const packages = parse(contents).package;
|
|
311
|
+
if (!Array.isArray(packages)) throw new Error("missing [[package]] entries");
|
|
312
|
+
const roots = packages.filter((entry) => entry.name === name);
|
|
313
|
+
if (!roots.length) throw new Error(`missing package ${name}`);
|
|
314
|
+
const seen = new Set();
|
|
315
|
+
const pending = [...roots];
|
|
316
|
+
while (pending.length) {
|
|
317
|
+
const entry = pending.pop();
|
|
318
|
+
if (seen.has(entry)) continue;
|
|
319
|
+
seen.add(entry);
|
|
320
|
+
if (!nonEmpty(entry.name) || !nonEmpty(entry.version)) throw new Error(`incomplete package identity for ${entry.name ?? name}`);
|
|
321
|
+
if (entry.dependencies !== undefined && !Array.isArray(entry.dependencies)) throw new Error(`invalid dependencies for ${entry.name}`);
|
|
322
|
+
for (const edge of entry.dependencies ?? []) {
|
|
323
|
+
if (!nonEmpty(edge)) throw new Error(`invalid dependency edge for ${entry.name}`);
|
|
324
|
+
const match = /^(\S+)(?: (\S+))?(?: \(([^)]+)\))?$/.exec(edge);
|
|
325
|
+
if (!match) throw new Error(`invalid dependency ${edge} of ${entry.name}`);
|
|
326
|
+
const candidates = packages.filter((candidate) => candidate.name === match[1]
|
|
327
|
+
&& (!match[2] || candidate.version === match[2]) && (!match[3] || candidate.source === match[3]));
|
|
328
|
+
if (!candidates.length) throw new Error(`unresolved dependency ${edge} of ${entry.name}`);
|
|
329
|
+
pending.push(...candidates);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return lockClosure(contents, name);
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
/**
|
|
@@ -289,7 +348,8 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
|
|
|
289
348
|
const known = git(["cat-file", "-t", commit], crateDir);
|
|
290
349
|
if (known.status !== 0 || known.stdout !== "commit") return `commit ${commit.slice(0, 12)} is not a commit in this repository (shallow clone?)`;
|
|
291
350
|
if (git(["merge-base", "--is-ancestor", commit, "HEAD"], crateDir).status !== 0) return `commit ${commit.slice(0, 12)} is not an ancestor of HEAD`;
|
|
292
|
-
const { root, specs } = measuredPathspecs(crateDir, { exists, readFile });
|
|
351
|
+
const { root, specs, problems } = measuredPathspecs(crateDir, { exists, readFile });
|
|
352
|
+
if (problems.length) return `cannot resolve measured dependency graph: ${problems.join("; ")}`;
|
|
293
353
|
// `git diff <commit>` (no second revision) compares the working tree to the commit.
|
|
294
354
|
const changed = git(["diff", "--name-only", commit, "--", ...specs], root);
|
|
295
355
|
if (changed.status !== 0) return "cannot diff the crate against the measured commit";
|
|
@@ -298,17 +358,29 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
|
|
|
298
358
|
if (untracked.status !== 0) return "cannot list untracked crate files";
|
|
299
359
|
if (untracked.stdout !== "") return `untracked files since the measurement: ${untracked.stdout.split("\n").slice(0, 5).join(", ")}`;
|
|
300
360
|
let name = null;
|
|
301
|
-
try {
|
|
361
|
+
try {
|
|
362
|
+
if (exists(path.join(crateDir, "Cargo.toml"))) {
|
|
363
|
+
name = readCargoPackage(crateDir, { exists, readFile }).name;
|
|
364
|
+
if (!nonEmpty(name)) return `cannot resolve Cargo.lock: ${path.join(crateDir, "Cargo.toml")} has no package.name`;
|
|
365
|
+
}
|
|
366
|
+
} catch (error) { return `cannot resolve Cargo.lock package identity: ${error.message}`; }
|
|
302
367
|
if (name) {
|
|
303
368
|
const lockPath = path.join(root, "Cargo.lock");
|
|
304
|
-
|
|
369
|
+
let now;
|
|
370
|
+
try { now = exists(lockPath) ? readFile(lockPath, "utf8") : null; }
|
|
371
|
+
catch (error) { return `cannot read Cargo.lock ${lockPath}: ${error.message}`; }
|
|
305
372
|
const then = git(["show", `${commit}:./Cargo.lock`], root);
|
|
306
373
|
const had = then.status === 0;
|
|
307
374
|
if (now === null && had) return "Cargo.lock was removed since the measurement";
|
|
308
375
|
if (now !== null && !had) return "Cargo.lock did not exist at the measured commit";
|
|
376
|
+
if (now === null && !had) {
|
|
377
|
+
// A failed show alone cannot distinguish an absent lock from an unreadable Git object.
|
|
378
|
+
const listed = git(["ls-tree", commit, "--", "Cargo.lock"], root);
|
|
379
|
+
if (listed.status !== 0 || listed.stdout !== "") return "cannot establish that Cargo.lock was absent at the measured commit";
|
|
380
|
+
}
|
|
309
381
|
if (now !== null) {
|
|
310
382
|
try {
|
|
311
|
-
if (JSON.stringify(
|
|
383
|
+
if (JSON.stringify(completeLockClosure(now, name)) !== JSON.stringify(completeLockClosure(then.stdout, name))) {
|
|
312
384
|
return `Cargo.lock changed in the dependency graph of ${name} since commit ${commit.slice(0, 12)}`;
|
|
313
385
|
}
|
|
314
386
|
} catch (error) { return `cannot compare Cargo.lock: ${error.message}`; }
|
|
@@ -500,7 +572,14 @@ export function statusApiDeclared(api, source) {
|
|
|
500
572
|
* `evidence` is the release gate evidence for it (the host-policy authority). `requireInstalled`
|
|
501
573
|
* (adoption) additionally needs a fresh in-ceiling installed-app measurement, optionally for `app`.
|
|
502
574
|
*/
|
|
503
|
-
export function evaluateRuntimeBudget(
|
|
575
|
+
export function evaluateRuntimeBudget(options, deps = {}) {
|
|
576
|
+
return evaluateRuntimeBudgetOwned(options, deps);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Private owner mode: no caller-supplied boolean can weaken publication or adoption.
|
|
580
|
+
const STRUCTURE_ONLY = Symbol("runtime-budget-structure-only");
|
|
581
|
+
function evaluateRuntimeBudgetOwned({ crate, crateDir, version, evidence, requireInstalled = false, app, heldResource = false }, { readFile = readFileSync, exists = existsSync, git = defaultGit } = {}, mode) {
|
|
582
|
+
const structural = mode === STRUCTURE_ONLY;
|
|
504
583
|
const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
|
|
505
584
|
if (!exists(file)) return { ok: false, problems: [`${RUNTIME_BUDGET_FILE} is missing (docs/RUNTIME-BUDGETS.md)`] };
|
|
506
585
|
let budget;
|
|
@@ -574,7 +653,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
574
653
|
|
|
575
654
|
// Measurements.
|
|
576
655
|
const records = Array.isArray(budget.measurements) ? budget.measurements : [];
|
|
577
|
-
if (records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
|
|
656
|
+
if (!structural && records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
|
|
578
657
|
const valid = [];
|
|
579
658
|
records.forEach((m, index) => {
|
|
580
659
|
const bad = validateMeasurement(m, index);
|
|
@@ -583,7 +662,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
583
662
|
if (evidenceProblems.length) problems.push(...evidenceProblems); else valid.push({ m, index });
|
|
584
663
|
});
|
|
585
664
|
const counted = valid.filter(({ m }) => m.crateVersion === version);
|
|
586
|
-
if (valid.length && nonEmpty(version) && counted.length === 0) {
|
|
665
|
+
if (!structural && valid.length && nonEmpty(version) && counted.length === 0) {
|
|
587
666
|
problems.push(`no measurement for version ${version} (recorded: ${[...new Set(valid.map(({ m }) => m.crateVersion))].join(", ")})`);
|
|
588
667
|
}
|
|
589
668
|
// Why a measurement fails one figure, or null. A delta-basis figure of a synthetic measurement is gated on
|
|
@@ -608,7 +687,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
608
687
|
}
|
|
609
688
|
return null;
|
|
610
689
|
};
|
|
611
|
-
for (const { m, index } of counted) {
|
|
690
|
+
for (const { m, index } of (structural ? valid : counted)) {
|
|
612
691
|
for (const key of BUDGET_FIGURES) {
|
|
613
692
|
const failure = figureFailure(m, key);
|
|
614
693
|
if (failure) problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} ${failure}`);
|
|
@@ -616,6 +695,14 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
616
695
|
}
|
|
617
696
|
const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && (usesDelta(m, key) ? deltaCeilings[key] !== undefined && figureFailure(m, key) === null : m.observed[key] === null ? figureUnavailableOn(m.host?.os, key) : m.observed[key] <= ceilings[key]));
|
|
618
697
|
const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
|
|
698
|
+
if (structural) {
|
|
699
|
+
if (!Array.isArray(budget.measurements)) problems.push("measurements must be an array");
|
|
700
|
+
const scope = measuredPathspecs(crateDir, { exists, readFile });
|
|
701
|
+
problems.push(...scope.problems);
|
|
702
|
+
for (const key of deltaFigures) if (!nonEmpty(budget.deltaSources?.[key])) problems.push(`deltaSources.${key} must name its ceiling source`);
|
|
703
|
+
if (budget.deltaSources !== undefined && (!budget.deltaSources || typeof budget.deltaSources !== "object" || Array.isArray(budget.deltaSources) || Object.keys(budget.deltaSources).some((key) => !deltaFigures.includes(key)))) problems.push("deltaSources must name only declared delta figures");
|
|
704
|
+
for (const { m, index } of valid) if (!atScale(m)) problems.push(`measurement[${index}] is below required scale`);
|
|
705
|
+
}
|
|
619
706
|
const staleness = new Map();
|
|
620
707
|
const stale = (m) => {
|
|
621
708
|
if (!staleness.has(m.commit)) staleness.set(m.commit, measurementStaleness(m.commit, crateDir, git, { exists, readFile }));
|
|
@@ -624,7 +711,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
624
711
|
|
|
625
712
|
// Required hosts: one fresh, in-ceiling, at-scale measurement of this version on each.
|
|
626
713
|
// A held-resource crate defers measurement to adoption: no per-host publish measurement is required.
|
|
627
|
-
const required = heldResource ? [] : requiredBudgetHosts(budget, evidence);
|
|
714
|
+
const required = heldResource || structural ? [] : requiredBudgetHosts(budget, evidence);
|
|
628
715
|
for (const host of required) {
|
|
629
716
|
const onHost = counted.filter(({ m }) => m.host.os === host);
|
|
630
717
|
if (onHost.length === 0) { problems.push(`no ${HOSTS[host]} measurement for version ${version} (required hosts: ${required.map((h) => HOSTS[h]).join(" and ")}${required.length > 1 ? "; apps ship on both and the release gate requires both unless a darwin-only policy with a reason is recorded" : `; ${required[0]}-only policy: ${String((evidence ?? budget).hostPolicyReason).trim()}`})`); continue; }
|
|
@@ -728,7 +815,17 @@ export function heldResourceProblems({ crate, crateDir, meta }, { readFile = rea
|
|
|
728
815
|
* naming every matched pattern), "held-resource" (resources table, adoption-time proof) or always-on
|
|
729
816
|
* (runtime-budget.json, fully checked). `readFile`, `exists` and `git` are injectable.
|
|
730
817
|
*/
|
|
731
|
-
export function runtimeBudgetCheck(
|
|
818
|
+
export function runtimeBudgetCheck(options, deps = {}) {
|
|
819
|
+
return runtimeBudgetCheckOwned(options, deps);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Source structure only; grants no runtime qualification or semantic admission. */
|
|
823
|
+
export function runtimeBudgetStructureCheck(options, deps = {}) {
|
|
824
|
+
const result = runtimeBudgetCheckOwned({ ...options, requireInstalled: false }, deps, STRUCTURE_ONLY);
|
|
825
|
+
return { ...result, name: "runtime-budget-structure" };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function runtimeBudgetCheckOwned({ crate, crateDir, version, evidence, requireInstalled = false, app }, deps = {}, mode) {
|
|
732
829
|
const { readFile = readFileSync, exists = existsSync } = deps;
|
|
733
830
|
let pkg;
|
|
734
831
|
try { pkg = readCargoPackage(crateDir, { exists, readFile }); } catch (error) {
|
|
@@ -775,9 +872,9 @@ export function runtimeBudgetCheck({ crate, crateDir, version, evidence, require
|
|
|
775
872
|
return fail(crate, crateDir, [`undeclared: none of ${RUNTIME_BUDGET_FILE}, continuous = false with a continuous_reason, or continuous = "held-resource" with resources`],
|
|
776
873
|
`declare the crate (docs/RUNTIME-BUDGETS.md)`);
|
|
777
874
|
}
|
|
778
|
-
const result =
|
|
875
|
+
const result = evaluateRuntimeBudgetOwned({ crate, crateDir, version: version ?? pkg.version, evidence, requireInstalled, app }, deps, mode);
|
|
779
876
|
if (result.ok) {
|
|
780
|
-
return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
|
|
877
|
+
return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: mode === STRUCTURE_ONLY ? `${crate} ${version ?? pkg.version} runtime budget structure is valid; qualification is separate` : `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
|
|
781
878
|
}
|
|
782
879
|
return fail(crate, crateDir, result.problems);
|
|
783
880
|
}
|
package/stage-sidecar.mjs
CHANGED
|
@@ -73,6 +73,28 @@ const MODES = new Set(["dev", "release"]);
|
|
|
73
73
|
const PROFILE = /^[a-z][a-z0-9_-]*$/;
|
|
74
74
|
const PLATFORMS = new Set(["mac", "win", "linux"]);
|
|
75
75
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
76
|
+
/** One path segment of an archive member path: no glob metacharacters, no separators, no drive colon. */
|
|
77
|
+
const MEMBER_SEGMENT = /^[A-Za-z0-9_.+@~-]+$/;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalize and guard a requested archive member. A bare file name (no `/`) selects the one member with that base name; a path
|
|
81
|
+
* (contains `/`) selects the one member with exactly that archive path. A leading `./` is stripped. Absolute paths, drive
|
|
82
|
+
* prefixes, backslashes, empty, `.` and `..` segments and glob metacharacters are refused. Returns { value, byPath } or
|
|
83
|
+
* { error }. The member never influences where the extracted file is written.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeArchiveMember(member) {
|
|
86
|
+
if (typeof member !== "string" || !member) return { error: "must be a non-empty string" };
|
|
87
|
+
if (member.includes("\\")) return { error: "must not contain a backslash" };
|
|
88
|
+
const value = member.startsWith("./") ? member.slice(2) : member;
|
|
89
|
+
if (value.startsWith("/")) return { error: "must not be an absolute path" };
|
|
90
|
+
if (/^[A-Za-z]:/.test(value)) return { error: "must not start with a drive prefix" };
|
|
91
|
+
const segments = value.split("/");
|
|
92
|
+
if (segments.some((segment) => segment === "")) return { error: "must not contain an empty segment" };
|
|
93
|
+
if (segments.some((segment) => segment === "." || segment === "..")) return { error: "must not contain a '.' or '..' segment" };
|
|
94
|
+
// A bare file name keeps today's matching (any characters but the refused ones above); a path is held to a strict segment alphabet so it can never act as a tar glob.
|
|
95
|
+
if (segments.length > 1 && !segments.every((segment) => MEMBER_SEGMENT.test(segment))) return { error: "has a segment with characters outside A-Z a-z 0-9 _ . + @ ~ -" };
|
|
96
|
+
return { value, byPath: segments.length > 1 };
|
|
97
|
+
}
|
|
76
98
|
const KEYS = Object.freeze({
|
|
77
99
|
"cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
|
|
78
100
|
ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "sources", "probe", "platforms"],
|
|
@@ -390,7 +412,7 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
390
412
|
if (item.dest === undefined && item.to === undefined) errors.push(`${where}: dest (or to) is required`);
|
|
391
413
|
const dest = destOf(item.dest ?? item.to, item.dest === undefined ? "to" : "dest");
|
|
392
414
|
if (item.size !== undefined && (!Number.isSafeInteger(item.size) || item.size <= 0)) errors.push(`${where}: size must be a positive integer (bytes)`);
|
|
393
|
-
if (item.extract !== undefined
|
|
415
|
+
if (item.extract !== undefined) { const member = normalizeArchiveMember(item.extract); if (member.error || (!member.byPath && !NAME.test(member.value))) errors.push(`${where}: extract must be the bare file name of one archive member or its archive path (posix, no leading /, no .. segment): ${member.error ?? "invalid file name"}`); }
|
|
394
416
|
if (item.memberSha256 !== undefined && (item.extract === undefined || !SHA256.test(item.memberSha256 ?? ""))) errors.push(`${where}: memberSha256 must be a lowercase 64-hex SHA-256 and needs extract`);
|
|
395
417
|
if (item.executable !== undefined && typeof item.executable !== "boolean") errors.push(`${where}: executable must be a boolean`);
|
|
396
418
|
const clean = cleanOf();
|
|
@@ -499,15 +521,28 @@ export function defaultRun(command, args, { cwd, env = process.env } = {}) {
|
|
|
499
521
|
});
|
|
500
522
|
}
|
|
501
523
|
|
|
502
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Default archive member extraction (bsdtar reads tgz and zip/nupkg). A bare `fileName` selects the one member whose basename it is;
|
|
526
|
+
* a `fileName` containing `/` selects the one member whose full archive path equals it (case-sensitive, leading `./` ignored on both
|
|
527
|
+
* sides), so archives with the same base name in several directories (ORT DirectML nupkgs) can be addressed. The returned file is
|
|
528
|
+
* always under `destDir`; callers copy it to their own declared output name, so the member path never decides where anything is written.
|
|
529
|
+
*/
|
|
503
530
|
export function defaultExtractMember({ archive, fileName, destDir, run = defaultRun }) {
|
|
531
|
+
const wanted = normalizeArchiveMember(fileName);
|
|
532
|
+
if (wanted.error) fail("STAGE_SIDECAR_EXTRACT_FAILED", `archive member ${JSON.stringify(fileName)} ${wanted.error}`);
|
|
504
533
|
const listing = run("tar", ["-tf", archive], { cwd: path.dirname(archive) });
|
|
505
534
|
if (listing.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot list ${archive}`);
|
|
506
|
-
const
|
|
507
|
-
|
|
535
|
+
const lines = String(listing.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
536
|
+
const members = wanted.byPath
|
|
537
|
+
? lines.filter((line) => (line.startsWith("./") ? line.slice(2) : line) === wanted.value)
|
|
538
|
+
: lines.filter((line) => path.posix.basename(line) === wanted.value);
|
|
539
|
+
if (members.length !== 1) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${archive} has ${members.length} members ${wanted.byPath ? "at the path" : "named"} ${wanted.value}, expected exactly one`);
|
|
508
540
|
const extracted = run("tar", ["-xf", archive, "-C", destDir, members[0]], { cwd: path.dirname(archive) });
|
|
509
541
|
if (extracted.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot extract ${members[0]} from ${archive}`);
|
|
510
|
-
|
|
542
|
+
const out = path.join(destDir, ...members[0].split("/"));
|
|
543
|
+
const inside = path.relative(path.resolve(destDir), path.resolve(out));
|
|
544
|
+
if (!inside || inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) fail("STAGE_SIDECAR_EXTRACT_FAILED", `member ${members[0]} of ${archive} resolves outside the extraction directory`);
|
|
545
|
+
return out;
|
|
511
546
|
}
|
|
512
547
|
|
|
513
548
|
async function defaultFetchBytes(url) {
|
|
@@ -900,6 +935,10 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
|
|
|
900
935
|
});
|
|
901
936
|
for (const { source } of notices) if (!fs.existsSync(source)) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file not found: ${source}`);
|
|
902
937
|
|
|
938
|
+
// The archive member is the catalog's archive.member (an archive path) when present, else the library file name; the output name never comes from it.
|
|
939
|
+
const member = row.archive.member ?? row.artifactFileName;
|
|
940
|
+
const memberCheck = normalizeArchiveMember(member);
|
|
941
|
+
if (memberCheck.error) fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} has an invalid archive member ${JSON.stringify(member)}: ${memberCheck.error}`);
|
|
903
942
|
const outName = entry.as ?? row.artifactFileName;
|
|
904
943
|
const dest = path.resolve(appRoot, destDir ?? entry.resourceDir, outName);
|
|
905
944
|
if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === row.sha256) {
|
|
@@ -915,9 +954,9 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
|
|
|
915
954
|
}
|
|
916
955
|
const scratch = ctx.makeTempDir();
|
|
917
956
|
try {
|
|
918
|
-
const library = await ctx.extractMember({ archive, fileName:
|
|
957
|
+
const library = await ctx.extractMember({ archive, fileName: member, destDir: scratch });
|
|
919
958
|
const actual = sha256Of(fs.readFileSync(library));
|
|
920
|
-
if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${
|
|
959
|
+
if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${member} extracted as ${actual}, the catalog pins ${row.sha256}; nothing staged`, { expected: row.sha256, actual });
|
|
921
960
|
copyInto(ctx, library, dest, { executable: false });
|
|
922
961
|
} finally {
|
|
923
962
|
ctx.removeDir(scratch);
|
|
@@ -1042,7 +1081,7 @@ async function ensureCachedDownload(ctx, { url, sha256, size }) {
|
|
|
1042
1081
|
|
|
1043
1082
|
/**
|
|
1044
1083
|
* A pinned https download. `dest` is copied only from the verified cache entry. With `extract` the download is an archive
|
|
1045
|
-
* (zip, tgz): the one member whose base name is `extract` is unpacked (digest-checked against `memberSha256` when given) and
|
|
1084
|
+
* (zip, tgz): the one member whose base name is `extract` (or, when `extract` contains `/`, whose archive path equals it) is unpacked (digest-checked against `memberSha256` when given) and
|
|
1046
1085
|
* only that member is staged, so no `command` entry or shell is needed to unpack it.
|
|
1047
1086
|
*/
|
|
1048
1087
|
async function stageRemote(entry, ctx) {
|
package/swap-helper-bundle.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// The Developer-ID-signed `bin/darwin/rightkit-swap` that @rightkit/release ships (Orthic-Labs/rightkit#258).
|
|
2
|
-
// rightkit-swap is the bin target of crates/rightkit-fs (renamex_np RENAME_SWAP).
|
|
3
|
-
//
|
|
4
|
-
// `
|
|
2
|
+
// rightkit-swap is the bin target of crates/rightkit-fs (renamex_np RENAME_SWAP). Two signing routes produce it:
|
|
3
|
+
// (a) local publish — `right-release publish npm --local` on the Mac calls `prepareSwapHelper` before packing;
|
|
4
|
+
// (b) CI qualification — `publish.yml` build-swap -> sign-swap -> pack (docs/CI.md), where sign-swap imports the
|
|
5
|
+
// Developer ID from the `release` environment secrets. Route (b) stays blocked until root provisions
|
|
6
|
+
// APPLE_CERTIFICATE_BASE64/APPLE_CERTIFICATE_PASSWORD there; exporting that identity needs an Aqua login session
|
|
7
|
+
// (SSH export fails errSecInteractionNotAllowed/-25308 & the signing agent has no key-export capability — never
|
|
8
|
+
// retry either). install-app calls `assertShippedSwapHelperSigned` before it trusts the helper.
|
|
5
9
|
import crypto from "node:crypto";
|
|
6
10
|
import nodeFs from "node:fs";
|
|
7
11
|
import path from "node:path";
|