@rightkit/release 0.2.11 → 0.2.13
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/cli/right-release.mjs +17 -3
- package/deps.mjs +0 -0
- package/lsclean.sh +0 -0
- package/mirror-root-artifact.test.mjs +4 -3
- package/notary-auth.test.mjs +2 -1
- package/package.json +2 -1
- package/publish-cargo.mjs +144 -0
- package/publish-cargo.test.mjs +43 -0
- package/publish-update.mjs +49 -0
- package/publish-update.test.mjs +30 -0
- package/release.mjs +14 -7
- package/release.test.mjs +26 -5
- package/right-suite-contract.test.mjs +53 -15
- package/rightkit-versions.json +14 -0
- package/suite-doctor.test.mjs +19 -0
- package/upload-large.mjs +0 -0
package/cli/right-release.mjs
CHANGED
|
@@ -35,9 +35,18 @@ if (first === "--version" || first === "-v") {
|
|
|
35
35
|
} else {
|
|
36
36
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
37
37
|
}
|
|
38
|
+
} else if (first === "suite-doctor") {
|
|
39
|
+
runTest(
|
|
40
|
+
["--test", path.join(packageRoot, "right-suite-contract.test.mjs")],
|
|
41
|
+
"[right-release] suite-doctor passed",
|
|
42
|
+
);
|
|
38
43
|
} else if (first === "publish") {
|
|
39
44
|
const rest = args.slice(1);
|
|
40
|
-
|
|
45
|
+
if (rest[0] === "cargo") {
|
|
46
|
+
run("publish-cargo.mjs", rest.slice(1));
|
|
47
|
+
} else {
|
|
48
|
+
run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
|
|
49
|
+
}
|
|
41
50
|
} else if (first === "lsclean") {
|
|
42
51
|
runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
|
|
43
52
|
} else if (first === "generate-dmg-background") {
|
|
@@ -66,7 +75,7 @@ function run(script, scriptArgs) {
|
|
|
66
75
|
child.on("exit", (code) => process.exit(code ?? 1));
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
function runTest(testArgs) {
|
|
78
|
+
function runTest(testArgs, successMessage) {
|
|
70
79
|
const child = spawn(process.execPath, testArgs, {
|
|
71
80
|
cwd: process.cwd(),
|
|
72
81
|
env: process.env,
|
|
@@ -77,7 +86,10 @@ function runTest(testArgs) {
|
|
|
77
86
|
console.error(`right-release: failed to start doctor --all: ${error.message}`);
|
|
78
87
|
process.exit(1);
|
|
79
88
|
});
|
|
80
|
-
child.on("exit", (code) =>
|
|
89
|
+
child.on("exit", (code) => {
|
|
90
|
+
if (code === 0 && successMessage) console.log(successMessage);
|
|
91
|
+
process.exit(code ?? 1);
|
|
92
|
+
});
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
function runBinary(cmd, cmdArgs) {
|
|
@@ -100,8 +112,10 @@ function printHelp() {
|
|
|
100
112
|
Commands:
|
|
101
113
|
release [--platform mac|win] --tier patch|update Build/package through the signed release lane
|
|
102
114
|
publish [--platform mac|win] --tier patch|update Release plus R2 upload + RightApps registration
|
|
115
|
+
publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
|
|
103
116
|
doctor [--platform mac|win] Inspect one app's release config
|
|
104
117
|
doctor --all Verify all Right Suite app release contracts
|
|
118
|
+
suite-doctor Verify all local Right Suite repositories
|
|
105
119
|
deps --check|--audit|--update Shared dependency lane
|
|
106
120
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
107
121
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
package/deps.mjs
CHANGED
|
File without changes
|
package/lsclean.sh
CHANGED
|
File without changes
|
|
@@ -4,8 +4,9 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "nod
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
|
-
const helper = new URL("./mirror-root-artifact.mjs", import.meta.url);
|
|
9
|
+
const helper = fileURLToPath(new URL("./mirror-root-artifact.mjs", import.meta.url));
|
|
9
10
|
|
|
10
11
|
test("mirrors a nested package artifact from a linked worktree into both package roots", () => {
|
|
11
12
|
const temp = mkdtempSync(path.join(os.tmpdir(), "right-release-mirror-"));
|
|
@@ -26,7 +27,7 @@ test("mirrors a nested package artifact from a linked worktree into both package
|
|
|
26
27
|
mkdirSync(path.dirname(source), { recursive: true });
|
|
27
28
|
writeFileSync(source, "signed-dmg-fixture");
|
|
28
29
|
|
|
29
|
-
execFileSync(process.execPath, [helper
|
|
30
|
+
execFileSync(process.execPath, [helper, "--file", source, "--package-root", packageRoot], { encoding: "utf8" });
|
|
30
31
|
|
|
31
32
|
assert.equal(readFileSync(path.join(packageRoot, "App_1.0.0.dmg"), "utf8"), "signed-dmg-fixture");
|
|
32
33
|
assert.equal(readFileSync(path.join(main, "apps", "desktop", "App_1.0.0.dmg"), "utf8"), "signed-dmg-fixture");
|
|
@@ -44,7 +45,7 @@ test("copies a main-worktree artifact into its package root", () => {
|
|
|
44
45
|
mkdirSync(path.dirname(source), { recursive: true });
|
|
45
46
|
writeFileSync(source, "main-worktree-dmg");
|
|
46
47
|
|
|
47
|
-
execFileSync(process.execPath, [helper
|
|
48
|
+
execFileSync(process.execPath, [helper, "--file", source, "--package-root", root], { encoding: "utf8" });
|
|
48
49
|
|
|
49
50
|
assert.equal(readFileSync(path.join(root, "App_2.0.0.dmg"), "utf8"), "main-worktree-dmg");
|
|
50
51
|
} finally {
|
package/notary-auth.test.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import test from "node:test";
|
|
4
5
|
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
5
6
|
|
|
@@ -9,7 +10,7 @@ test("prefers a complete App Store Connect API credential set", () => {
|
|
|
9
10
|
process.env.APPLE_API_KEY_PATH = "~/AuthKey.p8";
|
|
10
11
|
process.env.APPLE_API_KEY = "KEY123";
|
|
11
12
|
process.env.APPLE_API_ISSUER = "ISSUER123";
|
|
12
|
-
assert.deepEqual(notarytoolAuthArgs(), ["--key",
|
|
13
|
+
assert.deepEqual(notarytoolAuthArgs(), ["--key", path.join(os.homedir(), "AuthKey.p8"), "--key-id", "KEY123", "--issuer", "ISSUER123"]);
|
|
13
14
|
} finally {
|
|
14
15
|
process.env = before;
|
|
15
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"cli",
|
|
11
11
|
"*.mjs",
|
|
12
|
+
"*.json",
|
|
12
13
|
"*.sh",
|
|
13
14
|
"*.py"
|
|
14
15
|
],
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const FORBIDDEN_PACKAGE_PATH = /(^|\/)(?:\.env(?:\.[^/]*)?|target(?:\/.*)?|node_modules(?:\/.*)?|[^/]+\.(?:p8|p12|pem|key))$/i;
|
|
8
|
+
const SECRET_PATTERNS = [
|
|
9
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
10
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
11
|
+
/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
|
|
12
|
+
/\bnpm_[A-Za-z0-9]{30,}\b/,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function parseCargoPublishArgs(argv, cwd = process.cwd()) {
|
|
16
|
+
const crate = valueAfter(argv, "--crate");
|
|
17
|
+
if (!crate) throw new Error("publish cargo requires --crate <name>");
|
|
18
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(crate)) throw new Error(`invalid crate name: ${crate}`);
|
|
19
|
+
const allowed = new Set(["--crate", "--workspace", "--dry-run", "--allow-dirty"]);
|
|
20
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
21
|
+
const arg = argv[index];
|
|
22
|
+
if (!arg.startsWith("--")) continue;
|
|
23
|
+
if (!allowed.has(arg)) throw new Error(`unknown publish cargo option: ${arg}`);
|
|
24
|
+
if (arg !== "--dry-run" && arg !== "--allow-dirty") index += 1;
|
|
25
|
+
}
|
|
26
|
+
const dryRun = argv.includes("--dry-run");
|
|
27
|
+
const allowDirty = argv.includes("--allow-dirty");
|
|
28
|
+
if (allowDirty && !dryRun) throw new Error("--allow-dirty is only valid with --dry-run");
|
|
29
|
+
return {
|
|
30
|
+
crate,
|
|
31
|
+
dryRun,
|
|
32
|
+
allowDirty,
|
|
33
|
+
workspaceRoot: path.resolve(valueAfter(argv, "--workspace") ?? cwd),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCargoPublishPlan({ crate, crateDir, dryRun, allowDirty = false }) {
|
|
38
|
+
const dryRunArgs = ["publish", "--dry-run", "-p", crate];
|
|
39
|
+
const packageArgs = ["package", "--list", "-p", crate];
|
|
40
|
+
if (allowDirty) dryRunArgs.push("--allow-dirty");
|
|
41
|
+
if (allowDirty) packageArgs.push("--allow-dirty");
|
|
42
|
+
const plan = [
|
|
43
|
+
{ label: "secret scan", internal: "secret-scan", crateDir },
|
|
44
|
+
{ label: "format", command: "cargo", args: ["fmt", "--all", "--check"] },
|
|
45
|
+
{ label: "tests", command: "cargo", args: ["test", "-p", crate] },
|
|
46
|
+
{ label: "clippy", command: "cargo", args: ["clippy", "-p", crate, "--all-targets", "--", "-D", "warnings"] },
|
|
47
|
+
{ label: "package contents", command: "cargo", args: packageArgs, capture: true },
|
|
48
|
+
{ label: "registry dry-run", command: "cargo", args: dryRunArgs },
|
|
49
|
+
];
|
|
50
|
+
if (!dryRun) plan.push({ label: "registry publish", command: "cargo", args: ["publish", "-p", crate] });
|
|
51
|
+
return plan;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function packageFileViolations(files) {
|
|
55
|
+
return files
|
|
56
|
+
.map((file) => file.trim().replaceAll("\\", "/"))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.filter((file) => FORBIDDEN_PACKAGE_PATH.test(file));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function scanCrateForSecrets(crateDir) {
|
|
62
|
+
const violations = [];
|
|
63
|
+
for (const file of walkFiles(crateDir)) {
|
|
64
|
+
const relative = path.relative(crateDir, file).replaceAll("\\", "/");
|
|
65
|
+
if (FORBIDDEN_PACKAGE_PATH.test(relative)) {
|
|
66
|
+
violations.push(relative);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const bytes = fs.readFileSync(file);
|
|
70
|
+
if (bytes.includes(0)) continue;
|
|
71
|
+
const text = bytes.toString("utf8");
|
|
72
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(text))) violations.push(relative);
|
|
73
|
+
}
|
|
74
|
+
return [...new Set(violations)].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function runCargoPublish(options, execute = spawnSync) {
|
|
78
|
+
const crateDir = path.join(options.workspaceRoot, "crates", options.crate);
|
|
79
|
+
const manifest = path.join(crateDir, "Cargo.toml");
|
|
80
|
+
if (!fs.existsSync(manifest)) throw new Error(`crate manifest not found: ${manifest}`);
|
|
81
|
+
const plan = buildCargoPublishPlan({ ...options, crateDir });
|
|
82
|
+
|
|
83
|
+
for (const step of plan) {
|
|
84
|
+
process.stdout.write(`[right-release] cargo ${step.label}\n`);
|
|
85
|
+
if (step.internal === "secret-scan") {
|
|
86
|
+
const violations = scanCrateForSecrets(crateDir);
|
|
87
|
+
if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const result = execute(step.command, step.args, {
|
|
91
|
+
cwd: options.workspaceRoot,
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
stdio: step.capture ? "pipe" : "inherit",
|
|
95
|
+
});
|
|
96
|
+
if (result.error) throw result.error;
|
|
97
|
+
if (result.status !== 0) {
|
|
98
|
+
if (step.capture && result.stderr) process.stderr.write(result.stderr);
|
|
99
|
+
throw new Error(`${step.label} failed with exit code ${result.status}`);
|
|
100
|
+
}
|
|
101
|
+
if (step.capture) {
|
|
102
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
103
|
+
const files = String(result.stdout ?? "").split(/\r?\n/).filter(Boolean);
|
|
104
|
+
const violations = packageFileViolations(files);
|
|
105
|
+
if (violations.length) throw new Error(`package contents rejected: ${violations.join(", ")}`);
|
|
106
|
+
process.stdout.write(`${files.join("\n")}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function walkFiles(root) {
|
|
112
|
+
const files = [];
|
|
113
|
+
const pending = [root];
|
|
114
|
+
while (pending.length) {
|
|
115
|
+
const current = pending.pop();
|
|
116
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
117
|
+
if (entry.isDirectory() && [".git", "target", "node_modules"].includes(entry.name)) continue;
|
|
118
|
+
const absolute = path.join(current, entry.name);
|
|
119
|
+
if (entry.isDirectory()) pending.push(absolute);
|
|
120
|
+
else if (entry.isFile()) files.push(absolute);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function valueAfter(argv, flag) {
|
|
127
|
+
const index = argv.indexOf(flag);
|
|
128
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function main() {
|
|
132
|
+
const options = parseCargoPublishArgs(process.argv.slice(2));
|
|
133
|
+
runCargoPublish(options);
|
|
134
|
+
process.stdout.write(`[right-release] ${options.dryRun ? "validated" : "published"} ${options.crate}\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
138
|
+
try {
|
|
139
|
+
main();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
process.stderr.write(`right-release publish cargo: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildCargoPublishPlan,
|
|
5
|
+
packageFileViolations,
|
|
6
|
+
parseCargoPublishArgs,
|
|
7
|
+
} from "./publish-cargo.mjs";
|
|
8
|
+
|
|
9
|
+
test("cargo publish requires a safe explicit crate name", () => {
|
|
10
|
+
assert.throws(() => parseCargoPublishArgs([]), /--crate/);
|
|
11
|
+
assert.throws(() => parseCargoPublishArgs(["--crate", "../secret"]), /invalid crate/);
|
|
12
|
+
assert.throws(() => parseCargoPublishArgs(["--crate", "rightkit-license", "--allow-dirty"]), /only valid with --dry-run/);
|
|
13
|
+
assert.equal(parseCargoPublishArgs(["--crate", "rightkit-license"]).crate, "rightkit-license");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("cargo publish plan always runs local safety gates before upload", () => {
|
|
17
|
+
const plan = buildCargoPublishPlan({ crate: "rightkit-license", crateDir: "C:/rightkit/crates/rightkit-license", dryRun: false });
|
|
18
|
+
assert.deepEqual(plan.map((step) => step.label), [
|
|
19
|
+
"secret scan",
|
|
20
|
+
"format",
|
|
21
|
+
"tests",
|
|
22
|
+
"clippy",
|
|
23
|
+
"package contents",
|
|
24
|
+
"registry dry-run",
|
|
25
|
+
"registry publish",
|
|
26
|
+
]);
|
|
27
|
+
assert.deepEqual(plan.at(-1)?.args, ["publish", "-p", "rightkit-license"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("dry-run plan never contains a real registry upload", () => {
|
|
31
|
+
const plan = buildCargoPublishPlan({ crate: "rightkit-logs", crateDir: "C:/rightkit/crates/rightkit-logs", dryRun: true });
|
|
32
|
+
assert.equal(plan.at(-1)?.label, "registry dry-run");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("package inspection rejects secret and build-output paths", () => {
|
|
36
|
+
assert.deepEqual(packageFileViolations([
|
|
37
|
+
"Cargo.toml",
|
|
38
|
+
"src/lib.rs",
|
|
39
|
+
".env",
|
|
40
|
+
"keys/signing.p8",
|
|
41
|
+
"target/release/app",
|
|
42
|
+
]), [".env", "keys/signing.p8", "target/release/app"]);
|
|
43
|
+
});
|
package/publish-update.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
|
11
11
|
const API_BASE = process.env.RIGHTAPPS_API_URL || "https://api.spoares.com";
|
|
12
12
|
const DOWNLOAD_BASE = process.env.RIGHTAPPS_UPDATE_DOWNLOAD_BASE || "https://rightapps-license-gate.adrdsouza.workers.dev";
|
|
13
13
|
const PUBLIC_BASE = process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev";
|
|
14
|
+
const PIPELINE_VERSION = JSON.parse(await readFile(path.join(TOOL_ROOT, "package.json"), "utf8")).version;
|
|
14
15
|
const tier = process.env.RIGHT_RELEASE_TIER || "";
|
|
15
16
|
const args = process.argv.slice(2);
|
|
16
17
|
let configName = "right-release.config.mjs";
|
|
@@ -45,6 +46,8 @@ if (tier === "patch") {
|
|
|
45
46
|
}
|
|
46
47
|
const platforms = {};
|
|
47
48
|
const registrations = new Map();
|
|
49
|
+
const updaterManifestArtifacts = new Map();
|
|
50
|
+
const installerManifestArtifacts = new Map();
|
|
48
51
|
const uploadedKeys = new Set();
|
|
49
52
|
const uploadedSignatures = new Set();
|
|
50
53
|
for (const artifact of artifacts) {
|
|
@@ -75,6 +78,12 @@ if (tier === "patch") {
|
|
|
75
78
|
url: `${PUBLIC_BASE}/${installerKey}`,
|
|
76
79
|
};
|
|
77
80
|
registrations.set(installerKey, { artifactKey, path: installerKey, ...metadata });
|
|
81
|
+
updaterManifestArtifacts.set(installerKey, {
|
|
82
|
+
kind: "updater",
|
|
83
|
+
r2Key: installerKey,
|
|
84
|
+
...metadata,
|
|
85
|
+
updaterSignature: signature,
|
|
86
|
+
});
|
|
78
87
|
}
|
|
79
88
|
for (const installer of installers) {
|
|
80
89
|
const file = path.resolve(root, installer.file);
|
|
@@ -83,6 +92,13 @@ if (tier === "patch") {
|
|
|
83
92
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
|
|
84
93
|
uploadedKeys.add(installer.key);
|
|
85
94
|
}
|
|
95
|
+
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
96
|
+
installerManifestArtifacts.set(installer.key, {
|
|
97
|
+
kind: "installer",
|
|
98
|
+
r2Key: installer.key,
|
|
99
|
+
...metadata,
|
|
100
|
+
updaterSignature: null,
|
|
101
|
+
});
|
|
86
102
|
}
|
|
87
103
|
if (!artifacts.length) {
|
|
88
104
|
console.log(`${dryRun ? "[dry-run] " : ""}NO patch updater artifacts; skipping patch manifest registration`);
|
|
@@ -112,6 +128,13 @@ if (tier === "patch") {
|
|
|
112
128
|
platforms,
|
|
113
129
|
},
|
|
114
130
|
artifacts: [...registrations.values()],
|
|
131
|
+
artifactManifest: buildArtifactManifest({
|
|
132
|
+
appKey: config.app,
|
|
133
|
+
appVersion: version,
|
|
134
|
+
tier,
|
|
135
|
+
platform,
|
|
136
|
+
artifacts: [...installerManifestArtifacts.values(), ...updaterManifestArtifacts.values()],
|
|
137
|
+
}),
|
|
115
138
|
};
|
|
116
139
|
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}/v1/admin/apps/patches`);
|
|
117
140
|
console.log(JSON.stringify(body));
|
|
@@ -132,6 +155,7 @@ if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no u
|
|
|
132
155
|
|
|
133
156
|
const platforms = {};
|
|
134
157
|
const registrations = new Map();
|
|
158
|
+
const updaterManifestArtifacts = new Map();
|
|
135
159
|
const uploadedKeys = new Set();
|
|
136
160
|
const uploadedSignatures = new Set();
|
|
137
161
|
for (const artifact of artifacts) {
|
|
@@ -161,6 +185,12 @@ for (const artifact of artifacts) {
|
|
|
161
185
|
url: `${DOWNLOAD_BASE}/${artifact.key}`,
|
|
162
186
|
};
|
|
163
187
|
registrations.set(artifact.key, { artifactKey, path: artifact.key, ...metadata });
|
|
188
|
+
updaterManifestArtifacts.set(artifact.key, {
|
|
189
|
+
kind: "updater",
|
|
190
|
+
r2Key: artifact.key,
|
|
191
|
+
...metadata,
|
|
192
|
+
updaterSignature: signature,
|
|
193
|
+
});
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
const version = config.version || target.updater.version;
|
|
@@ -178,6 +208,13 @@ const body = {
|
|
|
178
208
|
platforms,
|
|
179
209
|
},
|
|
180
210
|
artifacts: [...registrations.values()],
|
|
211
|
+
artifactManifest: buildArtifactManifest({
|
|
212
|
+
appKey: config.app,
|
|
213
|
+
appVersion: version,
|
|
214
|
+
tier,
|
|
215
|
+
platform,
|
|
216
|
+
artifacts: [...updaterManifestArtifacts.values()],
|
|
217
|
+
}),
|
|
181
218
|
};
|
|
182
219
|
const route = "/v1/admin/apps/releases";
|
|
183
220
|
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}${route}`);
|
|
@@ -202,6 +239,18 @@ async function fileMetadata(file) {
|
|
|
202
239
|
};
|
|
203
240
|
}
|
|
204
241
|
|
|
242
|
+
function buildArtifactManifest({ appKey, appVersion, tier, platform, artifacts }) {
|
|
243
|
+
return {
|
|
244
|
+
schema: 1,
|
|
245
|
+
pipelineVersion: PIPELINE_VERSION,
|
|
246
|
+
appKey,
|
|
247
|
+
appVersion,
|
|
248
|
+
tier,
|
|
249
|
+
platform: platform === "mac" ? "darwin" : "windows",
|
|
250
|
+
artifacts,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
205
254
|
function run(cmd, runArgs, cwd) {
|
|
206
255
|
return new Promise((resolve, reject) => {
|
|
207
256
|
const child = spawn(cmd, runArgs, { cwd, env: process.env, stdio: "inherit", windowsHide: true });
|
package/publish-update.test.mjs
CHANGED
|
@@ -76,6 +76,14 @@ function run(tier) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function registrationBody(result) {
|
|
80
|
+
return result.stdout
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.filter((line) => line.startsWith('{'))
|
|
83
|
+
.map((line) => JSON.parse(line))
|
|
84
|
+
.find((body) => body.artifactManifest);
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
test("patch registers a free updater manifest backed by public installer-path objects", () => {
|
|
80
88
|
const result = run("patch");
|
|
81
89
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -95,6 +103,23 @@ test("patch registers a free updater manifest backed by public installer-path ob
|
|
|
95
103
|
assert.equal((result.stdout.match(/UPLOAD public\/fixture\/installers\/windows\/current\/Fixture\.exe\.sig/g) ?? []).length, 1);
|
|
96
104
|
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
97
105
|
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows keep=<none>/);
|
|
106
|
+
const body = registrationBody(result);
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
{
|
|
109
|
+
schema: body.artifactManifest.schema,
|
|
110
|
+
appKey: body.artifactManifest.appKey,
|
|
111
|
+
appVersion: body.artifactManifest.appVersion,
|
|
112
|
+
tier: body.artifactManifest.tier,
|
|
113
|
+
platform: body.artifactManifest.platform,
|
|
114
|
+
},
|
|
115
|
+
{ schema: 1, appKey: 'fixture', appVersion: '1.2.3', tier: 'patch', platform: 'windows' },
|
|
116
|
+
);
|
|
117
|
+
assert.match(body.artifactManifest.pipelineVersion, /^\d+\.\d+\.\d+$/);
|
|
118
|
+
assert.deepEqual(body.artifactManifest.artifacts.map((artifact) => [artifact.kind, artifact.r2Key]), [
|
|
119
|
+
['installer', 'fixture/installers/windows/current/Fixture-Setup.exe'],
|
|
120
|
+
['updater', 'fixture/installers/windows/current/Fixture.exe'],
|
|
121
|
+
]);
|
|
122
|
+
assert.match(body.artifactManifest.artifacts[1].updaterSignature, /^<signature:/);
|
|
98
123
|
});
|
|
99
124
|
|
|
100
125
|
test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
@@ -108,6 +133,11 @@ test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
|
108
133
|
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
109
134
|
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows/);
|
|
110
135
|
assert.doesNotMatch(result.stdout, /UPLOAD .*Fixture-Setup\.exe/);
|
|
136
|
+
const body = registrationBody(result);
|
|
137
|
+
assert.equal(body.artifactManifest.tier, 'update');
|
|
138
|
+
assert.deepEqual(body.artifactManifest.artifacts.map((artifact) => [artifact.kind, artifact.r2Key]), [
|
|
139
|
+
['updater', 'fixture/updates/windows/current/Fixture.exe'],
|
|
140
|
+
]);
|
|
111
141
|
});
|
|
112
142
|
|
|
113
143
|
test("allows an installer-only patch without registering an updater manifest", () => {
|
package/release.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -10,13 +10,10 @@ const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
|
10
10
|
const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
|
|
11
11
|
const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
|
|
12
12
|
const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
|
|
13
|
-
const
|
|
13
|
+
const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
|
|
14
|
+
const VERSION = RIGHTKIT_VERSIONS.npm["@rightkit/release"];
|
|
14
15
|
const TIERS = new Set(["patch", "update"]);
|
|
15
|
-
const RIGHTKIT_EXPECTED = new Map(
|
|
16
|
-
["@rightkit/license", "^0.1.5"],
|
|
17
|
-
["@rightkit/logs", "^0.1.3"],
|
|
18
|
-
["@rightkit/release", `^${VERSION}`],
|
|
19
|
-
]);
|
|
16
|
+
const RIGHTKIT_EXPECTED = new Map(Object.entries(RIGHTKIT_VERSIONS.npm));
|
|
20
17
|
const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
|
|
21
18
|
|
|
22
19
|
const args = process.argv.slice(2);
|
|
@@ -181,6 +178,16 @@ async function validateRightKitPackageContract(root, appName) {
|
|
|
181
178
|
const packageJsonPath = path.join(root, "package.json");
|
|
182
179
|
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
183
180
|
const scripts = pkg.scripts ?? {};
|
|
181
|
+
const workflowDir = path.join(root, ".github", "workflows");
|
|
182
|
+
if (existsSync(workflowDir)) {
|
|
183
|
+
const workflows = await readdir(workflowDir);
|
|
184
|
+
if (workflows.length) {
|
|
185
|
+
fail(`${appName ?? pkg.name ?? "app"} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (pkg.packageManager !== RIGHTKIT_VERSIONS.packageManager) {
|
|
189
|
+
fail(`${appName ?? pkg.name ?? "app"} packageManager must be ${RIGHTKIT_VERSIONS.packageManager}, got ${pkg.packageManager ?? "<missing>"}`);
|
|
190
|
+
}
|
|
184
191
|
if (JSON.stringify(scripts).includes("../tools/right-release") || JSON.stringify(scripts).includes("../tools/rightkit/packages/release")) {
|
|
185
192
|
fail(`${appName ?? pkg.name ?? "app"} package.json must call the right-release bin, not parent-workspace release source`);
|
|
186
193
|
}
|
package/release.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
|
|
9
9
|
const release = fileURLToPath(new URL("./release.mjs", import.meta.url));
|
|
10
|
+
const versions = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8"));
|
|
10
11
|
|
|
11
12
|
function fixture({ signed = true, publish = false, packageJson } = {}) {
|
|
12
13
|
const dir = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
|
|
@@ -17,8 +18,12 @@ function fixture({ signed = true, publish = false, packageJson } = {}) {
|
|
|
17
18
|
packageJson ?? {
|
|
18
19
|
name: "fixture",
|
|
19
20
|
scripts: { "release:patch:win": "right-release --platform win --tier patch" },
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
packageManager: versions.packageManager,
|
|
22
|
+
dependencies: {
|
|
23
|
+
"@rightkit/license": versions.npm["@rightkit/license"],
|
|
24
|
+
"@rightkit/logs": versions.npm["@rightkit/logs"],
|
|
25
|
+
},
|
|
26
|
+
devDependencies: { "@rightkit/release": versions.npm["@rightkit/release"] },
|
|
22
27
|
},
|
|
23
28
|
null,
|
|
24
29
|
2,
|
|
@@ -56,8 +61,12 @@ function lockFixture() {
|
|
|
56
61
|
{
|
|
57
62
|
name: "fixture-lock",
|
|
58
63
|
scripts: { "release:patch:mac": "right-release --platform mac --tier patch" },
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
packageManager: versions.packageManager,
|
|
65
|
+
dependencies: {
|
|
66
|
+
"@rightkit/license": versions.npm["@rightkit/license"],
|
|
67
|
+
"@rightkit/logs": versions.npm["@rightkit/logs"],
|
|
68
|
+
},
|
|
69
|
+
devDependencies: { "@rightkit/release": versions.npm["@rightkit/release"] },
|
|
61
70
|
},
|
|
62
71
|
null,
|
|
63
72
|
2,
|
|
@@ -134,6 +143,7 @@ test("rejects Git, path, or link RightKit app dependencies before release work s
|
|
|
134
143
|
fixture({
|
|
135
144
|
packageJson: {
|
|
136
145
|
name: "fixture",
|
|
146
|
+
packageManager: versions.packageManager,
|
|
137
147
|
scripts: { "release:patch:win": "node ../tools/right-release/release.mjs --tier patch" },
|
|
138
148
|
dependencies: {
|
|
139
149
|
"@rightkit/license": "git+https://github.com/adrdsouza/rightkit.git#main",
|
|
@@ -150,6 +160,17 @@ test("rejects Git, path, or link RightKit app dependencies before release work s
|
|
|
150
160
|
assert.match(result.stderr, /right-release bin|published npm package/i);
|
|
151
161
|
});
|
|
152
162
|
|
|
163
|
+
test("rejects hosted workflow files before release work starts", () => {
|
|
164
|
+
const config = fixture();
|
|
165
|
+
const workflowDir = path.join(path.dirname(config), ".github", "workflows");
|
|
166
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
167
|
+
writeFileSync(path.join(workflowDir, "ci.yml"), "name: forbidden\n");
|
|
168
|
+
|
|
169
|
+
const result = run(config, "--tier=patch");
|
|
170
|
+
assert.notEqual(result.status, 0);
|
|
171
|
+
assert.match(result.stderr, /hosted workflow.*forbidden/i);
|
|
172
|
+
});
|
|
173
|
+
|
|
153
174
|
test("publish runs the signed package step before the updater publication step", () => {
|
|
154
175
|
const result = run(fixture({ publish: true }), "--tier=update", "--upload");
|
|
155
176
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
7
|
const workspace = path.resolve(new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
|
|
8
|
+
const versionsPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1")), "rightkit-versions.json");
|
|
9
|
+
const versions = JSON.parse(readFileSync(versionsPath, "utf8"));
|
|
8
10
|
const pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK";
|
|
9
|
-
const licensePackage = "^0.1.5";
|
|
10
|
-
const logsPackage = "^0.1.3";
|
|
11
|
-
const releasePackage = "^0.2.11";
|
|
12
11
|
const apps = [
|
|
13
12
|
{ key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac-notarized.sh"] },
|
|
14
13
|
{ key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["package.sh"] },
|
|
15
14
|
{ key: "heardright", root: "heardright/tauri-app-next", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs", "scripts/publish-release.mjs"] },
|
|
16
|
-
{ key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac
|
|
15
|
+
{ key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac.sh"] },
|
|
17
16
|
{ key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
18
17
|
];
|
|
19
18
|
|
|
19
|
+
test("RightKit exposes one current version manifest", () => {
|
|
20
|
+
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
21
|
+
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
22
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.13");
|
|
23
|
+
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
24
|
+
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
25
|
+
assert.equal(versions.npm["@rightkit/updates"], "0.1.1");
|
|
26
|
+
assert.equal(versions.cargo["rightkit-license"], "0.1.1");
|
|
27
|
+
assert.equal(versions.cargo["rightkit-logs"], "0.1.0");
|
|
28
|
+
for (const [crate, version] of Object.entries(versions.cargo)) {
|
|
29
|
+
const manifest = readFileSync(path.join(workspace, `tools/rightkit/crates/${crate}/Cargo.toml`), "utf8");
|
|
30
|
+
assert.match(manifest, new RegExp(`^version\\s*=\\s*"${version.replaceAll(".", "\\.")}"$`, "m"));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("license v2 public vector is identical at every portable consumer boundary", () => {
|
|
35
|
+
const canonical = readFileSync(
|
|
36
|
+
path.join(workspace, "tools/rightkit/crates/rightkit-license/test-vectors/license-v2.json"),
|
|
37
|
+
"utf8",
|
|
38
|
+
);
|
|
39
|
+
for (const relativePath of [
|
|
40
|
+
"tools/rightkit/packages/license/test-vectors/license-v2.json",
|
|
41
|
+
"rightapps/packages/api/src/licensing/test-vectors/license-v2.json",
|
|
42
|
+
"scraperight/tests/fixtures/license-v2.json",
|
|
43
|
+
]) {
|
|
44
|
+
assert.equal(
|
|
45
|
+
readFileSync(path.join(workspace, relativePath), "utf8"),
|
|
46
|
+
canonical,
|
|
47
|
+
`${relativePath} must be the canonical public license vector byte-for-byte`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
20
52
|
for (const app of apps) {
|
|
21
53
|
test(`${app.key} follows the signed tiered Right Release contract`, async () => {
|
|
22
54
|
const root = path.join(workspace, app.root);
|
|
@@ -24,16 +56,16 @@ for (const app of apps) {
|
|
|
24
56
|
const rightKitDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
25
57
|
for (const [name, specifier] of Object.entries(rightKitDeps).filter(([name]) => name.startsWith("@rightkit/"))) {
|
|
26
58
|
assert.doesNotMatch(specifier, /^(?:git|file|link|workspace):|github/i, `${app.key} ${name} must come from the published npm package`);
|
|
59
|
+
if (versions.npm[name]) assert.equal(specifier, versions.npm[name], `${app.key} ${name} must match rightkit-versions.json exactly`);
|
|
27
60
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
assert.equal(pkg.devDependencies?.["@rightkit/release"], releasePackage);
|
|
61
|
+
assert.equal(pkg.packageManager, versions.packageManager);
|
|
62
|
+
assert.equal(pkg.devDependencies?.["@rightkit/release"], versions.npm["@rightkit/release"]);
|
|
31
63
|
assert(!JSON.stringify(pkg).includes("github:adrdsouza/claude#main&path:/tools/right-release"));
|
|
32
64
|
assert(!JSON.stringify(pkg).includes("github:adrdsouza/claude#main&path:/tools/rightkit/packages/release"));
|
|
33
65
|
assert(!JSON.stringify(pkg).includes("git+https://github.com/adrdsouza/rightkit.git"));
|
|
34
66
|
assert.equal(pkg.scripts["release:doctor"], "right-release doctor");
|
|
35
|
-
assert.equal(pkg.scripts["release:mac"], "
|
|
36
|
-
assert.equal(pkg.scripts["release:win"], "
|
|
67
|
+
assert.equal(pkg.scripts["release:mac"], undefined, "tierless release entry points are forbidden");
|
|
68
|
+
assert.equal(pkg.scripts["release:win"], undefined, "tierless release entry points are forbidden");
|
|
37
69
|
assert.equal(pkg.scripts["release:patch:mac"], "right-release --platform mac --tier patch");
|
|
38
70
|
assert.equal(pkg.scripts["release:patch:win"], "right-release --platform win --tier patch");
|
|
39
71
|
assert.equal(pkg.scripts["release:update:mac"], "right-release --platform mac --tier update");
|
|
@@ -44,7 +76,7 @@ for (const app of apps) {
|
|
|
44
76
|
assert.equal(pkg.scripts["publish:update:win"], "right-release publish --platform win --tier update");
|
|
45
77
|
assert.equal(pkg.scripts["deps:check"], "right-release deps --check");
|
|
46
78
|
assert.equal(pkg.scripts["deps:update"], "right-release deps --update");
|
|
47
|
-
assert.ok(!Object.entries(pkg.scripts).some(([name, command]) =>
|
|
79
|
+
assert.ok(!Object.entries(pkg.scripts).some(([name, command]) => /^(?:release|publish):/i.test(name) && /unsigned|--no-sign/i.test(command)), `${app.key} release/publish commands must not expose an unsigned macOS DMG mode`);
|
|
48
80
|
assert.ok(!JSON.stringify(pkg.scripts).includes("../tools/right-release"), `${app.key} scripts must not depend on the parent Claude workspace`);
|
|
49
81
|
assert.ok(!JSON.stringify(pkg.scripts).includes("../tools/rightkit/packages/release"), `${app.key} scripts must not depend on the parent Claude workspace`);
|
|
50
82
|
|
|
@@ -66,7 +98,8 @@ for (const app of apps) {
|
|
|
66
98
|
assert.match(updater.key, /\/updates\/(mac|windows)\/current\//, `${app.key} ${platform} updaters must replace the stable current R2 object`);
|
|
67
99
|
}
|
|
68
100
|
}
|
|
69
|
-
assert.
|
|
101
|
+
assert.equal(config.targets.mac.package.cmd, "pnpm", `${app.key} must use the shared pnpm package entry point`);
|
|
102
|
+
assert.deepEqual(config.targets.mac.package.args, ["run", "mac:dmg:notarized"], `${app.key} R2 release must use the signed and notarized macOS package entry point`);
|
|
70
103
|
for (const installer of config.targets.mac.installer.artifacts) {
|
|
71
104
|
assert.equal(path.dirname(installer.file), ".", `${app.key} macOS installer must be copied to the app package root before upload`);
|
|
72
105
|
}
|
|
@@ -85,12 +118,17 @@ for (const app of apps) {
|
|
|
85
118
|
assert.doesNotMatch(source, /(?:\.\.\/)+tools\/right-release|tools\/right-release\//, `${app.key} ${releaseFile} must consume the installed package`);
|
|
86
119
|
assert.match(source, /right-release["']?,?\s*["']mirror-root-artifact|right-release mirror-root-artifact/, `${app.key} ${releaseFile} must mirror the final DMG to the canonical package root`);
|
|
87
120
|
}
|
|
88
|
-
for (const forbidden of ["scripts/build-mac-unsigned.sh", "package-unsigned.sh"]) {
|
|
89
|
-
assert.equal(existsSync(path.join(root, forbidden)), false, `${app.key} must not ship ${forbidden}`);
|
|
90
|
-
}
|
|
91
121
|
});
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
test("Right Suite has no hosted workflow files", () => {
|
|
125
|
+
for (const root of ["viewright", "scraperight", "heardright", "mailright", "coderight", "genright", "voiceright", "tools/rightkit"]) {
|
|
126
|
+
const workflowDir = path.join(workspace, root, ".github", "workflows");
|
|
127
|
+
const workflows = existsSync(workflowDir) ? readdirSync(workflowDir) : [];
|
|
128
|
+
assert.deepEqual(workflows, [], `${root} must not contain hosted workflow files`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
94
132
|
test("RightApps brand hosts expose app-keyed update manifest proxies", () => {
|
|
95
133
|
for (const app of apps) {
|
|
96
134
|
const siteRoot = path.join(workspace, "rightapps", app.key);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"packageManager": "pnpm@11.12.0",
|
|
4
|
+
"npm": {
|
|
5
|
+
"@rightkit/license": "0.1.5",
|
|
6
|
+
"@rightkit/logs": "0.1.3",
|
|
7
|
+
"@rightkit/release": "0.2.13",
|
|
8
|
+
"@rightkit/updates": "0.1.1"
|
|
9
|
+
},
|
|
10
|
+
"cargo": {
|
|
11
|
+
"rightkit-license": "0.1.1",
|
|
12
|
+
"rightkit-logs": "0.1.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const cli = path.join(packageRoot, "cli", "right-release.mjs");
|
|
9
|
+
|
|
10
|
+
test("suite-doctor runs the complete local multi-repo contract", () => {
|
|
11
|
+
const result = spawnSync(process.execPath, [cli, "suite-doctor"], {
|
|
12
|
+
cwd: packageRoot,
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
18
|
+
assert.match(result.stdout, /\[right-release\] suite-doctor passed/);
|
|
19
|
+
});
|
package/upload-large.mjs
CHANGED
|
File without changes
|