@rightkit/release 0.6.24 → 0.6.25
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 +11 -35
- package/android-dev.mjs +2 -1
- package/app-check.mjs +1 -2
- package/app-closure.mjs +3 -2
- package/assembled-app.mjs +11 -3
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +83 -26
- package/build-helper-trace.mjs +264 -0
- package/build-helpers-command.mjs +184 -0
- package/build-launch.mjs +11 -75
- package/build-release.mjs +10 -11
- package/cargo-profile-contract.mjs +19 -29
- package/cargo-target-shim.mjs +31 -0
- package/cli/right-release.mjs +5 -7
- package/create-mac-updater.mjs +4 -0
- package/cross-targets.mjs +94 -0
- package/deterministic-scans.mjs +310 -20
- package/development.mjs +8 -8
- package/freshness-census.mjs +1 -1
- package/gate-stages.mjs +1 -1
- package/install-assembled-app.mjs +1 -1
- package/install-tauri-authenticode.mjs +3 -3
- package/local-publish.mjs +27 -5
- package/mac-package-recipe.mjs +55 -2
- package/managed-roots.mjs +12 -4
- package/package.json +1 -5
- package/pipeline-admission.mjs +1 -1
- package/pipeline-receipts.mjs +302 -302
- package/provenance-verify.mjs +3 -1
- package/qa-build-features.mjs +11 -2
- package/release-qa-audit.mjs +9 -27
- package/release-qa-command.mjs +23 -64
- package/release-qa-fixtures.mjs +9 -5
- package/release-qa-floor.json +0 -1
- package/release-qa-policy.mjs +9 -10
- package/release-qa-receipt.mjs +6 -44
- package/release-source-identity.mjs +1 -12
- package/release-switch-registry.json +4 -1
- package/rightkit-registry-resolver.mjs +35 -3
- package/rightkit-versions.json +2 -2
- package/sign-windows.mjs +1 -1
- package/snapshot-broker.mjs +33 -8
- package/snapshot-metadata.mjs +32 -0
- package/snapshot-request.mjs +6 -3
- package/stage-sidecar.mjs +28 -11
- package/suite-signing-policy.mjs +8 -58
- package/templates/cargo-profiles.toml +6 -25
- package/test-command.mjs +40 -140
- package/updater-shipped-key.mjs +26 -0
- package/web-build.mjs +29 -4
- package/qa-journeys-prebuilt-run.mjs +0 -18
- package/qa-journeys-prebuilt.mjs +0 -148
- package/qa-layout.mjs +0 -229
- package/qa-snapshot-run.mjs +0 -53
- package/qa-snapshot.mjs +0 -164
- package/sign-qa-bundle.mjs +0 -210
- package/suite-signing.mjs +0 -259
package/build-release.mjs
CHANGED
|
@@ -27,10 +27,9 @@ import { sccacheRefusal } from "./admission-host.mjs";
|
|
|
27
27
|
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
29
|
import { dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
30
|
-
import { assertInsideSource, assertReleaseSource, gitRootOf, isSnapshotSource,
|
|
30
|
+
import { assertInsideSource, assertReleaseSource, gitRootOf, isSnapshotSource, resolveReleaseSource, sourceRootOf } from "./release-source-identity.mjs";
|
|
31
31
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
32
32
|
import { ArtifactPathError, assertSplitCommandPaths, boundTargetsEvidence, releaseCommandSteps, resolveArtifactPath as resolveSplitArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
|
|
33
|
-
import { admitSourceQa } from "./release-qa-audit.mjs";
|
|
34
33
|
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
35
34
|
import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
|
|
36
35
|
import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
@@ -361,11 +360,6 @@ try {
|
|
|
361
360
|
for (const check of preflight) {
|
|
362
361
|
if (check.status !== "ok") console.error(`[right-release] preflight ${check.name}: ${check.detail}`);
|
|
363
362
|
}
|
|
364
|
-
// Hardening step 4: source QA is recorded, not enforced. A missing or rejected receipt warns loudly and the outcome
|
|
365
|
-
// is sealed into the manifest (`qa.status`) for `right-release qa audit`. Dev lanes (install-app --dev) never reach this file.
|
|
366
|
-
// A split compares the source QA receipt with the SELECTED commit (its tree, repository identity and lockfile closure from its objects), not HEAD.
|
|
367
|
-
const qaAdmission = admitSourceQa({ repoRoot: layout.repoRoot, releaseId, platform, configName: path.relative(process.cwd(), configPath) || path.basename(configPath), releaseConfigSha256: hashFile(configPath), ...qaAdmissionSourceOptions(releaseSource) });
|
|
368
|
-
|
|
369
363
|
// A tool being INSTALLED is not the same as it being the one PATH picks. The
|
|
370
364
|
// vendored-OpenSSL build shells out to `perl`, and under Git Bash/MSYS that
|
|
371
365
|
// resolves to MSYS Perl, which cannot configure OpenSSL. Put the real Windows
|
|
@@ -566,7 +560,13 @@ try {
|
|
|
566
560
|
if (linked.action === "moved-aside") console.error(`[right-release] hosted CI: moved existing ${targetLink} aside to ${linked.aside}`);
|
|
567
561
|
}
|
|
568
562
|
// A Cargo-less native iOS app installs Node dependencies only when it has a package.json.
|
|
569
|
-
|
|
563
|
+
const needsNodeInstall = nativeIos ? hasPackageJson : resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager;
|
|
564
|
+
// rightkit#1443.3: in a gate split the snapshot's node_modules were already installed by the gate, offline, from the same lockfile; a
|
|
565
|
+
// second install here would run inside the snapshot around the broker (which refuses it). It is skipped; a snapshot without them is refused.
|
|
566
|
+
if (needsNodeInstall && splitSource) {
|
|
567
|
+
if (!existsSync(path.join(appRoot, "node_modules"))) fail(`the gate's snapshot at ${appRoot} has no node_modules: the gate installs them before the package stage; nothing is installed here (rightkit#1443)`);
|
|
568
|
+
console.error(`[right-release] node_modules already installed by the gate in the snapshot; no second install (rightkit#1443)`);
|
|
569
|
+
} else if (needsNodeInstall) await runLocalStep(() => runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, heavyDecisionOfRun?.localSteps ? { ...env, RIGHTSUITE_HEAVY_WORK_OWNER: "1" } : env, cacheTarget, path.join(stateRoot, "stall-install.log")));
|
|
570
570
|
},
|
|
571
571
|
build: async () => {
|
|
572
572
|
throwIfInterrupted();
|
|
@@ -602,7 +602,7 @@ try {
|
|
|
602
602
|
});
|
|
603
603
|
},
|
|
604
604
|
seal: async ({ sealedDir }) => {
|
|
605
|
-
sealRelease({ configRoot: appRoot, managedCargoTarget, artifactPaths, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence,
|
|
605
|
+
sealRelease({ configRoot: appRoot, managedCargoTarget, artifactPaths, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot, stateRoot, rightkitRegistryFields: rightKitResolution ? registryManifestFields(rightKitResolution.snapshot, RELEASE_TOOL_VERSION) : undefined, splitSealSource: splitSource ? { commit: releaseSource.commit, tree: releaseSource.tree, snapshotAppRoot: appRoot, checkoutAppRoot: releaseSource.checkoutAppRoot } : null });
|
|
606
606
|
verifySealedRelease(sealedDir);
|
|
607
607
|
if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
|
|
608
608
|
checkpoint(stateRoot, "sealed");
|
|
@@ -695,7 +695,7 @@ function sqlCipherFeatures(text) {
|
|
|
695
695
|
.filter((value) => /sqlcipher|openssl/i.test(value));
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
-
function sealRelease({ configRoot, managedCargoTarget, artifactPaths = null, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence,
|
|
698
|
+
function sealRelease({ configRoot, managedCargoTarget, artifactPaths = null, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot, stateRoot, rightkitRegistryFields, splitSealSource = null }) {
|
|
699
699
|
// Every app's release config names installer/updater artifacts
|
|
700
700
|
// relative to its resolved native target. On a broker-managed host that
|
|
701
701
|
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
@@ -869,7 +869,6 @@ function sealRelease({ configRoot, managedCargoTarget, artifactPaths = null, and
|
|
|
869
869
|
// The exact registry resolution (identities, digests, registries, policy revision, observation time, observed latest)
|
|
870
870
|
// this release was built against. Upload and resume consume it; neither resolves again.
|
|
871
871
|
...rightkitRegistryFields,
|
|
872
|
-
qa: qaAdmission,
|
|
873
872
|
// DevTools allowance (owner's fixed design): { mode: "denied" } for every app; content-only only for an allow-listed app.
|
|
874
873
|
devtools: devtoolsEvidence(resolveDevtoolsAllowance({ config, root: configRoot })),
|
|
875
874
|
platform,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// RightKit standard Cargo profile contract (Adrian, 2026-10-16; Astra-reviewed option C).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
// - an environment-variable override gated by cfg(debug_assertions):
|
|
13
|
-
//
|
|
3
|
+
// One build output and one warm cache per app per host (GATE-CONTRACT GC-A01, GC-A07). Every gate compile (check, clippy,
|
|
4
|
+
// rustdoc, the test compile and both installers) builds with [profile.release] for the host's own target, whatever the caller
|
|
5
|
+
// passed (cross-targets.mjs oneOutput); the dev installer is the release profile plus the `qa-native` feature. So no gate build
|
|
6
|
+
// has debug_assertions, and the release tool's scans (not debug assertions) keep the QA control server out of a release.
|
|
7
|
+
// Every app workspace-root Cargo.toml therefore carries:
|
|
8
|
+
// - [profile.release] with panic = "unwind" (templates/cargo-profiles.toml),
|
|
9
|
+
// - no other profile table: no [profile.dev], [profile.test] or custom profile (each would be a second output directory),
|
|
10
|
+
// - no [profile.release-iterate] (retired; nor anything that inherits it).
|
|
11
|
+
// The same scan also flags a source pattern that behaves differently between the profiles:
|
|
12
|
+
// - an environment-variable override gated by cfg(debug_assertions): no gate build compiles it (tests and installers
|
|
13
|
+
// are all release), so only a developer's own cargo run reaches it. Gate it on an explicit feature instead. Heuristic: a
|
|
14
|
+
// cfg(debug_assertions) attribute or cfg! within 12 lines before an env read.
|
|
14
15
|
// Suppress a reviewed site with a `rightkit-allow: debug-assertions-env` comment on the cfg line or
|
|
15
16
|
// the line above it.
|
|
16
17
|
// The template file is the single source: this module reads it, it never copies the values.
|
|
@@ -28,12 +29,10 @@ const ENV_WINDOW_LINES = 12;
|
|
|
28
29
|
// Never part of an app's shipped source: tests, examples, benches and vendored reference copies.
|
|
29
30
|
const SOURCE_SKIP = new Set(["tests", "examples", "benches", "reference"]);
|
|
30
31
|
const ENV_READ = /env::var|var_os|env::set_var|env::remove_var|\benv!\s*\(|option_env!|_env\(|env_var|env_nonempty/;
|
|
31
|
-
const WINDOWS_SUBSYSTEM_GATED = /cfg_attr\s*\((?:(?!\]).)*?not\s*\(\s*debug_assertions\s*\)(?:(?!\]).)*?windows_subsystem/gs;
|
|
32
32
|
|
|
33
33
|
export function loadCargoProfileTemplate(file = CARGO_PROFILE_TEMPLATE) {
|
|
34
34
|
const profile = parse(readFileSync(file, "utf8")).profile ?? {};
|
|
35
|
-
|
|
36
|
-
return { dev, devPackageStar: packages["*"] ?? {}, releasePanic: profile.release?.panic };
|
|
35
|
+
return { releasePanic: profile.release?.panic };
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
const isTable = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -54,21 +53,15 @@ export function checkManifestProfiles(manifest, template, file) {
|
|
|
54
53
|
const findings = [];
|
|
55
54
|
const finding = (rule, message) => findings.push({ rule, file, message });
|
|
56
55
|
const profiles = isTable(manifest.profile) ? manifest.profile : {};
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
compareTable(dev ? devKeys : undefined, template.dev, "[profile.dev]", finding);
|
|
62
|
-
compareTable(isTable(devPackages) ? devPackages["*"] : undefined, template.devPackageStar, '[profile.dev.package."*"]', finding);
|
|
63
|
-
if (isTable(devPackages)) for (const name of Object.keys(devPackages)) if (name !== "*") finding("profile-extra", `[profile.dev.package.${show(name)}] is not in the template`);
|
|
64
|
-
if (dev) for (const name of Object.keys(dev)) if (isTable(dev[name]) && name !== "package") finding("profile-extra", `[profile.dev.${name}] is not in the template`);
|
|
65
|
-
// [profile.dev] panic is checked with the rest of the dev table above.
|
|
56
|
+
// One build output (GC-A01, GC-A07): no gate build uses any profile but release, so an app carries no other profile table.
|
|
57
|
+
for (const name of Object.keys(profiles)) {
|
|
58
|
+
if (name !== "release" && name !== RETIRED_PROFILE) finding("profile-extra", `[profile.${name}] is not allowed: every gate build uses [profile.release] for the host target (one output directory, GATE-CONTRACT GC-A07); delete the table`);
|
|
59
|
+
}
|
|
66
60
|
if (profiles.release?.panic !== template.releasePanic) finding("panic", `[profile.release] panic must be ${show(template.releasePanic)} (found ${show(profiles.release?.panic)})`);
|
|
67
61
|
if (RETIRED_PROFILE in profiles) finding("retired-profile", `[profile.${RETIRED_PROFILE}] is retired; delete it and its package overrides`);
|
|
68
62
|
for (const [name, table] of Object.entries(profiles)) {
|
|
69
63
|
if (isTable(table) && table.inherits === RETIRED_PROFILE) finding("retired-profile", `[profile.${name}] inherits the retired ${RETIRED_PROFILE} profile`);
|
|
70
64
|
}
|
|
71
|
-
if ("test" in profiles) finding("profile-extra", "[profile.test] overrides are not allowed; test inherits dev");
|
|
72
65
|
return findings;
|
|
73
66
|
}
|
|
74
67
|
|
|
@@ -149,7 +142,7 @@ function* rustFiles(dir) {
|
|
|
149
142
|
}
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
// Source patterns
|
|
145
|
+
// Source patterns that behave differently between the profiles (see the header). Returns findings with a line number.
|
|
153
146
|
export function scanDebugAssertionSources(root) {
|
|
154
147
|
const findings = [];
|
|
155
148
|
for (const file of rustFiles(root)) {
|
|
@@ -157,9 +150,6 @@ export function scanDebugAssertionSources(root) {
|
|
|
157
150
|
if (!text.includes("debug_assertions")) continue;
|
|
158
151
|
const label = path.relative(root, file).split(path.sep).join("/");
|
|
159
152
|
const lineAt = (index) => text.slice(0, index).split("\n").length;
|
|
160
|
-
for (const match of text.matchAll(WINDOWS_SUBSYSTEM_GATED)) {
|
|
161
|
-
findings.push({ rule: "windows-subsystem", file: label, line: lineAt(match.index), message: 'cfg_attr(not(debug_assertions), windows_subsystem = ...) opens a console for installed dev apps; set windows_subsystem = "windows" unconditionally' });
|
|
162
|
-
}
|
|
163
153
|
const lines = text.split("\n");
|
|
164
154
|
lines.forEach((line, index) => {
|
|
165
155
|
const trimmed = line.trim();
|
|
@@ -167,7 +157,7 @@ export function scanDebugAssertionSources(root) {
|
|
|
167
157
|
if (!/#!?\[cfg|cfg!\s*\(/.test(line)) return;
|
|
168
158
|
if (line.includes(ENV_ALLOW_MARKER) || (lines[index - 1] ?? "").includes(ENV_ALLOW_MARKER)) return;
|
|
169
159
|
if (ENV_READ.test(lines.slice(index, index + ENV_WINDOW_LINES + 1).join("\n"))) {
|
|
170
|
-
findings.push({ rule: "debug-assertions-env", file: label, line: index + 1, message: "environment-variable override gated by cfg(debug_assertions);
|
|
160
|
+
findings.push({ rule: "debug-assertions-env", file: label, line: index + 1, message: "environment-variable override gated by cfg(debug_assertions): present in cargo test, absent from both installers (dev and release use [profile.release]); inject it (a config value or a test root the caller passes), never read it from the environment" });
|
|
171
161
|
}
|
|
172
162
|
});
|
|
173
163
|
}
|
package/cargo-target-shim.mjs
CHANGED
|
@@ -60,6 +60,24 @@ const ALIASES = Object.freeze({ b: "build", c: "check", t: "test", r: "run", d:
|
|
|
60
60
|
const VERSION_PROBES = Object.freeze([["-V"], ["--version"], ["-vV"], ["-Vv"], ["--version", "--verbose"], ["-V", "--verbose"], ["-v", "-V"], ["version"]]);
|
|
61
61
|
// The read-only commands the broker admits to a carried request only in carriedScanRefusal's exact shapes (snapshot-metadata.mjs).
|
|
62
62
|
const CARRIED_SHAPE = new Set(["metadata", "deny", "audit", "fmt"]);
|
|
63
|
+
// GATE-CONTRACT GC-A11 (Adrian, 2026-11-02): a cargo call that compiles nothing never goes to the broker; in broker mode these run directly
|
|
64
|
+
// with the host's real toolchain cargo (map.realCargo), each into its own scratch target, never carried (rustright a2972f0 refuses them).
|
|
65
|
+
export const READ_ONLY_DIRECT = Object.freeze(new Set(["metadata", "fetch", "tree", "pkgid", "verify-project", "read-manifest", "locate-project", "deny", "audit", "fmt"]));
|
|
66
|
+
/** Whether an argv compiles nothing: a READ_ONLY_DIRECT subcommand, or `cargo package --list` (lists files, builds nothing). */
|
|
67
|
+
export function readOnlyCargo(args) {
|
|
68
|
+
const sub = cargoSubcommandOf(args);
|
|
69
|
+
return READ_ONLY_DIRECT.has(sub) || (sub === "package" && args.includes("--list"));
|
|
70
|
+
}
|
|
71
|
+
/** The cargo subcommand of an argv (after a +toolchain and global options), or null. */
|
|
72
|
+
export function cargoSubcommandOf(args) {
|
|
73
|
+
for (let i = args[0]?.startsWith("+") ? 1 : 0; i < args.length; i += 1) {
|
|
74
|
+
const w = args[i];
|
|
75
|
+
if (w === "--") return null;
|
|
76
|
+
if (GLOBAL_VALUE.has(w)) { i += 1; continue; }
|
|
77
|
+
if (!w.startsWith("-")) return ALIASES[w] ?? w;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
63
81
|
const insideOf = (child, parent) => { const rel = path.relative(parent, child); return rel === "" || !(rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)); };
|
|
64
82
|
// Global options that take a value (`--color always`, `--explain E0001`); the subcommand is the first word after them.
|
|
65
83
|
const GLOBAL_VALUE = new Set(["--color", "--explain"]);
|
|
@@ -306,6 +324,19 @@ export function runShim(argv, { env = process.env, cwd = process.cwd(), spawn =
|
|
|
306
324
|
let decision;
|
|
307
325
|
try { decision = decideCargoInvocation({ args: words, cwd, env, map, locate }); } catch (error) { return refuse({ code: "RK_CARGO_SHIM_ERROR", message: error.message }); }
|
|
308
326
|
if (decision.refuse) return refuse(decision.refuse);
|
|
327
|
+
if (broker && decision.exec.workspace && readOnlyCargo(decision.exec.args) && map.realCargo) {
|
|
328
|
+
// GC-A11: a non-compiling call runs directly with the real toolchain cargo, writing only to its own scratch target (removed after).
|
|
329
|
+
let scratch;
|
|
330
|
+
try { scratch = fs.mkdtempSync(path.join(path.dirname(map.ledgerDir), "read-scratch-")); }
|
|
331
|
+
catch (error) { return refuse({ code: "RK_CARGO_SHIM_ERROR", message: `cannot create a scratch target for a read-only call (${error.message})` }); }
|
|
332
|
+
const { CARGO_TARGET_DIR: _bound, CARGO_BUILD_TARGET_DIR: _bound2, ...rest } = decision.exec.env ?? env; void _bound; void _bound2;
|
|
333
|
+
let r;
|
|
334
|
+
try { r = spawn(map.realCargo, decision.exec.args, { cwd, env: { ...rest, CARGO: map.realCargo, CARGO_TARGET_DIR: scratch }, stdio: "inherit" }); }
|
|
335
|
+
finally { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* scratch under the wrapper's own directory */ } }
|
|
336
|
+
if (r.error) return refuse({ code: "RK_CARGO_SHIM_ERROR", message: `${map.realCargo} did not start (${r.error.message})` });
|
|
337
|
+
record({ ...base, status: "ran", direct: true, exit: r.status ?? null });
|
|
338
|
+
return r.status ?? (r.signal ? 128 : SHIM_REFUSAL_STATUS);
|
|
339
|
+
}
|
|
309
340
|
if (broker && decision.exec.workspace) {
|
|
310
341
|
// The `pending` record above stays this call's until the broker call finishes: its records carry the same call id.
|
|
311
342
|
return runBrokerCargo({ words: decision.exec.args, cwd, env, section: map.broker, workspace: decision.exec.workspace, checkoutWorkspace: decision.exec.checkoutWorkspace, record: (entry) => record({ ...entry, id }), spawn, err });
|
package/cli/right-release.mjs
CHANGED
|
@@ -21,7 +21,6 @@ const commands = new Map([
|
|
|
21
21
|
["sign-windows", "sign-windows.mjs"],
|
|
22
22
|
["sign-updater", "sign-updater.mjs"],
|
|
23
23
|
["sign-dev-artifact", "sign-dev-artifact.mjs"],
|
|
24
|
-
["sign-qa-bundle", "sign-qa-bundle.mjs"],
|
|
25
24
|
["sign-candidate", "sign-candidate.mjs"],
|
|
26
25
|
["engine-locate", "engine-locator.mjs"],
|
|
27
26
|
["stage-sidecar", "stage-sidecar-command.mjs"],
|
|
@@ -35,6 +34,7 @@ const commands = new Map([
|
|
|
35
34
|
["mirror-root-artifact", "mirror-root-artifact.mjs"],
|
|
36
35
|
["test", "test-command.mjs"],
|
|
37
36
|
["web-build", "web-build.mjs"],
|
|
37
|
+
["build-helpers", "build-helpers-command.mjs"],
|
|
38
38
|
["license-notices", "npm-license-notices.mjs"],
|
|
39
39
|
["toolchain", "toolchain-swift.mjs"],
|
|
40
40
|
["github", "github-release.mjs"],
|
|
@@ -329,8 +329,6 @@ Commands:
|
|
|
329
329
|
Fail when a built web dist/ carries a forbidden marker string or mock/fixture chunk name (dev, mock or fixture code in release)
|
|
330
330
|
sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]
|
|
331
331
|
Developer ID sign an existing CI dev artifact per file with fixed identifiers; never builds, notarizes or publishes (macOS)
|
|
332
|
-
sign-qa-bundle --app <abs .app> --qa-root <abs dir> [--entitlements <abs plist>] [--out <evidence.json>]
|
|
333
|
-
QA ONLY: ad hoc sign a bundle inside a QA scratch root, nested code first, outer last, never --deep, never the signing agent (macOS); not a release artifact
|
|
334
332
|
sign-candidate --platform win|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]
|
|
335
333
|
Sign the exact unsigned candidate bytes (sha256 checked against the candidate receipt) instead of rebuilding; writes a candidate-sha -> signed-sha binding receipt
|
|
336
334
|
engine-locate --engine cutright|genright [--sha256 <hex>] [--json]
|
|
@@ -342,10 +340,10 @@ Commands:
|
|
|
342
340
|
upload --platform ios --release <id> [--apply] Sealed IPA to TestFlight, then the exact build into the beta groups; dry run unless --apply (macOS)
|
|
343
341
|
ios-credentials store --key-id <KEYID> --issuer <uuid> | check
|
|
344
342
|
Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
|
|
345
|
-
qa --stage
|
|
346
|
-
Admission is per app
|
|
347
|
-
qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List
|
|
348
|
-
qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage
|
|
343
|
+
qa --stage 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
|
|
344
|
+
Admission is per app (installed = upload and publish; there is no source stage, GATE-CONTRACT GC-A07), in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks the publication. Only adrian moves an app from refuse back to warn, or approves a runner
|
|
345
|
+
qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List installed QA status per release; exit 1 if an uploaded build lacks passing QA
|
|
346
|
+
qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage installed]
|
|
349
347
|
Show the QA admission policy, or set an app's mode (both stages, or one with --stage)
|
|
350
348
|
qa policy approve-runner --runner <file> --version <v> --platform mac|win --set-by adrian [--note <text>]
|
|
351
349
|
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
|
package/create-mac-updater.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
3
3
|
import { existsSync, rmSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
6
|
+
import { assertUpdaterMatchesShippedKey } from "./updater-shipped-key.mjs";
|
|
6
7
|
import { ArtifactPathError, loadSplitArtifactContext, resolveArtifactPath, resolveArtifactWritePath } from "./release-artifact-path.mjs";
|
|
7
8
|
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -56,6 +57,9 @@ else {
|
|
|
56
57
|
console.log(`pnpm exec tauri signer sign ${outputPath}`);
|
|
57
58
|
try { signMacUpdaterFiles([outputPath], { cwd, env }); }
|
|
58
59
|
catch (error) { fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
60
|
+
// rightkit#1443: the signature must verify with the key the app ships (tauri.conf plugins.updater.pubkey), whatever key signed it.
|
|
61
|
+
try { assertUpdaterMatchesShippedKey({ appRoot: cwd, file: outputPath }); }
|
|
62
|
+
catch (error) { fail(`[MAC_UPDATER_SHIPPED_KEY] ${error.message}`); }
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
function run(command, commandArgs) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The three OS targets every crate is checked for on every host (Adrian, 2026-11-02: "anything run through the Mac or the Windows or
|
|
2
|
+
// Linux must all check for all 3"). The host builds and tests its own OS; the other two are `cargo clippy --target` checks, so code under
|
|
3
|
+
// cfg(target_os = "macos") or cfg(windows) fails at the gate instead of at publish.yml native verify (five crates did on 2026-11-02:
|
|
4
|
+
// system-monitor, local-gen, app-manager, dictation-models, credentials). Windows is checked as the GNU target: zig supplies its C
|
|
5
|
+
// toolchain from any host (scripts/ci/provision-zig.mjs), and cfg(windows) code is the same for both Windows ABIs.
|
|
6
|
+
// Shared by local-publish.mjs (unit plans and batch clippy), deterministic-scans.mjs (the gate's class B scans), stage-sidecar.mjs and
|
|
7
|
+
// scripts/ci/gate.mjs. Imports only node:fs and node:path.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
export const CROSS_TARGETS = Object.freeze({ linux: "x86_64-unknown-linux-gnu", darwin: "aarch64-apple-darwin", win32: "x86_64-pc-windows-gnu" });
|
|
12
|
+
/** zig's name for each cross target, for the C compiler wrappers scripts/ci/provision-zig.mjs writes. */
|
|
13
|
+
export const ZIG_TARGETS = Object.freeze({ "x86_64-unknown-linux-gnu": "x86_64-linux-gnu", "aarch64-apple-darwin": "aarch64-macos", "x86_64-pc-windows-gnu": "x86_64-windows-gnu" });
|
|
14
|
+
export const CROSS_CC_ENV = "RIGHTKIT_CROSS_CC_DIR";
|
|
15
|
+
|
|
16
|
+
/** The host's own build target, as the package stage names it: Windows builds MSVC (win-package-recipe), the Mac and Linux their native arch. */
|
|
17
|
+
export const HOST_TARGETS = Object.freeze({ "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "win32-x64": "x86_64-pc-windows-msvc", "win32-arm64": "aarch64-pc-windows-msvc", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu" });
|
|
18
|
+
export function hostTarget(platform = process.platform, arch = process.arch) {
|
|
19
|
+
const target = HOST_TARGETS[`${platform}-${arch}`];
|
|
20
|
+
if (!target) throw new Error(`no host Rust target for ${platform}-${arch}`);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The foreign targets every crate is checked for on `platform`: the other two of CROSS_TARGETS, always. There is no opt-out (Adrian,
|
|
26
|
+
* 2026-11-03): a crate that cannot be type-checked for an OS has a bug to fix (cfg-gate its OS code, or make its build script return early
|
|
27
|
+
* for a foreign CARGO_CFG_TARGET_OS). A `crossTargets` key in ci/gate.config.json is refused.
|
|
28
|
+
*/
|
|
29
|
+
export function crossTargetsFor(entry = {}, platform = "darwin") {
|
|
30
|
+
if (entry?.crossTargets !== undefined || entry?.crossTargetsSkip !== undefined) throw new Error("ci/gate.config.json crossTargets/crossTargetsSkip is not allowed: every crate is checked for all three OS targets");
|
|
31
|
+
const native = CROSS_TARGETS[platform];
|
|
32
|
+
if (!native) throw new Error(`no native target for ${platform}`);
|
|
33
|
+
return Object.values(CROSS_TARGETS).filter((target) => target !== native);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The provisioned zig wrapper directory: RIGHTKIT_CROSS_CC_DIR, else `<managed data root>/zig/cross-cc` (where scripts/ci/provision-zig.mjs
|
|
38
|
+
* writes on a Mac or Dell), else null.
|
|
39
|
+
*/
|
|
40
|
+
export function crossCcDir({ env = process.env, exists = fs.existsSync, platform = process.platform } = {}) {
|
|
41
|
+
if (env[CROSS_CC_ENV]) return env[CROSS_CC_ENV];
|
|
42
|
+
const managed = platform === "win32" ? "D:\\.rightkit-managed" : platform === "darwin" ? "/Volumes/D/.rightkit-managed" : null;
|
|
43
|
+
const dir = managed ? path.join(managed, "zig", "cross-cc") : null;
|
|
44
|
+
return dir && exists(dir) ? dir : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** cc-rs / cmake environment pointing a target's C, C++ and archiver at the provisioned zig wrappers (none when unprovisioned). */
|
|
48
|
+
export function crossCompileEnv(target, dir = crossCcDir(), platform = process.platform) {
|
|
49
|
+
if (!dir) return undefined;
|
|
50
|
+
const key = target.replaceAll("-", "_");
|
|
51
|
+
const ext = platform === "win32" ? ".cmd" : "";
|
|
52
|
+
return {
|
|
53
|
+
[`CC_${key}`]: path.join(dir, `cc-${target}${ext}`),
|
|
54
|
+
[`CXX_${key}`]: path.join(dir, `cxx-${target}${ext}`),
|
|
55
|
+
[`AR_${key}`]: path.join(dir, `ar-${target}${ext}`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One build output and one warm cache per app per host (Adrian: "Flag, no flag, it should go into 1 directory"; GATE-CONTRACT GC-A01, GC-A07).
|
|
60
|
+
// Cargo picks the output directory from the profile (dev -> debug, release -> release) and from whether a target is named
|
|
61
|
+
// (target/<profile> vs target/<triple>/<profile>), so every compile the gate launches is rewritten here, whatever its caller passed:
|
|
62
|
+
// `--profile release` is always set (a request for any other profile, `--debug` included, is refused), and a call that names no target gets
|
|
63
|
+
// the host's own. A call that names another target keeps it (the cross-target check: that OS's own directory, still release).
|
|
64
|
+
// Non-compiling subcommands (metadata, tree, deny, audit, package --list, fmt) are returned unchanged.
|
|
65
|
+
const COMPILE_SUBCOMMANDS = new Set(["build", "b", "check", "c", "clippy", "rustc", "rustdoc", "doc", "d", "test", "t", "bench", "run", "r"]);
|
|
66
|
+
export function oneOutput(args, { platform = process.platform, arch = process.arch } = {}) {
|
|
67
|
+
// The subcommand is the first word after an optional leading `cargo`, `+<toolchain>` and cargo's own global options.
|
|
68
|
+
let at = 0;
|
|
69
|
+
if (args[at] === "cargo") at += 1;
|
|
70
|
+
while (at < args.length && (args[at].startsWith("+") || args[at].startsWith("-"))) at += ["--config", "-Z", "-C", "--color"].includes(args[at]) ? 2 : 1;
|
|
71
|
+
if (!COMPILE_SUBCOMMANDS.has(args[at])) return [...args];
|
|
72
|
+
const head = args.slice(0, at + 1); const rest = [];
|
|
73
|
+
const tail = args.slice(at + 1);
|
|
74
|
+
const sep = tail.indexOf("--");
|
|
75
|
+
const own = sep < 0 ? tail : tail.slice(0, sep);
|
|
76
|
+
// Hard block (Adrian, 2026-11-03: "i wnat all dev completely gone. all debug too ... never another, hard block"): a compile that asks for any
|
|
77
|
+
// profile but release is refused, never rewritten. `--release` / `-r` is release and is normalized to `--profile release`.
|
|
78
|
+
for (let i = 0; i < own.length; i += 1) {
|
|
79
|
+
const a = own[i];
|
|
80
|
+
if (a === "--release" || a === "-r") continue;
|
|
81
|
+
const asked = a === "--debug" ? "dev" : a === "--profile" ? own[i + 1] : a.startsWith("--profile=") ? a.slice("--profile=".length) : null;
|
|
82
|
+
if (asked !== null) {
|
|
83
|
+
if (asked !== "release") throw new Error(`RK_PROFILE_REFUSED: cargo ${args[at]} asks for the ${JSON.stringify(asked)} profile; only release is allowed (one build output per app, GATE-CONTRACT GC-A07)`);
|
|
84
|
+
if (a === "--profile") i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
rest.push(a);
|
|
88
|
+
}
|
|
89
|
+
// GATE-CONTRACT GC-A09: tests are isolated by injection; no `test-mode` feature exists in any build (Adrian: "No, no flag").
|
|
90
|
+
const featureLists = rest.flatMap((a, i) => (a === "--features" || a === "-F" ? [rest[i + 1] ?? ""] : a.startsWith("--features=") ? [a.slice("--features=".length)] : []));
|
|
91
|
+
if (featureLists.some((list) => list.split(/[\s,]+/).some((f) => f === "test-mode" || f.endsWith("/test-mode")))) throw new Error(`RK_TEST_MODE_REFUSED: cargo ${args[at]} enables a test-mode feature; tests are isolated by injection (GATE-CONTRACT GC-A09), no build has a test mode`);
|
|
92
|
+
const named = rest.includes("--target") || rest.some((a) => a.startsWith("--target="));
|
|
93
|
+
return [...head, "--profile", "release", ...(named ? [] : ["--target", hostTarget(platform, arch)]), ...rest, ...(sep < 0 ? [] : tail.slice(sep))];
|
|
94
|
+
}
|