@rightkit/release 0.2.69 → 0.2.71
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/build-release.mjs +19 -14
- package/cache-command.mjs +7 -4
- package/cli/right-release.mjs +0 -0
- package/native-cargo-layout.mjs +239 -0
- package/native-release-finalization.mjs +259 -0
- package/package.json +11 -11
- package/preflight.mjs +6 -3
- package/registry-parity.mjs +2 -2
- package/release-invocation.mjs +10 -2
- package/release-state.mjs +5 -2
- package/release.mjs +220 -12
- package/rightkit-versions.json +4 -2
- package/addon-command.test.mjs +0 -54
- package/addon-contract.test.mjs +0 -48
- package/asr-artifact-adoption.test.mjs +0 -122
- package/build-invocation-contract.test.mjs +0 -124
- package/build-release.test.mjs +0 -242
- package/cache-command.test.mjs +0 -118
- package/cache-policy.test.mjs +0 -346
- package/cargo-guard.test.mjs +0 -195
- package/cargo-target.test.mjs +0 -82
- package/create-mac-updater.test.mjs +0 -14
- package/github-release.test.mjs +0 -103
- package/heavy-command.test.mjs +0 -221
- package/legal-contract.test.mjs +0 -151
- package/mirror-root-artifact.test.mjs +0 -58
- package/model-promote.test.mjs +0 -284
- package/notary-auth.test.mjs +0 -31
- package/nsis-payload.test.mjs +0 -139
- package/nsis-upgrade-contract.test.mjs +0 -57
- package/pipeline-normalization-contract.test.mjs +0 -140
- package/preflight.test.mjs +0 -123
- package/progress-control.test.mjs +0 -56
- package/prune-r2.test.mjs +0 -12
- package/publish-cargo.test.mjs +0 -43
- package/publish-swift.test.mjs +0 -44
- package/publish-update.test.mjs +0 -207
- package/qa-contract.test.mjs +0 -47
- package/registry-parity.test.mjs +0 -30
- package/release-cli-contract.test.mjs +0 -119
- package/release-invocation.test.mjs +0 -22
- package/release-state.test.mjs +0 -395
- package/release-token.test.mjs +0 -21
- package/release.test.mjs +0 -533
- package/right-suite-contract.test.mjs +0 -1011
- package/rightapps-register.test.mjs +0 -28
- package/runtime-artifact-manifest.test.mjs +0 -128
- package/sign-updater.test.mjs +0 -12
- package/source-gate.test.mjs +0 -25
- package/standalone-clone-evidence.json +0 -32
- package/standalone-clone-verify.test.mjs +0 -76
- package/suite-doctor.test.mjs +0 -19
- package/target-bridge.test.mjs +0 -269
- package/tauri-bundle-marker.test.mjs +0 -81
- package/upload-large.test.mjs +0 -70
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, readFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import vm from "node:vm";
|
|
8
|
-
import test from "node:test";
|
|
9
|
-
|
|
10
|
-
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const cli = path.join(root, "cli", "right-release.mjs");
|
|
12
|
-
const build = path.join(root, "build-release.mjs");
|
|
13
|
-
const upload = path.join(root, "upload-release.mjs");
|
|
14
|
-
const signWindows = path.join(root, "sign-windows.mjs");
|
|
15
|
-
|
|
16
|
-
test("CLI routes build and release only to the build state machine", () => {
|
|
17
|
-
const source = readFileSync(cli, "utf8");
|
|
18
|
-
assert.match(source, /\["build",\s*"build-release\.mjs"\]/);
|
|
19
|
-
assert.match(source, /\["release",\s*"build-release\.mjs"\]/);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("CLI routes upload and legacy publish only to the upload state machine", () => {
|
|
23
|
-
const source = readFileSync(cli, "utf8");
|
|
24
|
-
assert.match(source, /\["upload",\s*"upload-release\.mjs"\]/);
|
|
25
|
-
assert.match(source, /\["publish",\s*"upload-release\.mjs"\]/);
|
|
26
|
-
assert.doesNotMatch(source, /first === "publish"[\s\S]{0,500}run\("release\.mjs"/);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("upload dry-run cannot write a verified marker", () => {
|
|
30
|
-
const source = readFileSync(upload, "utf8");
|
|
31
|
-
assert.match(source, /if \(!dryRun\)\s+writeJson\(verifiedMarker,/);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("build is tier-neutral and rejects a tier before touching a repository", () => {
|
|
35
|
-
const result = spawnSync(process.execPath, [build, "--platform", "win", "--tier", "patch"], {
|
|
36
|
-
cwd: mkdtempSync(path.join(os.tmpdir(), "right-build-cli-")),
|
|
37
|
-
encoding: "utf8",
|
|
38
|
-
});
|
|
39
|
-
assert.notEqual(result.status, 0);
|
|
40
|
-
assert.match(result.stderr, /build is tier-neutral/i);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("build enforces primary-checkout and build-input cleanliness guards", () => {
|
|
44
|
-
const source = readFileSync(build, "utf8");
|
|
45
|
-
assert.match(source, /assertPrimaryReleaseCheckout\(repoRoot\)/);
|
|
46
|
-
assert.match(source, /dirtyBuildInputs\(repoRoot,/);
|
|
47
|
-
assert.match(source, /dirty files can change the packaged app/i);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("upload enforces the same primary-checkout vault", () => {
|
|
51
|
-
const source = readFileSync(upload, "utf8");
|
|
52
|
-
assert.match(source, /assertPrimaryReleaseCheckout\(repoRoot\)/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("upload requires an explicit patch or update tier before reading a release", () => {
|
|
56
|
-
const result = spawnSync(process.execPath, [upload, "--platform", "win", "--release", "fixture-1.0.0-deadbeef"], {
|
|
57
|
-
cwd: mkdtempSync(path.join(os.tmpdir(), "right-upload-cli-")),
|
|
58
|
-
encoding: "utf8",
|
|
59
|
-
});
|
|
60
|
-
assert.notEqual(result.status, 0);
|
|
61
|
-
assert.match(result.stderr, /tier is required.*patch\|update/i);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("GitHub upload delegates platform trust verification to the shared release lane", () => {
|
|
65
|
-
const uploadSource = readFileSync(upload, "utf8");
|
|
66
|
-
const githubSource = readFileSync(path.join(root, "github-release.mjs"), "utf8");
|
|
67
|
-
const signingSource = readFileSync(signWindows, "utf8");
|
|
68
|
-
assert.match(uploadSource, /publishGitHubRelease/);
|
|
69
|
-
assert.match(githubSource, /sign-windows\.mjs/);
|
|
70
|
-
assert.match(githubSource, /--verify-only/);
|
|
71
|
-
assert.doesNotMatch(uploadSource, /CLOUDFLARE_API_TOKEN|wrangler|R2/i);
|
|
72
|
-
assert.match(signingSource, /verifyOnly/);
|
|
73
|
-
assert.match(signingSource, /\["verify", "\/pa", "\/v", file\]/);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("Windows signer verification accepts CRLF subjects and retains identity checks", () => {
|
|
77
|
-
const source = readFileSync(signWindows, "utf8");
|
|
78
|
-
const functionSource = source.slice(source.indexOf("function assertVerification"), source.indexOf("\nfunction fileEvidence"));
|
|
79
|
-
const assertVerification = vm.runInNewContext(`(${functionSource})`, {
|
|
80
|
-
fail(message) { throw new Error(message); },
|
|
81
|
-
});
|
|
82
|
-
const valid = "Successfully verified\r\n\tIssued to: Damned Ventures LLC \r\nTimestamp: present\r\n";
|
|
83
|
-
assert.equal(JSON.stringify(assertVerification("fixture.exe", valid)), JSON.stringify({
|
|
84
|
-
authenticode: "Valid",
|
|
85
|
-
subject: "CN=Damned Ventures LLC",
|
|
86
|
-
timestampPresent: true,
|
|
87
|
-
}));
|
|
88
|
-
assert.throws(
|
|
89
|
-
() => assertVerification("fixture.exe", valid.replace("Damned Ventures LLC", "Other Company")),
|
|
90
|
-
/subject must be Damned Ventures LLC/,
|
|
91
|
-
);
|
|
92
|
-
assert.throws(
|
|
93
|
-
() => assertVerification("fixture.exe", valid.replace("Damned Ventures LLC", "damned ventures llc")),
|
|
94
|
-
/subject must be Damned Ventures LLC/,
|
|
95
|
-
);
|
|
96
|
-
assert.throws(
|
|
97
|
-
() => assertVerification("fixture.exe", valid.replace("Timestamp: present", "")),
|
|
98
|
-
/timestamp is missing/,
|
|
99
|
-
);
|
|
100
|
-
assert.throws(
|
|
101
|
-
() => assertVerification("fixture.exe", valid.replace("Successfully verified", "Verification failed")),
|
|
102
|
-
/did not report Valid/,
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("upload uses GitHub CLI and never invokes the R2 runner", () => {
|
|
107
|
-
const source = readFileSync(upload, "utf8");
|
|
108
|
-
const githubSource = readFileSync(path.join(root, "github-release.mjs"), "utf8");
|
|
109
|
-
assert.match(githubSource, /run\("gh", \["release", "upload"/);
|
|
110
|
-
assert.doesNotMatch(source, /wrangler|CLOUDFLARE_API_TOKEN|RIGHTAPPS_API_URL/i);
|
|
111
|
-
assert.doesNotMatch(source, /\bnpx\b/);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("upload accepts only GitHub Releases as configured product distribution", () => {
|
|
115
|
-
const source = readFileSync(upload, "utf8");
|
|
116
|
-
assert.match(source, /distribution\.provider/);
|
|
117
|
-
assert.match(source, /github-releases/);
|
|
118
|
-
assert.match(source, /releaseConfig\?\.distribution\?\.repository/);
|
|
119
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import test from "node:test";
|
|
4
|
-
import { resolvePrimaryWorktree } from "./release-invocation.mjs";
|
|
5
|
-
|
|
6
|
-
test("resolves a submodule primary worktree from core.worktree", () => {
|
|
7
|
-
assert.equal(resolvePrimaryWorktree({
|
|
8
|
-
repoRoot: path.resolve("/suite/membrane"),
|
|
9
|
-
worktrees: "worktree /suite/.git/modules/membrane\nHEAD abc\n",
|
|
10
|
-
gitDir: "/suite/.git/modules/membrane",
|
|
11
|
-
coreWorktree: "../../../membrane",
|
|
12
|
-
}), path.resolve("/suite/membrane"));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("preserves an ordinary primary worktree", () => {
|
|
16
|
-
assert.equal(resolvePrimaryWorktree({
|
|
17
|
-
repoRoot: "/suite/app",
|
|
18
|
-
worktrees: "worktree /suite/app\nHEAD abc\n",
|
|
19
|
-
gitDir: "/suite/app/.git",
|
|
20
|
-
coreWorktree: "",
|
|
21
|
-
}), "/suite/app");
|
|
22
|
-
});
|
package/release-state.test.mjs
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { writeFile } from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
cacheFingerprint,
|
|
11
|
-
commandOutputPortable,
|
|
12
|
-
watchProgress,
|
|
13
|
-
resolveReleaseLayout,
|
|
14
|
-
releaseEnvironment,
|
|
15
|
-
runBuildStateMachine,
|
|
16
|
-
runUploadStateMachine,
|
|
17
|
-
verifySealedRelease,
|
|
18
|
-
} from "./release-state.mjs";
|
|
19
|
-
|
|
20
|
-
const expectedPnpmVersion = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8")).packageManager.replace(/^pnpm@/, "");
|
|
21
|
-
|
|
22
|
-
test("progress watcher observes writes in nested Cargo target directories", async () => {
|
|
23
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-"));
|
|
24
|
-
const nested = path.join(root, "release", "build", "openssl");
|
|
25
|
-
mkdirSync(nested, { recursive: true });
|
|
26
|
-
const readyFile = path.join(root, "watch-ready");
|
|
27
|
-
const nestedFile = path.join(nested, "object.lib");
|
|
28
|
-
let resolveReady;
|
|
29
|
-
let resolveProgress;
|
|
30
|
-
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
31
|
-
const progress = new Promise((resolve) => { resolveProgress = resolve; });
|
|
32
|
-
const close = watchProgress([root], (_eventType, filename) => {
|
|
33
|
-
const eventName = String(filename ?? "");
|
|
34
|
-
if (eventName === path.basename(readyFile)) resolveReady();
|
|
35
|
-
if (eventName.endsWith(path.basename(nestedFile))) resolveProgress();
|
|
36
|
-
});
|
|
37
|
-
try {
|
|
38
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
39
|
-
await writeFile(readyFile, "ready");
|
|
40
|
-
await waitForEvent(ready, "watcher readiness event missing");
|
|
41
|
-
await writeFile(nestedFile, "progress");
|
|
42
|
-
await waitForEvent(progress, "nested progress event missing");
|
|
43
|
-
} finally {
|
|
44
|
-
close();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("fallback progress watchers cap open handles when recursive watching is unavailable", () => {
|
|
49
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-cap-"));
|
|
50
|
-
for (let index = 0; index < 10; index += 1) mkdirSync(path.join(root, `dir-${index}`));
|
|
51
|
-
const opened = [];
|
|
52
|
-
const close = watchProgress([root], () => {}, {
|
|
53
|
-
maxFallbackWatchers: 3,
|
|
54
|
-
watchFactory: (directory, options) => {
|
|
55
|
-
if (options?.recursive) throw new Error("recursive watch unsupported");
|
|
56
|
-
opened.push(directory);
|
|
57
|
-
return { close: () => {} };
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
try {
|
|
61
|
-
assert.equal(opened.length, 3);
|
|
62
|
-
} finally {
|
|
63
|
-
close();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
async function waitForEvent(event, message) {
|
|
68
|
-
let timer;
|
|
69
|
-
try {
|
|
70
|
-
await Promise.race([
|
|
71
|
-
event,
|
|
72
|
-
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), 5_000); }),
|
|
73
|
-
]);
|
|
74
|
-
} finally {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
|
|
80
|
-
assert.equal(commandOutputPortable("pnpm", ["--version"]), expectedPnpmVersion);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("nested app configs keep the vault at repo root and build from the app root", () => {
|
|
84
|
-
const layout = resolveReleaseLayout({
|
|
85
|
-
repoRoot: "D:/suite/heardright",
|
|
86
|
-
configPath: "D:/suite/heardright/tauri-app-next/right-release.config.mjs",
|
|
87
|
-
});
|
|
88
|
-
assert.equal(layout.vaultRoot, path.resolve("D:/suite/heardright/.right-release"));
|
|
89
|
-
assert.equal(layout.appRoot, path.resolve("D:/suite/heardright/tauri-app-next"));
|
|
90
|
-
assert.equal(layout.targetLink, path.resolve("D:/suite/heardright/tauri-app-next/src-tauri/target"));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("shared release environment is isolated from the app vault", () => {
|
|
94
|
-
const env = releaseEnvironment({
|
|
95
|
-
root: "/suite/.right-release",
|
|
96
|
-
cacheRoot: "/Users/test/Library/Caches/RightSuite/release",
|
|
97
|
-
platform: "mac",
|
|
98
|
-
architecture: "aarch64",
|
|
99
|
-
app: "fixture",
|
|
100
|
-
appRoot: "/suite/fixture",
|
|
101
|
-
cacheKey: "abcdef0123456789",
|
|
102
|
-
mode: "shared",
|
|
103
|
-
});
|
|
104
|
-
assert.match(env.CARGO_TARGET_DIR, /RightSuite[\\/]release[\\/]targets[\\/]mac[\\/]fixture[\\/]abcdef0123456789$/);
|
|
105
|
-
assert.match(env.CARGO_HOME, /RightSuite[\\/]release[\\/]cargo-home$/);
|
|
106
|
-
assert.equal(env.RIGHT_RELEASE_CACHE_OWNER, "rightkit-v2");
|
|
107
|
-
assert.equal(env.SCCACHE_CACHE_SIZE, "32G");
|
|
108
|
-
// A dropped sccache server connection must fall through to plain rustc
|
|
109
|
-
// instead of failing the build; seen on Windows as a bare error 10054.
|
|
110
|
-
assert.equal(env.SCCACHE_IGNORE_SERVER_IO_ERROR, "1");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("shared release environment honors the cache policy override", () => {
|
|
114
|
-
const env = releaseEnvironment({
|
|
115
|
-
root: "/suite/.right-release",
|
|
116
|
-
cacheRoot: "/cache",
|
|
117
|
-
platform: "mac",
|
|
118
|
-
architecture: "aarch64",
|
|
119
|
-
app: "fixture",
|
|
120
|
-
appRoot: "/suite/fixture",
|
|
121
|
-
cacheKey: "abcdef0123456789",
|
|
122
|
-
mode: "shared",
|
|
123
|
-
env: { RIGHT_RELEASE_SCCACHE_MAX_BYTES: String(48 * 1024 ** 3) },
|
|
124
|
-
});
|
|
125
|
-
assert.equal(env.SCCACHE_CACHE_SIZE, "48G");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
function sha256(value) {
|
|
129
|
-
return createHash("sha256").update(value).digest("hex");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function fixture() {
|
|
133
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-state-"));
|
|
134
|
-
const sealedDir = path.join(root, ".right-release", "sealed", "fixture-1.2.3-abcdef12", "windows");
|
|
135
|
-
mkdirSync(sealedDir, { recursive: true });
|
|
136
|
-
const installer = path.join(sealedDir, "Fixture_x64-setup.exe");
|
|
137
|
-
const signature = `${installer}.sig`;
|
|
138
|
-
writeFileSync(installer, "signed-installer");
|
|
139
|
-
writeFileSync(signature, "updater-signature");
|
|
140
|
-
const manifest = {
|
|
141
|
-
schema: 1,
|
|
142
|
-
releaseId: "fixture-1.2.3-abcdef12",
|
|
143
|
-
app: "fixture",
|
|
144
|
-
version: "1.2.3",
|
|
145
|
-
commit: "abcdef1234567890",
|
|
146
|
-
platform: "win",
|
|
147
|
-
files: [
|
|
148
|
-
{ role: "installer", name: path.basename(installer), sha256: sha256("signed-installer"), sizeBytes: 16 },
|
|
149
|
-
{ role: "updater-signature", name: path.basename(signature), sha256: sha256("updater-signature"), sizeBytes: 17 },
|
|
150
|
-
],
|
|
151
|
-
routes: {
|
|
152
|
-
patch: [{ role: "installer", bucket: "public", key: "fixture/installers/windows/current/Fixture_x64-setup.exe" }],
|
|
153
|
-
update: [{ role: "installer", bucket: "private", key: "fixture/updates/windows/current/Fixture_x64-setup.exe" }],
|
|
154
|
-
},
|
|
155
|
-
checkpoints: ["preflight_complete", "build_complete", "signed", "hardened", "sealed"],
|
|
156
|
-
};
|
|
157
|
-
writeFileSync(path.join(sealedDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
158
|
-
return { root, sealedDir, installer, signature, manifest };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
test("upload has no build operation and restores the previous stable object when registration fails", async () => {
|
|
162
|
-
const fx = fixture();
|
|
163
|
-
const calls = [];
|
|
164
|
-
await assert.rejects(
|
|
165
|
-
runUploadStateMachine({
|
|
166
|
-
root: fx.root,
|
|
167
|
-
releaseId: fx.manifest.releaseId,
|
|
168
|
-
platform: "win",
|
|
169
|
-
tier: "patch",
|
|
170
|
-
ops: {
|
|
171
|
-
verifyAuthenticode: async () => calls.push("verify-authenticode"),
|
|
172
|
-
harden: async () => calls.push("harden"),
|
|
173
|
-
backup: async () => calls.push("backup"),
|
|
174
|
-
upload: async () => calls.push("upload"),
|
|
175
|
-
register: async () => { calls.push("register"); throw new Error("R2/API unavailable"); },
|
|
176
|
-
verifyRemote: async () => calls.push("verify-remote"),
|
|
177
|
-
restore: async () => calls.push("restore"),
|
|
178
|
-
discardBackup: async () => calls.push("discard-backup"),
|
|
179
|
-
},
|
|
180
|
-
}),
|
|
181
|
-
/R2\/API unavailable/,
|
|
182
|
-
);
|
|
183
|
-
assert.deepEqual(calls, ["verify-authenticode", "harden", "backup", "upload", "register", "restore"]);
|
|
184
|
-
});
|
|
185
|
-
test("missing runtime input fails before build preparation and cannot touch an existing sealed installer", async () => {
|
|
186
|
-
const fx = fixture();
|
|
187
|
-
let prepared = false;
|
|
188
|
-
await assert.rejects(
|
|
189
|
-
runBuildStateMachine({
|
|
190
|
-
root: fx.root,
|
|
191
|
-
app: "fixture",
|
|
192
|
-
version: "1.2.4",
|
|
193
|
-
commit: "1234567890abcdef",
|
|
194
|
-
platform: "win",
|
|
195
|
-
requiredInputs: [path.join(fx.root, "missing-directml.dll")],
|
|
196
|
-
ops: {
|
|
197
|
-
preflight: async ({ requiredInputs }) => {
|
|
198
|
-
for (const file of requiredInputs) readFileSync(file);
|
|
199
|
-
},
|
|
200
|
-
prepare: async () => { prepared = true; },
|
|
201
|
-
build: async () => {},
|
|
202
|
-
sign: async () => {},
|
|
203
|
-
harden: async () => {},
|
|
204
|
-
seal: async () => {},
|
|
205
|
-
},
|
|
206
|
-
}),
|
|
207
|
-
/ENOENT/,
|
|
208
|
-
);
|
|
209
|
-
assert.equal(prepared, false);
|
|
210
|
-
assert.equal(readFileSync(fx.installer, "utf8"), "signed-installer");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("a version already sealed from another commit cannot be rebuilt", async () => {
|
|
214
|
-
const fx = fixture();
|
|
215
|
-
let prepared = false;
|
|
216
|
-
await assert.rejects(
|
|
217
|
-
runBuildStateMachine({
|
|
218
|
-
root: fx.root,
|
|
219
|
-
app: "fixture",
|
|
220
|
-
version: fx.manifest.version,
|
|
221
|
-
commit: "1234567890abcdef",
|
|
222
|
-
platform: "win",
|
|
223
|
-
requiredInputs: [],
|
|
224
|
-
ops: {
|
|
225
|
-
preflight: async () => {},
|
|
226
|
-
prepare: async () => { prepared = true; },
|
|
227
|
-
build: async () => {},
|
|
228
|
-
sign: async () => {},
|
|
229
|
-
harden: async () => {},
|
|
230
|
-
seal: async () => {},
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
233
|
-
/version 1\.2\.3 is already sealed from commit abcdef1234567890/,
|
|
234
|
-
);
|
|
235
|
-
assert.equal(prepared, false);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("an interrupted build leaves every previously sealed release untouched", async () => {
|
|
239
|
-
const fx = fixture();
|
|
240
|
-
await assert.rejects(
|
|
241
|
-
runBuildStateMachine({
|
|
242
|
-
root: fx.root,
|
|
243
|
-
app: "fixture",
|
|
244
|
-
version: "1.2.4",
|
|
245
|
-
commit: "1234567890abcdef",
|
|
246
|
-
platform: "win",
|
|
247
|
-
requiredInputs: [],
|
|
248
|
-
ops: {
|
|
249
|
-
preflight: async () => {},
|
|
250
|
-
prepare: async () => {},
|
|
251
|
-
build: async () => { throw new Error("SIGINT"); },
|
|
252
|
-
sign: async () => {},
|
|
253
|
-
harden: async () => {},
|
|
254
|
-
seal: async () => {},
|
|
255
|
-
},
|
|
256
|
-
}),
|
|
257
|
-
/SIGINT/,
|
|
258
|
-
);
|
|
259
|
-
assert.equal(readFileSync(fx.installer, "utf8"), "signed-installer");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("a valid sealed release resumes without rebuilding", async () => {
|
|
263
|
-
const fx = fixture();
|
|
264
|
-
let builds = 0;
|
|
265
|
-
const result = await runBuildStateMachine({
|
|
266
|
-
root: fx.root,
|
|
267
|
-
app: "fixture",
|
|
268
|
-
version: fx.manifest.version,
|
|
269
|
-
commit: fx.manifest.commit,
|
|
270
|
-
platform: "win",
|
|
271
|
-
requiredInputs: [],
|
|
272
|
-
ops: {
|
|
273
|
-
preflight: async () => {},
|
|
274
|
-
prepare: async () => {},
|
|
275
|
-
build: async () => { builds += 1; },
|
|
276
|
-
sign: async () => {},
|
|
277
|
-
harden: async () => {},
|
|
278
|
-
seal: async () => {},
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
assert.equal(result.status, "sealed");
|
|
282
|
-
assert.equal(result.resumed, true);
|
|
283
|
-
assert.equal(builds, 0);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("sealed build resume rejects changed input or cache identity", async () => {
|
|
287
|
-
const fx = fixture();
|
|
288
|
-
fx.manifest.inputs = { "src-tauri/Cargo.lock": "old-hash" };
|
|
289
|
-
fx.manifest.cacheKey = "old-cache";
|
|
290
|
-
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
291
|
-
|
|
292
|
-
await assert.rejects(
|
|
293
|
-
runBuildStateMachine({
|
|
294
|
-
root: fx.root,
|
|
295
|
-
app: fx.manifest.app,
|
|
296
|
-
version: fx.manifest.version,
|
|
297
|
-
commit: fx.manifest.commit,
|
|
298
|
-
platform: fx.manifest.platform,
|
|
299
|
-
inputHashes: { "src-tauri/Cargo.lock": "new-hash" },
|
|
300
|
-
cacheKey: fx.manifest.cacheKey,
|
|
301
|
-
ops: {},
|
|
302
|
-
}),
|
|
303
|
-
/sealed release input identity mismatch/i,
|
|
304
|
-
);
|
|
305
|
-
await assert.rejects(
|
|
306
|
-
runBuildStateMachine({
|
|
307
|
-
root: fx.root,
|
|
308
|
-
app: fx.manifest.app,
|
|
309
|
-
version: fx.manifest.version,
|
|
310
|
-
commit: fx.manifest.commit,
|
|
311
|
-
platform: fx.manifest.platform,
|
|
312
|
-
inputHashes: fx.manifest.inputs,
|
|
313
|
-
cacheKey: "new-cache",
|
|
314
|
-
ops: {},
|
|
315
|
-
}),
|
|
316
|
-
/sealed release cache identity mismatch/i,
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("manifest or file tampering blocks upload before any mutation", async () => {
|
|
321
|
-
const fx = fixture();
|
|
322
|
-
writeFileSync(fx.installer, "changed");
|
|
323
|
-
assert.throws(() => verifySealedRelease(fx.sealedDir), /hash mismatch/);
|
|
324
|
-
let uploaded = false;
|
|
325
|
-
await assert.rejects(
|
|
326
|
-
runUploadStateMachine({
|
|
327
|
-
root: fx.root,
|
|
328
|
-
releaseId: fx.manifest.releaseId,
|
|
329
|
-
platform: "win",
|
|
330
|
-
tier: "patch",
|
|
331
|
-
ops: {
|
|
332
|
-
upload: async () => { uploaded = true; },
|
|
333
|
-
},
|
|
334
|
-
}),
|
|
335
|
-
/hash mismatch/,
|
|
336
|
-
);
|
|
337
|
-
assert.equal(uploaded, false);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("tier or non-current route mismatch fails before R2 backup", async () => {
|
|
341
|
-
const fx = fixture();
|
|
342
|
-
fx.manifest.routes.patch[0].bucket = "private";
|
|
343
|
-
fx.manifest.routes.patch[0].key = "fixture/updates/windows/1.2.3/Fixture.exe";
|
|
344
|
-
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
345
|
-
let backedUp = false;
|
|
346
|
-
await assert.rejects(
|
|
347
|
-
runUploadStateMachine({
|
|
348
|
-
root: fx.root,
|
|
349
|
-
releaseId: fx.manifest.releaseId,
|
|
350
|
-
platform: "win",
|
|
351
|
-
tier: "patch",
|
|
352
|
-
ops: { backup: async () => { backedUp = true; } },
|
|
353
|
-
}),
|
|
354
|
-
/patch route.*public.*installers.*current/i,
|
|
355
|
-
);
|
|
356
|
-
assert.equal(backedUp, false);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("release ids and R2 keys reject traversal and shell metacharacters", async () => {
|
|
360
|
-
const fx = fixture();
|
|
361
|
-
await assert.rejects(
|
|
362
|
-
runUploadStateMachine({ root: fx.root, releaseId: "../../outside", platform: "win", tier: "patch" }),
|
|
363
|
-
/invalid release id/i,
|
|
364
|
-
);
|
|
365
|
-
fx.manifest.routes.patch[0].key = "fixture/installers/windows/current/Fixture.exe&whoami";
|
|
366
|
-
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
367
|
-
await assert.rejects(
|
|
368
|
-
runUploadStateMachine({ root: fx.root, releaseId: fx.manifest.releaseId, platform: "win", tier: "patch" }),
|
|
369
|
-
/unsafe R2 key/i,
|
|
370
|
-
);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("release and test Cargo targets are disjoint", () => {
|
|
374
|
-
const root = "C:/repo/.right-release";
|
|
375
|
-
const build = releaseEnvironment({ root, platform: "win", cacheKey: "deps123", kind: "release" });
|
|
376
|
-
const testEnv = releaseEnvironment({ root, platform: "win", cacheKey: "deps123", kind: "test" });
|
|
377
|
-
assert.notEqual(build.CARGO_TARGET_DIR, testEnv.CARGO_TARGET_DIR);
|
|
378
|
-
assert.match(build.CARGO_TARGET_DIR, /cache[\\/]cargo-target[\\/]win[\\/]deps123/);
|
|
379
|
-
assert.match(testEnv.CARGO_TARGET_DIR, /cache[\\/]test-target[\\/]win[\\/]deps123/);
|
|
380
|
-
assert.match(build.SCCACHE_DIR, /cache[\\/]sccache/);
|
|
381
|
-
assert.equal(build.RUSTC_WRAPPER, "sccache");
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test("dependency cache survives source commits and invalidates only dependency/toolchain inputs", () => {
|
|
385
|
-
const base = {
|
|
386
|
-
cargoLockSha256: "lock-a",
|
|
387
|
-
rustc: "rustc 1.90.0 host x86_64-pc-windows-msvc",
|
|
388
|
-
target: "x86_64-pc-windows-msvc",
|
|
389
|
-
features: ["bundled-sqlcipher-vendored-openssl"],
|
|
390
|
-
};
|
|
391
|
-
assert.equal(cacheFingerprint({ ...base, commit: "aaa" }), cacheFingerprint({ ...base, commit: "bbb" }));
|
|
392
|
-
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, cargoLockSha256: "lock-b" }));
|
|
393
|
-
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, rustc: "rustc 1.91.0" }));
|
|
394
|
-
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, features: ["bundled-sqlcipher"] }));
|
|
395
|
-
});
|
package/release-token.test.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import { defaultWindowsReleaseTokenFile, loadReleaseToken } from "./release-token.mjs";
|
|
7
|
-
|
|
8
|
-
test("loads an explicit release token without exposing admin-session fallback", async () => {
|
|
9
|
-
assert.equal(await loadReleaseToken({ env: { RIGHTAPPS_RELEASE_TOKEN: " durable-token ", RIGHTAPPS_ADMIN_TOKEN: "admin-session" } }), "durable-token");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("loads the release token from an explicit protected-file path", async () => {
|
|
13
|
-
const dir = mkdtempSync(path.join(os.tmpdir(), "right-release-token-"));
|
|
14
|
-
const file = path.join(dir, "token");
|
|
15
|
-
writeFileSync(file, "file-token\n");
|
|
16
|
-
assert.equal(await loadReleaseToken({ env: { RIGHTAPPS_RELEASE_TOKEN_FILE: file }, platform: "linux" }), "file-token");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("uses the portable Windows token location", () => {
|
|
20
|
-
assert.equal(defaultWindowsReleaseTokenFile({ APPDATA: "C:\\Users\\fixture\\AppData\\Roaming" }), path.join("C:\\Users\\fixture\\AppData\\Roaming", "RightKit", "rightapps-release.token"));
|
|
21
|
-
});
|