@rightkit/release 0.3.0 → 0.4.0
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 +25 -4
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +11 -2
- package/codesign-environment.mjs +2 -2
- package/content-digest.mjs +209 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/github-updater-release.mjs +70 -7
- package/in-place-install.mjs +17 -14
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +21 -40
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +92 -37
- package/managed-cargo.mjs +8 -0
- package/mirror-root-artifact.mjs +5 -1
- package/native-cargo-layout.mjs +47 -4
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +1 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +49 -7
- package/package-recipe-targets.mjs +41 -1
- package/package.json +1 -1
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +15 -4
- package/publish-cargo.mjs +109 -22
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +45 -17
- package/release.mjs +69 -17
- package/rightkit-ownership.json +1 -1
- package/rightkit-versions.json +379 -189
- package/runtime-budgets.mjs +120 -16
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +160 -32
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
package/runtime-budgets.mjs
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// "hostPolicyReason": "no Windows backend",
|
|
19
19
|
// "ceilings": { "residentMemoryBytes": N, "idleCpuPercent": N, "wakeupsPerSecond": N, "diskOpsPerSecond": N }, // null = undeclared, refused
|
|
20
20
|
// "ceilingSources": { "residentMemoryBytes": "path:line" },
|
|
21
|
+
// "ceilingBasis": { "wakeupsPerSecond": "delta-over-bare" }, // optional; see "Delta basis" below
|
|
22
|
+
// "deltaCeilings": { "wakeupsPerSecond": N }, // required for, and only for, a figure with a delta basis
|
|
21
23
|
// "requiredScale": { "unit": "entries", "minCount": 1000000, "source": "path:line" }, // null = undeclared, refused
|
|
22
24
|
// "status": { "api": "Index::status()", "file": "src/index.rs", "exposes": ["residentMemoryBytes", ...] },
|
|
23
25
|
// "measurements": [ {
|
|
@@ -32,8 +34,20 @@
|
|
|
32
34
|
// Measurements are written by `node runtime-budgets.mjs record`, never typed: the raw bench output is
|
|
33
35
|
// stored under runtime-budget.evidence/, and the gate re-parses it and compares every value.
|
|
34
36
|
//
|
|
37
|
+
// Delta basis. A whole-process wake-up rate on a desktop host is dominated by host and window-server
|
|
38
|
+
// activity the crate does not own. A crate may declare `ceilingBasis.wakeupsPerSecond = "delta-over-bare"`:
|
|
39
|
+
// its synthetic bench then measures the crate's workload and a bare runtime loop back to back (separate
|
|
40
|
+
// processes), and the gate compares (wake-ups - bare wake-ups), computed here from the two raw figures,
|
|
41
|
+
// to `deltaCeilings.wakeupsPerSecond`. A synthetic measurement of that crate must carry `baseline` and
|
|
42
|
+
// `delta` (`{ wakeupsPerSecond }`); one without a baseline is refused, and a reported delta that differs
|
|
43
|
+
// from raw minus bare is refused (the gate never trusts it). `ceilings.wakeupsPerSecond` stays the
|
|
44
|
+
// whole-process ceiling and keeps governing every other scope (real, installed) and app adoption, so an
|
|
45
|
+
// installed app is never compared to a delta ceiling and nothing weakens for apps. Measurements of
|
|
46
|
+
// other versions are history and are not re-gated, so they need no baseline.
|
|
47
|
+
//
|
|
35
48
|
// Bench output the `record` command accepts (exactly one such line in the file):
|
|
36
49
|
// RESULT budget crate=<name> version=<semver> resident_bytes=<int> idle_cpu_percent=<n> wakeups_per_s=<n> disk_ops_per_s=<n> scale_unit=<word> scale_count=<int>
|
|
50
|
+
// optionally followed by `bare_wakeups_per_s=<n> wakeups_delta_per_s=<n>` (both or neither; delta may be negative).
|
|
37
51
|
//
|
|
38
52
|
// Check objects use the preflight shape ({ name, status, detail, fix }) so formatPreflight() in
|
|
39
53
|
// preflight.mjs prints them as `[FAIL] runtime-budget: ...`.
|
|
@@ -45,7 +59,7 @@ import path from "node:path";
|
|
|
45
59
|
import { fileURLToPath } from "node:url";
|
|
46
60
|
import { spawnSync } from "./background-process.mjs";
|
|
47
61
|
import { parse } from "smol-toml";
|
|
48
|
-
import {
|
|
62
|
+
import { singleHostOfPolicy } from "./publication-gate.mjs";
|
|
49
63
|
import { findWorkspaceRoot, lockClosure, pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
|
|
50
64
|
|
|
51
65
|
export const RUNTIME_BUDGET_CHECK = "runtime-budget";
|
|
@@ -83,6 +97,14 @@ const HOSTS = Object.freeze({ darwin: "macOS", win32: "Windows" });
|
|
|
83
97
|
const SHA = /^[0-9a-f]{40}$/;
|
|
84
98
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
85
99
|
const SCOPES = Object.freeze(["synthetic", "real", "installed"]);
|
|
100
|
+
// Figures that may be gated as a delta over a bare-runtime baseline, and the only scope that carries one.
|
|
101
|
+
export const CEILING_BASIS_DELTA = "delta-over-bare";
|
|
102
|
+
export const DELTA_FIGURES = Object.freeze(["wakeupsPerSecond"]);
|
|
103
|
+
const DELTA_SCOPE = "synthetic";
|
|
104
|
+
// Each rate is printed to 3 decimals, so raw - bare and the printed delta may differ by rounding only.
|
|
105
|
+
const DELTA_TOLERANCE = 0.002;
|
|
106
|
+
// A baseline more than this far above its own watchdog run is a mismatched or corrupt pair, not a quiet crate.
|
|
107
|
+
const DELTA_FLOOR = -2;
|
|
86
108
|
|
|
87
109
|
// Long-lived work a crate must declare. Each id must be named in `continuous_reason` for a `continuous = false`
|
|
88
110
|
// crate; for a held-resource crate every file that matches must be listed in `resources`.
|
|
@@ -132,7 +154,8 @@ export function isRuntimeBudgetCrate(crate, manifestText) {
|
|
|
132
154
|
|
|
133
155
|
/** Hosts the budget must be measured on. `evidence` (release evidence) is the policy authority when given. */
|
|
134
156
|
export function requiredBudgetHosts(budget, evidence) {
|
|
135
|
-
|
|
157
|
+
const single = singleHostOfPolicy(evidence ?? budget);
|
|
158
|
+
return single ? [single] : ["darwin", "win32"];
|
|
136
159
|
}
|
|
137
160
|
|
|
138
161
|
// ---------------------------------------------------------------- README view (generated from the JSON)
|
|
@@ -183,7 +206,10 @@ export function renderRuntimeBudgetBody(budget) {
|
|
|
183
206
|
out.push("");
|
|
184
207
|
out.push(`- Resident memory ceiling: ${figure("residentMemoryBytes", budget.ceilings?.residentMemoryBytes)}.`);
|
|
185
208
|
out.push(`- CPU ceiling (idle, steady state): ${figure("idleCpuPercent", budget.ceilings?.idleCpuPercent)}.`);
|
|
186
|
-
|
|
209
|
+
if (budget.ceilingBasis?.wakeupsPerSecond === CEILING_BASIS_DELTA) {
|
|
210
|
+
out.push(`- Wake-ups ceiling (whole process; installed and real measurements): ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
|
|
211
|
+
out.push(`- Wake-ups gate (synthetic bench): at most ${figure("wakeupsPerSecond", budget.deltaCeilings?.wakeupsPerSecond)} over a bare runtime loop measured in the same bench run.`);
|
|
212
|
+
} else out.push(`- Wake-ups ceiling: ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
|
|
187
213
|
out.push(`- Disk operations ceiling: ${figure("diskOpsPerSecond", budget.ceilings?.diskOpsPerSecond)}.`);
|
|
188
214
|
const rs = budget.requiredScale;
|
|
189
215
|
out.push(rs && rs.unit && Number.isFinite(rs.minCount) ? `- Required scale: ${count(rs.minCount)} ${rs.unit} or more.` : "- Required scale: undeclared.");
|
|
@@ -200,7 +226,9 @@ export function renderRuntimeBudgetBody(budget) {
|
|
|
200
226
|
const scale = `${count(m.dataScale?.count)} ${m.dataScale?.unit} (${m.dataScale?.description})`;
|
|
201
227
|
const app = m.scope === "installed" && m.app ? `, app ${m.app.name} ${m.app.version}` : "";
|
|
202
228
|
const observed = BUDGET_FIGURES.map((k) => `${FIGURE_LABEL[k]} ${m.observed?.[k] === undefined || m.observed?.[k] === null ? "n/a (not measurable on this host)" : FORMAT[k](m.observed[k])}`).join(", ");
|
|
203
|
-
|
|
229
|
+
const bare = isNumber(m.baseline?.wakeupsPerSecond) && isNumber(m.delta?.wakeupsPerSecond)
|
|
230
|
+
? `; bare baseline wake-ups ${FORMAT.wakeupsPerSecond(m.baseline.wakeupsPerSecond)}, delta ${FORMAT.wakeupsPerSecond(m.delta.wakeupsPerSecond)}` : "";
|
|
231
|
+
out.push(`- Measured: ${host}, ${budget.crate} ${m.crateVersion} at ${String(m.commit).slice(0, 12)}, ${scale}, ${m.scope}${app}, \`${m.command}\`, ${m.date}, evidence ${String(m.evidence?.sha256 ?? "none").slice(0, 12)}: ${observed}${bare}.`);
|
|
204
232
|
}
|
|
205
233
|
return out;
|
|
206
234
|
}
|
|
@@ -297,6 +325,8 @@ export const HOST_UNAVAILABLE_FIGURES = Object.freeze({ darwin: Object.freeze(["
|
|
|
297
325
|
export const figureUnavailableOn = (os, key) => (HOST_UNAVAILABLE_FIGURES[os] ?? []).includes(key);
|
|
298
326
|
const RESULT_NUMBERS = Object.freeze({ resident_bytes: "residentMemoryBytes", idle_cpu_percent: "idleCpuPercent", wakeups_per_s: "wakeupsPerSecond", disk_ops_per_s: "diskOpsPerSecond", scale_count: "scaleCount" });
|
|
299
327
|
const RESULT_WORDS = Object.freeze(["crate", "version", "scale_unit"]);
|
|
328
|
+
// Optional pair emitted by a delta-basis bench: the bare-runtime wake-up rate and the bench's own reported delta.
|
|
329
|
+
const RESULT_DELTA = Object.freeze({ bare_wakeups_per_s: "bareWakeupsPerSecond", wakeups_delta_per_s: "wakeupsDeltaPerSecond" });
|
|
300
330
|
|
|
301
331
|
/** Parses the one `RESULT budget ...` line of a bench's raw output. */
|
|
302
332
|
export function parseBudgetResult(text) {
|
|
@@ -308,7 +338,7 @@ export function parseBudgetResult(text) {
|
|
|
308
338
|
const eq = token.indexOf("=");
|
|
309
339
|
const key = eq > 0 ? token.slice(0, eq) : token;
|
|
310
340
|
const value = eq > 0 ? token.slice(eq + 1) : "";
|
|
311
|
-
if (!(key in RESULT_NUMBERS) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
|
|
341
|
+
if (!(key in RESULT_NUMBERS) && !(key in RESULT_DELTA) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
|
|
312
342
|
else if (key in fields) problems.push(`duplicate field ${key}`);
|
|
313
343
|
else fields[key] = value;
|
|
314
344
|
}
|
|
@@ -320,6 +350,14 @@ export function parseBudgetResult(text) {
|
|
|
320
350
|
else if (fields[key] === undefined || fields[key] === "" || !Number.isFinite(n) || n < 0) problems.push(`field ${key} must be a non-negative number`);
|
|
321
351
|
else value[name] = n;
|
|
322
352
|
}
|
|
353
|
+
const present = Object.keys(RESULT_DELTA).filter((key) => key in fields);
|
|
354
|
+
if (present.length === 1) problems.push(`fields ${Object.keys(RESULT_DELTA).join(" and ")} must appear together`);
|
|
355
|
+
for (const key of present) {
|
|
356
|
+
const n = Number(fields[key]);
|
|
357
|
+
// The delta is a difference and may be negative; the bare rate is a rate and may not.
|
|
358
|
+
if (fields[key] === "" || !Number.isFinite(n) || (key === "bare_wakeups_per_s" && n < 0)) problems.push(`field ${key} must be a ${key === "bare_wakeups_per_s" ? "non-negative " : ""}number`);
|
|
359
|
+
else value[RESULT_DELTA[key]] = n;
|
|
360
|
+
}
|
|
323
361
|
return problems.length ? { ok: false, problems } : { ok: true, problems: [], value };
|
|
324
362
|
}
|
|
325
363
|
|
|
@@ -343,6 +381,14 @@ function checkEvidence(m, index, budget, crateDir, { exists, readFile }) {
|
|
|
343
381
|
if (v.version !== m.crateVersion) problems.push(`evidence version ${v.version} != recorded ${m.crateVersion}`);
|
|
344
382
|
for (const key of BUDGET_FIGURES) if (v[key] !== m.observed[key]) problems.push(`evidence ${key} ${v[key]} != recorded ${m.observed[key]}`);
|
|
345
383
|
if (v.scale_unit !== m.dataScale.unit || v.scaleCount !== m.dataScale.count) problems.push("evidence data scale differs from the record");
|
|
384
|
+
// The baseline and delta belong to the record exactly when the raw output carries them.
|
|
385
|
+
const recordedBare = m.baseline?.wakeupsPerSecond;
|
|
386
|
+
const recordedDelta = m.delta?.wakeupsPerSecond;
|
|
387
|
+
if (v.bareWakeupsPerSecond !== recordedBare) problems.push(`evidence bare_wakeups_per_s ${v.bareWakeupsPerSecond} != recorded baseline ${recordedBare}`);
|
|
388
|
+
if (v.wakeupsDeltaPerSecond !== recordedDelta) problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} != recorded delta ${recordedDelta}`);
|
|
389
|
+
if (v.bareWakeupsPerSecond !== undefined && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
|
|
390
|
+
problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} disagrees with wakeups_per_s ${v.wakeupsPerSecond} minus bare_wakeups_per_s ${v.bareWakeupsPerSecond}`);
|
|
391
|
+
}
|
|
346
392
|
return problems.map((p) => `${at} ${p}`);
|
|
347
393
|
}
|
|
348
394
|
|
|
@@ -423,9 +469,19 @@ function validateMeasurement(m, index) {
|
|
|
423
469
|
if (isNumber(m.observed?.residentMemoryBytes) && m.observed.residentMemoryBytes === 0) {
|
|
424
470
|
problems.push(`${at} observed.residentMemoryBytes of 0 is implausible for a running process`);
|
|
425
471
|
}
|
|
472
|
+
// Optional bare-runtime baseline and reported delta: shape only here; the gate recomputes the delta.
|
|
473
|
+
for (const [field, signed] of [["baseline", false], ["delta", true]]) {
|
|
474
|
+
if (m[field] === undefined) continue;
|
|
475
|
+
const ok = m[field] && typeof m[field] === "object" && Object.keys(m[field]).every((key) => DELTA_FIGURES.includes(key) && isNumber(m[field][key]) && (signed || m[field][key] >= 0));
|
|
476
|
+
if (!ok) problems.push(`${at} ${field} must map ${DELTA_FIGURES.join(", ")} to ${signed ? "numbers" : "non-negative numbers"}`);
|
|
477
|
+
}
|
|
478
|
+
if ((m.baseline === undefined) !== (m.delta === undefined)) problems.push(`${at} baseline and delta must be recorded together`);
|
|
426
479
|
return problems;
|
|
427
480
|
}
|
|
428
481
|
|
|
482
|
+
/** Figures of `budget` gated as a delta over the bare baseline (validated separately; unknown bases never match). */
|
|
483
|
+
const deltaFiguresOf = (budget) => DELTA_FIGURES.filter((key) => budget.ceilingBasis?.[key] === CEILING_BASIS_DELTA);
|
|
484
|
+
|
|
429
485
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
430
486
|
|
|
431
487
|
/** True when `api` (`Type::method()` or a bare type) is a `pub fn` / `pub struct|enum|trait` item in `source`, not in a comment. */
|
|
@@ -456,14 +512,15 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
456
512
|
if (budget.schema !== RUNTIME_BUDGET_SCHEMA) problems.push(`${RUNTIME_BUDGET_FILE} schema must be ${RUNTIME_BUDGET_SCHEMA}`);
|
|
457
513
|
if (budget.crate !== crate) problems.push(`${RUNTIME_BUDGET_FILE} is for ${budget.crate}, expected ${crate}`);
|
|
458
514
|
if (!nonEmpty(version)) problems.push("publishing version is unknown, cannot bind the measurements to it");
|
|
459
|
-
if (budget.hostPolicy !== undefined && budget.hostPolicy !== "darwin-only") problems.push('hostPolicy must be "darwin-only" when present');
|
|
460
|
-
if (budget.hostPolicy === "darwin-only" && !nonEmpty(budget.hostPolicyReason)) problems.push(
|
|
515
|
+
if (budget.hostPolicy !== undefined && budget.hostPolicy !== "darwin-only" && budget.hostPolicy !== "win32-only") problems.push('hostPolicy must be "darwin-only" or "win32-only" when present');
|
|
516
|
+
if ((budget.hostPolicy === "darwin-only" || budget.hostPolicy === "win32-only") && !nonEmpty(budget.hostPolicyReason)) problems.push(`hostPolicy ${budget.hostPolicy} needs a hostPolicyReason`);
|
|
461
517
|
// One source of truth for the host policy: the release evidence. The budget file may repeat it, never differ.
|
|
462
518
|
if (evidence) {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
519
|
+
const policyLabel = (hosts) => (hosts.length === 1 ? `${hosts[0]}-only` : "macOS and Windows");
|
|
520
|
+
const evidenceHosts = requiredBudgetHosts(null, evidence);
|
|
521
|
+
const budgetHosts = requiredBudgetHosts(budget);
|
|
522
|
+
if (evidenceHosts.join() !== budgetHosts.join()) {
|
|
523
|
+
problems.push(`hostPolicy disagrees with the release evidence (evidence: ${policyLabel(evidenceHosts)}; ${RUNTIME_BUDGET_FILE}: ${policyLabel(budgetHosts)})`);
|
|
467
524
|
}
|
|
468
525
|
}
|
|
469
526
|
|
|
@@ -476,6 +533,24 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
476
533
|
else ceilings[key] = value;
|
|
477
534
|
}
|
|
478
535
|
|
|
536
|
+
// Ceiling basis: an explicit, closed declaration. A delta figure needs its own positive delta ceiling,
|
|
537
|
+
// and a delta ceiling without the basis would be dead text, so both directions are refused.
|
|
538
|
+
const basis = budget.ceilingBasis;
|
|
539
|
+
if (basis !== undefined && (!basis || typeof basis !== "object" || Array.isArray(basis))) problems.push("ceilingBasis must be an object");
|
|
540
|
+
else for (const [key, value] of Object.entries(basis ?? {})) {
|
|
541
|
+
if (!DELTA_FIGURES.includes(key)) problems.push(`ceilingBasis.${key} is not a figure that can use a delta basis (${DELTA_FIGURES.join(", ")})`);
|
|
542
|
+
else if (value !== CEILING_BASIS_DELTA) problems.push(`ceilingBasis.${key} must be "${CEILING_BASIS_DELTA}"`);
|
|
543
|
+
}
|
|
544
|
+
const deltaFigures = deltaFiguresOf(budget);
|
|
545
|
+
const deltaCeilings = {};
|
|
546
|
+
for (const key of DELTA_FIGURES) {
|
|
547
|
+
const value = budget.deltaCeilings?.[key];
|
|
548
|
+
if (deltaFigures.includes(key)) {
|
|
549
|
+
if (!isNumber(value) || value <= 0) problems.push(`deltaCeilings.${key} must be a positive number (ceilingBasis.${key} is ${CEILING_BASIS_DELTA})`);
|
|
550
|
+
else deltaCeilings[key] = value;
|
|
551
|
+
} else if (value !== undefined) problems.push(`deltaCeilings.${key} is declared but ceilingBasis.${key} is not ${CEILING_BASIS_DELTA}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
479
554
|
// Required scale: the unit and minimum the crate's measurement must reach.
|
|
480
555
|
const scale = budget.requiredScale;
|
|
481
556
|
let requiredScale = null;
|
|
@@ -511,14 +586,35 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
511
586
|
if (valid.length && nonEmpty(version) && counted.length === 0) {
|
|
512
587
|
problems.push(`no measurement for version ${version} (recorded: ${[...new Set(valid.map(({ m }) => m.crateVersion))].join(", ")})`);
|
|
513
588
|
}
|
|
589
|
+
// Why a measurement fails one figure, or null. A delta-basis figure of a synthetic measurement is gated on
|
|
590
|
+
// observed - baseline, recomputed here; the reported delta must agree and the baseline must exist. Every
|
|
591
|
+
// other scope (real, installed) keeps the whole-process ceiling, so an app is never held to a delta.
|
|
592
|
+
const usesDelta = (m, key) => deltaFigures.includes(key) && m.scope === DELTA_SCOPE;
|
|
593
|
+
const figureFailure = (m, key) => {
|
|
594
|
+
if (usesDelta(m, key)) {
|
|
595
|
+
const raw = m.observed[key];
|
|
596
|
+
const bare = m.baseline?.[key];
|
|
597
|
+
const reported = m.delta?.[key];
|
|
598
|
+
if (deltaCeilings[key] === undefined) return null; // the declaration problem is already reported
|
|
599
|
+
if (!isNumber(raw)) return `${FIGURE_LABEL[key]} is not measurable here but the ${FIGURE_LABEL[key]} ceiling is a delta over a bare baseline`;
|
|
600
|
+
if (!isNumber(bare) || !isNumber(reported)) return `has no bare baseline for ${FIGURE_LABEL[key]} (ceilingBasis.${key} is ${CEILING_BASIS_DELTA}; re-run the bench and record it again)`;
|
|
601
|
+
const delta = Number((raw - bare).toFixed(3)); // the bench prints 3 decimals; compare at that precision
|
|
602
|
+
if (Math.abs(delta - reported) > DELTA_TOLERANCE) return `reported ${FIGURE_LABEL[key]} delta ${reported} disagrees with observed ${raw} minus baseline ${bare}`;
|
|
603
|
+
if (delta < DELTA_FLOOR) return `bare baseline ${bare} is more than ${-DELTA_FLOOR} ${FIGURE_LABEL[key]}/s above observed ${raw}: a mismatched pair; re-run the bench`;
|
|
604
|
+
return delta > deltaCeilings[key] ? `exceeds the ${FIGURE_LABEL[key]} delta ceiling over the bare baseline: ${raw} - ${bare} = ${delta} > ${deltaCeilings[key]}` : null;
|
|
605
|
+
}
|
|
606
|
+
if (ceilings[key] !== undefined && m.observed[key] !== null && m.observed[key] > ceilings[key]) {
|
|
607
|
+
return `exceeds the ${FIGURE_LABEL[key]} ceiling: observed ${m.observed[key]} > ${ceilings[key]}`;
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
};
|
|
514
611
|
for (const { m, index } of counted) {
|
|
515
612
|
for (const key of BUDGET_FIGURES) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
613
|
+
const failure = figureFailure(m, key);
|
|
614
|
+
if (failure) problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} ${failure}`);
|
|
519
615
|
}
|
|
520
616
|
}
|
|
521
|
-
const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && (m.observed[key] === null ? figureUnavailableOn(m.host?.os, key) : m.observed[key] <= ceilings[key]));
|
|
617
|
+
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]));
|
|
522
618
|
const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
|
|
523
619
|
const staleness = new Map();
|
|
524
620
|
const stale = (m) => {
|
|
@@ -531,7 +627,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
531
627
|
const required = heldResource ? [] : requiredBudgetHosts(budget, evidence);
|
|
532
628
|
for (const host of required) {
|
|
533
629
|
const onHost = counted.filter(({ m }) => m.host.os === host);
|
|
534
|
-
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" : `;
|
|
630
|
+
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; }
|
|
535
631
|
const fresh = onHost.filter(({ m }) => stale(m) === null);
|
|
536
632
|
if (fresh.length === 0) { problems.push(`${HOSTS[host]} measurement is stale: ${onHost.map(({ m }) => stale(m)).join("; ")}`); continue; }
|
|
537
633
|
const within = fresh.filter(({ m }) => withinCeilings(m));
|
|
@@ -839,6 +935,13 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
|
|
|
839
935
|
if (stale) throw new Error(`refusing to record: ${stale} (commit the code under test, then re-run the bench)`);
|
|
840
936
|
const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
|
|
841
937
|
const budget = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
938
|
+
const hasBaseline = v.bareWakeupsPerSecond !== undefined;
|
|
939
|
+
if (scope === DELTA_SCOPE && !hasBaseline && deltaFiguresOf(budget).length) {
|
|
940
|
+
throw new Error(`bench output rejected: ${RUNTIME_BUDGET_FILE} gates ${deltaFiguresOf(budget).join(", ")} as ${CEILING_BASIS_DELTA}, so the RESULT line needs bare_wakeups_per_s and wakeups_delta_per_s`);
|
|
941
|
+
}
|
|
942
|
+
if (hasBaseline && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
|
|
943
|
+
throw new Error("bench output rejected: wakeups_delta_per_s disagrees with wakeups_per_s minus bare_wakeups_per_s");
|
|
944
|
+
}
|
|
842
945
|
const evidenceName = `${host.os}-${host.arch}-${pkg.version}-${scope}${scope === "installed" ? `-${app.name}` : ""}.txt`.replace(/[^\w.-]/g, "_");
|
|
843
946
|
const evidenceRel = `${RUNTIME_BUDGET_EVIDENCE_DIR}/${evidenceName}`;
|
|
844
947
|
fs.mkdirSync(path.join(crateDir, RUNTIME_BUDGET_EVIDENCE_DIR), { recursive: true });
|
|
@@ -849,6 +952,7 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
|
|
|
849
952
|
dataScale: { description, unit: v.scale_unit, count: v.scaleCount },
|
|
850
953
|
command,
|
|
851
954
|
observed: Object.fromEntries(BUDGET_FIGURES.map((key) => [key, v[key]])),
|
|
955
|
+
...(hasBaseline ? { baseline: { wakeupsPerSecond: v.bareWakeupsPerSecond }, delta: { wakeupsPerSecond: v.wakeupsDeltaPerSecond } } : {}),
|
|
852
956
|
evidence: { file: evidenceRel, sha256: sha256Of(raw) },
|
|
853
957
|
};
|
|
854
958
|
const same = (m) => m.host?.os === host.os && m.host?.arch === host.arch && m.crateVersion === pkg.version && m.scope === scope && (m.app?.name ?? null) === (app?.name ?? null);
|
package/sign-updater.mjs
CHANGED
|
@@ -1,68 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
6
|
-
import { refuseManagedEnvSecrets
|
|
6
|
+
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
7
|
+
import { execWithUpdaterCredential } from "./protected-credentials.mjs";
|
|
8
|
+
import { resolvePnpmCli } from "./development.mjs";
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const files = [];
|
|
10
|
-
let dryRun = false;
|
|
11
|
-
let keyFile = process.env.RIGHT_UPDATER_KEY_FILE || "";
|
|
12
|
-
for (let i = 0; i < args.length; i++) {
|
|
13
|
-
if (args[i] === "--file") files.push(path.resolve(args[++i]));
|
|
14
|
-
else if (args[i] === "--key-file") keyFile = args[++i] ?? "";
|
|
15
|
-
else if (args[i] === "--dry-run") dryRun = true;
|
|
16
|
-
else fail(`unknown argument: ${args[i]}`);
|
|
17
|
-
}
|
|
18
|
-
if (!files.length) fail("at least one --file is required");
|
|
10
|
+
export const UPDATER_SIGNING_SECRET = "updater-signing-key";
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
export function runSignUpdater(args, {
|
|
13
|
+
env = process.env, platform = process.platform, cwd = process.cwd(),
|
|
14
|
+
execCredential = execWithUpdaterCredential, resolvePnpm = resolvePnpmCli,
|
|
15
|
+
signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, log = console.log,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const files = [];
|
|
18
|
+
let dryRun = false;
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
if (args[i] === "--file") {
|
|
21
|
+
if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--file requires an artifact path");
|
|
22
|
+
files.push(path.resolve(cwd, args[++i]));
|
|
23
|
+
} else if (args[i] === "--key-file") throw new Error("--key-file is removed: updater keys come from the OS credential provider");
|
|
24
|
+
else if (args[i] === "--dry-run") dryRun = true;
|
|
25
|
+
else throw new Error(`unknown argument: ${args[i]}`);
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
27
|
+
if (!files.length) throw new Error("at least one --file is required");
|
|
28
|
+
if (!dryRun) {
|
|
29
|
+
refuseManagedEnvSecrets(env, ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]);
|
|
30
|
+
for (const key of Object.keys(env)) {
|
|
31
|
+
if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase()) && env[key]) {
|
|
32
|
+
throw new Error("updater signing key/password must come from the OS credential provider");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const file of files) if (!exists(file)) throw new Error(`updater artifact not found: ${file}`);
|
|
36
|
+
}
|
|
37
|
+
const signingEnv = { ...env };
|
|
38
|
+
for (const key of Object.keys(signingEnv)) {
|
|
39
|
+
if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase())) delete signingEnv[key];
|
|
40
|
+
}
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
|
|
43
|
+
if (dryRun) continue;
|
|
44
|
+
remove(`${file}.sig`, { force: true });
|
|
45
|
+
if (platform === "darwin") continue;
|
|
46
|
+
const result = execCredential(process.execPath,
|
|
47
|
+
[resolvePnpm({ root: cwd, env: signingEnv }), "exec", "tauri", "signer", "sign", file],
|
|
48
|
+
{ cwd, env: signingEnv });
|
|
49
|
+
if (result.error || result.status !== 0) {
|
|
50
|
+
const error = new Error(`updater credential provider/signing failed (exit ${result.status ?? "unavailable"})`);
|
|
51
|
+
error.exitCode = result.status ?? 1;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!dryRun && platform === "darwin") signMac(files, { env: signingEnv, cwd });
|
|
56
|
+
return { files, dryRun };
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
60
|
+
try { runSignUpdater(process.argv.slice(2)); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`sign-updater: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
63
|
+
process.exitCode = error.exitCode ?? 1;
|
|
64
|
+
}
|
|
68
65
|
}
|
package/source-gate.mjs
CHANGED
|
@@ -30,11 +30,18 @@ export function stripOwnLockRecords(status) {
|
|
|
30
30
|
return kept.length ? `${kept.join("\0")}\0` : "";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Path of one porcelain v1 record ("XY path"). Tolerates a record whose leading status column was
|
|
34
|
+
* trimmed away by a caller ("M path"), so the first path never loses its first character (#782). */
|
|
35
|
+
function porcelainPath(entry) {
|
|
36
|
+
const m = /^([ MADRCU?!]{2} |[MADRCU?!] )/.exec(entry);
|
|
37
|
+
return m ? entry.slice(m[0].length) : entry;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
export function assertCleanSource({ status, commandId = "release" }) {
|
|
34
41
|
if (typeof status !== "string") throw new Error("release source status is required");
|
|
35
42
|
status = stripOwnLockRecords(status);
|
|
36
43
|
if (status.length !== 0) {
|
|
37
|
-
const files = status.split("\0").filter(Boolean).map(
|
|
44
|
+
const files = status.split("\0").filter(Boolean).map(porcelainPath).slice(0, 10);
|
|
38
45
|
throw new Error(`${commandId} requires a clean working tree; commit or stash first:\n${files.map((file) => `- ${file}`).join("\n")}`);
|
|
39
46
|
}
|
|
40
47
|
}
|