@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/release-switch-scan.mjs
CHANGED
|
@@ -84,22 +84,60 @@ export function featureClosureFindings(treeText, { forbidden = [...REGISTRY.forb
|
|
|
84
84
|
// M7 lane E: rightkit-daemon `remote` (the HTTPS host with tokio, axum and hyper) may enter a release graph only for an app
|
|
85
85
|
// that declares `releaseSwitches.remoteHost: true` (the Hetzner Pulse and PostRight server builds). `testing` implies `remote`,
|
|
86
86
|
// so it is caught by the same line. Fail-closed: a value other than a boolean throws, and `false` or an absent key refuses.
|
|
87
|
+
// PK17-2: a `remote` that appears ONLY beneath the daemon `remote-loopback` feature (the loopback feature enables the shared
|
|
88
|
+
// router and auth internals) is not a host declaration; that case is judged by `remoteLoopbackFindings`, not here. Any other
|
|
89
|
+
// path to `remote` (a direct request, `testing`, another crate's feature) is still refused exactly as before.
|
|
87
90
|
export const REMOTE_SERVICE_CRATE = "rightkit-daemon";
|
|
88
91
|
export const REMOTE_SERVICE_FEATURE = "remote";
|
|
92
|
+
export const REMOTE_LOOPBACK_FEATURE = "remote-loopback";
|
|
93
|
+
|
|
94
|
+
// Feature lines of a `cargo tree -e features` text with their parent. Default `--prefix indent` output indents each level by
|
|
95
|
+
// four columns (`├── `, `│ `, ` `; `--charset ascii` keeps the width), so depth is the prefix width / 4 and the parent is
|
|
96
|
+
// the nearest earlier line one level up. A flat tree (`--prefix none`) has depth 0 everywhere, so no feature has a parent and
|
|
97
|
+
// every parent-dependent rule treats it as the direct (stricter) case: this fails closed.
|
|
98
|
+
const TREE_PREFIX = /^[\s│├└─|`+\\-]*/;
|
|
99
|
+
function featureTreeEntries(treeText) {
|
|
100
|
+
const entries = []; const stack = [];
|
|
101
|
+
for (const line of String(treeText).split(/\r?\n/)) {
|
|
102
|
+
if (!line.trim()) continue;
|
|
103
|
+
const depth = Math.floor(TREE_PREFIX.exec(line)[0].length / 4);
|
|
104
|
+
const m = FEATURE_LINE.exec(line);
|
|
105
|
+
const entry = { crate: m ? m[1] : null, feature: m ? m[2] : null, parent: depth > 0 ? (stack[depth - 1] ?? null) : null };
|
|
106
|
+
stack[depth] = entry; stack.length = depth + 1;
|
|
107
|
+
if (m) entries.push(entry);
|
|
108
|
+
}
|
|
109
|
+
return entries;
|
|
110
|
+
}
|
|
111
|
+
const impliedByLoopback = (entry) => entry.parent?.crate === REMOTE_SERVICE_CRATE && entry.parent?.feature === REMOTE_LOOPBACK_FEATURE;
|
|
112
|
+
|
|
89
113
|
export function remoteServiceFindings(treeText, { remoteHost = false } = {}) {
|
|
90
114
|
if (typeof remoteHost !== "boolean") throw new Error(`releaseSwitches.remoteHost must be true or false, got ${JSON.stringify(remoteHost)}`);
|
|
91
115
|
if (remoteHost) return [];
|
|
92
116
|
const findings = []; const seen = new Set();
|
|
93
|
-
for (const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
seen.add(m[1]);
|
|
117
|
+
for (const entry of featureTreeEntries(treeText)) {
|
|
118
|
+
if (entry.crate !== REMOTE_SERVICE_CRATE || entry.feature !== REMOTE_SERVICE_FEATURE || seen.has(entry.crate) || impliedByLoopback(entry)) continue;
|
|
119
|
+
seen.add(entry.crate);
|
|
97
120
|
const id = "remote-feature-undeclared";
|
|
98
121
|
findings.push({ layer: "feature-closure", rule: id, file: "<cargo tree>", reason: `${id}: crate \`${REMOTE_SERVICE_CRATE}\` has feature \`${REMOTE_SERVICE_FEATURE}\` enabled in the resolved graph, but the app does not declare releaseSwitches.remoteHost: true (only Pulse and PostRight server builds may enable it)` });
|
|
99
122
|
}
|
|
100
123
|
return findings;
|
|
101
124
|
}
|
|
102
125
|
|
|
126
|
+
// PK17-2 / PK06-5: the daemon loopback feature (CodeRight's loopback HTTP + bearer host, loopback-only bind) is its own release
|
|
127
|
+
// declaration, `releaseSwitches.remoteLoopback: true`, distinct from `remoteHost`. Neither declaration implies the other: a server
|
|
128
|
+
// build with `remoteHost: true` still needs `remoteLoopback: true` to ship the loopback feature, and a loopback app that also
|
|
129
|
+
// enables `remote` on its own still needs `remoteHost: true`. Same fail-closed boolean rule as `remoteHost`.
|
|
130
|
+
export function remoteLoopbackFindings(treeText, { remoteLoopback = false } = {}) {
|
|
131
|
+
if (typeof remoteLoopback !== "boolean") throw new Error(`releaseSwitches.remoteLoopback must be true or false, got ${JSON.stringify(remoteLoopback)}`);
|
|
132
|
+
if (remoteLoopback) return [];
|
|
133
|
+
for (const entry of featureTreeEntries(treeText)) {
|
|
134
|
+
if (entry.crate !== REMOTE_SERVICE_CRATE || entry.feature !== REMOTE_LOOPBACK_FEATURE) continue;
|
|
135
|
+
const id = "remote-loopback-undeclared";
|
|
136
|
+
return [{ layer: "feature-closure", rule: id, file: "<cargo tree>", reason: `${id}: crate \`${REMOTE_SERVICE_CRATE}\` has feature \`${REMOTE_LOOPBACK_FEATURE}\` enabled in the resolved graph, but the app does not declare releaseSwitches.remoteLoopback: true (only a headless engine build that serves its own loopback API may enable it; releaseSwitches.remoteHost does not cover it)` }];
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
// A build command (shell string or { cmd, args }) as one flat token list, whole arguments plus their shell re-tokenisation.
|
|
104
142
|
export function commandTokens(command) {
|
|
105
143
|
const parts = typeof command === "string" ? [command] : [command?.cmd, ...(command?.args ?? [])].filter((v) => v !== undefined).map(String);
|
|
@@ -445,7 +483,7 @@ export function scanSwitchPaths(paths, { denyMap = denyNamesFor() } = {}) {
|
|
|
445
483
|
// Every layer is optional input; a layer given no input is skipped, except that `requireBinary` fails a scan that
|
|
446
484
|
// found no native binary. `bypass: ["feature-closure"]` exists only for the negative fixtures that prove the binary
|
|
447
485
|
// layer independently; release.mjs never passes it.
|
|
448
|
-
export function releaseSwitchFindings({ treeText, commands = [], sourceRoot, sourceRoots, inventoryText, binaryPaths = [], appForbid = [], requireBinary = true, bypass = [], remoteHost, devtools } = {}) {
|
|
486
|
+
export function releaseSwitchFindings({ treeText, commands = [], sourceRoot, sourceRoots, inventoryText, binaryPaths = [], appForbid = [], requireBinary = true, bypass = [], remoteHost, remoteLoopback, devtools } = {}) {
|
|
449
487
|
const findings = [];
|
|
450
488
|
const bypassed = new Set(bypass);
|
|
451
489
|
const denyMap = denyNamesFor({ appForbid: [...appForbid, ...(inventoryText != null ? inventoryDenyNames(inventoryText) : [])] });
|
|
@@ -453,6 +491,7 @@ export function releaseSwitchFindings({ treeText, commands = [], sourceRoot, sou
|
|
|
453
491
|
if (treeText != null) {
|
|
454
492
|
findings.push(...featureClosureFindings(treeText, { allowDevtools: devtools?.mode === "content-only" }));
|
|
455
493
|
findings.push(...remoteServiceFindings(treeText, { remoteHost }));
|
|
494
|
+
findings.push(...remoteLoopbackFindings(treeText, { remoteLoopback }));
|
|
456
495
|
}
|
|
457
496
|
for (const argv of commands) findings.push(...commandSwitchFindings(argv, { allowDevtools: devtools?.mode === "content-only" }));
|
|
458
497
|
}
|
package/release.mjs
CHANGED
|
@@ -54,6 +54,7 @@ import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mj
|
|
|
54
54
|
import { runAndroidRelease, validateAndroidTarget } from "./android-release.mjs";
|
|
55
55
|
import { runIosPackageRecipe } from "./ios-package-recipe.mjs";
|
|
56
56
|
import { releaseSwitchInputs } from "./ios-build-context.mjs";
|
|
57
|
+
import { productionBuildEnv, productionModeProblems } from "./production-mode.mjs";
|
|
57
58
|
|
|
58
59
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
59
60
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -382,6 +383,13 @@ try {
|
|
|
382
383
|
{ label: "target.gradle", command: target.gradle },
|
|
383
384
|
] });
|
|
384
385
|
} catch (error) { fail(error.message); }
|
|
386
|
+
// M12: the frontend build resolves in production mode only (development-only conditional exports such as
|
|
387
|
+
// `@rightkit/license/testing` never resolve). `run` pins NODE_ENV=production for build children; this refuses a
|
|
388
|
+
// Vite config that lists the `development` resolve condition explicitly, which the pin cannot override.
|
|
389
|
+
{
|
|
390
|
+
const modeProblems = productionModeProblems({ appRoot: root });
|
|
391
|
+
if (modeProblems.length) fail(`${config.app ?? "app"} release frontend is not production-only:\n- ${modeProblems.join("\n- ")}`);
|
|
392
|
+
}
|
|
385
393
|
|
|
386
394
|
// D4 (M21/M23) source layer: every environment/argument read in app source is registered in the app's switches.json and
|
|
387
395
|
// every S1-S4 selector is compiled out of release. The inventory is `config.releaseSwitches.inventory` (relative to the
|
|
@@ -415,7 +423,8 @@ try {
|
|
|
415
423
|
console.log(`right-release: dry run: ${error.message}`);
|
|
416
424
|
}
|
|
417
425
|
// remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
|
|
418
|
-
|
|
426
|
+
// remoteLoopback: the daemon `remote-loopback` feature is a separate declaration (releaseSwitches.remoteLoopback: true); remoteHost does not cover it.
|
|
427
|
+
const closureInput = { treeText, devtools: devtoolsAllowance, remoteHost: config.releaseSwitches?.remoteHost, remoteLoopback: config.releaseSwitches?.remoteLoopback, commands: closureCommands.map((c) => commandTokens(c)) };
|
|
419
428
|
if (releaseSwitchInventoryText != null) {
|
|
420
429
|
// iOS: only what the iPhone app compiles — the Xcode project and its local packages, plus the local crates in the
|
|
421
430
|
// resolved aarch64-apple-ios cargo graph (feature-aware, unlike the ownership closure). Other targets walk the app root.
|
|
@@ -979,8 +988,8 @@ function sweepStaleRustArtifacts() {
|
|
|
979
988
|
}
|
|
980
989
|
|
|
981
990
|
async function runInstall(pm, cwd) {
|
|
982
|
-
if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd);
|
|
983
|
-
else if (pm === "npm") await run("npm", ["ci"], cwd);
|
|
991
|
+
if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd, {}, { install: true });
|
|
992
|
+
else if (pm === "npm") await run("npm", ["ci"], cwd, {}, { install: true });
|
|
984
993
|
else fail(`unsupported package manager: ${pm}`);
|
|
985
994
|
}
|
|
986
995
|
|
|
@@ -1772,7 +1781,8 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
1772
1781
|
const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
|
|
1773
1782
|
const child = spawn(cmd, runArgs, {
|
|
1774
1783
|
cwd,
|
|
1775
|
-
|
|
1784
|
+
// Build children resolve in production mode; package installs keep the caller's NODE_ENV (production drops devDependencies).
|
|
1785
|
+
env: macosCodesignEnvironment(options.install ? { ...process.env, ...releaseEnv, ...env } : productionBuildEnv({ ...process.env, ...releaseEnv, ...env })),
|
|
1776
1786
|
stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
1777
1787
|
shell: useShell,
|
|
1778
1788
|
windowsHide: true,
|
package/rightkit-sync.mjs
CHANGED
|
@@ -192,6 +192,14 @@ export function stageSourceVersions(entries, { file = VERSIONS_FILE, root = righ
|
|
|
192
192
|
return { changes, copies, written };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/** Feed file and consumer-copy root of one requested workspace. recordPublished/stageSourceVersions default to the
|
|
196
|
+
* location of THIS module, which is inside the package store when right-release runs from an installed
|
|
197
|
+
* @rightkit/release; every caller that knows its workspace passes these explicitly. */
|
|
198
|
+
export function workspaceFeedOptions(workspaceRoot) {
|
|
199
|
+
const root = path.resolve(workspaceRoot);
|
|
200
|
+
return { root, file: path.join(root, "packages", "release", "rightkit-versions.json") };
|
|
201
|
+
}
|
|
202
|
+
|
|
195
203
|
/** Record one just-published version (the registry index can lag the upload) and refresh the copies.
|
|
196
204
|
* Called by right-release after a verified cargo or npm publish; never fails the publish itself unless `strict` is set. */
|
|
197
205
|
export function recordPublished({ registry, name, version }, { file = VERSIONS_FILE, root = rightkitRoot, log = (line) => process.stdout.write(`${line}\n`), strict = false } = {}) {
|