@rightkit/release 0.6.5 → 0.6.7
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 +196 -11
- package/admission-files.mjs +3 -2
- package/advisory-db.json +4 -0
- package/advisory-db.mjs +124 -0
- package/advisory-exceptions.mjs +13 -10
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +702 -26
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +671 -133
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +20 -12
- package/cache-command.mjs +4 -3
- package/cargo-license-notices.mjs +2 -1
- package/cargo-manifest-text.mjs +25 -0
- package/cargo-profile-contract.mjs +26 -13
- package/cli/right-release.mjs +37 -7
- package/command-spec.mjs +4 -5
- package/compile-inputs.mjs +66 -0
- package/consumer-contracts.json +1044 -0
- package/consumer-contracts.mjs +627 -0
- package/consumer-input-contracts.mjs +422 -0
- package/consumer-policy.mjs +588 -0
- package/consumer-source-contracts.mjs +469 -0
- package/deny-default.toml +42 -0
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +513 -54
- package/development.mjs +5 -5
- package/disk-command.mjs +270 -30
- package/disk-policy.json +50 -10
- package/disk-policy.mjs +482 -35
- package/disk-report.mjs +129 -0
- package/door-policy.mjs +228 -0
- package/gate-executor.mjs +30 -0
- package/gate-stages.mjs +110 -0
- package/github-release.mjs +5 -3
- package/github-updater-release.mjs +48 -8
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +175 -8
- package/ios-upload.mjs +4 -4
- package/legion-semantic-contracts.mjs +428 -0
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/ownership-class-rules.mjs +277 -0
- package/ownership-key-authority.mjs +3 -1
- package/ownership-mandatory-set.mjs +3 -2
- package/package.json +46 -3
- package/pipeline-admission.mjs +12 -0
- package/publish-cargo.mjs +219 -4
- package/publish-update.mjs +19 -14
- package/release-config.mjs +87 -11
- package/release-dependencies-command.mjs +4 -3
- package/release-qa-audit.mjs +12 -10
- package/release-qa-command.mjs +42 -7
- package/release-qa-hardening.mjs +10 -3
- package/release-qa-policy.mjs +214 -47
- package/release.mjs +4 -4
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +178 -170
- package/shell-contract-bridge.json +251 -0
- package/shell-contract.mjs +1069 -0
- package/sign-updater.mjs +24 -4
- package/source-closure-rules.mjs +204 -0
- package/stage-sidecar-command.mjs +7 -6
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
|
@@ -18,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
20
|
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
21
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
21
22
|
import { escapeHtml } from "./npm-license-notices.mjs";
|
|
22
23
|
|
|
23
24
|
const fail = (message) => { throw new CommandSpecError(message); };
|
|
@@ -123,7 +124,7 @@ export function generateBinaryNotices({ appRoot, config, flags = {}, check = fal
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export function parseBinaryNoticeArgs(argv) {
|
|
126
|
-
const opts = { config:
|
|
127
|
+
const opts = { config: defaultReleaseConfigFile(), output: undefined, productName: undefined, receipt: undefined, check: false };
|
|
127
128
|
for (let i = 0; i < argv.length; i += 1) {
|
|
128
129
|
const arg = argv[i];
|
|
129
130
|
if (arg === "--config" || arg === "--output" || arg === "--product-name" || arg === "--receipt") {
|
package/build-release.mjs
CHANGED
|
@@ -23,7 +23,8 @@ import { spawn, spawnSync } from "./background-process.mjs";
|
|
|
23
23
|
import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord, writeSnapshot } from "./rightkit-registry-resolver.mjs";
|
|
24
24
|
import { formatAdmission, formatNotRun, runAdmission } from "./admission.mjs";
|
|
25
25
|
import { sccacheRefusal } from "./admission-host.mjs";
|
|
26
|
-
import {
|
|
26
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
27
28
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
28
29
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
29
30
|
import { admitSourceQa } from "./release-qa-audit.mjs";
|
|
@@ -51,7 +52,7 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
|
51
52
|
import { ANDROID_RECEIPT_NAME, androidSealEntry, validateAndroidTarget, verifyAndroidReceipt } from "./android-release.mjs";
|
|
52
53
|
import { IOS_RECEIPT_NAME, iosSealEntry, verifyIosReceipt } from "./ios-package-recipe.mjs";
|
|
53
54
|
import { expandIosPackageTarget } from "./package-recipe-targets.mjs";
|
|
54
|
-
import { assertUpdaterSignature } from "./github-updater-release.mjs";
|
|
55
|
+
import { assertUpdaterKeyId, assertUpdaterSignature, isSuiteUpdaterApp, resolveUpdaterTrust } from "./github-updater-release.mjs";
|
|
55
56
|
|
|
56
57
|
// The iOS lane's own packaging code, kept out of the win/mac fingerprint for the same reason as the Android one.
|
|
57
58
|
const IOS_PIPELINE_FINGERPRINT = createHash("sha256")
|
|
@@ -111,7 +112,7 @@ const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
|
111
112
|
const WORKER = path.join(TOOL_ROOT, "release.mjs");
|
|
112
113
|
const RELEASE_TOOL_VERSION = JSON.parse(readFileSync(path.join(TOOL_ROOT, "package.json"), "utf8")).version;
|
|
113
114
|
const args = process.argv.slice(2);
|
|
114
|
-
let configName =
|
|
115
|
+
let configName = defaultReleaseConfigFile();
|
|
115
116
|
let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
|
|
116
117
|
let dryRun = false;
|
|
117
118
|
let skipChecks = false;
|
|
@@ -195,7 +196,7 @@ try {
|
|
|
195
196
|
const shortCommit = commit.slice(0, 8);
|
|
196
197
|
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
197
198
|
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
198
|
-
const config =
|
|
199
|
+
const config = await importReleaseConfig(configPath, { query: `commit=${commit}` });
|
|
199
200
|
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
200
201
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.app) || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(config.version)) {
|
|
201
202
|
fail("app and version must be safe release-id components");
|
|
@@ -566,7 +567,7 @@ async function admit() {
|
|
|
566
567
|
commit = git(invocationRoot, ["rev-parse", "HEAD"]);
|
|
567
568
|
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
568
569
|
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
569
|
-
try { config =
|
|
570
|
+
try { config = await importReleaseConfig(configPath, { query: `commit=${commit}` }); } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
|
|
570
571
|
} catch (error) {
|
|
571
572
|
if (error?.reported) process.exit(1);
|
|
572
573
|
throw error;
|
|
@@ -630,7 +631,7 @@ function sqlCipherFeatures(text) {
|
|
|
630
631
|
}
|
|
631
632
|
|
|
632
633
|
function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot, rightkitRegistryFields }) {
|
|
633
|
-
// Every app's
|
|
634
|
+
// Every app's release config names installer/updater artifacts
|
|
634
635
|
// relative to its resolved native target. On a broker-managed host that
|
|
635
636
|
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
636
637
|
// targetBridge.ensure() deliberately leaves a real local directory alone,
|
|
@@ -687,17 +688,24 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
687
688
|
}
|
|
688
689
|
if (sources.size === 0) fail(`nothing to seal: declare installer.artifacts, updater.artifacts or artifacts for the ${platform} target`);
|
|
689
690
|
for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
|
|
690
|
-
// Every minisign updater
|
|
691
|
-
//
|
|
691
|
+
// Every minisign updater is checked against a trust root, by key id and over the full bytes, before any seal directory or
|
|
692
|
+
// artifact copy is created. A Right Suite app's root is the Right Suite contract's; any other app must declare its own key.
|
|
692
693
|
const updaterPublicKey = config.distribution?.updaterPublicKey ?? config.distribution?.updaterPubkey ?? target?.updater?.pubkey ?? target?.updater?.publicKey;
|
|
693
694
|
if ((target.updater?.artifacts?.length ?? 0) > 0 && target.updater?.kind !== "appinstaller") {
|
|
694
|
-
if (typeof updaterPublicKey !== "string" || !updaterPublicKey.trim()) fail("updater public key is required to verify updater artifacts before sealing");
|
|
695
|
+
if (!isSuiteUpdaterApp(config.app) && (typeof updaterPublicKey !== "string" || !updaterPublicKey.trim())) fail("updater public key is required to verify updater artifacts before sealing");
|
|
696
|
+
let trust;
|
|
697
|
+
try { trust = resolveUpdaterTrust({ app: config.app, configuredPublicKey: updaterPublicKey, configuredKeyId: config.distribution?.updaterKeyId }); }
|
|
698
|
+
catch (error) { fail(`updater trust rejected at seal: ${error.message}`); }
|
|
695
699
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
696
700
|
const source = resolveArtifactPath(artifact.file);
|
|
697
701
|
const signature = readFileSync(resolveArtifactPath(artifact.signature), "utf8").trim();
|
|
698
702
|
if (!signature) fail(`updater signature is empty, refusing to seal: ${artifact.signature}`);
|
|
699
|
-
|
|
700
|
-
|
|
703
|
+
const label = `signature of ${path.basename(artifact.file)}`;
|
|
704
|
+
// The id comes first: a signature by another key would otherwise fail verification without saying whose it is.
|
|
705
|
+
try {
|
|
706
|
+
assertUpdaterKeyId(signature, trust.keyId, label, { app: config.app });
|
|
707
|
+
assertUpdaterSignature(readFileSync(source), signature, trust.publicKey, label);
|
|
708
|
+
} catch (error) { fail(`updater signature rejected at seal: ${error.message}`); }
|
|
701
709
|
}
|
|
702
710
|
}
|
|
703
711
|
// Android: the build receipt must describe exactly the AAB being sealed (bytes re-hashed) and its signer must be the pinned
|
|
@@ -1053,7 +1061,7 @@ function writeJson(file, value) {
|
|
|
1053
1061
|
}
|
|
1054
1062
|
|
|
1055
1063
|
function usage(code) {
|
|
1056
|
-
console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.
|
|
1064
|
+
console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.json] [--dry-run]");
|
|
1057
1065
|
process.exit(code);
|
|
1058
1066
|
}
|
|
1059
1067
|
|
package/cache-command.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import {
|
|
6
6
|
applyCachePrune,
|
|
7
7
|
inspectCache,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveSharedCacheIdentity,
|
|
13
13
|
resolveSharedCacheRoot,
|
|
14
14
|
} from "./cache-policy.mjs";
|
|
15
|
+
import { importReleaseConfig } from "./release-config.mjs";
|
|
15
16
|
import { resolveReleaseLayout } from "./release-state.mjs";
|
|
16
17
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
17
18
|
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
@@ -52,11 +53,11 @@ export async function runCacheCommand(args = process.argv.slice(2), { env = proc
|
|
|
52
53
|
}
|
|
53
54
|
if (command === "migrate") {
|
|
54
55
|
const configPath = option(rest, "--config");
|
|
55
|
-
if (!configPath) throw new Error("cache migrate requires --config <right-release.config.
|
|
56
|
+
if (!configPath) throw new Error("cache migrate requires --config <right-release.config.json>");
|
|
56
57
|
rejectUnknown(rest.filter((value) => value !== "--config" && value !== configPath), new Set(["--json", "--dry-run", "--apply"]));
|
|
57
58
|
if (rest.includes("--dry-run") && rest.includes("--apply")) throw new Error("cache migrate accepts either --dry-run or --apply");
|
|
58
59
|
const configFile = path.resolve(configPath);
|
|
59
|
-
const config =
|
|
60
|
+
const config = await importReleaseConfig(configFile);
|
|
60
61
|
if (!config?.app) throw new Error("cache migrate config must expose app");
|
|
61
62
|
const appRoot = path.dirname(configFile);
|
|
62
63
|
const configuredTarget = config.targets?.[platform] ?? {};
|
|
@@ -15,6 +15,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
18
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
18
19
|
import { runCargoSync } from "./managed-cargo.mjs";
|
|
19
20
|
|
|
20
21
|
const fail = (message) => { throw new CommandSpecError(message); };
|
|
@@ -73,7 +74,7 @@ export function generateCargoNotices({ appRoot, config, flags = {}, check = fals
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export function parseCargoNoticeArgs(argv) {
|
|
76
|
-
const opts = { config:
|
|
77
|
+
const opts = { config: defaultReleaseConfigFile(), output: undefined, check: false };
|
|
77
78
|
for (let i = 0; i < argv.length; i += 1) {
|
|
78
79
|
const arg = argv[i];
|
|
79
80
|
if (arg === "--config" || arg === "--output") {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A line reading of the [package] table of a Cargo.toml, for the scans that must run without a TOML parser (the license scan and
|
|
2
|
+
// the secret scan of the repository gate). This module has no imports on purpose: publish-cargo.mjs is in the trusted runtime
|
|
3
|
+
// helper closure (scripts/ci/runtime-supplement.mjs), which may only follow static relative imports of plain modules.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `{ hasPackage, unpublished }` for the text of a Cargo.toml: whether it has a `[package]` table, and whether that table says
|
|
7
|
+
* `publish = false` or `publish = []`. A crate that can never be published ships its licence to no one, like a package.json with
|
|
8
|
+
* `"private": true`. An array-of-tables header (`[[bin]]`, `[[package.metadata.x]]`) starts a new table like any other, so a
|
|
9
|
+
* `publish` key under it is not the package's.
|
|
10
|
+
*/
|
|
11
|
+
export function cargoManifestState(text) {
|
|
12
|
+
let table = "";
|
|
13
|
+
let hasPackage = false;
|
|
14
|
+
let unpublished = false;
|
|
15
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
16
|
+
const header = /^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/.exec(line);
|
|
17
|
+
if (header) {
|
|
18
|
+
table = header[1].trim();
|
|
19
|
+
if (table === "package") hasPackage = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (table === "package" && /^\s*publish\s*=\s*(?:false\b|\[\s*\])/.test(line)) unpublished = true;
|
|
23
|
+
}
|
|
24
|
+
return { hasPackage, unpublished };
|
|
25
|
+
}
|
|
@@ -19,6 +19,7 @@ import path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { parse } from "smol-toml";
|
|
21
21
|
import { findCargoManifests, isExcludedScanDirectory } from "./cargo-contract.mjs";
|
|
22
|
+
import { defaultReleaseConfigFile, isJsonReleaseConfig } from "./release-config.mjs";
|
|
22
23
|
|
|
23
24
|
export const CARGO_PROFILE_TEMPLATE = fileURLToPath(new URL("./templates/cargo-profiles.toml", import.meta.url));
|
|
24
25
|
export const RETIRED_PROFILE = "release-iterate";
|
|
@@ -74,28 +75,40 @@ export function checkManifestProfiles(manifest, template, file) {
|
|
|
74
75
|
// Non-shipping workspaces: fixed prefixes plus the app's declared `cargoProfileExempt` directories.
|
|
75
76
|
const NON_SHIPPING_PREFIXES = ["qa", "benchmarks"];
|
|
76
77
|
const CARGO_PROFILE_EXEMPT_KEY = "cargoProfileExempt";
|
|
77
|
-
const RELEASE_CONFIG = "right-release.config.mjs";
|
|
78
78
|
const STRING_LITERAL = /"([^"\\\n]*)"|'([^'\\\n]*)'/g;
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// This scanner is synchronous and has only the app root, so it reads `cargoProfileExempt` from the release config file itself and
|
|
81
|
+
// never imports it. A JSON config (the one `defaultReleaseConfigFile` picks when it exists) is parsed as data: the array must hold
|
|
82
|
+
// plain strings. A legacy `.mjs` config, which release.mjs still imports as an ES module, is read as text: the literal array must
|
|
83
|
+
// hold plain string literals. Either way every entry names a directory inside the app, and anything else is a finding, never
|
|
84
|
+
// silently ignored. M24 command admission is not run here: this is a pure file reader.
|
|
83
85
|
export function readCargoProfileExempt(root) {
|
|
84
|
-
const
|
|
86
|
+
const configFile = defaultReleaseConfigFile(root);
|
|
87
|
+
const file = path.join(root, configFile);
|
|
85
88
|
const findings = [];
|
|
86
89
|
if (!existsSync(file)) return { paths: [], findings };
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
let entries;
|
|
91
|
+
if (isJsonReleaseConfig(configFile)) {
|
|
92
|
+
let declared;
|
|
93
|
+
try { declared = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""))?.[CARGO_PROFILE_EXEMPT_KEY]; }
|
|
94
|
+
catch (error) { return { paths: [], findings: [{ rule: "invalid-exempt", file: configFile, message: `is not valid JSON, so ${CARGO_PROFILE_EXEMPT_KEY} cannot be read: ${error.message}` }] }; }
|
|
95
|
+
if (declared === undefined) return { paths: [], findings };
|
|
96
|
+
if (!Array.isArray(declared) || declared.some((entry) => typeof entry !== "string")) findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} must be an array of strings` });
|
|
97
|
+
entries = Array.isArray(declared) ? declared.filter((entry) => typeof entry === "string") : [];
|
|
98
|
+
} else {
|
|
99
|
+
const match = new RegExp(`\\b${CARGO_PROFILE_EXEMPT_KEY}\\s*:\\s*\\[([^\\]]*)\\]`).exec(readFileSync(file, "utf8"));
|
|
100
|
+
if (!match) return { paths: [], findings };
|
|
101
|
+
const body = match[1];
|
|
102
|
+
if (body.replace(STRING_LITERAL, "").replace(/[,\s]/g, "") !== "") {
|
|
103
|
+
findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} must be an array of string literals` });
|
|
104
|
+
}
|
|
105
|
+
entries = [...body.matchAll(STRING_LITERAL)].map(([, double, single]) => double ?? single);
|
|
92
106
|
}
|
|
93
107
|
const paths = [];
|
|
94
|
-
for (const
|
|
95
|
-
const entry = double ?? single;
|
|
108
|
+
for (const entry of entries) {
|
|
96
109
|
const normalized = path.posix.normalize(entry.replace(/\\/g, "/")).replace(/\/+$/, "");
|
|
97
110
|
if (normalized === "." || normalized === "" || path.posix.isAbsolute(normalized) || normalized.split("/").includes("..")) {
|
|
98
|
-
findings.push({ rule: "invalid-exempt", file:
|
|
111
|
+
findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} entry ${JSON.stringify(entry)} must be a relative directory inside the app` });
|
|
99
112
|
} else paths.push(normalized);
|
|
100
113
|
}
|
|
101
114
|
return { paths, findings };
|
package/cli/right-release.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const commands = new Map([
|
|
|
13
13
|
["publish", "upload-release.mjs"],
|
|
14
14
|
["upload", "upload-release.mjs"],
|
|
15
15
|
["deps", "deps.mjs"],
|
|
16
|
+
["update", "outdated-command.mjs"],
|
|
16
17
|
["publish-update", "publish-update.mjs"],
|
|
17
18
|
["hardening", "hardeningscan.mjs"],
|
|
18
19
|
["harden", "hardeningscan.mjs"],
|
|
@@ -56,8 +57,27 @@ const args = process.argv.slice(2);
|
|
|
56
57
|
const first = args[0];
|
|
57
58
|
// The commands that compile, package, sign, ship or inspect a release run the app check (../app-check.mjs, CHECKED_COMMANDS) in this
|
|
58
59
|
// process before they start a child. A red check prints its findings and exits 1; there is no flag that skips it.
|
|
60
|
+
const WORKTREE_COMPILES = new Set(["dev", "test", "stage-sidecar"]);
|
|
59
61
|
async function requireGreenCheck() {
|
|
60
|
-
const { guardCommand } = await import("../app-check.mjs");
|
|
62
|
+
const { guardCommand, checkedCommandOf, checkModeFor, appDirectory } = await import("../app-check.mjs");
|
|
63
|
+
// One door (Adrian, 2026-10-25): a commit-bound command (build, package, release, qa, upload, publish, signing) runs only when
|
|
64
|
+
// rightkit-gate --to launched it for HEAD, after class A and the compile stage passed.
|
|
65
|
+
const checked = checkedCommandOf(args);
|
|
66
|
+
// dev, test and stage-sidecar work on the uncommitted tree but compile, so they too run only under a door run
|
|
67
|
+
// (rightkit-gate --to dev|test, class A on the working tree first).
|
|
68
|
+
// Gate on the mode, not the key: a flag-first call (`right-release --platform mac`) is a build with no command word.
|
|
69
|
+
if (checkModeFor(args) === "commit" || (checked && WORKTREE_COMPILES.has(checked))) {
|
|
70
|
+
const { requireDoorRun } = await import("../gate-stages.mjs");
|
|
71
|
+
const { spawnSync } = await import("../background-process.mjs");
|
|
72
|
+
const cwd = appDirectory(args, process.cwd());
|
|
73
|
+
const gitOut = (a) => { const r = spawnSync("git", a, { cwd, encoding: "utf8" }); return r.status === 0 ? r.stdout.trim() : ""; };
|
|
74
|
+
const common = gitOut(["rev-parse", "--git-common-dir"]);
|
|
75
|
+
// Outside a git repository the app check below refuses (nothing can be bound to a commit); inside one, only a door run admits.
|
|
76
|
+
if (common) {
|
|
77
|
+
const refusal = requireDoorRun({ commonDir: path.resolve(cwd, common), commit: gitOut(["rev-parse", "HEAD"]), command: `right-release ${checked ?? args.join(" ")}`.trim() });
|
|
78
|
+
if (refusal) { console.error(`right-release: ${refusal}`); process.exit(2); }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
const refused = guardCommand({ args });
|
|
62
82
|
if (!refused) return;
|
|
63
83
|
for (const line of refused.lines) console.error(line);
|
|
@@ -102,9 +122,11 @@ if (first === "--version" || first === "-v") {
|
|
|
102
122
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
103
123
|
}
|
|
104
124
|
} else if (first === "check") {
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
// Exactly `rightkit-gate check` (one door): the app check, then every class A scan over the whole commit. The repository is the git
|
|
126
|
+
// top-level of --config's folder, else the cwd.
|
|
127
|
+
const { appDirectory } = await import("../app-check.mjs");
|
|
128
|
+
const { main } = await import("../bin/rightkit-gate.mjs");
|
|
129
|
+
process.exit(await main(["check", ...args.slice(1)], { cwd: appDirectory(args, process.cwd()) }));
|
|
108
130
|
} else if (first === "hooks" && args[1] === "install") {
|
|
109
131
|
// One installer: `hooks install` is `rightkit-gate install-hook` (pre-push check and gate, ownership pre-commit step) run in the app.
|
|
110
132
|
const app = args.indexOf("--app");
|
|
@@ -295,8 +317,12 @@ Commands:
|
|
|
295
317
|
ios-credentials store --key-id <KEYID> --issuer <uuid> | check
|
|
296
318
|
Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
|
|
297
319
|
qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa (and the journeys lane when the config sets qa.journeys) and write the sealed QA receipt. Refused inside a QA run. The receipt must match the platform and release id, come from an approved runner digest, and show every release-critical journey passed
|
|
298
|
-
Admission is per app in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks
|
|
320
|
+
Admission is per app, and per stage (source = build, installed = upload and publish), in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks that stage. Only adrian moves an app or stage from refuse back to warn, or approves a runner
|
|
299
321
|
qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List source/installed QA status per release; exit 1 if an uploaded build lacks passing QA
|
|
322
|
+
qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage source|installed]
|
|
323
|
+
Show the QA admission policy, or set an app's mode (both stages, or one with --stage)
|
|
324
|
+
qa policy approve-runner --runner <file> --version <v> --platform mac|win --set-by adrian [--note <text>]
|
|
325
|
+
Approve a rightkit-qa runner build for one platform: the file must report "rightkit-qa <v>" from --version and <v> must be the published version
|
|
300
326
|
publish-update --release <id> --platform mac|win --tier patch|update
|
|
301
327
|
Upload sealed bytes to R2 & register RightApps
|
|
302
328
|
release Compatibility alias for build
|
|
@@ -368,9 +394,13 @@ Commands:
|
|
|
368
394
|
disk check [--json] Per-writer sizes vs caps and free-space floor (exit 3 on warning)
|
|
369
395
|
disk sweep [--dry-run] [--days N] [--delete] [--json]
|
|
370
396
|
Move stale idle heavy data to the heavy volume (delete only with --delete)
|
|
371
|
-
disk run
|
|
372
|
-
disk
|
|
397
|
+
disk run [--auto|--light] self-check, then the full pass (check, sweep, purge, daily report); --light is the stat-only hourly pass (floors, temp-like writers, scratch; purges only below the agent volume's soft floor); --auto picks full once a day after 03:30, else light (the scheduled job)
|
|
398
|
+
disk run-root <name> [--days N] [--json] Lease a run directory under the scratch root (default 3 days): the sweep skips it until the lease ends
|
|
399
|
+
disk run-root --release <name> End a run-root lease
|
|
400
|
+
disk schedule install|uninstall|status Hourly launchd job (Mac) / hidden Task Scheduler entry (Windows) running "disk run --auto", bound to the stable per-user copy; status fails when no full pass finished in 26 h
|
|
373
401
|
deps --check|--audit|--update Shared dependency lane
|
|
402
|
+
update check|apply|gate|land|discard|run|schedule [--repo <path>]
|
|
403
|
+
Run the pinned @rightkit/outdated updater (right-update) that moves stale pins; reads .rightkit/update.json
|
|
374
404
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
375
405
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
|
376
406
|
generate-dmg-background <options> Generate the branded multi-resolution DMG background
|
package/command-spec.mjs
CHANGED
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
// `cwd` and `node` are app-relative and may not leave the app. `nodeFlags` go before the script, `nodeOptions` are merged
|
|
6
6
|
// (de-duplicated) into NODE_OPTIONS for the child. Unknown keys are refused so a typo cannot silently change a run.
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
9
9
|
|
|
10
|
-
/** Load an app's `right-release.config.mjs
|
|
11
|
-
export async function loadAppConfig(configFile =
|
|
10
|
+
/** Load an app's release config (`right-release.config.json`, or the legacy `.mjs` while it is the only one); the app root is the config file's directory. */
|
|
11
|
+
export async function loadAppConfig(configFile = defaultReleaseConfigFile()) {
|
|
12
12
|
const configPath = path.resolve(configFile);
|
|
13
|
-
|
|
14
|
-
return { config: loaded.default ?? loaded.config ?? loaded, appRoot: path.dirname(configPath), configPath };
|
|
13
|
+
return { config: await importReleaseConfig(configPath, { lenient: true }), appRoot: path.dirname(configPath), configPath };
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export class CommandSpecError extends Error {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The declared compile inputs a RustRight broker captured for a compile (RIGHTKIT_COMPILE_INPUTS): the gate judges exactly
|
|
2
|
+
// those bytes, where the broker copied them, so what is checked is what the compile reads. The tree's tracked files are
|
|
3
|
+
// judged from the commit as always; the declared inputs are the ignored paths (a built `dist`, generated assets) the
|
|
4
|
+
// commit does not hold, listed in the captured tree's `.rightkit/compile-inputs.json`.
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawnSync } from "./background-process.mjs";
|
|
9
|
+
import { SECRET_PATTERNS } from "./publish-cargo.mjs";
|
|
10
|
+
|
|
11
|
+
export const COMPILE_INPUTS_FILE = ".rightkit/compile-inputs.json";
|
|
12
|
+
|
|
13
|
+
/** Files of one declared input in the broker's order (children by name), refusing links; [{ name, abs }]. */
|
|
14
|
+
function inputFiles(root, input) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const walk = (abs, name) => {
|
|
17
|
+
const st = fs.lstatSync(abs);
|
|
18
|
+
if (st.isSymbolicLink()) throw new Error(`declared compile input holds a symbolic link at ${name}`);
|
|
19
|
+
if (st.isDirectory()) {
|
|
20
|
+
for (const child of fs.readdirSync(abs).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) walk(path.join(abs, child), `${name}/${child}`);
|
|
21
|
+
} else if (st.isFile()) out.push({ name, abs });
|
|
22
|
+
};
|
|
23
|
+
walk(path.join(root, input), input);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Judges the captured declared inputs under `root`: each declared path must exist without links; every `.js/.mjs/.cjs`
|
|
29
|
+
* file parses (`node --check`), every `.json` file parses, and no file carries a secret. Returns { inputs: [{ path, sha256,
|
|
30
|
+
* files }], findings: [string] }; `sha256` is computed as the broker records it (name, NUL, bytes, NUL per file, in order),
|
|
31
|
+
* so the receipt and the gate name the same bytes.
|
|
32
|
+
*/
|
|
33
|
+
export function judgeCompileInputs(root, { spawn = spawnSync, execPath = process.execPath } = {}) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
let declared = [];
|
|
36
|
+
const file = path.join(root, COMPILE_INPUTS_FILE);
|
|
37
|
+
if (!fs.existsSync(file)) return { inputs: [], findings };
|
|
38
|
+
try {
|
|
39
|
+
declared = JSON.parse(fs.readFileSync(file, "utf8")).inputs;
|
|
40
|
+
if (!Array.isArray(declared) || declared.some((p) => typeof p !== "string")) throw new Error("`inputs` must be a list of paths");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { inputs: [], findings: [`${COMPILE_INPUTS_FILE}: ${e.message}`] };
|
|
43
|
+
}
|
|
44
|
+
const inputs = [];
|
|
45
|
+
for (const input of declared) {
|
|
46
|
+
if (path.isAbsolute(input) || input.split(/[\\/]/).some((part) => part === ".." || part === "." || part === "")) { findings.push(`${input}: not a plain relative path`); continue; }
|
|
47
|
+
let files;
|
|
48
|
+
try { files = inputFiles(root, input); } catch (e) { findings.push(`${input}: ${e.code === "ENOENT" ? "missing from the captured tree" : e.message}`); continue; }
|
|
49
|
+
const hash = crypto.createHash("sha256");
|
|
50
|
+
for (const f of files) {
|
|
51
|
+
const bytes = fs.readFileSync(f.abs);
|
|
52
|
+
hash.update(f.name); hash.update(Buffer.from([0])); hash.update(bytes); hash.update(Buffer.from([0]));
|
|
53
|
+
const text = bytes.toString("utf8");
|
|
54
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(text))) findings.push(`${f.name}: holds a secret (token or private key pattern)`);
|
|
55
|
+
if (/\.(m|c)?js$/.test(f.name)) {
|
|
56
|
+
const r = spawn(execPath, ["--check", f.abs], { encoding: "utf8" });
|
|
57
|
+
if (r.status !== 0) findings.push(`${f.name}: does not parse (${String(r.stderr ?? "").split("\n").find((l) => /Error/.test(l))?.trim() ?? `node --check exit ${r.status}`})`);
|
|
58
|
+
}
|
|
59
|
+
if (/\.json$/.test(f.name)) {
|
|
60
|
+
try { JSON.parse(text); } catch (e) { findings.push(`${f.name}: invalid JSON (${e.message})`); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
inputs.push({ path: input, sha256: hash.digest("hex"), files: files.length });
|
|
64
|
+
}
|
|
65
|
+
return { inputs, findings };
|
|
66
|
+
}
|