@rightkit/release 0.2.12 → 0.2.14
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/package.json +1 -1
- package/publish-cargo.mjs +144 -0
- package/publish-cargo.test.mjs +43 -0
- package/publish-update.mjs +52 -16
- package/publish-update.test.mjs +34 -2
- package/right-suite-contract.test.mjs +25 -1
- package/rightkit-versions.json +5 -1
- package/suite-doctor.test.mjs +19 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
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": {
|
|
@@ -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,8 +46,9 @@ 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
|
-
const uploadedSignatures = new Set();
|
|
50
52
|
for (const artifact of artifacts) {
|
|
51
53
|
assertCurrentKey(artifact.key, "updater");
|
|
52
54
|
const installerKey = patchInstallerKey(artifact);
|
|
@@ -62,11 +64,6 @@ if (tier === "patch") {
|
|
|
62
64
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, installerKey, "public"], root);
|
|
63
65
|
uploadedKeys.add(installerKey);
|
|
64
66
|
}
|
|
65
|
-
if (!uploadedSignatures.has(`${installerKey}.sig`)) {
|
|
66
|
-
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}.sig`);
|
|
67
|
-
if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${installerKey}.sig`, "public"], root);
|
|
68
|
-
uploadedSignatures.add(`${installerKey}.sig`);
|
|
69
|
-
}
|
|
70
67
|
const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
|
|
71
68
|
const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
|
|
72
69
|
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
@@ -75,6 +72,12 @@ if (tier === "patch") {
|
|
|
75
72
|
url: `${PUBLIC_BASE}/${installerKey}`,
|
|
76
73
|
};
|
|
77
74
|
registrations.set(installerKey, { artifactKey, path: installerKey, ...metadata });
|
|
75
|
+
updaterManifestArtifacts.set(installerKey, {
|
|
76
|
+
kind: "updater",
|
|
77
|
+
r2Key: installerKey,
|
|
78
|
+
...metadata,
|
|
79
|
+
updaterSignature: signature,
|
|
80
|
+
});
|
|
78
81
|
}
|
|
79
82
|
for (const installer of installers) {
|
|
80
83
|
const file = path.resolve(root, installer.file);
|
|
@@ -83,6 +86,13 @@ if (tier === "patch") {
|
|
|
83
86
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
|
|
84
87
|
uploadedKeys.add(installer.key);
|
|
85
88
|
}
|
|
89
|
+
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
90
|
+
installerManifestArtifacts.set(installer.key, {
|
|
91
|
+
kind: "installer",
|
|
92
|
+
r2Key: installer.key,
|
|
93
|
+
...metadata,
|
|
94
|
+
updaterSignature: null,
|
|
95
|
+
});
|
|
86
96
|
}
|
|
87
97
|
if (!artifacts.length) {
|
|
88
98
|
console.log(`${dryRun ? "[dry-run] " : ""}NO patch updater artifacts; skipping patch manifest registration`);
|
|
@@ -92,7 +102,7 @@ if (tier === "patch") {
|
|
|
92
102
|
dryRun,
|
|
93
103
|
keep: {
|
|
94
104
|
public: installers.map((artifact) => artifact.key),
|
|
95
|
-
private: target.updater.artifacts.
|
|
105
|
+
private: target.updater.artifacts.map((artifact) => artifact.key),
|
|
96
106
|
},
|
|
97
107
|
});
|
|
98
108
|
process.exit(0);
|
|
@@ -112,6 +122,13 @@ if (tier === "patch") {
|
|
|
112
122
|
platforms,
|
|
113
123
|
},
|
|
114
124
|
artifacts: [...registrations.values()],
|
|
125
|
+
artifactManifest: buildArtifactManifest({
|
|
126
|
+
appKey: config.app,
|
|
127
|
+
appVersion: version,
|
|
128
|
+
tier,
|
|
129
|
+
platform,
|
|
130
|
+
artifacts: [...installerManifestArtifacts.values(), ...updaterManifestArtifacts.values()],
|
|
131
|
+
}),
|
|
115
132
|
};
|
|
116
133
|
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}/v1/admin/apps/patches`);
|
|
117
134
|
console.log(JSON.stringify(body));
|
|
@@ -121,7 +138,7 @@ if (tier === "patch") {
|
|
|
121
138
|
platform: platform === "mac" ? "mac" : "windows",
|
|
122
139
|
dryRun,
|
|
123
140
|
keep: {
|
|
124
|
-
public: [...installers.map((artifact) => artifact.key), ...registrations.keys()
|
|
141
|
+
public: [...installers.map((artifact) => artifact.key), ...registrations.keys()],
|
|
125
142
|
private: [],
|
|
126
143
|
},
|
|
127
144
|
});
|
|
@@ -132,8 +149,8 @@ if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no u
|
|
|
132
149
|
|
|
133
150
|
const platforms = {};
|
|
134
151
|
const registrations = new Map();
|
|
152
|
+
const updaterManifestArtifacts = new Map();
|
|
135
153
|
const uploadedKeys = new Set();
|
|
136
|
-
const uploadedSignatures = new Set();
|
|
137
154
|
for (const artifact of artifacts) {
|
|
138
155
|
assertCurrentKey(artifact.key, "updater");
|
|
139
156
|
const file = path.resolve(root, artifact.file);
|
|
@@ -147,12 +164,6 @@ for (const artifact of artifacts) {
|
|
|
147
164
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, "private"], root);
|
|
148
165
|
uploadedKeys.add(artifact.key);
|
|
149
166
|
}
|
|
150
|
-
if (!uploadedSignatures.has(`${artifact.key}.sig`)) {
|
|
151
|
-
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}.sig`);
|
|
152
|
-
if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${artifact.key}.sig`, "private"], root);
|
|
153
|
-
uploadedSignatures.add(`${artifact.key}.sig`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
167
|
const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
|
|
157
168
|
const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
|
|
158
169
|
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
@@ -161,6 +172,12 @@ for (const artifact of artifacts) {
|
|
|
161
172
|
url: `${DOWNLOAD_BASE}/${artifact.key}`,
|
|
162
173
|
};
|
|
163
174
|
registrations.set(artifact.key, { artifactKey, path: artifact.key, ...metadata });
|
|
175
|
+
updaterManifestArtifacts.set(artifact.key, {
|
|
176
|
+
kind: "updater",
|
|
177
|
+
r2Key: artifact.key,
|
|
178
|
+
...metadata,
|
|
179
|
+
updaterSignature: signature,
|
|
180
|
+
});
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
const version = config.version || target.updater.version;
|
|
@@ -178,6 +195,13 @@ const body = {
|
|
|
178
195
|
platforms,
|
|
179
196
|
},
|
|
180
197
|
artifacts: [...registrations.values()],
|
|
198
|
+
artifactManifest: buildArtifactManifest({
|
|
199
|
+
appKey: config.app,
|
|
200
|
+
appVersion: version,
|
|
201
|
+
tier,
|
|
202
|
+
platform,
|
|
203
|
+
artifacts: [...updaterManifestArtifacts.values()],
|
|
204
|
+
}),
|
|
181
205
|
};
|
|
182
206
|
const route = "/v1/admin/apps/releases";
|
|
183
207
|
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}${route}`);
|
|
@@ -190,7 +214,7 @@ await pruneReleaseObjects({
|
|
|
190
214
|
dryRun,
|
|
191
215
|
keep: {
|
|
192
216
|
public: installers.map((artifact) => artifact.key),
|
|
193
|
-
private: [...registrations.keys()
|
|
217
|
+
private: [...registrations.keys()],
|
|
194
218
|
},
|
|
195
219
|
});
|
|
196
220
|
|
|
@@ -202,6 +226,18 @@ async function fileMetadata(file) {
|
|
|
202
226
|
};
|
|
203
227
|
}
|
|
204
228
|
|
|
229
|
+
function buildArtifactManifest({ appKey, appVersion, tier, platform, artifacts }) {
|
|
230
|
+
return {
|
|
231
|
+
schema: 1,
|
|
232
|
+
pipelineVersion: PIPELINE_VERSION,
|
|
233
|
+
appKey,
|
|
234
|
+
appVersion,
|
|
235
|
+
tier,
|
|
236
|
+
platform: platform === "mac" ? "darwin" : "windows",
|
|
237
|
+
artifacts,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
205
241
|
function run(cmd, runArgs, cwd) {
|
|
206
242
|
return new Promise((resolve, reject) => {
|
|
207
243
|
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);
|
|
@@ -92,9 +100,27 @@ test("patch registers a free updater manifest backed by public installer-path ob
|
|
|
92
100
|
assert.match(result.stdout, /POST .*\/v1\/admin\/apps\/patches/);
|
|
93
101
|
assert.match(result.stdout, /"tier":"patch"/);
|
|
94
102
|
assert.match(result.stdout, /"platform":"windows"/);
|
|
95
|
-
assert.
|
|
103
|
+
assert.doesNotMatch(result.stdout, /UPLOAD .*\.sig/);
|
|
104
|
+
assert.equal((result.stdout.match(/UPLOAD /g) ?? []).length, 2, "patch may upload only one installer and one updater object per OS");
|
|
96
105
|
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
97
106
|
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows keep=<none>/);
|
|
107
|
+
const body = registrationBody(result);
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
{
|
|
110
|
+
schema: body.artifactManifest.schema,
|
|
111
|
+
appKey: body.artifactManifest.appKey,
|
|
112
|
+
appVersion: body.artifactManifest.appVersion,
|
|
113
|
+
tier: body.artifactManifest.tier,
|
|
114
|
+
platform: body.artifactManifest.platform,
|
|
115
|
+
},
|
|
116
|
+
{ schema: 1, appKey: 'fixture', appVersion: '1.2.3', tier: 'patch', platform: 'windows' },
|
|
117
|
+
);
|
|
118
|
+
assert.match(body.artifactManifest.pipelineVersion, /^\d+\.\d+\.\d+$/);
|
|
119
|
+
assert.deepEqual(body.artifactManifest.artifacts.map((artifact) => [artifact.kind, artifact.r2Key]), [
|
|
120
|
+
['installer', 'fixture/installers/windows/current/Fixture-Setup.exe'],
|
|
121
|
+
['updater', 'fixture/installers/windows/current/Fixture.exe'],
|
|
122
|
+
]);
|
|
123
|
+
assert.match(body.artifactManifest.artifacts[1].updaterSignature, /^<signature:/);
|
|
98
124
|
});
|
|
99
125
|
|
|
100
126
|
test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
@@ -104,10 +130,16 @@ test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
|
104
130
|
assert.match(result.stdout, /"tier":"update"/);
|
|
105
131
|
assert.match(result.stdout, /"platform":"windows"/);
|
|
106
132
|
assert.match(result.stdout, /UPLOAD private\/fixture\/updates\/windows\/current\/Fixture\.exe/);
|
|
107
|
-
assert.
|
|
133
|
+
assert.doesNotMatch(result.stdout, /UPLOAD .*\.sig/);
|
|
134
|
+
assert.equal((result.stdout.match(/UPLOAD /g) ?? []).length, 1, "feature update may upload only one updater object per OS");
|
|
108
135
|
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
109
136
|
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows/);
|
|
110
137
|
assert.doesNotMatch(result.stdout, /UPLOAD .*Fixture-Setup\.exe/);
|
|
138
|
+
const body = registrationBody(result);
|
|
139
|
+
assert.equal(body.artifactManifest.tier, 'update');
|
|
140
|
+
assert.deepEqual(body.artifactManifest.artifacts.map((artifact) => [artifact.kind, artifact.r2Key]), [
|
|
141
|
+
['updater', 'fixture/updates/windows/current/Fixture.exe'],
|
|
142
|
+
]);
|
|
111
143
|
});
|
|
112
144
|
|
|
113
145
|
test("allows an installer-only patch without registering an updater manifest", () => {
|
|
@@ -19,10 +19,34 @@ const apps = [
|
|
|
19
19
|
test("RightKit exposes one current version manifest", () => {
|
|
20
20
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
21
21
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
22
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
22
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.14");
|
|
23
23
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
24
24
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
25
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
|
+
}
|
|
26
50
|
});
|
|
27
51
|
|
|
28
52
|
for (const app of apps) {
|
package/rightkit-versions.json
CHANGED
|
@@ -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
|
+
});
|