@rightkit/release 0.6.4 → 0.6.6
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 +109 -7
- package/admission-files.mjs +279 -0
- package/admission-host.mjs +403 -0
- package/admission.mjs +146 -0
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +1326 -0
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +495 -69
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +87 -26
- 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 +45 -5
- package/command-spec.mjs +4 -5
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +301 -37
- package/development.mjs +5 -5
- package/github-release.mjs +5 -3
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +134 -8
- package/inventory-check.mjs +12 -1
- package/ios-upload.mjs +4 -4
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/ownership-dependency-allowlist.json +2 -0
- package/package.json +41 -3
- package/pipeline-admission.mjs +12 -0
- package/prebuild-gates.mjs +7 -3
- package/publish-cargo.mjs +215 -3
- package/publish-update.mjs +5 -3
- package/release-config.mjs +36 -5
- 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 +117 -62
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +18 -13
- package/stage-sidecar-command.mjs +5 -5
- 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
|
@@ -20,8 +20,11 @@ import {
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { preflightOwnershipScope } from "./ownership-scope.mjs";
|
|
22
22
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
23
|
-
import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord } from "./rightkit-registry-resolver.mjs";
|
|
24
|
-
import {
|
|
23
|
+
import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord, writeSnapshot } from "./rightkit-registry-resolver.mjs";
|
|
24
|
+
import { formatAdmission, formatNotRun, runAdmission } from "./admission.mjs";
|
|
25
|
+
import { sccacheRefusal } from "./admission-host.mjs";
|
|
26
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
25
28
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
26
29
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
27
30
|
import { admitSourceQa } from "./release-qa-audit.mjs";
|
|
@@ -109,7 +112,7 @@ const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
|
109
112
|
const WORKER = path.join(TOOL_ROOT, "release.mjs");
|
|
110
113
|
const RELEASE_TOOL_VERSION = JSON.parse(readFileSync(path.join(TOOL_ROOT, "package.json"), "utf8")).version;
|
|
111
114
|
const args = process.argv.slice(2);
|
|
112
|
-
let configName =
|
|
115
|
+
let configName = defaultReleaseConfigFile();
|
|
113
116
|
let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
|
|
114
117
|
let dryRun = false;
|
|
115
118
|
let skipChecks = false;
|
|
@@ -129,12 +132,15 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
129
132
|
if (platform !== "win" && platform !== "mac" && platform !== "android" && platform !== "ios") fail("--platform must be win, mac, android or ios");
|
|
130
133
|
if (resumeFrom && !["prepackage", "sign", "package", "post-sign", "harden"].includes(resumeFrom)) fail("--resume-from must be prepackage, sign, package, post-sign, or harden");
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
// Native real paths: Git reports the long form of a Windows short name (RUNNER~1) and the target of a symlinked temp root
|
|
136
|
+
// (macOS /var -> /private/var). The config path is resolved the same way the doctor resolves it (release.mjs), so every path
|
|
137
|
+
// derived from it (app root, build inputs, scan roots) compares equal to what Git reports; a non-canonical app root made the
|
|
138
|
+
// build-inputs qualification drop every input and the clean-source gate speak before the dirty-inputs row.
|
|
139
|
+
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
140
|
+
const configPath = nativeReal(path.resolve(configName));
|
|
133
141
|
const invocationRoot = path.dirname(configPath);
|
|
134
142
|
const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
|
|
135
143
|
assertPrimaryReleaseCheckout(repoRoot);
|
|
136
|
-
// Native real paths: Git reports the long form of a Windows short name (RUNNER~1).
|
|
137
|
-
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
138
144
|
const relativeConfig = path.relative(nativeReal(repoRoot), nativeReal(configPath)).replaceAll("\\", "/");
|
|
139
145
|
if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
|
|
140
146
|
const appRoot = path.dirname(configPath);
|
|
@@ -148,21 +154,19 @@ let heavySlot;
|
|
|
148
154
|
let interrupted;
|
|
149
155
|
let requestActiveRunStop = null;
|
|
150
156
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
157
|
+
// Admission (admission.mjs): every refusal the committed files, this host or the registries can decide is raised here, before the
|
|
158
|
+
// dry-run exit, the clean-source gate, the release lock, any state file, the install, a slot or the worker. `right-release doctor`
|
|
159
|
+
// runs the same rows with the same text, so the two cannot disagree (rightkit#946); a refused build leaves nothing behind.
|
|
160
|
+
const admitted = await admit();
|
|
161
|
+
|
|
162
|
+
// A dry-run is an inspection contract, not a partial build. It prints the admission rows and exits with their verdict, so it can
|
|
163
|
+
// never pass what the build refuses. It must finish before locks, cache directories, receipts, dependency installation, or any
|
|
153
164
|
// child build/sign process can exist.
|
|
154
165
|
if (dryRun) {
|
|
155
|
-
|
|
156
|
-
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
157
|
-
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
158
|
-
const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
|
|
159
|
-
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
160
|
-
const target = config.targets?.[platform];
|
|
161
|
-
// Android builds with target.gradle (validated by release.mjs), not a package command; iOS with targets.ios.iosPackage.
|
|
162
|
-
if (platform !== "android" && platform !== "ios" && !target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
|
|
163
|
-
console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
|
|
166
|
+
console.log(`right-release build dry-run: ${admitted.config.app} ${admitted.config.version} ${platform}`);
|
|
164
167
|
console.log(`config: ${configPath}`);
|
|
165
|
-
console.log(`commit: ${commit}`);
|
|
168
|
+
console.log(`commit: ${admitted.commit}`);
|
|
169
|
+
for (const line of formatAdmission(admitted.result)) console.log(line);
|
|
166
170
|
process.exit(0);
|
|
167
171
|
}
|
|
168
172
|
|
|
@@ -192,7 +196,7 @@ try {
|
|
|
192
196
|
const shortCommit = commit.slice(0, 8);
|
|
193
197
|
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
194
198
|
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
195
|
-
const config =
|
|
199
|
+
const config = await importReleaseConfig(configPath, { query: `commit=${commit}` });
|
|
196
200
|
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
197
201
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.app) || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(config.version)) {
|
|
198
202
|
fail("app and version must be safe release-id components");
|
|
@@ -396,7 +400,8 @@ try {
|
|
|
396
400
|
let rightKitResolution = null;
|
|
397
401
|
if (!legacySealed) {
|
|
398
402
|
try {
|
|
399
|
-
|
|
403
|
+
// A fresh build already asked the registries in admission(); the snapshot is bound to this release's state root below.
|
|
404
|
+
rightKitResolution = (!rightKitReuse && admitted.registry) || await prepareRightKitResolution({
|
|
400
405
|
root: appRoot,
|
|
401
406
|
extraLocks: rightKitLockInventory({ requiredInputs, cargoLockPaths: pinnedCargoLocks, nativeLayout, config, root: appRoot }),
|
|
402
407
|
versions: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-versions.json"), "utf8")),
|
|
@@ -414,6 +419,7 @@ try {
|
|
|
414
419
|
if (!(error instanceof RightKitRegistryError)) throw error;
|
|
415
420
|
fail(`${error.recoverable ? "RECOVERABLE " : ""}${error.message}`);
|
|
416
421
|
}
|
|
422
|
+
if (rightKitResolution === admitted.registry) writeSnapshot(rightKitSnapshotFile, rightKitResolution.snapshot);
|
|
417
423
|
for (const line of rightKitResolution.freshness.tasks) console.error(`[right-release] LANE TASK ${line}`);
|
|
418
424
|
for (const line of rightKitResolution.freshness.warnings) console.error(`[right-release] WARNING ${line}`);
|
|
419
425
|
console.error(`[right-release] rightkit-registry: ${rightKitResolution.summary}`);
|
|
@@ -552,6 +558,52 @@ try {
|
|
|
552
558
|
lock?.release();
|
|
553
559
|
}
|
|
554
560
|
|
|
561
|
+
// Read the config and run the admission rows (files, this host and, for a fresh build, the registries) with no side effect: no lock,
|
|
562
|
+
// state directory, cache directory, install or snapshot file. A finding is printed with its row and the process exits 1, no stack.
|
|
563
|
+
async function admit() {
|
|
564
|
+
let commit;
|
|
565
|
+
let config;
|
|
566
|
+
try {
|
|
567
|
+
commit = git(invocationRoot, ["rev-parse", "HEAD"]);
|
|
568
|
+
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
569
|
+
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
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)}`); }
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error?.reported) process.exit(1);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
// A fresh build asks the registries once, here; a resume or a re-run over a sealed release reuses its recorded snapshot instead.
|
|
576
|
+
const platformName = platform === "win" ? "windows" : platform;
|
|
577
|
+
const sealed = config?.app && config?.version
|
|
578
|
+
&& existsSync(path.join(repoRoot, ".right-release", "sealed", `${config.app}-${config.version}-${commit.slice(0, 8)}`, platformName, "release-manifest.json"));
|
|
579
|
+
const reuse = Boolean(resumeFrom) || Boolean(sealed);
|
|
580
|
+
const result = await runAdmission({
|
|
581
|
+
root: appRoot, repoRoot, configPath, config: config ?? {}, platform, commit, skipChecks,
|
|
582
|
+
tiers: reuse ? ["files", "host"] : ["files", "host", "network"],
|
|
583
|
+
registry: async (ctx) => {
|
|
584
|
+
const inputs = ctx.buildInputs();
|
|
585
|
+
if (!inputs.ok) return undefined; // the build-inputs row reports it
|
|
586
|
+
return prepareRightKitResolution({
|
|
587
|
+
root: appRoot,
|
|
588
|
+
extraLocks: rightKitLockInventory({ requiredInputs: inputs.value.requiredInputs, cargoLockPaths: inputs.value.cargoLockPaths, nativeLayout: inputs.value.nativeLayout, config, root: appRoot }),
|
|
589
|
+
versions: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-versions.json"), "utf8")),
|
|
590
|
+
ownership: JSON.parse(readFileSync(path.join(TOOL_ROOT, "rightkit-ownership.json"), "utf8")),
|
|
591
|
+
releaseVersion: RELEASE_TOOL_VERSION,
|
|
592
|
+
// Build admission: refuse every pin behind the registry's latest. A dry run asks, but keeps no cache on disk.
|
|
593
|
+
admission: true,
|
|
594
|
+
...(dryRun ? { cacheDir: "" } : {}),
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
for (const finding of result.findings) console.error(`right-release build: ${finding.row}: ${finding.detail}`);
|
|
599
|
+
for (const line of formatNotRun(result)) console.error(`right-release build: ${line}`);
|
|
600
|
+
if (result.findings.length > 0) {
|
|
601
|
+
console.error(`right-release build: admission found ${result.findings.length} blocking problem(s); nothing was started`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
return { config, commit, result, registry: result.out.registry ?? null };
|
|
605
|
+
}
|
|
606
|
+
|
|
555
607
|
function hashInputs(files) {
|
|
556
608
|
const hashes = {};
|
|
557
609
|
for (const file of files) {
|
|
@@ -579,7 +631,7 @@ function sqlCipherFeatures(text) {
|
|
|
579
631
|
}
|
|
580
632
|
|
|
581
633
|
function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot, rightkitRegistryFields }) {
|
|
582
|
-
// Every app's
|
|
634
|
+
// Every app's release config names installer/updater artifacts
|
|
583
635
|
// relative to its resolved native target. On a broker-managed host that
|
|
584
636
|
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
585
637
|
// targetBridge.ensure() deliberately leaves a real local directory alone,
|
|
@@ -610,6 +662,14 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
610
662
|
current.add(role);
|
|
611
663
|
sources.set(source, current);
|
|
612
664
|
};
|
|
665
|
+
// Named refusals for a config the admission's seal-routes row already refused, so nothing here crashes on an undefined field.
|
|
666
|
+
for (const artifact of [...(target.installer?.artifacts ?? []), ...(target.updater?.artifacts ?? [])]) {
|
|
667
|
+
if (typeof artifact?.file !== "string" || !artifact.file) fail("installer and updater artifacts must each declare a file");
|
|
668
|
+
}
|
|
669
|
+
for (const artifact of target.updater?.artifacts ?? []) {
|
|
670
|
+
if (target.updater?.kind !== "appinstaller" && (typeof artifact.signature !== "string" || !artifact.signature)) fail(`updater artifact ${artifact.file} must declare the signature file sign-updater writes next to it`);
|
|
671
|
+
if (artifact.key === undefined && config.distribution?.provider !== "github-releases") fail(`updater artifact ${artifact.file} must declare the R2 route key (key); only github-releases names the route from the file`);
|
|
672
|
+
}
|
|
613
673
|
for (const artifact of target.installer?.artifacts ?? []) addSource(resolveArtifactPath(artifact.file), "installer");
|
|
614
674
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
615
675
|
addSource(resolveArtifactPath(artifact.file), "updater");
|
|
@@ -741,9 +801,9 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
741
801
|
// an unsound seal, not a portable release.
|
|
742
802
|
signing: signingIdentity ? { ...signingIdentity, receipts: (() => {
|
|
743
803
|
const present = signingIdentity.receiptInputs.filter((file) => existsSync(file));
|
|
744
|
-
if (present.length === 0)
|
|
804
|
+
if (present.length === 0) fail(`no signing receipts found to seal; expected at least ${signingIdentity.receiptInputs[0]}`);
|
|
745
805
|
if (platform === "mac") {
|
|
746
|
-
if (present.length !== signingIdentity.receiptInputs.length)
|
|
806
|
+
if (present.length !== signingIdentity.receiptInputs.length) fail("missing current-build macOS signing or notarization receipt");
|
|
747
807
|
validateNotarizationReceipt({ receiptPath: signingIdentity.receiptInputs[1], archive: resolveArtifactPath(target.notarize.file) });
|
|
748
808
|
}
|
|
749
809
|
return present.map((file) => ({ file, sha256: hashFile(file) }));
|
|
@@ -941,7 +1001,7 @@ function commandExists(name) {
|
|
|
941
1001
|
|
|
942
1002
|
function assertSccacheVersion(version) {
|
|
943
1003
|
const match = String(version ?? "").match(/sccache\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
944
|
-
if (!match || Number(match[1]) === 0 && Number(match[2]) < 15) fail(
|
|
1004
|
+
if (!match || Number(match[1]) === 0 && Number(match[2]) < 15) fail(sccacheRefusal(match ? match.slice(1).join(".") : null));
|
|
945
1005
|
}
|
|
946
1006
|
|
|
947
1007
|
function commandOutput(cmd, runArgs) {
|
|
@@ -994,13 +1054,14 @@ function writeJson(file, value) {
|
|
|
994
1054
|
}
|
|
995
1055
|
|
|
996
1056
|
function usage(code) {
|
|
997
|
-
console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.
|
|
1057
|
+
console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.json] [--dry-run]");
|
|
998
1058
|
process.exit(code);
|
|
999
1059
|
}
|
|
1000
1060
|
|
|
1001
1061
|
function fail(message) {
|
|
1002
1062
|
console.error(`right-release build: ${message}`);
|
|
1003
|
-
|
|
1063
|
+
// `reported` marks an error whose text is already printed, so a caller that exits on it prints nothing twice.
|
|
1064
|
+
throw Object.assign(new Error(message), { reported: true });
|
|
1004
1065
|
}
|
|
1005
1066
|
|
|
1006
1067
|
function throwIfInterrupted() { if (interrupted) throw interrupted; }
|
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"],
|
|
@@ -54,7 +55,17 @@ const commands = new Map([
|
|
|
54
55
|
|
|
55
56
|
const args = process.argv.slice(2);
|
|
56
57
|
const first = args[0];
|
|
58
|
+
// The commands that compile, package, sign, ship or inspect a release run the app check (../app-check.mjs, CHECKED_COMMANDS) in this
|
|
59
|
+
// process before they start a child. A red check prints its findings and exits 1; there is no flag that skips it.
|
|
60
|
+
async function requireGreenCheck() {
|
|
61
|
+
const { guardCommand } = await import("../app-check.mjs");
|
|
62
|
+
const refused = guardCommand({ args });
|
|
63
|
+
if (!refused) return;
|
|
64
|
+
for (const line of refused.lines) console.error(line);
|
|
65
|
+
process.exit(refused.exitCode);
|
|
66
|
+
}
|
|
57
67
|
if (first === "dev") {
|
|
68
|
+
await requireGreenCheck();
|
|
58
69
|
import("../development.mjs").then(({ main }) => main(args.slice(1))).catch((error) => {
|
|
59
70
|
console.error(`right-release dev: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
60
71
|
process.exitCode = 1;
|
|
@@ -81,6 +92,8 @@ if (!first || first === "-h" || first === "--help") {
|
|
|
81
92
|
process.exit(0);
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
await requireGreenCheck();
|
|
96
|
+
|
|
84
97
|
if (first === "--version" || first === "-v") {
|
|
85
98
|
run("release.mjs", ["--version"]);
|
|
86
99
|
} else if (first === "doctor") {
|
|
@@ -89,6 +102,18 @@ if (first === "--version" || first === "-v") {
|
|
|
89
102
|
} else {
|
|
90
103
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
91
104
|
}
|
|
105
|
+
} else if (first === "check") {
|
|
106
|
+
// The same offline check as `rightkit-gate check`; the repository is the git top-level of --config's folder, else the cwd.
|
|
107
|
+
const { appDirectory, main } = await import("../app-check.mjs");
|
|
108
|
+
process.exit(await main(args.slice(1), { label: "check", cwd: appDirectory(args, process.cwd()) }));
|
|
109
|
+
} else if (first === "hooks" && args[1] === "install") {
|
|
110
|
+
// One installer: `hooks install` is `rightkit-gate install-hook` (pre-push check and gate, ownership pre-commit step) run in the app.
|
|
111
|
+
const app = args.indexOf("--app");
|
|
112
|
+
if (app >= 0 && (!args[app + 1] || args[app + 1].startsWith("-"))) {
|
|
113
|
+
console.error("right-release hooks install: --app needs a directory");
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
run("bin/rightkit-gate.mjs", ["install-hook", ...(args.includes("--force") ? ["--force"] : [])], { cwd: app >= 0 ? path.resolve(args[app + 1]) : process.cwd() });
|
|
92
117
|
} else if (first === "hooks") {
|
|
93
118
|
run("ownership-hooks.mjs", args.slice(1));
|
|
94
119
|
} else if (first === "suite-doctor") {
|
|
@@ -170,9 +195,9 @@ if (first === "--version" || first === "-v") {
|
|
|
170
195
|
process.exit(2);
|
|
171
196
|
}
|
|
172
197
|
|
|
173
|
-
function run(script, scriptArgs) {
|
|
198
|
+
function run(script, scriptArgs, { cwd = process.cwd() } = {}) {
|
|
174
199
|
const child = spawn(process.execPath, [path.join(packageRoot, script), ...scriptArgs], {
|
|
175
|
-
cwd
|
|
200
|
+
cwd,
|
|
176
201
|
env: process.env,
|
|
177
202
|
stdio: "inherit",
|
|
178
203
|
windowsHide: true,
|
|
@@ -271,8 +296,12 @@ Commands:
|
|
|
271
296
|
ios-credentials store --key-id <KEYID> --issuer <uuid> | check
|
|
272
297
|
Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
|
|
273
298
|
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
|
|
274
|
-
Admission is per app in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks
|
|
299
|
+
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
|
|
275
300
|
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
|
|
301
|
+
qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage source|installed]
|
|
302
|
+
Show the QA admission policy, or set an app's mode (both stages, or one with --stage)
|
|
303
|
+
qa policy approve-runner --runner <file> --version <v> --platform mac|win --set-by adrian [--note <text>]
|
|
304
|
+
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
|
|
276
305
|
publish-update --release <id> --platform mac|win --tier patch|update
|
|
277
306
|
Upload sealed bytes to R2 & register RightApps
|
|
278
307
|
release Compatibility alias for build
|
|
@@ -308,12 +337,21 @@ Commands:
|
|
|
308
337
|
apple profile renew [--config <file>] [--profile <file>] [--dry-run]
|
|
309
338
|
Renew macOS profile for current Keychain signer
|
|
310
339
|
signing-agent install|uninstall|status Manage invisible Aqua-session signing service
|
|
311
|
-
|
|
340
|
+
check [--repo <dir>] [--config <file>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
|
|
341
|
+
The offline app check, same as rightkit-gate check: committed files only, every finding in one run (exit 0 pass, 1 findings, 2 could not run).
|
|
342
|
+
Build, package, dev, test, web-build, swift-package, recipe-build, stage-sidecar, qa, upload, publish, publish-update, github, doctor,
|
|
343
|
+
sign-windows, sign-updater, sign-candidate, sign-dev-artifact, addon build and create-mac-updater run it first and start nothing when it is red; there is no flag that skips it.
|
|
344
|
+
Baseline: .rightkit/app-check-baseline.json (entries carry issue, added and sunset; the sunset is at most 90 days after added)
|
|
345
|
+
doctor [--platform mac|win] [--gates] [--inspect] [--strict]
|
|
346
|
+
Inspect one app's release config the way the build would refuse it; --gates also runs the build's pre-build source gates
|
|
347
|
+
--inspect lists admission findings (newer RightKit versions) as information instead of refusing them
|
|
348
|
+
--strict makes a row that could not run on this host (NOT RUN) a blocking problem; without it NOT RUN is printed and does not fail
|
|
349
|
+
--admission is still accepted and does nothing (admission is the default); it cannot be combined with --inspect
|
|
312
350
|
doctor --all Verify all Right Suite app release contracts
|
|
313
351
|
suite-doctor Verify all local Right Suite repositories
|
|
314
352
|
ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
|
|
315
353
|
ownership --staged [--app <dir>] [--json] Pre-commit gate: scan the staged index for block-tier findings
|
|
316
|
-
hooks install [--app <dir>]
|
|
354
|
+
hooks install [--app <dir>] [--force] Alias of rightkit-gate install-hook: the pre-push check and gate, and the advisory pre-commit ownership gate (pinned @rightkit/release)
|
|
317
355
|
ownership enroll [--root] [--name <device>] [--root-fingerprint <sha256>] [--app <dir>]
|
|
318
356
|
Enroll one native key or create a signed-device request
|
|
319
357
|
ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
|
|
@@ -338,6 +376,8 @@ Commands:
|
|
|
338
376
|
disk run self-check, check, dry-run sweep, move sweep, warn (the scheduled job)
|
|
339
377
|
disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows), bound to the stable per-user copy
|
|
340
378
|
deps --check|--audit|--update Shared dependency lane
|
|
379
|
+
update check|apply|gate|land|discard|run|schedule [--repo <path>]
|
|
380
|
+
Run the pinned @rightkit/outdated updater (right-update) that moves stale pins; reads .rightkit/update.json
|
|
341
381
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
342
382
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
|
343
383
|
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 {
|
package/deps.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { cargoAuditWorkdirs, runCargoAudits } from "./deps-cargo.mjs";
|
|
7
7
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const opts = {
|
|
10
|
-
config:
|
|
10
|
+
config: defaultReleaseConfigFile(),
|
|
11
11
|
mode: "check",
|
|
12
12
|
latest: false,
|
|
13
13
|
};
|
|
@@ -25,7 +25,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const configPath = path.resolve(opts.config);
|
|
28
|
-
|
|
28
|
+
let config;
|
|
29
|
+
try { config = await importReleaseConfig(configPath); } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
|
|
29
30
|
if (config?.schema !== 1) fail(`unsupported config schema: ${config?.schema ?? "<missing>"} (expected 1)`);
|
|
30
31
|
if (config.packageManager !== "pnpm") fail(`deps tool expects pnpm, got ${config.packageManager}`);
|
|
31
32
|
|
|
@@ -69,7 +70,7 @@ if (failures.length > 0) {
|
|
|
69
70
|
|
|
70
71
|
function usage(code, message) {
|
|
71
72
|
if (message) console.error(`right-deps: ${message}\n`);
|
|
72
|
-
console.error("usage: node deps.mjs [--config right-release.config.
|
|
73
|
+
console.error("usage: node deps.mjs [--config right-release.config.json] [--check|--audit|--update] [--latest]");
|
|
73
74
|
process.exit(code);
|
|
74
75
|
}
|
|
75
76
|
|