@rightkit/release 0.2.173 → 0.3.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 +188 -5
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +34 -6
- package/command-spec.mjs +80 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development-signing.mjs +135 -4
- package/development.mjs +12 -11
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +32 -7
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +68 -11
- package/install-tauri-devid.mjs +106 -47
- package/local-publish.mjs +317 -29
- package/mac-bundle-signing.mjs +143 -1
- package/mac-signing-matrix.mjs +73 -6
- package/macho-approval-hash.mjs +181 -0
- package/macos-signing-session.mjs +12 -2
- package/managed-cargo.mjs +49 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +69 -21
- package/npm-license-notices.mjs +159 -0
- package/ownership-dependency-allowlist.json +1 -0
- package/ownership.mjs +37 -4
- package/package-recipe-targets.mjs +180 -16
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/portable-development.mjs +9 -9
- package/publication-gate.mjs +70 -3
- package/publish-cargo.mjs +26 -7
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-switch-scan.mjs +17 -7
- package/release.mjs +98 -12
- package/rightkit-ownership.json +46 -2
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +28 -12
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +36 -4
- package/stage-sidecar.mjs +926 -74
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/web-build.mjs +67 -0
package/release.mjs
CHANGED
|
@@ -45,7 +45,9 @@ import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
|
45
45
|
import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
46
46
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
47
47
|
import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs";
|
|
48
|
+
import { assertReleaseDependencies } from "./release-dependencies.mjs";
|
|
48
49
|
import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
|
|
50
|
+
import { expandRecipeBuildCommands } from "./recipe-build.mjs";
|
|
49
51
|
import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
|
|
50
52
|
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
51
53
|
import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
|
|
@@ -356,7 +358,7 @@ try {
|
|
|
356
358
|
{ label: "target.package", command: target.package },
|
|
357
359
|
{ label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
|
|
358
360
|
{ label: "nativeAssembly.finalizer", command: nativeFinalizer },
|
|
359
|
-
{ label:
|
|
361
|
+
...commandList(target.postSign).map((command, index) => ({ label: Array.isArray(target.postSign) ? `target.postSign[${index}]` : "target.postSign", command })),
|
|
360
362
|
{ label: "target.gradle", command: target.gradle },
|
|
361
363
|
] });
|
|
362
364
|
} catch (error) { fail(error.message); }
|
|
@@ -370,6 +372,10 @@ try {
|
|
|
370
372
|
const releaseSwitchInventoryPath = path.resolve(root, config.releaseSwitches?.inventory ?? "switches.json");
|
|
371
373
|
const releaseSwitchInventoryText = existsSync(releaseSwitchInventoryPath) ? readFileSync(releaseSwitchInventoryPath, "utf8") : null;
|
|
372
374
|
try {
|
|
375
|
+
// A `node recipe-build.mjs --plan <JSON>` prePackage hides each step's `--features` in one argument: the closure and the
|
|
376
|
+
// command token scan see every prepare / build / postBuild step (a plan that cannot be read or validated fails closed).
|
|
377
|
+
const closureCommands = [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, ...commandList(target.postSign), target.gradle].filter(Boolean).flatMap((c) => expandRecipeBuildCommands(c, { root }));
|
|
378
|
+
assertReleaseDependencies({ root, config, target, commands: closureCommands });
|
|
373
379
|
let treeText;
|
|
374
380
|
try {
|
|
375
381
|
const closure = resolveFeatureClosure({
|
|
@@ -379,7 +385,7 @@ try {
|
|
|
379
385
|
rust: config.releaseSwitches?.rust,
|
|
380
386
|
features: config.releaseSwitches?.features ?? [],
|
|
381
387
|
triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
382
|
-
commandTexts:
|
|
388
|
+
commandTexts: closureCommands,
|
|
383
389
|
});
|
|
384
390
|
if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
|
|
385
391
|
else treeText = closure.treeText;
|
|
@@ -388,7 +394,7 @@ try {
|
|
|
388
394
|
console.log(`right-release: dry run: ${error.message}`);
|
|
389
395
|
}
|
|
390
396
|
// remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
|
|
391
|
-
const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands:
|
|
397
|
+
const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: closureCommands.map((c) => commandTokens(c)) };
|
|
392
398
|
if (releaseSwitchInventoryText != null) {
|
|
393
399
|
assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
|
|
394
400
|
} else if (treeText != null || closureInput.commands.length) {
|
|
@@ -494,7 +500,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
494
500
|
assertNoQaControlBinaries("macOS app staged for the DMG", [stagedApp], { requireBinary: true, prePack: true });
|
|
495
501
|
}
|
|
496
502
|
if (runMacPhase("sign")) {
|
|
497
|
-
await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
|
|
503
|
+
await signMacos(signingEntries, root, "signing", macSigning.nestedCode, macSigning.nestedSigning);
|
|
498
504
|
if (sidecarPlan && !opts.dryRun) {
|
|
499
505
|
const app = signingEntries.find((entry) => /\.app\/?$/.test(entry.file));
|
|
500
506
|
if (!app) fail("mediaSidecar on macOS needs an .app entry in sign.prePackageFiles");
|
|
@@ -563,14 +569,14 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
563
569
|
// and still installs a binary Windows rejects.
|
|
564
570
|
await verifyWindowsEmbedding({ root, target, files, markers: bundleMarkerReceipts });
|
|
565
571
|
await signWindows(files, "installer", root);
|
|
566
|
-
if (target.postSign) await
|
|
572
|
+
if (target.postSign) await runCommands(target.postSign, root);
|
|
567
573
|
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
568
574
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
569
575
|
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...(target.updater?.keyFile ? ["--key-file", String(target.updater.keyFile)] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
570
576
|
}
|
|
571
577
|
|
|
572
578
|
if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || runMacPhase("post-sign"))) {
|
|
573
|
-
await
|
|
579
|
+
await runCommands(target.postSign, root);
|
|
574
580
|
}
|
|
575
581
|
|
|
576
582
|
if (opts.platform === "mac") {
|
|
@@ -933,6 +939,17 @@ async function runPackageScript(pm, script, cwd) {
|
|
|
933
939
|
await run(pm, ["run", script], cwd);
|
|
934
940
|
}
|
|
935
941
|
|
|
942
|
+
// A hook may be one command object or an ordered array of them (e.g. create-mac-updater then lsclean).
|
|
943
|
+
function commandList(value) {
|
|
944
|
+
if (value == null) return [];
|
|
945
|
+
return Array.isArray(value) ? value : [value];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Runs in order; run() exits the process on the first failure, so the chain fails closed.
|
|
949
|
+
async function runCommands(value, root, options) {
|
|
950
|
+
for (const command of commandList(value)) await runCommand(command, root, options);
|
|
951
|
+
}
|
|
952
|
+
|
|
936
953
|
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
937
954
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
938
955
|
return run(command.cmd, command.args ?? [], cwd, { ...(await commandEnv(command, root)), ...env }, {
|
|
@@ -1117,9 +1134,13 @@ function signExistingRefusal(source) {
|
|
|
1117
1134
|
// The renderer comes from this monorepo's own packages/ci when present, else from the repository's installed @rightkit/ci; with
|
|
1118
1135
|
// neither the file is refused, because an unverifiable sign-existing.yml must not be accepted.
|
|
1119
1136
|
async function loadRightKitCiRenderer(root) {
|
|
1120
|
-
|
|
1137
|
+
return (await loadRightKitCiModule(root, "sync.mjs", "renderLanes"))?.renderLanes ?? null;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function loadRightKitCiModule(root, file, exportName) {
|
|
1141
|
+
const candidates = [pathToFileURL(fileURLToPath(new URL(`../ci/${file}`, import.meta.url))).href];
|
|
1121
1142
|
try {
|
|
1122
|
-
candidates.push(pathToFileURL(createRequire(path.join(root, "package.json")).resolve(
|
|
1143
|
+
candidates.push(pathToFileURL(createRequire(path.join(root, "package.json")).resolve(`@rightkit/ci/${file}`)).href);
|
|
1123
1144
|
} catch {
|
|
1124
1145
|
// not installed in the repository; the sibling candidate may still apply
|
|
1125
1146
|
}
|
|
@@ -1127,7 +1148,7 @@ async function loadRightKitCiRenderer(root) {
|
|
|
1127
1148
|
try {
|
|
1128
1149
|
if (href.startsWith("file:") && !existsSync(fileURLToPath(href))) continue;
|
|
1129
1150
|
const loaded = await import(href);
|
|
1130
|
-
if (typeof loaded
|
|
1151
|
+
if (typeof loaded[exportName] === "function") return loaded;
|
|
1131
1152
|
} catch {
|
|
1132
1153
|
// try the next candidate
|
|
1133
1154
|
}
|
|
@@ -1150,6 +1171,57 @@ async function signExistingRenderRefusal(root, manifest, source) {
|
|
|
1150
1171
|
}
|
|
1151
1172
|
|
|
1152
1173
|
|
|
1174
|
+
// `workflows` block entries render from code, so they are admitted only when the block passes @rightkit/ci's own
|
|
1175
|
+
// validateWorkflows and each file is byte-for-byte the @rightkit/ci render. With no resolvable @rightkit/ci they are refused.
|
|
1176
|
+
async function validateDeclaredWorkflowBlock(root, appName, manifest, declaredFiles) {
|
|
1177
|
+
const validator = (await loadRightKitCiModule(root, "workflows.mjs", "validateWorkflows"))?.validateWorkflows;
|
|
1178
|
+
const renderLanes = await loadRightKitCiRenderer(root);
|
|
1179
|
+
if (!validator || !renderLanes) fail(`${appName} right-git-ci-only policy cannot verify .rightgit.json workflows: @rightkit/ci is not available (install @rightkit/ci as a devDependency)`);
|
|
1180
|
+
const errors = [];
|
|
1181
|
+
validator(manifest, errors);
|
|
1182
|
+
if (errors.length > 0) fail(`${appName} right-git-ci-only policy refuses the .rightgit.json workflows block: ${errors.join("; ")}`);
|
|
1183
|
+
let rendered;
|
|
1184
|
+
try {
|
|
1185
|
+
rendered = renderLanes(manifest, root);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
fail(`${appName} right-git-ci-only policy cannot render the .rightgit.json workflows block: ${error.message}`);
|
|
1188
|
+
}
|
|
1189
|
+
for (const file of declaredFiles) {
|
|
1190
|
+
const expected = rendered.find((entry) => entry.filename === file)?.content;
|
|
1191
|
+
const actual = await readFile(path.join(root, ".github", "workflows", file), "utf8");
|
|
1192
|
+
if (typeof expected !== "string" || actual.replace(/\r\n?/g, "\n") !== expected.replace(/\r\n?/g, "\n")) {
|
|
1193
|
+
fail(`${appName} ${file} is not byte-for-byte the @rightkit/ci render of its .rightgit.json workflows entry (run right-git sync)`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Structural backstop for declared workflows (after render equality): the one top-level permissions block is exactly
|
|
1199
|
+
// `contents: read`, no other permissions key exists, no secrets reference, every `uses:` is commit-pinned, and every
|
|
1200
|
+
// checkout step sets persist-credentials: false. Steps are split on step-list indentation, not matched as free text.
|
|
1201
|
+
function declaredWorkflowPolicyRefusal(appName, workflow, source) {
|
|
1202
|
+
const refuse = (why) => fail(`${appName} right-git managed ${workflow} violates the declared-workflow policy: ${why}`);
|
|
1203
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
1204
|
+
const permissionsAt = lines.flatMap((line, i) => (/^\s*permissions\s*:/.test(line) ? [i] : []));
|
|
1205
|
+
if (permissionsAt.length !== 1 || lines[permissionsAt[0]] !== "permissions:" || lines[permissionsAt[0] + 1] !== " contents: read" || /^ {2,}\S/.test(lines[permissionsAt[0] + 2] ?? "")) refuse("permissions must be exactly one top-level `contents: read`");
|
|
1206
|
+
if (/\bsecrets\b/i.test(source)) refuse("it references the secrets context");
|
|
1207
|
+
let step = null;
|
|
1208
|
+
const steps = [];
|
|
1209
|
+
for (const line of lines) {
|
|
1210
|
+
if (/^ {6}- /.test(line)) { step = [line]; steps.push(step); }
|
|
1211
|
+
else if (step && (/^ {7,}\S/.test(line) || line === "")) step.push(line);
|
|
1212
|
+
else step = null;
|
|
1213
|
+
}
|
|
1214
|
+
for (const body of steps) {
|
|
1215
|
+
const uses = /^ {6}- uses: (\S+)|^ {8}uses: (\S+)/m.exec(body.join("\n"));
|
|
1216
|
+
if (!uses) continue;
|
|
1217
|
+
const ref = uses[1] ?? uses[2];
|
|
1218
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+@[0-9a-f]{40}$/.test(ref)) refuse(`action ${ref} is not commit-pinned`);
|
|
1219
|
+
if (/^actions\/checkout@/.test(ref) && !body.some((line) => /^ {10}persist-credentials: false\s*$/.test(line))) refuse("a checkout step does not set persist-credentials: false");
|
|
1220
|
+
if (body.some((line) => /persist-credentials:\s*true/.test(line))) refuse("persist-credentials: true");
|
|
1221
|
+
}
|
|
1222
|
+
if (/^\s*(?:- )?uses:/m.test(lines.filter((l) => !steps.some((b) => b.includes(l))).join("\n"))) refuse("a uses: sits outside a step list");
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1153
1225
|
async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
1154
1226
|
if (policy !== "right-git-ci-only") {
|
|
1155
1227
|
fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
@@ -1173,15 +1245,29 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
1173
1245
|
|| lanes.some((lane) => !safeLanes.has(lane))) {
|
|
1174
1246
|
fail(`${appName} right-git-ci-only policy requires compile=github-actions-only plus unique .rightgit.json lanes drawn from [${[...safeLanes].map((lane) => `"${lane}"`).join(", ")}] and including "ci" or "swift-check"`);
|
|
1175
1247
|
}
|
|
1176
|
-
|
|
1248
|
+
// Workflows declared under the manifest's `workflows` block (@rightkit/ci workflows.mjs: explicit `file`,
|
|
1249
|
+
// else `<name>.yml`) are managed files too, in addition to the lane files.
|
|
1250
|
+
const declaredWorkflows = manifest?.workflows;
|
|
1251
|
+
if (declaredWorkflows !== undefined && (declaredWorkflows === null || typeof declaredWorkflows !== "object" || Array.isArray(declaredWorkflows))) {
|
|
1252
|
+
fail(`${appName} right-git-ci-only policy requires .rightgit.json workflows to be an object`);
|
|
1253
|
+
}
|
|
1254
|
+
const declaredFiles = Object.entries(declaredWorkflows ?? {}).map(([name, config]) => {
|
|
1255
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) fail(`${appName} right-git-ci-only policy requires .rightgit.json workflows.${name} to be an object`);
|
|
1256
|
+
const file = typeof config?.file === "string" ? config.file : `${name}.yml`;
|
|
1257
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,60}\.yml$/.test(file)) fail(`${appName} right-git-ci-only policy refuses workflow file name ${JSON.stringify(file)} declared by .rightgit.json`);
|
|
1258
|
+
return file;
|
|
1259
|
+
});
|
|
1260
|
+
const expectedWorkflows = [...new Set([...lanes.map((lane) => `${lane}.yml`), ...declaredFiles])].sort();
|
|
1177
1261
|
if (JSON.stringify(workflows) !== JSON.stringify(expectedWorkflows)) {
|
|
1178
1262
|
fail(`${appName} right-git-ci-only policy requires workflows declared by .rightgit.json; expected ${expectedWorkflows.join(", ")}, got: ${workflows.join(", ")}`);
|
|
1179
1263
|
}
|
|
1264
|
+
if (declaredFiles.length > 0) await validateDeclaredWorkflowBlock(root, appName, manifest, declaredFiles);
|
|
1180
1265
|
for (const workflow of expectedWorkflows) {
|
|
1181
1266
|
const source = await readFile(path.join(root, ".github", "workflows", workflow), "utf8");
|
|
1182
1267
|
if (!source.startsWith("# Managed by right-git")) {
|
|
1183
1268
|
fail(`${appName} ${workflow} is not marked as right-git managed`);
|
|
1184
1269
|
}
|
|
1270
|
+
if (declaredFiles.includes(workflow)) declaredWorkflowPolicyRefusal(appName, workflow, source);
|
|
1185
1271
|
const jobSpan = (jobName) => {
|
|
1186
1272
|
const marker = `\n ${jobName}:\n`;
|
|
1187
1273
|
const start = source.indexOf(marker);
|
|
@@ -1462,12 +1548,12 @@ function macSignInvocation(items, receiptPath, extra = []) {
|
|
|
1462
1548
|
return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
|
|
1463
1549
|
}
|
|
1464
1550
|
|
|
1465
|
-
async function signMacos(entries, root, phase = "signing", nestedCode = []) {
|
|
1551
|
+
async function signMacos(entries, root, phase = "signing", nestedCode = [], nestedSigning) {
|
|
1466
1552
|
const files = entries.map((entry) => entry.file);
|
|
1467
1553
|
const receipt = phase === "signing" ? macosReceiptPath(root, "signing") : path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
1468
1554
|
// Directory entries (.app bundles): nested code first (default settings unless sign.nestedCode names it), then the bundle itself.
|
|
1469
1555
|
let nested = [];
|
|
1470
|
-
try { ({ nested } = opts.dryRun ? { nested: [] } : planMacSigning({ entries, nestedCode })); } catch (error) { fail(error.message); }
|
|
1556
|
+
try { ({ nested } = opts.dryRun ? { nested: [] } : planMacSigning({ entries, nestedCode, nestedSigning, root })); } catch (error) { fail(error.message); }
|
|
1471
1557
|
if (opts.dryRun && (nestedCode.length || entries.some(hasNonDefaultSettings))) {
|
|
1472
1558
|
console.log(`right-release: dry-run: per-file signing matrix (${nestedCode.length} nestedCode rule(s), ${entries.filter(hasNonDefaultSettings).length} entry setting(s)); nested code is resolved against the built bundle at sign time`);
|
|
1473
1559
|
}
|
package/rightkit-ownership.json
CHANGED
|
@@ -671,6 +671,17 @@
|
|
|
671
671
|
"Fingerprints identify review candidates; published two-host replacement qualification is not recorded."
|
|
672
672
|
]
|
|
673
673
|
},
|
|
674
|
+
{
|
|
675
|
+
"package": "rightkit-crypto",
|
|
676
|
+
"capability": "crypto-primitives",
|
|
677
|
+
"summary": "Neutral SHA-256, HMAC-SHA256, constant-time compare, base64/base64url, hex, BLAKE3 text hash, canonical JSON and HMAC approval tokens",
|
|
678
|
+
"fingerprints": [],
|
|
679
|
+
"detection": "declaration-only",
|
|
680
|
+
"policyNotes": [
|
|
681
|
+
"Declaration-only: apps call this crate instead of hand-rolling HMAC, constant-time compare, base64 or approval tokens.",
|
|
682
|
+
"Not a general cryptography suite: no encryption, signatures or key derivation."
|
|
683
|
+
]
|
|
684
|
+
},
|
|
674
685
|
{
|
|
675
686
|
"package": "rightkit-daemon",
|
|
676
687
|
"capability": "local-services",
|
|
@@ -1320,6 +1331,17 @@
|
|
|
1320
1331
|
"Fingerprints identify review candidates (competing dependencies and distinctive implementation code); published two-host replacement qualification is not recorded."
|
|
1321
1332
|
]
|
|
1322
1333
|
},
|
|
1334
|
+
{
|
|
1335
|
+
"package": "rightkit-google-identity",
|
|
1336
|
+
"capability": "google-identity",
|
|
1337
|
+
"summary": "Google sign-in building blocks: PKCE, redirect, token request body, ID-token claim validation and refresh-token store; Android Credential Manager seam",
|
|
1338
|
+
"fingerprints": [],
|
|
1339
|
+
"detection": "declaration-only",
|
|
1340
|
+
"policyNotes": [
|
|
1341
|
+
"Declaration-only: apps call this crate instead of hand-rolling Google OAuth request shapes.",
|
|
1342
|
+
"ID-token signatures are not verified here; callers must not grant access on claims alone."
|
|
1343
|
+
]
|
|
1344
|
+
},
|
|
1323
1345
|
{
|
|
1324
1346
|
"package": "rightkit-hooks",
|
|
1325
1347
|
"capability": "lifecycle-hooks",
|
|
@@ -1416,6 +1438,17 @@
|
|
|
1416
1438
|
"Fingerprints identify review candidates; published two-host replacement qualification is not recorded."
|
|
1417
1439
|
]
|
|
1418
1440
|
},
|
|
1441
|
+
{
|
|
1442
|
+
"package": "rightkit-identity",
|
|
1443
|
+
"capability": "installation-identity",
|
|
1444
|
+
"summary": "Installation identity with clone detection and startup claim, build identity (git rev, dirty flag, profile, channel, UTC stamp), device pairing with signed transcript, owner approval and resume tickets, bearer-token device auth with per-IP rate limit",
|
|
1445
|
+
"fingerprints": [],
|
|
1446
|
+
"detection": "declaration-only",
|
|
1447
|
+
"policyNotes": [
|
|
1448
|
+
"Declaration-only: apps call this crate instead of writing their own install ids, pairing transcripts or bearer-token checks.",
|
|
1449
|
+
"Key agreement is out of scope: the caller supplies the shared secret."
|
|
1450
|
+
]
|
|
1451
|
+
},
|
|
1419
1452
|
{
|
|
1420
1453
|
"package": "rightkit-job-queue",
|
|
1421
1454
|
"capability": "persistent-jobs",
|
|
@@ -2946,12 +2979,12 @@
|
|
|
2946
2979
|
{
|
|
2947
2980
|
"package": "rightkit-spend",
|
|
2948
2981
|
"capability": "spend-ledger",
|
|
2949
|
-
"summary": "Spend reservation, allowance and compare-and-swap ledger on SQLite: integer micro-units, idempotent reserve on request id, in-flight and overrun handling, opaque allowance periods",
|
|
2982
|
+
"summary": "Spend reservation, allowance and compare-and-swap ledger on SQLite: integer micro-units, idempotent reserve on request id, in-flight and overrun handling, opaque allowance periods, optional account key and per-reservation currency, billed-unknown commit, caller-transaction (`_in`) variants, strict transition mode",
|
|
2950
2983
|
"fingerprints": [],
|
|
2951
2984
|
"detection": "declaration-only",
|
|
2952
2985
|
"policyNotes": [
|
|
2953
2986
|
"Declaration-only: the ledger tables and reservation state machine are not an ownership fingerprint.",
|
|
2954
|
-
"Price rows live in rightkit-llm; the ledger stores opaque route and price-version text
|
|
2987
|
+
"Price rows live in rightkit-llm; the ledger stores opaque route and price-version text; an allowance holds no currency and a reservation holds only the currency text the caller passes."
|
|
2955
2988
|
]
|
|
2956
2989
|
},
|
|
2957
2990
|
{
|
|
@@ -3078,6 +3111,17 @@
|
|
|
3078
3111
|
"Fingerprints identify review candidates; published two-host replacement qualification is not recorded."
|
|
3079
3112
|
]
|
|
3080
3113
|
},
|
|
3114
|
+
{
|
|
3115
|
+
"package": "rightkit-time",
|
|
3116
|
+
"capability": "time-civil",
|
|
3117
|
+
"summary": "RFC 3339 parse and format, fixed UTC offsets, civil date math and HTTP-date formatting",
|
|
3118
|
+
"fingerprints": [],
|
|
3119
|
+
"detection": "declaration-only",
|
|
3120
|
+
"policyNotes": [
|
|
3121
|
+
"Declaration-only: apps call this crate instead of hand-rolling RFC 3339, civil date math or HTTP-date formatting.",
|
|
3122
|
+
"No time zone database: named zones and DST rules are not owned here; callers pass a fixed offset."
|
|
3123
|
+
]
|
|
3124
|
+
},
|
|
3081
3125
|
{
|
|
3082
3126
|
"package": "rightkit-transcript",
|
|
3083
3127
|
"capability": "timed-transcripts",
|
|
@@ -11,6 +11,11 @@ import { parseCodesignArgv } from "./codesign-argv.mjs";
|
|
|
11
11
|
|
|
12
12
|
export const AGENT_LABEL = "dev.rightkit.signing-agent";
|
|
13
13
|
export const MAX_FRAME = 1024 * 1024;
|
|
14
|
+
// A typed codesign `--identifier` (reverse-DNS characters only; never starts with "-", so it cannot read as an option).
|
|
15
|
+
export const AGENT_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
16
|
+
// Request schema 2 is codesign-only and is the sole schema that may carry per-item `identifier`. Schema 1 stays valid and
|
|
17
|
+
// byte-compatible for everything else; an agent that predates schema 2 refuses it (and any unknown item field) outright.
|
|
18
|
+
export const CODESIGN_IDENTIFIER_SCHEMA = 2;
|
|
14
19
|
export const NOTARIZATION_TIMEOUT_MS = 1800000;
|
|
15
20
|
export const MAX_NOTARIZATION_TIMEOUT_MS = 7200000;
|
|
16
21
|
export class SigningAgentError extends Error {
|
|
@@ -72,7 +77,7 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
72
77
|
const notary = ["notarize", "staple"].includes(request?.type);
|
|
73
78
|
if (!["codesign", "codesign-argv", "updater", "notarize", "staple"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
|
|
74
79
|
shape(request, [...common, ...(notary ? ["path", "profile", "staple", "app", "validateOnly"] : request.type === "codesign-argv" ? ["argv", "targets"] : [request.type === "codesign" ? "items" : "files"])]);
|
|
75
|
-
if (request.schema
|
|
80
|
+
if (!(request.schema === 1 || (request.type === "codesign" && request.schema === CODESIGN_IDENTIFIER_SCHEMA)) || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > (notary ? MAX_NOTARIZATION_TIMEOUT_MS : 180000)) {
|
|
76
81
|
fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
|
|
77
82
|
}
|
|
78
83
|
if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
|
|
@@ -142,7 +147,8 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
142
147
|
}
|
|
143
148
|
if (!Array.isArray(request.items) || !request.items.length || request.items.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign items required");
|
|
144
149
|
const items = request.items.map((item) => {
|
|
145
|
-
shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements"]);
|
|
150
|
+
shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements", ...(request.schema === CODESIGN_IDENTIFIER_SCHEMA ? ["identifier"] : [])]);
|
|
151
|
+
if (item.identifier !== undefined && (typeof item.identifier !== "string" || !AGENT_IDENTIFIER_PATTERN.test(item.identifier))) fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid codesign identifier");
|
|
146
152
|
if (typeof item.identity !== "string" || !item.identity.trim() || item.identity === "-" || item.identity.includes("\0") || item.identity.length > 512) fail("MAC_SIGNING_AGENT_PROTOCOL", "Real signing identity required");
|
|
147
153
|
for (const field of ["runtime", "timestamp", "force", "preserveEntitlements"]) if (item[field] !== undefined && typeof item[field] !== "boolean") fail("MAC_SIGNING_AGENT_PROTOCOL", "Signing options must be boolean");
|
|
148
154
|
return { ...item, path: checked(item.path), ...(item.entitlements !== undefined ? { entitlements: checked(item.entitlements, { entitlements: true }) } : {}) };
|
|
@@ -156,7 +162,7 @@ export function authenticateSigningToken(candidate, token) {
|
|
|
156
162
|
}
|
|
157
163
|
export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
|
|
158
164
|
username = os.userInfo().username, pnpm, environmentPath, now = Date.now } = {}) {
|
|
159
|
-
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple"] };
|
|
165
|
+
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple", "codesign-identifier"] };
|
|
160
166
|
const deadline = now() + request.timeoutMs;
|
|
161
167
|
const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
|
|
162
168
|
if (request.type === "codesign-argv") {
|
|
@@ -191,7 +197,7 @@ export function executeSigningRequest(request, { run = spawnSync, home = os.user
|
|
|
191
197
|
// Preserve caller's inside-out ordering. No arbitrary argv reaches spawn.
|
|
192
198
|
for (const item of request.items) {
|
|
193
199
|
const args = [...(item.force ? ["--force"] : []), ...(item.runtime ? ["--options", "runtime"] : []),
|
|
194
|
-
...(item.timestamp ? ["--timestamp"] : []), "--sign", item.identity,
|
|
200
|
+
...(item.timestamp ? ["--timestamp"] : []), ...(item.identifier !== undefined ? ["--identifier", item.identifier] : []), "--sign", item.identity,
|
|
195
201
|
...(item.entitlements ? ["--entitlements", item.entitlements] : []),
|
|
196
202
|
...(item.preserveEntitlements ? ["--preserve-metadata=entitlements"] : []), item.path];
|
|
197
203
|
results.push(invoke("/usr/bin/codesign", args).result);
|
package/rightkit-versions.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@rightkit/qa": "0.4.9",
|
|
32
32
|
"@rightkit/qa-chrome": "0.1.8",
|
|
33
33
|
"@rightkit/react-shell": "0.6.8",
|
|
34
|
-
"@rightkit/release": "0.
|
|
34
|
+
"@rightkit/release": "0.3.0",
|
|
35
35
|
"@rightkit/settings-ui": "0.5.0",
|
|
36
36
|
"@rightkit/tauri": "0.2.1",
|
|
37
37
|
"@rightkit/tauri-shell": "0.1.1",
|
|
@@ -210,7 +210,9 @@
|
|
|
210
210
|
"0.2.163",
|
|
211
211
|
"0.2.164",
|
|
212
212
|
"0.2.165",
|
|
213
|
-
"0.2.172"
|
|
213
|
+
"0.2.172",
|
|
214
|
+
"0.2.173",
|
|
215
|
+
"0.2.174"
|
|
214
216
|
],
|
|
215
217
|
"@rightkit/qa": [
|
|
216
218
|
"0.1.0",
|
|
@@ -384,7 +386,7 @@
|
|
|
384
386
|
"rightkit-automation": "0.4.1",
|
|
385
387
|
"rightkit-browser": "0.3.1",
|
|
386
388
|
"rightkit-cache": "0.2.0",
|
|
387
|
-
"rightkit-channel-memory": "0.1
|
|
389
|
+
"rightkit-channel-memory": "0.2.1",
|
|
388
390
|
"rightkit-child-process": "0.4.3",
|
|
389
391
|
"rightkit-compose": "0.1.0",
|
|
390
392
|
"rightkit-contracts": "0.1.5",
|
|
@@ -456,8 +458,8 @@
|
|
|
456
458
|
"rightkit-automation": "0.4.1",
|
|
457
459
|
"rightkit-browser": "0.3.1",
|
|
458
460
|
"rightkit-cache": "0.2.0",
|
|
459
|
-
"rightkit-channel-memory": "0.1
|
|
460
|
-
"rightkit-child-process": "0.4.
|
|
461
|
+
"rightkit-channel-memory": "0.2.1",
|
|
462
|
+
"rightkit-child-process": "0.4.3",
|
|
461
463
|
"rightkit-compose": "0.1.0",
|
|
462
464
|
"rightkit-contracts": "0.1.5",
|
|
463
465
|
"rightkit-control": "0.3.0",
|
|
@@ -541,7 +543,7 @@
|
|
|
541
543
|
"@rightkit/qa": "0.4.9",
|
|
542
544
|
"@rightkit/qa-chrome": "0.1.8",
|
|
543
545
|
"@rightkit/react-shell": "0.6.8",
|
|
544
|
-
"@rightkit/release": "0.2.
|
|
546
|
+
"@rightkit/release": "0.2.174",
|
|
545
547
|
"@rightkit/settings-ui": "0.5.0",
|
|
546
548
|
"@rightkit/shell": "0.1.1",
|
|
547
549
|
"@rightkit/tauri": "0.2.1",
|
|
@@ -812,6 +814,9 @@
|
|
|
812
814
|
],
|
|
813
815
|
"rightkit-hotkeys": [
|
|
814
816
|
"0.1.0"
|
|
817
|
+
],
|
|
818
|
+
"rightkit-channel-memory": [
|
|
819
|
+
"0.1.0"
|
|
815
820
|
]
|
|
816
821
|
}
|
|
817
822
|
}
|
package/runtime-budgets.mjs
CHANGED
|
@@ -199,7 +199,7 @@ export function renderRuntimeBudgetBody(budget) {
|
|
|
199
199
|
const host = `${m.host?.os}/${m.host?.arch}${m.host?.model ? ` (${m.host.model})` : ""}`;
|
|
200
200
|
const scale = `${count(m.dataScale?.count)} ${m.dataScale?.unit} (${m.dataScale?.description})`;
|
|
201
201
|
const app = m.scope === "installed" && m.app ? `, app ${m.app.name} ${m.app.version}` : "";
|
|
202
|
-
const observed = BUDGET_FIGURES.map((k) => `${FIGURE_LABEL[k]} ${m.observed?.[k] === undefined ? "n/a" : FORMAT[k](m.observed[k])}`).join(", ");
|
|
202
|
+
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
203
|
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}.`);
|
|
204
204
|
}
|
|
205
205
|
return out;
|
|
@@ -291,6 +291,10 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
|
|
|
291
291
|
|
|
292
292
|
// ---------------------------------------------------------------- bench evidence
|
|
293
293
|
|
|
294
|
+
// Figures a host cannot measure by design. macOS has no per-process disk-operation count (rightkit-child-process
|
|
295
|
+
// usage.rs: ru_inblock/ru_oublock stay 0 there), so disk_ops_per_s may be "unavailable" on darwin only.
|
|
296
|
+
export const HOST_UNAVAILABLE_FIGURES = Object.freeze({ darwin: Object.freeze(["diskOpsPerSecond"]) });
|
|
297
|
+
export const figureUnavailableOn = (os, key) => (HOST_UNAVAILABLE_FIGURES[os] ?? []).includes(key);
|
|
294
298
|
const RESULT_NUMBERS = Object.freeze({ resident_bytes: "residentMemoryBytes", idle_cpu_percent: "idleCpuPercent", wakeups_per_s: "wakeupsPerSecond", disk_ops_per_s: "diskOpsPerSecond", scale_count: "scaleCount" });
|
|
295
299
|
const RESULT_WORDS = Object.freeze(["crate", "version", "scale_unit"]);
|
|
296
300
|
|
|
@@ -312,7 +316,8 @@ export function parseBudgetResult(text) {
|
|
|
312
316
|
for (const key of RESULT_WORDS) { if (!nonEmpty(fields[key])) problems.push(`missing field ${key}`); else value[key] = fields[key]; }
|
|
313
317
|
for (const [key, name] of Object.entries(RESULT_NUMBERS)) {
|
|
314
318
|
const n = Number(fields[key]);
|
|
315
|
-
if (fields[key] ===
|
|
319
|
+
if (fields[key] === "unavailable" && name !== "residentMemoryBytes" && name !== "scaleCount") value[name] = null;
|
|
320
|
+
else if (fields[key] === undefined || fields[key] === "" || !Number.isFinite(n) || n < 0) problems.push(`field ${key} must be a non-negative number`);
|
|
316
321
|
else value[name] = n;
|
|
317
322
|
}
|
|
318
323
|
return problems.length ? { ok: false, problems } : { ok: true, problems: [], value };
|
|
@@ -412,6 +417,7 @@ function validateMeasurement(m, index) {
|
|
|
412
417
|
}
|
|
413
418
|
if (!nonEmpty(m.command)) problems.push(`${at} has no command`);
|
|
414
419
|
for (const key of BUDGET_FIGURES) {
|
|
420
|
+
if (m.observed?.[key] === null && figureUnavailableOn(m.host?.os, key)) continue;
|
|
415
421
|
if (!isNumber(m.observed?.[key]) || m.observed[key] < 0) problems.push(`${at} observed.${key} must be a non-negative number`);
|
|
416
422
|
}
|
|
417
423
|
if (isNumber(m.observed?.residentMemoryBytes) && m.observed.residentMemoryBytes === 0) {
|
|
@@ -507,12 +513,12 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
507
513
|
}
|
|
508
514
|
for (const { m, index } of counted) {
|
|
509
515
|
for (const key of BUDGET_FIGURES) {
|
|
510
|
-
if (ceilings[key] !== undefined && m.observed[key] > ceilings[key]) {
|
|
516
|
+
if (ceilings[key] !== undefined && m.observed[key] !== null && m.observed[key] > ceilings[key]) {
|
|
511
517
|
problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} exceeds the ${FIGURE_LABEL[key]} ceiling: observed ${m.observed[key]} > ${ceilings[key]}`);
|
|
512
518
|
}
|
|
513
519
|
}
|
|
514
520
|
}
|
|
515
|
-
const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && m.observed[key] <= ceilings[key]);
|
|
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]));
|
|
516
522
|
const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
|
|
517
523
|
const staleness = new Map();
|
|
518
524
|
const stale = (m) => {
|
|
@@ -749,16 +755,18 @@ export function appRuntimeBudgetAdoptionCheck({ appRoot, app, version, cargoLock
|
|
|
749
755
|
const used = Object.keys(classes).filter((name) => locked.has(name));
|
|
750
756
|
if (used.length === 0) return null;
|
|
751
757
|
const problems = [];
|
|
758
|
+
// A crate whose ceilings or scale RightKit has not declared yet cannot be measured against anything, so it
|
|
759
|
+
// never blocks an app: it is reported as pending (warn) until the crate declares its budget.
|
|
760
|
+
const pending = used.filter((name) => !classes[name].ceilings || !classes[name].requiredScale);
|
|
761
|
+
const measurable = used.filter((name) => !pending.includes(name));
|
|
752
762
|
const adoptionPath = path.join(appRoot, APP_ADOPTION_FILE);
|
|
753
763
|
let records = [];
|
|
754
|
-
if (!exists(adoptionPath)) problems.push(`${APP_ADOPTION_FILE} is missing`);
|
|
764
|
+
if (!exists(adoptionPath)) { if (measurable.length) problems.push(`${APP_ADOPTION_FILE} is missing`); }
|
|
755
765
|
else {
|
|
756
766
|
try { records = JSON.parse(readFile(adoptionPath, "utf8")).measurements ?? []; } catch (error) { problems.push(`${APP_ADOPTION_FILE} is not valid JSON: ${error.message}`); }
|
|
757
767
|
}
|
|
758
|
-
for (const crate of
|
|
768
|
+
for (const crate of measurable) {
|
|
759
769
|
const { ceilings, requiredScale } = classes[crate];
|
|
760
|
-
if (!ceilings) { problems.push(`${crate}: ceilings undeclared (declare them in its runtime-budget.json and regenerate ${CLASSES_FILE})`); continue; }
|
|
761
|
-
if (!requiredScale) { problems.push(`${crate}: requiredScale undeclared`); continue; }
|
|
762
770
|
const mine = records.map((m, index) => ({ m, index })).filter(({ m }) => m?.crate === crate && m.scope === "installed" && locked.get(crate).has(m.crateVersion));
|
|
763
771
|
if (mine.length === 0) { problems.push(`${crate} ${[...locked.get(crate)].join("/")}: no installed-scope measurement of ${app}`); continue; }
|
|
764
772
|
const good = mine.filter(({ m, index }) => {
|
|
@@ -768,16 +776,21 @@ export function appRuntimeBudgetAdoptionCheck({ appRoot, app, version, cargoLock
|
|
|
768
776
|
if (evidenceProblems.length) { problems.push(`${crate}: ${evidenceProblems.join("; ")}`); return false; }
|
|
769
777
|
if (m.app.name !== app) { problems.push(`${crate}: measurement[${index}] is for app ${m.app.name}, not ${app}`); return false; }
|
|
770
778
|
if (version && m.app.version !== version) { problems.push(`${crate}: measurement[${index}] was taken at ${app} ${m.app.version}, not the current ${version}`); return false; }
|
|
771
|
-
const over = BUDGET_FIGURES.filter((k) => m.observed[k] > ceilings[k]);
|
|
779
|
+
const over = BUDGET_FIGURES.filter((k) => m.observed[k] !== null && m.observed[k] > ceilings[k]);
|
|
772
780
|
if (over.length) { problems.push(`${crate}: measurement[${index}] exceeds ${over.join(", ")}`); return false; }
|
|
773
781
|
if (m.dataScale.unit !== requiredScale.unit || m.dataScale.count < requiredScale.minCount) { problems.push(`${crate}: measurement[${index}] is below the required scale ${requiredScale.minCount} ${requiredScale.unit}`); return false; }
|
|
774
782
|
return true;
|
|
775
783
|
});
|
|
776
784
|
if (good.length === 0 && !problems.some((p) => p.startsWith(`${crate}`))) problems.push(`${crate}: no qualifying measurement`);
|
|
777
785
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
786
|
+
const pendingNote = pending.length ? `; pending RightKit budget declaration (not blocking): ${pending.join(", ")}` : "";
|
|
787
|
+
if (problems.length) {
|
|
788
|
+
return { name: RUNTIME_BUDGET_CHECK, status: "fail", detail: `${app} uses held-resource crate(s) ${measurable.join(", ")}: ${problems.join("; ")}${pendingNote}`, fix: `record an installed measurement of ${app} for each crate in ${APP_ADOPTION_FILE} (docs/RUNTIME-BUDGETS.md)` };
|
|
789
|
+
}
|
|
790
|
+
if (pending.length) {
|
|
791
|
+
return { name: RUNTIME_BUDGET_CHECK, status: "warn", detail: `${app} uses held-resource crate(s) whose RightKit ceilings are not declared yet: ${pending.join(", ")}${measurable.length ? `; measured: ${measurable.join(", ")}` : ""}`, fix: "RightKit declares the crate ceilings; then record installed measurements (docs/RUNTIME-BUDGETS.md)" };
|
|
792
|
+
}
|
|
793
|
+
return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${app} has installed measurements for ${used.join(", ")}` };
|
|
781
794
|
}
|
|
782
795
|
|
|
783
796
|
// ---------------------------------------------------------------- record (measurement provenance)
|
|
@@ -813,6 +826,9 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
|
|
|
813
826
|
const v = parsed.value;
|
|
814
827
|
if (v.crate !== pkg.name) throw new Error(`bench output is for ${v.crate}, crate is ${pkg.name}`);
|
|
815
828
|
if (v.version !== pkg.version) throw new Error(`bench output is for version ${v.version}, crate is ${pkg.version}`);
|
|
829
|
+
for (const key of BUDGET_FIGURES) {
|
|
830
|
+
if (v[key] === null && !figureUnavailableOn(host.os, key)) throw new Error(`bench output rejected: ${key} is unavailable, which is allowed only where the host cannot measure it (${JSON.stringify(HOST_UNAVAILABLE_FIGURES)})`);
|
|
831
|
+
}
|
|
816
832
|
if (!SCOPES.includes(scope)) throw new Error(`--scope must be one of ${SCOPES.join(", ")}`);
|
|
817
833
|
if (scope === "installed" && !(nonEmpty(app?.name) && nonEmpty(app?.version))) throw new Error("--scope installed needs --app and --app-version");
|
|
818
834
|
if (!nonEmpty(command)) throw new Error("--command (the exact bench command) is required");
|
package/stage-binary.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveTargetRoot } from "./cargo-target.mjs";
|
|
|
9
9
|
* The build broker writes Cargo output to per-generation folders (`<managed>/rightkit-build-control/ws/<hash>/g/<gen>/...`)
|
|
10
10
|
* and `cargo-guard` uses a hashed `dev-targets/<project>-<hash>` folder. Those are build output only: their path is not
|
|
11
11
|
* stable, so NOTHING may run from them. macOS ties privacy grants to the path an app runs from, so a dev or QA binary is
|
|
12
|
-
* copied in place to `<root>/.rightkit-dev-bin/<triple-or-host>/<name>` (
|
|
12
|
+
* copied in place to `<root>/.rightkit-dev-bin/<triple-or-host>/<name>` (replaced in place, no `.previous` kept) and run from there.
|
|
13
13
|
*/
|
|
14
14
|
export const DEV_STAGE_DIRECTORY = ".rightkit-dev-bin";
|
|
15
15
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `right-release stage-sidecar [--config right-release.config.mjs] [--target <triple>]` (bogusyogi/rightkit#345):
|
|
3
|
-
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming.
|
|
2
|
+
// `right-release stage-sidecar [--config right-release.config.mjs] [--target <triple>] [--check] [--json]` (bogusyogi/rightkit#345):
|
|
3
|
+
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming. `--check` validates
|
|
4
|
+
// the manifest and prints the plan without building, downloading, copying or deleting anything (for template gates).
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
|
|
7
|
+
import { planSidecarStaging, resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
|
|
7
8
|
|
|
8
9
|
export function parseStageSidecarArgs(argv) {
|
|
9
|
-
const opts = { config: "right-release.config.mjs", target: undefined, json: false };
|
|
10
|
+
const opts = { config: "right-release.config.mjs", target: undefined, json: false, check: false };
|
|
10
11
|
for (let i = 0; i < argv.length; i += 1) {
|
|
11
12
|
const arg = argv[i];
|
|
12
13
|
if (arg === "--config" || arg === "--target") {
|
|
@@ -15,11 +16,35 @@ export function parseStageSidecarArgs(argv) {
|
|
|
15
16
|
opts[arg.slice(2)] = value;
|
|
16
17
|
i += 1;
|
|
17
18
|
} else if (arg === "--json") opts.json = true;
|
|
19
|
+
else if (arg === "--check") opts.check = true;
|
|
18
20
|
else throw new Error(`unknown stage-sidecar argument: ${arg}`);
|
|
19
21
|
}
|
|
20
22
|
return opts;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
const NODE_ARCH = { x64: "x86_64", arm64: "aarch64" };
|
|
26
|
+
const NODE_OS = { darwin: "apple-darwin", win32: "pc-windows-msvc", linux: "unknown-linux-gnu" };
|
|
27
|
+
|
|
28
|
+
/** Host triple from Node alone (no subprocess), so `--check` stays free of effects. */
|
|
29
|
+
export function nodeHostTriple(platform = process.platform, arch = process.arch) {
|
|
30
|
+
const cpu = NODE_ARCH[arch];
|
|
31
|
+
const system = NODE_OS[platform];
|
|
32
|
+
if (!cpu || !system) throw new Error(`cannot derive a target triple from ${platform}/${arch}; pass --target`);
|
|
33
|
+
return `${cpu}-${system}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Human-readable plan lines for `--check`. */
|
|
37
|
+
export function formatSidecarPlan(plan) {
|
|
38
|
+
const lines = [`right-release stage-sidecar --check: target ${plan.target}, mode ${plan.mode}, stageDir ${plan.stageDir}${plan.wipeStageDir ? " (wipeStageDir)" : ""}`];
|
|
39
|
+
for (const entry of plan.entries) {
|
|
40
|
+
if (entry.skipped) lines.push(` [${entry.index}] ${entry.kind}: skipped (platforms ${entry.platforms.join(",")})`);
|
|
41
|
+
else lines.push(` [${entry.index}] ${entry.kind}: ${entry.outputs.join(", ")}${entry.externalBin ? " (externalBin)" : ""}${entry.clean ? " (clean)" : ""}${entry.url ? ` <- ${entry.url}` : ""}`);
|
|
42
|
+
}
|
|
43
|
+
if (plan.nestedBrokerRisk) lines.push(" warning: running inside an outer broker build; staging would fail with STAGE_SIDECAR_NESTED_BROKER (run stage-sidecar in recipe prepare, before the Tauri build)");
|
|
44
|
+
lines.push("right-release stage-sidecar --check: manifest is valid; nothing was staged");
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
async function main() {
|
|
24
49
|
const opts = parseStageSidecarArgs(process.argv.slice(2));
|
|
25
50
|
const configPath = path.resolve(opts.config);
|
|
@@ -27,6 +52,13 @@ async function main() {
|
|
|
27
52
|
const loaded = await import(pathToFileURL(configPath).href);
|
|
28
53
|
const config = loaded.default ?? loaded.config ?? loaded;
|
|
29
54
|
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
55
|
+
if (opts.check) {
|
|
56
|
+
const target = opts.target ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
57
|
+
const plan = planSidecarStaging({ manifest: config.sidecars, target });
|
|
58
|
+
if (opts.json) console.log(JSON.stringify(plan, null, 2));
|
|
59
|
+
else for (const line of formatSidecarPlan(plan)) console.log(line);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
30
62
|
const target = resolveTargetTriple({ target: opts.target, cwd: appRoot });
|
|
31
63
|
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target });
|
|
32
64
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|