@rightkit/release 0.2.150 → 0.2.153
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +119 -8
- package/build-release.mjs +16 -2
- package/cli/right-release.mjs +18 -5
- package/committed-source.mjs +19 -1
- package/crate-graph.mjs +121 -0
- package/development-signing.mjs +8 -0
- package/development.mjs +46 -13
- package/devtools-policy.json +5 -0
- package/devtools-policy.mjs +119 -0
- package/direct-bootstrap.mjs +9 -3
- package/dmg-build.mjs +19 -5
- package/dmg-layout.mjs +2 -2
- package/dmg-spec.mjs +97 -0
- package/execution-policy.mjs +17 -1
- package/github-release.mjs +25 -7
- package/github-upload-gate.mjs +84 -0
- package/hardeningscan.mjs +3 -0
- package/inventory-check.mjs +4 -4
- package/mac-bundle-signing.mjs +1 -1
- package/mac-sign-core.mjs +97 -0
- package/mac-signing-matrix.mjs +186 -0
- package/macos-notarization.mjs +18 -2
- package/msix-appinstaller.mjs +4 -1
- package/msix-config.mjs +5 -1
- package/msix-repair-guard.mjs +24 -0
- package/msix-repair-guard.ps1 +356 -0
- package/notarize-macos.mjs +4 -3
- package/ownership-catalog-config.json +6 -7
- package/ownership-command.mjs +69 -8
- package/ownership-evidence.mjs +1 -1
- package/ownership-key-authority.mjs +1 -1
- package/ownership-key-command.mjs +15 -3
- package/ownership-key-elevation.mjs +11 -2
- package/ownership-mandatory-set.json +420 -0
- package/ownership-mandatory-set.mjs +273 -0
- package/ownership.mjs +2 -2
- package/package.json +15 -3
- package/packaged-app-identity.mjs +88 -0
- package/preflight.mjs +25 -3
- package/prune-r2.mjs +28 -3
- package/publication-gate.mjs +49 -10
- package/publish-cargo.mjs +21 -1
- package/publish-swift.mjs +78 -141
- package/publish-update.mjs +24 -2
- package/qa-build-features.mjs +216 -0
- package/qa-control-scan.mjs +435 -0
- package/registry-parity.mjs +18 -1
- package/release-invocation.mjs +2 -1
- package/release-state.mjs +9 -0
- package/release-switch-registry.json +33 -0
- package/release-switch-scan.mjs +452 -0
- package/release.mjs +324 -26
- package/renamed-away.mjs +29 -0
- package/rightkit-ownership.json +682 -709
- package/rightkit-sync.mjs +8 -4
- package/rightkit-version-policy.mjs +18 -0
- package/rightkit-versions.json +212 -127
- package/runtime-budget-classes.json +4 -0
- package/runtime-budgets.mjs +870 -0
- package/runtime-bundle-scan.mjs +61 -0
- package/sign-macos.mjs +28 -30
- package/sign-updater.mjs +5 -2
- package/swift-mac-dmg.mjs +7 -2
- package/swift-package-command.mjs +1 -1
- package/upload-release.mjs +10 -37
package/publish-cargo.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { runCargoSync } from "./managed-cargo.mjs";
|
|
|
7
7
|
import { readVerifiedArtifact, assertArtifactUnchanged } from "./publication-artifacts.mjs";
|
|
8
8
|
import { verifyCargoRegistry } from "./cargo-registry-readback.mjs";
|
|
9
9
|
import { recordPublished } from "./rightkit-sync.mjs";
|
|
10
|
+
import { readCargoPackage } from "./crate-graph.mjs";
|
|
11
|
+
import { runtimeBudgetCheck, RUNTIME_BUDGET_CODE } from "./runtime-budgets.mjs";
|
|
12
|
+
import { formatPreflight, PREFLIGHT_FAIL } from "./preflight.mjs";
|
|
10
13
|
|
|
11
14
|
const FORBIDDEN_PACKAGE_PATH = /(^|\/)(?:\.env(?:\.[^/]*)?|target(?:\/.*)?|node_modules(?:\/.*)?|[^/]+\.(?:p8|p12|pem|key))$/i;
|
|
12
15
|
const SECRET_PATTERNS = [
|
|
@@ -46,6 +49,9 @@ export function buildCargoPublishPlan({ crate, crateDir, dryRun, allowDirty = fa
|
|
|
46
49
|
if (allowDirty) packageArgs.push("--allow-dirty");
|
|
47
50
|
const plan = [
|
|
48
51
|
{ label: "secret scan", internal: "secret-scan", crateDir },
|
|
52
|
+
// Runtime-budget crates (runtime-budgets.mjs) must carry a measured runtime-budget.json for this
|
|
53
|
+
// version on every required host. Dry-run reports the check; a real publish refuses it.
|
|
54
|
+
{ label: "runtime budget", internal: "runtime-budget", crate, crateDir },
|
|
49
55
|
{ label: "format", command: "cargo", args: ["fmt", "-p", crate, "--check"] },
|
|
50
56
|
{ label: "tests", command: "cargo", args: ["test", "-p", crate, "--", "--test-threads=1"] },
|
|
51
57
|
{ label: "clippy", command: "cargo", args: ["clippy", "-p", crate, "--all-targets", "--", "-D", "warnings"] },
|
|
@@ -91,7 +97,11 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
91
97
|
const crateDir = path.join(options.workspaceRoot, "crates", options.crate);
|
|
92
98
|
const manifest = path.join(crateDir, "Cargo.toml");
|
|
93
99
|
if (!fs.existsSync(manifest)) throw new Error(`crate manifest not found: ${manifest}`);
|
|
94
|
-
|
|
100
|
+
// Parsed, not pattern-matched: `version.workspace = true` resolves through the workspace root, and a
|
|
101
|
+
// manifest whose [package].name is not the requested crate is refused before anything else runs.
|
|
102
|
+
const pkg = readCargoPackage(crateDir);
|
|
103
|
+
if (pkg.name !== options.crate) throw new Error(`Cargo.toml [package].name ${JSON.stringify(pkg.name)} differs from --crate ${options.crate}`);
|
|
104
|
+
const cargoVersion = pkg.version ?? undefined;
|
|
95
105
|
const gateOptions = {
|
|
96
106
|
kind: "cargo", name: options.crate, version: cargoVersion, packagePath: crateDir,
|
|
97
107
|
evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(options.workspaceRoot, "cargo", options.crate)),
|
|
@@ -110,6 +120,16 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
110
120
|
if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
|
|
111
121
|
continue;
|
|
112
122
|
}
|
|
123
|
+
if (step.internal === "runtime-budget") {
|
|
124
|
+
const check = runtimeBudgetCheck({ crate: options.crate, crateDir, version: cargoVersion, evidence: gate.evidence ?? undefined });
|
|
125
|
+
if (check) process.stdout.write(`${formatPreflight([check])}\n`);
|
|
126
|
+
if (check?.status === PREFLIGHT_FAIL && !options.dryRun) {
|
|
127
|
+
const error = new Error(`runtime budget refused: ${check.detail}. Fix: ${check.fix}`);
|
|
128
|
+
error.code = RUNTIME_BUDGET_CODE;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
113
133
|
if (step.internal === "verify-archive") {
|
|
114
134
|
// Revalidate source/evidence after packaging; preserve that exact evidence object.
|
|
115
135
|
const current = (deps.enforceGate ?? enforcePublicationGate)(gateOptions);
|
package/publish-swift.mjs
CHANGED
|
@@ -1,129 +1,73 @@
|
|
|
1
|
-
// Publish RightKitSwift by the ONE Swift distribution contract: a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// Publish RightKitSwift by the ONE Swift distribution contract: a plain semver git tag on this
|
|
2
|
+
// repository (github.com/Orthic-Labs/rightkit), resolved by apps through the root Package.swift
|
|
3
|
+
// (docs/RIGHTKIT-SHARED-PACKAGES.md, Swift section). There is no mirror and no second repository.
|
|
4
|
+
//
|
|
5
|
+
// `right-release publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]` verifies, in order:
|
|
6
|
+
// 1. VERSION (swift/RightKitSwift/VERSION) is plain semver (X.Y.Z, no "v", no prerelease);
|
|
7
|
+
// 2. the checkout is on main, has a clean tree, and HEAD is reachable from origin/main;
|
|
8
|
+
// 3. the root Package.swift and the nested manifest agree (scripts/swift-root-manifest.test.mjs);
|
|
9
|
+
// 4. the tag <VERSION> exists neither locally nor on origin;
|
|
10
|
+
// 5. the publication gate evidence is bound to HEAD and to the package tree at HEAD;
|
|
11
|
+
// then creates the tag on HEAD and pushes it:
|
|
12
|
+
// git tag <VERSION> <HEAD sha>
|
|
13
|
+
// git push origin refs/tags/<VERSION>:refs/tags/<VERSION>
|
|
14
|
+
// A dry-run performs every check and prints those commands without running them.
|
|
9
15
|
import fs from "node:fs";
|
|
10
|
-
import os from "node:os";
|
|
11
16
|
import path from "node:path";
|
|
12
17
|
import { fileURLToPath } from "node:url";
|
|
13
18
|
import { spawnSync } from "./background-process.mjs";
|
|
14
19
|
import { digestBytes } from "./publication-artifacts.mjs";
|
|
15
|
-
import { scanCrateForSecrets } from "./publish-cargo.mjs";
|
|
16
20
|
import { enforcePublicationGate, defaultEvidencePath } from "./publication-gate.mjs";
|
|
17
21
|
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
// compared), so only untracked build state is excluded. Tracked Xcode scheme
|
|
21
|
-
// metadata under .swiftpm ships with the package; SwiftPM consumers ignore it.
|
|
22
|
-
const MIRROR_EXCLUDES = new Set([".build", ".git", "Package.resolved", ".DS_Store"]);
|
|
22
|
+
export const SWIFT_DISTRIBUTION_URL = "https://github.com/Orthic-Labs/rightkit";
|
|
23
|
+
const DRIFT_TEST = path.join("scripts", "swift-root-manifest.test.mjs");
|
|
23
24
|
|
|
24
|
-
export function parseSwiftPublishArgs(argv
|
|
25
|
-
const allowed = new Set(["--
|
|
26
|
-
let repo = env.RIGHTKIT_SWIFT_GIT_URL ?? DEFAULT_SWIFT_GIT_URL;
|
|
25
|
+
export function parseSwiftPublishArgs(argv) {
|
|
26
|
+
const allowed = new Set(["--dry-run", "--evidence", "--workspace"]);
|
|
27
27
|
let dryRun = false;
|
|
28
|
-
let allowDirty = false;
|
|
29
28
|
let evidence;
|
|
30
29
|
let workspace;
|
|
31
30
|
for (let index = 0; index < argv.length; index += 1) {
|
|
32
31
|
const arg = argv[index];
|
|
33
32
|
if (!allowed.has(arg)) throw new Error(`unexpected argument: ${arg}`);
|
|
34
|
-
if (arg === "--dry-run") dryRun = true;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (arg === "--evidence") evidence = value;
|
|
40
|
-
else if (arg === "--workspace") workspace = value;
|
|
41
|
-
else repo = value;
|
|
42
|
-
}
|
|
33
|
+
if (arg === "--dry-run") { dryRun = true; continue; }
|
|
34
|
+
const value = argv[(index += 1)];
|
|
35
|
+
if (value === undefined) throw new Error(`${arg} needs a value`);
|
|
36
|
+
if (arg === "--evidence") evidence = value;
|
|
37
|
+
else workspace = value;
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
if (allowDirty && !dryRun) throw new Error("--allow-dirty is only valid with --dry-run");
|
|
46
|
-
return { repo, dryRun, allowDirty, evidence, workspace };
|
|
39
|
+
return { dryRun, evidence, workspace };
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
//
|
|
50
|
-
// version must be a clean semver so the registry archive is reproducible.
|
|
42
|
+
// VERSION is the single source of truth; the tag is exactly its text.
|
|
51
43
|
export function readPackageIdentity(packageDir) {
|
|
52
44
|
const manifest = path.join(packageDir, "Package.swift");
|
|
53
45
|
if (!fs.existsSync(manifest)) throw new Error(`Package.swift not found: ${manifest}`);
|
|
54
46
|
const versionFile = path.join(packageDir, "VERSION");
|
|
55
47
|
if (!fs.existsSync(versionFile)) throw new Error(`VERSION not found: ${versionFile}`);
|
|
56
48
|
const version = fs.readFileSync(versionFile, "utf8").trim();
|
|
57
|
-
if (!/^\d+\.\d+\.\d
|
|
58
|
-
throw new Error(`VERSION is not clean semver: "${version}"`);
|
|
59
|
-
}
|
|
49
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`VERSION is not plain semver (X.Y.Z): "${version}"`);
|
|
60
50
|
const nameMatch = fs.readFileSync(manifest, "utf8").match(/name:\s*"([^"]+)"/);
|
|
61
51
|
if (!nameMatch) throw new Error("could not read package name from Package.swift");
|
|
62
52
|
return { name: nameMatch[1], version };
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Dry-run mirrors into a fresh local repo; a real publish clones the distribution repo first.
|
|
71
|
-
{ label: "mirror and tag", internal: "mirror", tag: version, repo: dryRun ? null : repo },
|
|
72
|
-
{ label: "fresh consumer resolution (exact pin)", internal: "consume", tag: version, name },
|
|
55
|
+
// The exact commands a real publish runs after every check passes.
|
|
56
|
+
export function swiftTagCommands(version, sha) {
|
|
57
|
+
return [
|
|
58
|
+
["git", ["tag", version, sha]],
|
|
59
|
+
["git", ["push", "origin", `refs/tags/${version}:refs/tags/${version}`]],
|
|
73
60
|
];
|
|
74
|
-
if (!dryRun) plan.push({ label: "git push (branch and tag)", internal: "push", tag: version, repo });
|
|
75
|
-
return plan;
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
function git(args, cwd, execute
|
|
63
|
+
function git(args, cwd, execute) {
|
|
79
64
|
const result = execute("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
80
65
|
if (result.error) throw result.error;
|
|
81
66
|
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${(result.stderr ?? "").trim()}`);
|
|
82
67
|
return (result.stdout ?? "").trim();
|
|
83
68
|
}
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
fs.mkdirSync(to, { recursive: true });
|
|
87
|
-
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
88
|
-
if (MIRROR_EXCLUDES.has(entry.name)) continue;
|
|
89
|
-
const source = path.join(from, entry.name);
|
|
90
|
-
const target = path.join(to, entry.name);
|
|
91
|
-
if (entry.isDirectory()) copyTree(source, target);
|
|
92
|
-
else if (entry.isFile()) { fs.copyFileSync(source, target); fs.chmodSync(target, fs.statSync(source).mode & 0o777); }
|
|
93
|
-
else throw new Error(`unsupported Swift source entry: ${entry.name}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mirrorAndTag(step, packageDir, work, execute) {
|
|
98
|
-
const mirror = path.join(work, "rightkit-swift"); // SwiftPM names the package by repo directory
|
|
99
|
-
if (step.repo) git(["clone", step.repo, mirror], work, execute);
|
|
100
|
-
else { fs.mkdirSync(mirror, { recursive: true }); git(["init", "-q", "-b", "main"], mirror, execute); }
|
|
101
|
-
for (const entry of fs.readdirSync(mirror)) if (entry !== ".git") fs.rmSync(path.join(mirror, entry), { recursive: true, force: true });
|
|
102
|
-
copyTree(packageDir, mirror);
|
|
103
|
-
git(["add", "-A"], mirror, execute);
|
|
104
|
-
git(["-c", "user.email=release@rightsuite.invalid", "-c", "user.name=right-release", "commit", "-q", "--allow-empty", "-m", `RightKitSwift ${step.tag}`], mirror, execute);
|
|
105
|
-
if (git(["tag", "-l", step.tag], mirror, execute) !== "") throw new Error(`tag ${step.tag} already exists: bump VERSION`);
|
|
106
|
-
git(["tag", step.tag], mirror, execute);
|
|
107
|
-
return mirror;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function consumeFresh(step, mirror, work, execute) {
|
|
111
|
-
const app = path.join(work, "consumer");
|
|
112
|
-
fs.mkdirSync(path.join(app, "Sources", "App"), { recursive: true });
|
|
113
|
-
fs.writeFileSync(path.join(app, "Package.swift"), [
|
|
114
|
-
"// swift-tools-version: 6.0", "import PackageDescription", "let package = Package(",
|
|
115
|
-
' name: "App", platforms: [.iOS(.v17), .macOS(.v14)],',
|
|
116
|
-
` dependencies: [.package(url: "file://${mirror}", exact: "${step.tag}")],`,
|
|
117
|
-
' targets: [.target(name: "App", dependencies: [.product(name: "RightKitStore", package: "rightkit-swift")])]', ")", "",
|
|
118
|
-
].join("\n"));
|
|
119
|
-
fs.writeFileSync(path.join(app, "Sources", "App", "App.swift"), "import RightKitStore\npublic let ok = true\n");
|
|
120
|
-
const result = execute("swift", ["package", "resolve"], { cwd: app, encoding: "utf8", windowsHide: true, stdio: "inherit" });
|
|
121
|
-
if (result.error || result.status !== 0) throw new Error("fresh consumer resolution failed");
|
|
122
|
-
const pins = fs.readFileSync(path.join(app, "Package.resolved"), "utf8");
|
|
123
|
-
if (!pins.includes(`"version" : "${step.tag}"`)) throw new Error(`consumer did not resolve exact tag ${step.tag}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Evidence binds this exact git-archive command, rooted at the package tree.
|
|
70
|
+
// Evidence binds this exact git-archive of the package tree at HEAD.
|
|
127
71
|
export function verifySwiftSourceArchive(bytes, evidence) {
|
|
128
72
|
if (!/^[a-f0-9]{64}$/.test(evidence?.artifactSha256 ?? "")) throw new Error("Swift source archive hash evidence is required");
|
|
129
73
|
if (digestBytes(bytes) !== evidence.artifactSha256) throw new Error("Swift source archive sha256 does not match evidence artifactSha256");
|
|
@@ -133,70 +77,63 @@ export function verifySwiftSourceArchive(bytes, evidence) {
|
|
|
133
77
|
export function runSwiftPublish(options, execute = spawnSync) {
|
|
134
78
|
const packageDir = options.packageDir;
|
|
135
79
|
const { name, version } = readPackageIdentity(packageDir);
|
|
80
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], packageDir, execute);
|
|
81
|
+
const out = (message) => process.stdout.write(`[right-release] swift ${message}\n`);
|
|
82
|
+
|
|
83
|
+
const branch = git(["symbolic-ref", "--short", "-q", "HEAD"], toplevel, execute);
|
|
84
|
+
if (branch !== "main") throw new Error(`publish swift runs only on main (current: ${branch || "detached HEAD"})`);
|
|
85
|
+
if (git(["status", "--porcelain", "--untracked-files=normal"], toplevel, execute) !== "") throw new Error("working tree is not clean: commit or remove changes before tagging");
|
|
86
|
+
git(["fetch", "--quiet", "origin", "main"], toplevel, execute);
|
|
87
|
+
const sha = git(["rev-parse", "HEAD"], toplevel, execute);
|
|
88
|
+
const ancestor = execute("git", ["merge-base", "--is-ancestor", sha, "origin/main"], { cwd: toplevel, encoding: "utf8", windowsHide: true });
|
|
89
|
+
if (ancestor.error) throw ancestor.error;
|
|
90
|
+
if (ancestor.status !== 0) throw new Error(`HEAD ${sha} is not on origin/main: push it and let CI pass first`);
|
|
91
|
+
out("branch, clean tree, HEAD on origin/main: ok");
|
|
92
|
+
|
|
93
|
+
const drift = execute(process.execPath, ["--test", DRIFT_TEST], { cwd: toplevel, encoding: "utf8", windowsHide: true });
|
|
94
|
+
if (drift.error) throw drift.error;
|
|
95
|
+
if (drift.status !== 0) throw new Error(`root Package.swift drifted from swift/RightKitSwift/Package.swift:\n${drift.stdout ?? ""}${drift.stderr ?? ""}`);
|
|
96
|
+
out("root manifest matches nested manifest: ok");
|
|
97
|
+
|
|
98
|
+
if (git(["tag", "-l", version], toplevel, execute) !== "") throw new Error(`tag ${version} already exists locally: bump VERSION`);
|
|
99
|
+
if (git(["ls-remote", "--tags", "origin", `refs/tags/${version}`], toplevel, execute) !== "") throw new Error(`tag ${version} already exists on origin: bump VERSION`);
|
|
100
|
+
out(`tag ${version} is free: ok`);
|
|
101
|
+
|
|
136
102
|
const workspaceRoot = path.resolve(options.workspace ?? path.join(packageDir, "..", ".."));
|
|
137
103
|
const gate = enforcePublicationGate({
|
|
138
104
|
kind: "swift", name, version, packagePath: packageDir,
|
|
139
105
|
evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(workspaceRoot, "swift", name)),
|
|
140
106
|
}, { dryRun: options.dryRun });
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const snapshot = path.join(work, "source");
|
|
156
|
-
fs.mkdirSync(snapshot);
|
|
157
|
-
try {
|
|
158
|
-
const extracted = execute("tar", ["-xf", "-", "-C", snapshot], { input: bytes, windowsHide: true });
|
|
159
|
-
if (extracted.error || extracted.status !== 0) throw new Error("Swift source archive extraction failed");
|
|
160
|
-
const identity = readPackageIdentity(snapshot);
|
|
161
|
-
if (identity.name !== name || identity.version !== version) throw new Error("Swift source archive identity mismatch");
|
|
162
|
-
for (const step of plan) {
|
|
163
|
-
process.stdout.write(`[right-release] swift ${step.label}\n`);
|
|
164
|
-
if (step.internal === "secret-scan") {
|
|
165
|
-
const violations = scanCrateForSecrets(snapshot);
|
|
166
|
-
if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (step.internal === "mirror") { mirror = mirrorAndTag(step, snapshot, work, execute);
|
|
170
|
-
if (git(["rev-parse", `${step.tag}^{tree}`], mirror, execute) !== tree) throw new Error("Swift mirror differs from verified source archive");
|
|
171
|
-
sealedCommit = git(["rev-parse", step.tag], mirror, execute);
|
|
172
|
-
continue; }
|
|
173
|
-
if (step.internal === "consume") { consumeFresh(step, mirror, work, execute); continue; }
|
|
174
|
-
if (step.internal === "push") {
|
|
175
|
-
if (git(["rev-parse", `${sealedCommit}^{tree}`], mirror, execute) !== tree || git(["rev-parse", step.tag], mirror, execute) !== sealedCommit) throw new Error("Swift publication tag changed after verification");
|
|
176
|
-
const branch = git(["symbolic-ref", "--short", "HEAD"], mirror, execute);
|
|
177
|
-
git(["push", "--atomic", "origin", `${sealedCommit}:refs/heads/${branch}`, `${sealedCommit}:refs/tags/${step.tag}`], mirror, execute);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const result = execute(step.command, step.args.map((arg) => arg === packageDir ? snapshot : arg), { cwd: snapshot, encoding: "utf8", windowsHide: true, stdio: "inherit" });
|
|
181
|
-
if (result.error) {
|
|
182
|
-
if (result.error.code === "ENOENT") {
|
|
183
|
-
throw new Error(`${step.command} not found -- run publish swift on macOS/Linux with the Swift toolchain installed`);
|
|
184
|
-
}
|
|
185
|
-
throw result.error;
|
|
186
|
-
}
|
|
187
|
-
if (result.status !== 0) throw new Error(`${step.label} failed with exit code ${result.status}`);
|
|
188
|
-
}
|
|
189
|
-
} finally {
|
|
190
|
-
fs.rmSync(work, { recursive: true, force: true });
|
|
107
|
+
if (gate.ok) {
|
|
108
|
+
if (gate.evidence?.commit !== sha) throw new Error(`publication evidence commit ${gate.evidence?.commit} is not HEAD ${sha}`);
|
|
109
|
+
const prefix = git(["rev-parse", "--show-prefix"], packageDir, execute).replace(/\/$/, "");
|
|
110
|
+
const tree = git(["rev-parse", prefix ? `${sha}:${prefix}` : `${sha}^{tree}`], packageDir, execute);
|
|
111
|
+
// `git archive` filters by the current subdirectory, so archive the tree id from the toplevel.
|
|
112
|
+
const archive = execute("git", ["archive", "--format=tar", "--mtime=1970-01-01T00:00:00Z", tree], { cwd: toplevel, windowsHide: true, maxBuffer: 128 * 1024 * 1024 });
|
|
113
|
+
if (archive.error || archive.status !== 0 || !archive.stdout?.length) throw new Error("Swift package tree archive creation failed");
|
|
114
|
+
verifySwiftSourceArchive(Buffer.from(archive.stdout), gate.evidence);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const commands = swiftTagCommands(version, sha);
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
for (const [command, args] of commands) out(`dry-run would run: ${command} ${args.join(" ")}`);
|
|
120
|
+
return { name, version, commit: sha, published: false };
|
|
191
121
|
}
|
|
192
|
-
|
|
122
|
+
const [tagCommand, pushCommand] = commands;
|
|
123
|
+
git(tagCommand[1], toplevel, execute);
|
|
124
|
+
try { git(pushCommand[1], toplevel, execute); }
|
|
125
|
+
catch (error) { git(["tag", "-d", version], toplevel, execute); throw error; }
|
|
126
|
+
const remote = git(["ls-remote", "--tags", "origin", `refs/tags/${version}`], toplevel, execute);
|
|
127
|
+
if (!remote.startsWith(sha)) throw new Error(`origin does not show tag ${version} at ${sha}`);
|
|
128
|
+
out(`tagged ${version} at ${sha} on ${SWIFT_DISTRIBUTION_URL}`);
|
|
129
|
+
return { name, version, commit: sha, published: true };
|
|
193
130
|
}
|
|
194
131
|
|
|
195
132
|
const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
|
|
196
133
|
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
197
134
|
try {
|
|
198
135
|
const options = parseSwiftPublishArgs(process.argv.slice(2));
|
|
199
|
-
const packageDir = path.resolve(path.dirname(
|
|
136
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../swift/RightKitSwift");
|
|
200
137
|
runSwiftPublish({ ...options, packageDir });
|
|
201
138
|
} catch (error) {
|
|
202
139
|
process.stderr.write(`${error.message}\n`);
|
package/publish-update.mjs
CHANGED
|
@@ -114,6 +114,7 @@ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
|
|
|
114
114
|
if (tier === "patch") {
|
|
115
115
|
for (const installer of installers) {
|
|
116
116
|
assertCurrentKey(installer.key, "installer");
|
|
117
|
+
assertVersionedKey(installer.versionedKey);
|
|
117
118
|
artifactFile(installer.file);
|
|
118
119
|
}
|
|
119
120
|
const platforms = {};
|
|
@@ -161,6 +162,13 @@ if (tier === "patch") {
|
|
|
161
162
|
...metadata,
|
|
162
163
|
updaterSignature: null,
|
|
163
164
|
});
|
|
165
|
+
// The versioned copy is the App Installer install source: Windows repairs by re-adding from the URI the
|
|
166
|
+
// package came from, so it must outlive the version that replaces it.
|
|
167
|
+
if (installer.versionedKey && !uploadedKeys.has(installer.versionedKey)) {
|
|
168
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.versionedKey}`);
|
|
169
|
+
if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installer.versionedKey, "public", { cwd: root });
|
|
170
|
+
uploadedKeys.add(installer.versionedKey);
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
if (!artifacts.length) {
|
|
166
174
|
console.log(`${dryRun ? "[dry-run] " : ""}NO patch updater artifacts; skipping patch manifest registration`);
|
|
@@ -169,9 +177,10 @@ if (tier === "patch") {
|
|
|
169
177
|
platform: platform === "mac" ? "mac" : "windows",
|
|
170
178
|
dryRun,
|
|
171
179
|
keep: {
|
|
172
|
-
public:
|
|
180
|
+
public: installerKeepKeys(),
|
|
173
181
|
private: target.updater.artifacts.map((artifact) => artifact.key),
|
|
174
182
|
},
|
|
183
|
+
retainPreviousVersions: installers.some((artifact) => artifact.versionedKey) ? 1 : 0,
|
|
175
184
|
});
|
|
176
185
|
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
|
|
177
186
|
process.exit(0);
|
|
@@ -207,9 +216,10 @@ if (tier === "patch") {
|
|
|
207
216
|
platform: platform === "mac" ? "mac" : "windows",
|
|
208
217
|
dryRun,
|
|
209
218
|
keep: {
|
|
210
|
-
public: [...
|
|
219
|
+
public: [...installerKeepKeys(), ...registrations.keys()],
|
|
211
220
|
private: [],
|
|
212
221
|
},
|
|
222
|
+
retainPreviousVersions: installers.some((artifact) => artifact.versionedKey) ? 1 : 0,
|
|
213
223
|
});
|
|
214
224
|
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
|
|
215
225
|
process.exit(0);
|
|
@@ -311,6 +321,18 @@ function assertCurrentKey(key, kind) {
|
|
|
311
321
|
}
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
// Current + versioned installer keys: both stay public so the old install source is never deleted.
|
|
325
|
+
function installerKeepKeys() {
|
|
326
|
+
return installers.flatMap((artifact) => [artifact.key, ...(artifact.versionedKey ? [artifact.versionedKey] : [])]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function assertVersionedKey(key) {
|
|
330
|
+
if (key === undefined) return;
|
|
331
|
+
if (!/\/installers\/(?:mac|windows)\/versions\/[^/]+\/[^/]+$/.test(key)) {
|
|
332
|
+
fail(`installer versionedKey must be <app>/installers/<os>/versions/<version>/<file>, got: ${key}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
314
336
|
function patchInstallerKey(artifact) {
|
|
315
337
|
if (artifact.patchKey) return artifact.patchKey;
|
|
316
338
|
if (!artifact.key.includes("/updates/")) fail(`patch updater key must be convertible to installers/current, got: ${artifact.key}`);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Source-level layer of "no test-automation surface ships": a release or update build must not enable the cargo
|
|
2
|
+
// features that compile one in. The binary layer (`qa-control-scan.mjs`) cannot see every surface (a stripped
|
|
3
|
+
// binary, compressed installers), so right-release also refuses, before it builds anything:
|
|
4
|
+
// - a build command (prePackage, package, native package hook, finalizer) that enables one of the forbidden
|
|
5
|
+
// features, directly (`--features`, `-F`, `-f`, `--features=`, `--all-features`) or through a package.json
|
|
6
|
+
// script it runs (`pnpm run <script>`, followed up to three levels);
|
|
7
|
+
// - an app Cargo.toml whose DEFAULT feature closure reaches one of them, or that lists `devtools` on `tauri`,
|
|
8
|
+
// (`devtools` and rightkit-tauri-shell's `content-devtools` are accepted only for an allow-listed app, see devtools-policy.mjs)
|
|
9
|
+
// `qa-control` on `rightkit-qa-driver`, `dev-inspect` on `rightkit-tauri-shell`, or `tauri-plugin-wdio-webdriver` as a non-optional dependency.
|
|
10
|
+
// Dev builds are unaffected: `development.mjs` never calls this.
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
export const FORBIDDEN_FEATURES = Object.freeze(["devtools", "content-devtools", "qa-native", "qa-control", "dev-native", "dev-inspect",
|
|
15
|
+
// Per-app test-automation features (M20 owner gap): HeardRight `qa-shot`/`qa-tools`, ViewRight `dev-control`/`bench-native`.
|
|
16
|
+
"qa-shot", "qa-tools", "dev-control", "bench-native",
|
|
17
|
+
// D4 (M21/M23): rightkit-daemon `test-mode` compiles the override selectors; never in a release build.
|
|
18
|
+
"test-mode"]);
|
|
19
|
+
// The only forbidden features an allow-listed app may enable: rightkit-tauri-shell's `content-devtools` (and the Tauri
|
|
20
|
+
// `devtools` it switches on). See devtools-policy.mjs; every other app is refused them.
|
|
21
|
+
const DEVTOOLS_FEATURES = Object.freeze(["devtools", "content-devtools"]);
|
|
22
|
+
const FORBIDDEN_DEPENDENCY = "tauri-plugin-wdio-webdriver";
|
|
23
|
+
const MANAGERS = new Set(["pnpm", "npm", "yarn", "bun", "npx"]);
|
|
24
|
+
|
|
25
|
+
// "dep:foo" -> "foo"; "tauri/devtools" and "tauri?/devtools" -> "devtools".
|
|
26
|
+
function featureName(item) {
|
|
27
|
+
const bare = String(item).trim().replace(/^dep:/, "");
|
|
28
|
+
return bare.includes("/") ? bare.slice(bare.lastIndexOf("/") + 1) : bare;
|
|
29
|
+
}
|
|
30
|
+
export function isForbiddenFeature(item, { allowDevtools = false } = {}) {
|
|
31
|
+
const text = String(item).trim();
|
|
32
|
+
const name = featureName(text);
|
|
33
|
+
if (allowDevtools && DEVTOOLS_FEATURES.includes(name)) return false;
|
|
34
|
+
return text === `dep:${FORBIDDEN_DEPENDENCY}` || FORBIDDEN_FEATURES.includes(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function tokenize(text) {
|
|
38
|
+
return (String(text).match(/"[^"]*"|'[^']*'|[^\s"']+/g) ?? []).map((t) => t.replace(/^["']|["']$/g, ""));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Feature names a token list enables, plus whether it asks for every feature.
|
|
42
|
+
export function featuresInTokens(tokens) {
|
|
43
|
+
const enabled = []; let allFeatures = false;
|
|
44
|
+
const take = (value) => { for (const part of String(value).split(/[,\s]+/)) if (part) enabled.push(part); };
|
|
45
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
46
|
+
const t = tokens[i];
|
|
47
|
+
if (t === "--all-features") allFeatures = true;
|
|
48
|
+
else if (t === "--features" || t === "-F" || t === "-f" || t === "--feature") { if (i + 1 < tokens.length) take(tokens[i + 1]); }
|
|
49
|
+
else if (t.startsWith("--features=")) take(t.slice("--features=".length));
|
|
50
|
+
else if (/^-F.+/.test(t)) take(t.slice(2));
|
|
51
|
+
}
|
|
52
|
+
return { enabled, allFeatures };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function scriptTarget(tokens) {
|
|
56
|
+
const first = tokens.findIndex((t) => MANAGERS.has(path.basename(t).replace(/\.(cmd|exe)$/i, "")));
|
|
57
|
+
if (first < 0) return null;
|
|
58
|
+
let i = first + 1;
|
|
59
|
+
while (i < tokens.length && tokens[i].startsWith("-")) i += 1;
|
|
60
|
+
if (["run", "run-script"].includes(tokens[i])) { i += 1; while (i < tokens.length && tokens[i].startsWith("-")) i += 1; }
|
|
61
|
+
return tokens[i] ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Issues (strings) for one command object { cmd, args, env? } or a shell string.
|
|
65
|
+
export function commandFeatureIssues(command, { label = "command", scripts = {}, depth = 0, seen = new Set(), allowDevtools = false } = {}) {
|
|
66
|
+
const parts = typeof command === "string" ? [command] : [command?.cmd, ...(command?.args ?? [])].filter((v) => v !== undefined).map(String);
|
|
67
|
+
// Two views: every argument whole (`--features` followed by "a b qa-native"), and every argument re-tokenised as
|
|
68
|
+
// shell text (`sh -c "cargo build --features qa-native"`).
|
|
69
|
+
const views = [parts, parts.flatMap((p) => tokenize(p))];
|
|
70
|
+
const issues = new Set();
|
|
71
|
+
for (const tokens of views) {
|
|
72
|
+
const { enabled, allFeatures } = featuresInTokens(tokens);
|
|
73
|
+
if (allFeatures) issues.add(`${label}: --all-features enables every feature including ${FORBIDDEN_FEATURES.join(", ")}`);
|
|
74
|
+
for (const feature of enabled) if (isForbiddenFeature(feature, { allowDevtools })) issues.add(`${label}: enables feature \`${feature}\``);
|
|
75
|
+
}
|
|
76
|
+
if (command && typeof command === "object") {
|
|
77
|
+
for (const [key, value] of Object.entries(command.env ?? {})) {
|
|
78
|
+
if (/FEATURES$/i.test(key)) for (const feature of String(value).split(/[,\s]+/)) if (feature && isForbiddenFeature(feature, { allowDevtools })) issues.add(`${label}: env ${key} enables feature \`${feature}\``);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const script = views.map(scriptTarget).find(Boolean);
|
|
82
|
+
if (script && depth < 3 && Object.hasOwn(scripts, script) && !seen.has(script)) {
|
|
83
|
+
for (const issue of commandFeatureIssues(String(scripts[script]), { label: `${label} -> package script \`${script}\``, scripts, depth: depth + 1, seen: new Set([...seen, script]), allowDevtools })) issues.add(issue);
|
|
84
|
+
}
|
|
85
|
+
return [...issues];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- Cargo.toml ---------------------------------------------------------------------------------------------
|
|
89
|
+
function stripComments(text) {
|
|
90
|
+
let out = ""; let quote = null;
|
|
91
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
92
|
+
const c = text[i];
|
|
93
|
+
if (quote) { out += c; if (c === quote && text[i - 1] !== "\\") quote = null; continue; }
|
|
94
|
+
if (c === '"' || c === "'") { quote = c; out += c; continue; }
|
|
95
|
+
if (c === "#") { while (i < text.length && text[i] !== "\n") i += 1; out += "\n"; continue; }
|
|
96
|
+
out += c;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sectionsOf(text) {
|
|
102
|
+
const sections = []; let current = { name: "", body: "" };
|
|
103
|
+
for (const line of text.split(/\r?\n/)) {
|
|
104
|
+
const header = line.match(/^\s*\[\s*([^\[\]]+?)\s*\]\s*$/);
|
|
105
|
+
if (header) { sections.push(current); current = { name: header[1].replace(/\s+/g, ""), body: "" }; }
|
|
106
|
+
else current.body += `${line}\n`;
|
|
107
|
+
}
|
|
108
|
+
sections.push(current);
|
|
109
|
+
return sections;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function balanced(text, from, open, close) {
|
|
113
|
+
let depth = 0; let quote = null;
|
|
114
|
+
for (let i = from; i < text.length; i += 1) {
|
|
115
|
+
const c = text[i];
|
|
116
|
+
if (quote) { if (c === quote && text[i - 1] !== "\\") quote = null; continue; }
|
|
117
|
+
if (c === '"' || c === "'") quote = c;
|
|
118
|
+
else if (c === open) depth += 1;
|
|
119
|
+
else if (c === close) { depth -= 1; if (depth === 0) return text.slice(from, i + 1); }
|
|
120
|
+
}
|
|
121
|
+
return text.slice(from);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const strings = (text) => [...String(text).matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2]);
|
|
125
|
+
const unquote = (key) => key.replace(/^["']|["']$/g, "");
|
|
126
|
+
|
|
127
|
+
export function parseCargoFeaturesAndDependencies(manifestText) {
|
|
128
|
+
const sections = sectionsOf(stripComments(manifestText));
|
|
129
|
+
const features = new Map(); const dependencies = new Map();
|
|
130
|
+
for (const { name, body } of sections) {
|
|
131
|
+
if (name === "features") {
|
|
132
|
+
for (const m of body.matchAll(/^\s*([A-Za-z0-9_\-"']+)\s*=\s*\[/gm)) {
|
|
133
|
+
features.set(unquote(m[1]), strings(balanced(body, m.index + m[0].length - 1, "[", "]")));
|
|
134
|
+
}
|
|
135
|
+
} else if (/^(?:target\.[^\n]+\.)?dependencies$/.test(name)) {
|
|
136
|
+
for (const m of body.matchAll(/^\s*([A-Za-z0-9_\-"']+)\s*=\s*(\{)?/gm)) {
|
|
137
|
+
const depName = unquote(m[1]);
|
|
138
|
+
const spec = m[2] ? balanced(body, m.index + m[0].length - 1, "{", "}") : "";
|
|
139
|
+
dependencies.set(depName, dependencyInfo(spec));
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const table = name.match(/^(?:target\.[^\n]+\.)?dependencies\.([A-Za-z0-9_\-]+)$/);
|
|
143
|
+
if (table) dependencies.set(table[1], dependencyInfo(body));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { features, dependencies };
|
|
147
|
+
}
|
|
148
|
+
function dependencyInfo(spec) {
|
|
149
|
+
const list = spec.match(/features\s*=\s*(\[)/);
|
|
150
|
+
return {
|
|
151
|
+
optional: /optional\s*=\s*true/.test(spec),
|
|
152
|
+
features: list ? strings(balanced(spec, list.index + list[0].length - 1, "[", "]")) : [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function cargoManifestIssues(manifestText, label = "Cargo.toml", { allowDevtools = false } = {}) {
|
|
157
|
+
const { features, dependencies } = parseCargoFeaturesAndDependencies(manifestText);
|
|
158
|
+
const issues = [];
|
|
159
|
+
const tauri = dependencies.get("tauri");
|
|
160
|
+
if (!allowDevtools && tauri?.features.includes("devtools")) issues.push(`${label}: dependency \`tauri\` lists feature \`devtools\` unconditionally (inspector ships in release builds)`);
|
|
161
|
+
// The crate was renamed from rightkit-control; apps still on the old published name must stay covered.
|
|
162
|
+
for (const name of ["rightkit-qa-driver", "rightkit-control"]) {
|
|
163
|
+
const control = dependencies.get(name);
|
|
164
|
+
if (control && !control.optional && control.features.includes("qa-control")) issues.push(`${label}: dependency \`${name}\` lists \`qa-control\` unconditionally`);
|
|
165
|
+
}
|
|
166
|
+
// The crate was renamed from rightkit-shell; apps still on the old published name must stay covered.
|
|
167
|
+
for (const name of ["rightkit-tauri-shell", "rightkit-shell"]) {
|
|
168
|
+
const shell = dependencies.get(name);
|
|
169
|
+
if (shell && !shell.optional && shell.features.includes("dev-inspect")) issues.push(`${label}: dependency \`${name}\` lists \`dev-inspect\` unconditionally (developer inspection ships in release builds)`);
|
|
170
|
+
}
|
|
171
|
+
const service = dependencies.get("rightkit-daemon");
|
|
172
|
+
if (service && !service.optional && service.features.includes("test-mode")) issues.push(`${label}: dependency \`rightkit-daemon\` lists \`test-mode\` unconditionally (test-mode capability ships in release builds)`);
|
|
173
|
+
const wdio = dependencies.get(FORBIDDEN_DEPENDENCY);
|
|
174
|
+
if (wdio && !wdio.optional) issues.push(`${label}: dependency \`${FORBIDDEN_DEPENDENCY}\` is not optional`);
|
|
175
|
+
const reached = new Set(); const stack = [...(features.get("default") ?? [])];
|
|
176
|
+
while (stack.length) {
|
|
177
|
+
const item = stack.pop();
|
|
178
|
+
if (reached.has(item)) continue;
|
|
179
|
+
reached.add(item);
|
|
180
|
+
if (isForbiddenFeature(item, { allowDevtools })) issues.push(`${label}: default feature set reaches \`${item}\``);
|
|
181
|
+
if (features.has(item)) stack.push(...features.get(item));
|
|
182
|
+
}
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// commands: [{ label, command }]. Reads package.json scripts and the app Cargo manifests under `root`.
|
|
187
|
+
// `devtools`: the allowance resolved by devtools-policy.mjs (mode "content-only") for an allow-listed app, else undefined.
|
|
188
|
+
// Single manifest resolver shared by the closure gate (release-switch-scan.mjs) and the default-feature check below:
|
|
189
|
+
// targets.<os>.cargoManifest -> releaseSwitches.manifest -> src-tauri/Cargo.toml -> Cargo.toml. `rel` is the first
|
|
190
|
+
// candidate that exists (undefined when none does); `declared` are the configured paths.
|
|
191
|
+
export function resolveCargoManifestPath({ root, cargoManifest, manifest } = {}) {
|
|
192
|
+
const declared = [cargoManifest, manifest].filter(Boolean);
|
|
193
|
+
const candidates = [...declared, path.join("src-tauri", "Cargo.toml"), "Cargo.toml"];
|
|
194
|
+
return { rel: candidates.find((c) => existsSync(path.join(root, c))), declared, candidates };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildFeatureIssues({ root, commands, devtools, cargoManifest, manifest }) {
|
|
198
|
+
const allowDevtools = devtools?.mode === "content-only";
|
|
199
|
+
let scripts = {};
|
|
200
|
+
try { scripts = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).scripts ?? {}; } catch { /* no package.json */ }
|
|
201
|
+
const issues = [];
|
|
202
|
+
for (const { label, command } of commands) if (command) issues.push(...commandFeatureIssues(command, { label, scripts, allowDevtools }));
|
|
203
|
+
const { rel } = resolveCargoManifestPath({ root, cargoManifest, manifest });
|
|
204
|
+
if (rel) issues.push(...cargoManifestIssues(readFileSync(path.join(root, rel), "utf8"), rel, { allowDevtools }));
|
|
205
|
+
return issues;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function assertNoForbiddenBuildFeatures({ root, commands, tier = "release", devtools, cargoManifest, manifest }) {
|
|
209
|
+
const issues = buildFeatureIssues({ root, commands, devtools, cargoManifest, manifest });
|
|
210
|
+
if (issues.length === 0) return;
|
|
211
|
+
throw new Error(
|
|
212
|
+
`${tier} build refused: it enables a test-automation or debugging feature (${FORBIDDEN_FEATURES.join(", ")}); none may ship:\n- ${issues.join("\n- ")}\n`
|
|
213
|
+
+ "Remove the feature from the release build command, the app's default features and its tauri dependency; dev builds (`right-release dev`) may keep it. "
|
|
214
|
+
+ "No app may ship devtools unless it is on the allow-list in devtools-policy.json and declares `devtools: \"content-only\"` (rightkit-tauri-shell DevToolsPolicy::ContentOnly).",
|
|
215
|
+
);
|
|
216
|
+
}
|