feat: add safe Gitea sync and signed updates
ForgeFlow quality gate / secret-scan (push) Failing after 32s
ForgeFlow quality gate / quality (push) Failing after 0s

This commit is contained in:
NuklearRabbit
2026-08-27 00:38:58 +02:00
parent cb9bdcd713
commit d47c7b5e41
46 changed files with 1658 additions and 246 deletions
+2
View File
@@ -110,6 +110,8 @@ test("configuration mutations persist mappings, favorites, reviews, trends, oper
assert.equal(state.preferences.preferredCloneProtocol, "https");
assert.equal(state.preferences.diagnosticLevel, "info");
assert.equal(state.preferences.maxLogFileMb, 50);
const manualRemoteAwareness = await store.setPreferences({ fetchIntervalMinutes: 0 });
assert.equal(manualRemoteAwareness.preferences.fetchIntervalMinutes, 0);
await store.removeMapping("owner/app");
assert.equal(store.data.repositoryMappings["owner/app"], undefined);
});
+104
View File
@@ -53,6 +53,35 @@ test('GitService reads changes and commits/pushes selected files to a real bare
assert.equal(remoteLog.stdout.trim(), 'Add desktop cockpit copy');
});
test('untracked diff rendering refuses links outside the repository and oversized files', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-diff-boundary-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const repository = path.join(root, 'repository');
const outside = path.join(root, 'outside');
await fs.mkdir(repository, { recursive: true });
await fs.mkdir(outside, { recursive: true });
await git(['init'], repository);
await fs.writeFile(path.join(outside, 'secret.txt'), 'outside-secret');
try {
await fs.symlink(outside, path.join(repository, 'linked'), process.platform === 'win32' ? 'junction' : 'dir');
} catch {
t.skip('this platform does not allow creating directory links');
return;
}
const service = new GitService();
await assert.rejects(
service.diff(repository, 'linked/secret.txt'),
(error) => error.code === 'DIFF_TARGET_OUTSIDE_REPOSITORY',
);
await fs.writeFile(path.join(repository, 'too-large.txt'), Buffer.alloc(16 * 1024 * 1024 + 1, 0x61));
await assert.rejects(
service.diff(repository, 'too-large.txt'),
(error) => error.code === 'DIFF_FILE_TOO_LARGE' && error.recoverable === true,
);
});
test('stages and pushes deleted and renamed files selected from the working tree', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-git-delete-rename-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
@@ -204,6 +233,81 @@ test('detects and removes a stale HEAD.lock while skipping Git object storage',
assert.ok(await fs.stat(ignoredObjectLock));
});
test('previews and safely mirrors a workspace to Gitea while preserving every class of local work', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-workspace-sync-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const remote = path.join(root, 'remote.git');
const working = path.join(root, 'working');
const external = path.join(root, 'external');
await git(['init', '--bare', remote], root);
await git(['clone', remote, working], root);
await git(['config', 'user.name', 'ForgeFlow Test'], working);
await git(['config', 'user.email', 'forgeflow@example.invalid'], working);
await fs.writeFile(path.join(working, '.gitignore'), 'runtime/\n');
await fs.writeFile(path.join(working, 'README.md'), 'initial\n');
await fs.writeFile(path.join(working, 'obsolete.txt'), 'remove remotely\n');
await git(['add', '.'], working);
await git(['commit', '-m', 'Initial'], working);
await git(['branch', '-M', 'main'], working);
await git(['push', '-u', 'origin', 'main'], working);
await git(['clone', remote, external], root);
await git(['config', 'user.name', 'External Gitea Test'], external);
await git(['config', 'user.email', 'external@example.invalid'], external);
await git(['checkout', 'main'], external);
await fs.writeFile(path.join(external, 'README.md'), 'changed on Gitea\n');
await fs.rm(path.join(external, 'obsolete.txt'));
await fs.writeFile(path.join(external, 'remote-only.txt'), 'new on Gitea\n');
await git(['add', '-A'], external);
await git(['commit', '-m', 'External cleanup'], external);
await git(['push', 'origin', 'main'], external);
await fs.writeFile(path.join(working, 'local-commit.txt'), 'local committed work\n');
await git(['add', 'local-commit.txt'], working);
await git(['commit', '-m', 'Local Codex work'], working);
const localHead = (await git(['rev-parse', 'HEAD'], working)).stdout.trim();
await fs.appendFile(path.join(working, 'README.md'), 'local uncommitted edit\n');
await fs.writeFile(path.join(working, 'local-notes.txt'), 'untracked local notes\n');
await fs.mkdir(path.join(working, 'runtime'), { recursive: true });
await fs.writeFile(path.join(working, 'runtime', 'local.db'), 'ignored runtime state\n');
const service = new GitService();
const firstPlan = await service.previewWorkspaceSync(working);
assert.match(firstPlan.id, /^[0-9a-f]{64}$/);
assert.equal(firstPlan.summary.localCommitsToProtect, 1);
assert.equal(firstPlan.summary.incomingCommits, 1);
assert.equal(firstPlan.summary.localFilesToStash, 2);
assert.equal(firstPlan.summary.untrackedFilesToStash, 1);
assert.ok(firstPlan.changes.some((item) => item.path === 'obsolete.txt' && item.code === 'D'));
assert.equal(firstPlan.recovery.ignoredFilesPreserved, true);
await fs.writeFile(path.join(working, 'changed-after-preview.txt'), 'forces a stale plan\n');
await assert.rejects(
service.synchronizeWorkspace(working, firstPlan.id),
(error) => error.code === 'WORKSPACE_SYNC_PLAN_STALE'
);
assert.equal(await fs.readFile(path.join(working, 'changed-after-preview.txt'), 'utf8'), 'forces a stale plan\n');
const reviewedPlan = await service.previewWorkspaceSync(working);
const result = await service.synchronizeWorkspace(working, reviewedPlan.id);
assert.equal(result.applied, true);
assert.equal(result.status.clean, true);
assert.equal(result.status.head, reviewedPlan.targetSha);
assert.match(result.backupBranch, /^forgeflow\/recovery-main-/);
assert.ok(result.stash?.sha);
assert.equal((await git(['rev-parse', result.backupBranch], working)).stdout.trim(), localHead);
assert.equal((await fs.readFile(path.join(working, 'README.md'), 'utf8')).replace(/\r\n/g, '\n'), 'changed on Gitea\n');
assert.equal((await fs.readFile(path.join(working, 'remote-only.txt'), 'utf8')).replace(/\r\n/g, '\n'), 'new on Gitea\n');
await assert.rejects(fs.stat(path.join(working, 'obsolete.txt')), (error) => error.code === 'ENOENT');
await assert.rejects(fs.stat(path.join(working, 'local-commit.txt')), (error) => error.code === 'ENOENT');
await assert.rejects(fs.stat(path.join(working, 'local-notes.txt')), (error) => error.code === 'ENOENT');
assert.equal(await fs.readFile(path.join(working, 'runtime', 'local.db'), 'utf8'), 'ignored runtime state\n');
const stashedPaths = (await git(['stash', 'show', '--include-untracked', '--name-only', result.stash.ref], working)).stdout;
assert.match(stashedPaths, /README\.md/);
assert.match(stashedPaths, /local-notes\.txt/);
assert.match(stashedPaths, /changed-after-preview\.txt/);
});
test('repairs a diverged branch by creating a safety branch before resetting to upstream', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-diverged-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
+43
View File
@@ -121,6 +121,49 @@ test("repository troubleshooting offers personalized synchronization repair acti
assert.match(ipc, /repository:repair-sync/);
});
test("Gitea workspace sync is preview-driven, recoverable and never deletes ignored runtime data", async () => {
const renderer = await rendererSource();
const preload = await readFile(new URL("../preload.cjs", import.meta.url), "utf8");
const ipc = await ipcSource();
assert.match(renderer, /Gitea workspace sync/);
assert.match(renderer, /preview-workspace-sync/);
assert.match(renderer, /confirm-workspace-sync/);
assert.match(renderer, /Ignored runtime files remain in place/);
assert.match(renderer, /recovery branch/);
assert.match(renderer, /named Git stash/);
assert.match(renderer, /Gitea fetch interval/);
assert.match(preload, /previewWorkspaceSync/);
assert.match(preload, /applyWorkspaceSync/);
assert.match(ipc, /repository:workspace-sync-preview/);
assert.match(ipc, /repository:workspace-sync-apply/);
});
test("demo bridge implements the complete Git recovery flow", async () => {
const source = await readFile(
new URL("../src/renderer/mock-repository-bridge.js", import.meta.url),
"utf8",
);
for (const method of [
"gitRecoveryStatus",
"reconcileRepository",
"repairGitLocks",
"repairRepositorySync",
]) {
assert.match(source, new RegExp(`async ${method}\\(`));
}
});
test("Git tools rows retain their content height inside the scrollable tab", async () => {
const styles = await readFile(
new URL("../src/renderer/styles.css", import.meta.url),
"utf8",
);
assert.match(
styles,
/\.git-tools-grid\s*\{[^}]*grid-auto-rows:\s*max-content/s,
);
});
test("advanced Git, desktop, backup, policy and audit workflows are exposed in the renderer", async () => {
const renderer = await rendererSource();
const preload = await readFile(
+44
View File
@@ -102,3 +102,47 @@ test('a watched repository is read on filesystem activity instead of on every in
monitor.stop();
assert.equal(monitor.watchers.size, 0, 'stopping releases every watcher');
});
test('background Gitea awareness fetches read-only remote state with bounded concurrency', async () => {
let active = 0;
let peak = 0;
const changes = [];
const git = {
fetch: async (localPath) => {
active += 1;
peak = Math.max(peak, active);
await new Promise((resolve) => setTimeout(resolve, 15));
active -= 1;
return { status: { localPath, revision: 2, branch: { head: 'main', ahead: 0, behind: 1 }, counts: {} } };
},
statusFingerprint: (status) => String(status.revision),
};
const store = { data: { preferences: { autoRefresh: true, repositoryPollSeconds: 2, fetchIntervalMinutes: 1 } } };
const monitor = new RepositoryMonitor({ store, git, onChange: (change) => changes.push(change) });
const paths = Array.from({ length: 6 }, (_, index) => `/repo-${index}`);
monitor.setPaths(paths);
for (const localPath of paths) {
monitor.fingerprints.set(localPath, '1');
monitor.lastFetchedAt.set(localPath, Date.now() - 61_000);
}
await monitor.fetchRemoteUpdates();
assert.equal(peak, 2);
assert.equal(active, 0);
assert.equal(changes.length, paths.length);
assert.ok(changes.every((change) => change.reason === 'remote-state-changed'));
});
test('a zero remote fetch interval disables background network access', async () => {
let fetches = 0;
const git = {
fetch: async () => { fetches += 1; return { status: { revision: 2 } }; },
statusFingerprint: (status) => String(status.revision),
};
const store = { data: { preferences: { autoRefresh: true, repositoryPollSeconds: 2, fetchIntervalMinutes: 0 } } };
const monitor = new RepositoryMonitor({ store, git });
monitor.setPaths(['/repo']);
monitor.lastFetchedAt.set('/repo', 0);
await monitor.fetchRemoteUpdates(Date.now() + 24 * 60 * 60_000);
assert.equal(fetches, 0);
});
+2
View File
@@ -19,6 +19,8 @@ const { redactSecrets } = redaction;
test('rejects credentials embedded in service URLs', () => {
assert.throws(() => normalizeBaseUrl(`https://${['jens', 'secret'].join(':')}@gitea.example.test`), /credentials/i);
assert.throws(() => normalizeBaseUrl('http://gitea.example.test'), /must use HTTPS/i);
assert.equal(normalizeBaseUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000');
assert.throws(() => assertHttpUrl(`https://${['user', 'secret'].join(':')}@app.example.test/health`), /credentials/i);
});
+29
View File
@@ -73,6 +73,35 @@ test("connection options enforce host identity and support password credentials"
const firstUse = await service.connectionOptions({ id: 'one', host: 'server', username: 'root', authType: 'password' }, { trustOnFirstUse: true });
assert.equal(firstUse.options.port, 22);
assert.equal(firstUse.options.hostVerifier(key), true);
const previewBound = await service.connectionOptions(
{ id: 'one', host: 'server', username: 'root', authType: 'password' },
{ expectedFingerprint: fingerprint },
);
assert.equal(previewBound.options.hostVerifier(key), true);
assert.equal(previewBound.options.hostVerifier(Buffer.from('changed')), false);
});
test("SSH host fingerprint preview rejects the handshake before credentials are requested", async () => {
const key = Buffer.from("untrusted-server-key");
let connectedOptions = null;
class ProbeClient extends EventEmitter {
connect(options) {
connectedOptions = options;
assert.equal(options.hostVerifier(key), false);
queueMicrotask(() => this.emit("error", Object.assign(new Error("host rejected"), { code: "HOST_VERIFIER_REJECTED" })));
}
end() {}
}
const store = {
getServer: () => ({ id: "server", name: "Unraid", host: "192.0.2.10", port: 2222, username: "root", authType: "password" }),
getServerCredentials: () => { throw new Error("credentials must not be read during a fingerprint preview"); },
};
const service = new SshService({ store, diagnostics: null, clientFactory: () => ProbeClient });
const result = await service.probeHostFingerprint("server");
assert.equal(result.fingerprint, fingerprintKey(key));
assert.deepEqual(result.server, { id: "server", name: "Unraid", host: "192.0.2.10", port: 2222 });
assert.equal("password" in connectedOptions, false);
assert.equal("privateKey" in connectedOptions, false);
});
test("connection options report unreadable private keys without leaking credentials", async () => {
+80
View File
@@ -320,6 +320,86 @@ test("low-level inventory scan is read-only and user discovery auto-links exact
assert.deepEqual(discovery.refreshedProfileIds, [profiles[0].id]);
});
test("a stale deployment link cannot block adoption of its running replacement", async () => {
const repository = {
fullName: "Jens/DevRunBook",
name: "DevRunBook",
defaultBranch: "main",
sshUrl: "git@gitea.test:Jens/DevRunBook.git",
};
const stale = {
workloadId: "old-devrunbook",
status: "stale",
classification: { type: "stale-link" },
runtime: { running: false, health: "missing" },
link: { profileId: "old-profile", repositoryFullName: repository.fullName },
candidates: [{ repositoryFullName: repository.fullName, score: 100, exact: true }],
};
const replacement = {
workloadId: "devrunbook-runtime",
serverId: "unraid",
displayName: "DevRunBook",
status: "suggested",
classification: { type: "active-application" },
runtime: { running: true, health: "healthy" },
link: null,
candidates: [{
repositoryFullName: repository.fullName,
score: 85,
exact: false,
identityExact: true,
}],
compose: {
project: "devrunbook",
workingDir: "/mnt/user/appdata/DevRunBook",
configFiles: ["/mnt/user/appdata/DevRunBook/compose.yml"],
services: ["app"],
},
containers: [{ name: "DevRunBook", running: true, mounts: [], ports: [] }],
metadata: { branch: "main" },
remoteFolderCandidate: "DevRunBook",
};
const saved = [];
const service = new UnraidDeploymentService({
store: {
data: {
deploymentProfiles: {
[repository.fullName]: [{
id: "old-profile",
provider: "ssh-unraid",
serverId: "unraid",
workloadIdentity: { workloadId: stale.workloadId, linkSource: "automatic" },
}],
},
},
createRecoverySnapshot: async () => ({}),
saveDeploymentProfile: async (_fullName, profile) => {
saved.push(profile);
return profile;
},
saveDeploymentState: async () => ({}),
},
});
service.collectServerInventory = async () => ({
server: { id: "unraid", name: "Unraid", basePath: "/mnt/user/appdata" },
inventory: { capabilities: {}, warnings: [] },
workloads: [stale, replacement],
});
const plan = service.reconciliationPlan(
{ id: "unraid" },
[stale, replacement],
[repository],
{ autoLink: true },
);
assert.deepEqual(plan.additions.map((item) => item.workloadId), [replacement.workloadId]);
const result = await service.scanServerInventory("unraid", [repository], { autoLink: true });
assert.equal(result.adopted, 1);
assert.equal(replacement.link.repositoryFullName, repository.fullName);
assert.equal(saved.length, 1);
});
test("server inventory includes stopped DockerMan containers without Git and keeps name matches manual", () => {
+124 -24
View File
@@ -5,7 +5,7 @@ import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { EventEmitter } from "node:events";
import { createHash } from "node:crypto";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
@@ -14,10 +14,36 @@ const require = createRequire(import.meta.url);
const execFileAsync = promisify(execFile);
const {
UpdateService,
verifyReleaseManifest,
waitForUpdaterStarted,
windowsUpdaterSpawnOptions,
} = require("../src/main/update-service.cjs");
function createSignedReleaseFixture({
version,
remoteSha,
assetName,
binary,
}) {
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const sha256 = createHash("sha256").update(binary).digest("hex");
const manifest = {
schemaVersion: 1,
product: "ForgeFlow",
version,
tag: `v${version}`,
commit: remoteSha,
buildId: "test-build",
signature: { algorithm: "Ed25519", keyId: "SHA256:test" },
artifacts: [{ name: assetName, bytes: binary.length, sha256 }],
};
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
const signatureBytes = Buffer.from(
`${sign(null, manifestBytes, privateKey).toString("base64")}\n`,
);
return { publicKey, sha256, manifestBytes, signatureBytes };
}
test("Windows updater uses a hidden non-detached PowerShell child", () => {
assert.deepEqual(windowsUpdaterSpawnOptions("C:\\updates"), {
detached: false,
@@ -324,6 +350,8 @@ test("packaged updater passes Gitea browser download URLs to the asset downloade
);
assert.match(source, /downloadUrl: asset\.browser_download_url/);
assert.match(source, /downloadUrl: checksumAsset\.browser_download_url/);
assert.match(source, /downloadUrl: manifestAsset\.browser_download_url/);
assert.match(source, /downloadUrl: signatureAsset\.browser_download_url/);
assert.match(source, /RELEASE_ASSET_METADATA_RECEIVED/);
});
test("PowerShell helper replaces an existing launching status with a Windows-safe file API", async () => {
@@ -480,15 +508,22 @@ test("updater handshake rejects a stale status from another update request", asy
await rm(temp, { recursive: true, force: true });
});
test("packaged updater downloads only a published checksum-matched Windows asset", async () => {
test("packaged updater downloads only a publisher-signed Windows asset", async () => {
const temp = await mkdtemp(
path.join(os.tmpdir(), "forgeflow-binary-update-"),
);
const binary = Buffer.alloc(1_100_000, 0x5a);
binary[0] = 0x4d;
binary[1] = 0x5a;
const sha256 = createHash("sha256").update(binary).digest("hex");
const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe";
const remoteSha = "a".repeat(40);
const signed = createSignedReleaseFixture({
version: "0.8.2",
remoteSha,
assetName,
binary,
});
const manifestName = "ForgeFlow-0.8.2-release-manifest.json";
const gitea = {
async getReleaseByTag(_owner, _repo, tag) {
if (tag !== "v0.8.2") return null;
@@ -508,18 +543,32 @@ test("packaged updater downloads only a published checksum-matched Windows asset
id: 42,
browser_download_url: "http://wrong-origin.test/checksum",
},
{
name: manifestName,
id: 43,
browser_download_url: "http://wrong-origin.test/manifest",
},
{
name: `${manifestName}.sig`,
id: 44,
browser_download_url: "http://wrong-origin.test/signature",
},
],
};
},
async downloadReleaseAsset(_owner, _repo, releaseId, assetId, options) {
assert.equal(releaseId, 82);
assert.equal(
options.downloadUrl,
assetId === 42
? "http://wrong-origin.test/checksum"
: "http://wrong-origin.test/setup",
);
return assetId === 42 ? Buffer.from(`${sha256} ${assetName}\n`) : binary;
const downloads = {
41: ["http://wrong-origin.test/setup", binary],
42: [
"http://wrong-origin.test/checksum",
Buffer.from(`${signed.sha256} ${assetName}\n`),
],
43: ["http://wrong-origin.test/manifest", signed.manifestBytes],
44: ["http://wrong-origin.test/signature", signed.signatureBytes],
};
assert.equal(options.downloadUrl, downloads[assetId][0]);
return downloads[assetId][1];
},
};
const service = new UpdateService({
@@ -537,14 +586,17 @@ test("packaged updater downloads only a published checksum-matched Windows asset
sourcePath: temp,
userDataPath: temp,
platform: "win32",
updatePublicKey: signed.publicKey,
});
const result = await service.downloadPackaged({
owner: "Jens",
repo: "ForgeFlow",
remoteVersion: "0.8.2",
remoteSha,
});
assert.equal(result.downloaded, true);
assert.equal(result.sha256, sha256);
assert.equal(result.sha256, signed.sha256);
assert.equal(result.publisherKeyId, "SHA256:test");
assert.equal(result.portable, false);
assert.equal((await readFile(result.binaryPath)).length, binary.length);
await rm(temp, { recursive: true, force: true });
@@ -558,6 +610,14 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
binary[0] = 0x4d;
binary[1] = 0x5a;
const assetName = "ForgeFlow-Portable-0.8.2-win-x64.exe";
const remoteSha = "b".repeat(40);
const signed = createSignedReleaseFixture({
version: "0.8.2",
remoteSha,
assetName,
binary,
});
const manifestName = "ForgeFlow-0.8.2-release-manifest.json";
const service = new UpdateService({
store: { data: { gitea: {} }, save: async () => {} },
gitea: {
@@ -576,14 +636,18 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
name: `${assetName}.sha256`,
browser_download_url: "http://wrong-origin.test/checksum",
},
{ id: 53, name: manifestName },
{ id: 54, name: `${manifestName}.sig` },
],
};
},
async downloadReleaseAsset(_owner, _repo, releaseId, assetId) {
assert.equal(releaseId, 83);
return assetId === 52
? Buffer.from(`${"0".repeat(64)} ${assetName}`)
: binary;
if (assetId === 51) return binary;
if (assetId === 52)
return Buffer.from(`${"0".repeat(64)} ${assetName}`);
if (assetId === 53) return signed.manifestBytes;
return signed.signatureBytes;
},
},
diagnostics: null,
@@ -595,6 +659,7 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
sourcePath: temp,
userDataPath: temp,
platform: "win32",
updatePublicKey: signed.publicKey,
});
await assert.rejects(
() =>
@@ -602,17 +667,45 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
owner: "Jens",
repo: "ForgeFlow",
remoteVersion: "0.8.2",
remoteSha,
}),
/SHA-256 verification/,
/does not match the signed publisher manifest/,
);
await rm(temp, { recursive: true, force: true });
});
test("Windows release pipeline preserves optional signing checks and emits provenance plus SBOM", async () => {
const [pkgSource, signatureSource, checksumSource] = await Promise.all([
test("release manifest verification rejects a different publisher key", () => {
const binary = Buffer.alloc(1_100_000, 0x5a);
const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe";
const fixture = createSignedReleaseFixture({
version: "0.8.2",
remoteSha: "c".repeat(40),
assetName,
binary,
});
const otherKey = generateKeyPairSync("ed25519").publicKey;
assert.throws(
() =>
verifyReleaseManifest({
manifestBytes: fixture.manifestBytes,
signatureBytes: fixture.signatureBytes,
publicKey: otherKey,
update: {
remoteVersion: "0.8.2",
remoteSha: "c".repeat(40),
},
assetName,
}),
(error) => error.code === "RELEASE_SIGNATURE_INVALID",
);
});
test("Windows release pipeline emits signed provenance, manifest and SBOM evidence", async () => {
const [pkgSource, signatureSource, checksumSource, manifestSigner] = await Promise.all([
readFile(new URL("../package.json", import.meta.url), "utf8"),
readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
]);
assert.match(pkgSource, /verify-release-signatures\.mjs/);
assert.match(signatureSource, /FORGEFLOW_SIGNED_RELEASE/);
@@ -623,6 +716,10 @@ test("Windows release pipeline preserves optional signing checks and emits prove
assert.match(checksumSource, /provenance\.json/);
assert.match(checksumSource, /sbom\.cdx\.json/);
assert.match(checksumSource, /CycloneDX/);
assert.match(checksumSource, /publisherManifestSignature/);
assert.match(manifestSigner, /Ed25519/);
assert.match(manifestSigner, /release-manifest\.json/);
assert.match(pkgSource, /sign-release-manifest\.mjs/);
const publisher = await readFile(new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8");
assert.match(publisher, /draft: true/);
assert.match(publisher, /requiredAssets/);
@@ -630,17 +727,20 @@ test("Windows release pipeline preserves optional signing checks and emits prove
assert.match(publisher, /sbom\.cdx\.json/);
});
test("the supported Windows build is free, checksum-protected and updater-compatible", async () => {
const [pkg, signatureCheck, checksumWriter] = await Promise.all([
test("the supported Windows build uses free offline Ed25519 publisher signing", async () => {
const [pkg, keySetup, manifestSigner, publicKey] = await Promise.all([
readFile(new URL("../package.json", import.meta.url), "utf8"),
readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/setup-update-signing-key.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
readFile(new URL("../build/update-signing-public.pem", import.meta.url), "utf8"),
]);
assert.doesNotMatch(pkg, /dist:win:signed/);
assert.match(pkg, /dist:win/);
assert.match(pkg, /write-release-checksums\.mjs/);
assert.match(signatureCheck, /checksum-protected unsigned artifact/);
assert.match(checksumWriter, /sha256/);
assert.match(pkg, /signing:setup/);
assert.match(keySetup, /release-signing-private\.pem/);
assert.match(manifestSigner, /sign\(null, manifestBytes, privateKey\)/);
assert.match(publicKey, /BEGIN PUBLIC KEY/);
assert.doesNotMatch(publicKey, /PRIVATE KEY/);
});
test("binary update helper verifies, waits, applies and records restart state", async () => {