feat(desktop): add Windows preview distribution
This commit is contained in:
1 parent
6582a891d7
commit
383bafa83f
37 files changed
+1496
-107
No files matched your search
@@ -8,6 +8,7 @@ export const nativeReleaseAssetsReady = ({
|
||||
currentTag,
|
||||
desktopVersion,
|
||||
assetNames,
|
||||
requireWindowsSignature = true,
|
||||
}) => {
|
||||
if (platform === "mobile") {
|
||||
const apkNames = assetNames.filter((name) =>
|
||||
@@ -47,6 +48,29 @@ export const nativeReleaseAssetsReady = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
const windowsInstallerNames = assetNames.filter((name) =>
|
||||
/^EdgeEver-(.+)-windows-x64\.exe$/.test(name)
|
||||
);
|
||||
if (windowsInstallerNames.length !== 1) return false;
|
||||
const windowsVersion = /^EdgeEver-(.+)-windows-x64\.exe$/
|
||||
.exec(windowsInstallerNames[0])?.[1];
|
||||
if (!windowsVersion) return false;
|
||||
versions.add(windowsVersion);
|
||||
for (const name of [
|
||||
"latest.yml",
|
||||
"latest-windows.json",
|
||||
"SHA256SUMS-windows.txt",
|
||||
]) {
|
||||
if (assetNames.filter((assetName) => assetName === name).length !== 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const signatureCount = assetNames.filter(
|
||||
(name) => name === "latest-windows.json.sig",
|
||||
).length;
|
||||
if (requireWindowsSignature ? signatureCount !== 1 : signatureCount > 1) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
versions.size === 1 &&
|
||||
(!rebuild || versions.has(desktopVersion)) &&
|
||||
@@ -58,15 +82,16 @@ export const nativeReleaseAssetsReady = ({
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
const [platform, rebuildValue, currentTag, desktopVersion = ""] =
|
||||
const [platform, rebuildValue, currentTag, desktopVersion = "", signatureMode = "required"] =
|
||||
process.argv.slice(2);
|
||||
if (
|
||||
!["mobile", "desktop"].includes(platform) ||
|
||||
!["true", "false"].includes(rebuildValue) ||
|
||||
!currentTag
|
||||
!currentTag ||
|
||||
!["required", "allow-missing-windows-signature"].includes(signatureMode)
|
||||
) {
|
||||
console.error(
|
||||
"Usage: node scripts/check-native-release-assets.mjs <mobile|desktop> <true|false> <current-tag> [desktop-version]",
|
||||
"Usage: node scripts/check-native-release-assets.mjs <mobile|desktop> <true|false> <current-tag> [desktop-version] [required|allow-missing-windows-signature]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -80,6 +105,7 @@ const run = () => {
|
||||
currentTag,
|
||||
desktopVersion,
|
||||
assetNames,
|
||||
requireWindowsSignature: signatureMode === "required",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,14 @@ import { describe, expect, test } from "bun:test";
|
||||
import { nativeReleaseAssetsReady } from "./check-native-release-assets.mjs";
|
||||
|
||||
describe("native release asset readiness", () => {
|
||||
const windowsAssets = (version) => [
|
||||
`EdgeEver-${version}-windows-x64.exe`,
|
||||
"latest.yml",
|
||||
"latest-windows.json",
|
||||
"latest-windows.json.sig",
|
||||
"SHA256SUMS-windows.txt",
|
||||
];
|
||||
|
||||
test("accepts one reused Android APK but requires the current name after a rebuild", () => {
|
||||
const reused = ["edgeever-android-v1.6.30-arm64-v8a.apk"];
|
||||
expect(
|
||||
@@ -53,6 +61,7 @@ describe("native release asset readiness", () => {
|
||||
"EdgeEver-1.6.33-mac-x64.zip",
|
||||
"EdgeEver-1.6.33-mac-x64.zip.blockmap",
|
||||
"latest-mac.yml",
|
||||
...windowsAssets("1.6.33"),
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
@@ -69,6 +78,7 @@ describe("native release asset readiness", () => {
|
||||
"EdgeEver-1.6.35-mac-x64.zip",
|
||||
"EdgeEver-1.6.35-mac-x64.zip.blockmap",
|
||||
"latest-mac.yml",
|
||||
...windowsAssets("1.6.35"),
|
||||
];
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
@@ -103,8 +113,39 @@ describe("native release asset readiness", () => {
|
||||
"EdgeEver-1.6.33-mac-arm64.zip",
|
||||
"EdgeEver-1.6.33-mac-arm64.zip.blockmap",
|
||||
"latest-mac.yml",
|
||||
...windowsAssets("1.6.33"),
|
||||
],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("allows the CI staging audit to omit only the offline signature", () => {
|
||||
const assets = [
|
||||
"EdgeEver-1.6.35-mac-arm64.dmg",
|
||||
"EdgeEver-1.6.35-mac-arm64.dmg.blockmap",
|
||||
"EdgeEver-1.6.35-mac-arm64.zip",
|
||||
"EdgeEver-1.6.35-mac-arm64.zip.blockmap",
|
||||
"EdgeEver-1.6.35-mac-x64.dmg",
|
||||
"EdgeEver-1.6.35-mac-x64.dmg.blockmap",
|
||||
"EdgeEver-1.6.35-mac-x64.zip",
|
||||
"EdgeEver-1.6.35-mac-x64.zip.blockmap",
|
||||
"latest-mac.yml",
|
||||
...windowsAssets("1.6.35").filter((name) => name !== "latest-windows.json.sig"),
|
||||
];
|
||||
expect(nativeReleaseAssetsReady({
|
||||
platform: "desktop",
|
||||
rebuild: true,
|
||||
currentTag: "v1.6.35",
|
||||
desktopVersion: "1.6.35",
|
||||
assetNames: assets,
|
||||
requireWindowsSignature: false,
|
||||
})).toBe(true);
|
||||
expect(nativeReleaseAssetsReady({
|
||||
platform: "desktop",
|
||||
rebuild: true,
|
||||
currentTag: "v1.6.35",
|
||||
desktopVersion: "1.6.35",
|
||||
assetNames: assets,
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
WINDOWS_UPDATE_KEY_ID,
|
||||
WINDOWS_UPDATE_MANIFEST_NAME,
|
||||
} from "../apps/desktop/src/main/windows-update-trust.mjs";
|
||||
|
||||
const STABLE_VERSION = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
const hashFile = (path, algorithm, encoding) => new Promise((resolveHash, rejectHash) => {
|
||||
const hash = createHash(algorithm);
|
||||
const stream = createReadStream(path);
|
||||
stream.on("error", rejectHash);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("end", () => resolveHash(hash.digest(encoding)));
|
||||
});
|
||||
|
||||
export const createWindowsUpdateMetadata = async ({ directory, version }) => {
|
||||
if (!STABLE_VERSION.test(version)) {
|
||||
throw new Error(`Windows update version must be stable X.Y.Z: ${version}`);
|
||||
}
|
||||
const installerName = `EdgeEver-${version}-windows-x64.exe`;
|
||||
const installerPath = join(directory, installerName);
|
||||
const stats = statSync(installerPath);
|
||||
if (!stats.isFile() || stats.size <= 0) {
|
||||
throw new Error(`Windows installer is missing or empty: ${installerPath}`);
|
||||
}
|
||||
const [sha512, sha256] = await Promise.all([
|
||||
hashFile(installerPath, "sha512", "base64"),
|
||||
hashFile(installerPath, "sha256", "hex"),
|
||||
]);
|
||||
const releaseDate = new Date().toISOString();
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
keyId: WINDOWS_UPDATE_KEY_ID,
|
||||
version,
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
releaseDate,
|
||||
file: {
|
||||
name: installerName,
|
||||
size: stats.size,
|
||||
sha512,
|
||||
sha256,
|
||||
},
|
||||
};
|
||||
writeFileSync(
|
||||
join(directory, WINDOWS_UPDATE_MANIFEST_NAME),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
);
|
||||
writeFileSync(join(directory, "latest.yml"), [
|
||||
`version: ${version}`,
|
||||
"files:",
|
||||
` - url: ${installerName}`,
|
||||
` sha512: ${sha512}`,
|
||||
` size: ${stats.size}`,
|
||||
`path: ${installerName}`,
|
||||
`sha512: ${sha512}`,
|
||||
`releaseDate: '${releaseDate}'`,
|
||||
"",
|
||||
].join("\n"));
|
||||
writeFileSync(
|
||||
join(directory, "SHA256SUMS-windows.txt"),
|
||||
`${sha256} ${installerName}\n`,
|
||||
);
|
||||
return manifest;
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const [directoryValue, version] = process.argv.slice(2);
|
||||
if (!directoryValue || !version) {
|
||||
throw new Error("Usage: node scripts/create-windows-update-metadata.mjs <directory> <version>");
|
||||
}
|
||||
const directory = resolve(directoryValue);
|
||||
const packageVersion = JSON.parse(readFileSync("apps/desktop/package.json", "utf8")).version;
|
||||
if (version !== packageVersion) {
|
||||
throw new Error(`Requested version ${version} does not match desktop package version ${packageVersion}`);
|
||||
}
|
||||
const manifest = await createWindowsUpdateMetadata({ directory, version });
|
||||
process.stdout.write(`${basename(directory)}: ${manifest.file.name}\n`);
|
||||
};
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
await run();
|
||||
}
|
||||
@@ -61,7 +61,7 @@ describe("desktop release workflow", () => {
|
||||
});
|
||||
|
||||
test("reports timings after builds without instrumenting native build steps", () => {
|
||||
expect(workflow).toContain("name: Report macOS build timings");
|
||||
expect(workflow).toContain("name: Report desktop build timings");
|
||||
expect(workflow).toContain("--platform desktop");
|
||||
expect(workflow).toContain("name: edgeever-desktop-build-timings");
|
||||
expect(mobileWorkflow).toContain("name: Report Android build timings");
|
||||
@@ -69,4 +69,14 @@ describe("desktop release workflow", () => {
|
||||
expect(mobileWorkflow).toContain("name: edgeever-android-build-timings");
|
||||
expect(mobileWorkflow).not.toContain("Build signed release APK for GitHub Release\n run: time");
|
||||
});
|
||||
|
||||
test("builds an unsigned Windows x64 Preview and audits its signed update metadata", () => {
|
||||
expect(workflow).toContain("name: Windows x64 unsigned Preview");
|
||||
expect(workflow).toContain("EDGE_EVER_DESKTOP_TARGET: win");
|
||||
expect(workflow).toContain("Get-AuthenticodeSignature");
|
||||
expect(workflow).toContain("create-windows-update-metadata.mjs");
|
||||
expect(workflow).toContain("allow-missing-windows-signature");
|
||||
expect(workflow).toContain("name: Audit signed Windows update");
|
||||
expect(workflow).toContain("verify-windows-update-release.mjs");
|
||||
});
|
||||
});
|
||||
@@ -51,9 +51,12 @@ export const planNativeRelease = (platform, changedFiles) => {
|
||||
".github/workflows/desktop-build.yml",
|
||||
"bun.lock",
|
||||
"scripts/create-mac-update-metadata.mjs",
|
||||
"scripts/create-windows-update-metadata.mjs",
|
||||
"scripts/desktop-icns.mjs",
|
||||
"scripts/prepare-desktop-icons.mjs",
|
||||
"scripts/run-desktop-builder.mjs",
|
||||
"scripts/sign-windows-update-manifest.mjs",
|
||||
"scripts/verify-windows-update-release.mjs",
|
||||
"scripts/verify-desktop-package.mjs",
|
||||
]);
|
||||
|
||||
|
||||
@@ -71,9 +71,12 @@ describe("native release planning", () => {
|
||||
const changedFiles = [
|
||||
".github/workflows/desktop-build.yml",
|
||||
"scripts/create-mac-update-metadata.mjs",
|
||||
"scripts/create-windows-update-metadata.mjs",
|
||||
"scripts/prepare-desktop-icons.mjs",
|
||||
"scripts/desktop-icns.mjs",
|
||||
"scripts/run-desktop-builder.mjs",
|
||||
"scripts/sign-windows-update-manifest.mjs",
|
||||
"scripts/verify-windows-update-release.mjs",
|
||||
];
|
||||
expect(planNativeRelease("desktop", changedFiles)).toEqual({
|
||||
rebuild: true,
|
||||
|
||||
+77
-3
@@ -16,6 +16,10 @@ import { join, resolve, sep } from "node:path";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { nativeReleaseAssetsReady } from "./check-native-release-assets.mjs";
|
||||
import { planNativeRelease } from "./plan-native-release.mjs";
|
||||
import {
|
||||
assertWindowsUpdateSigningKey,
|
||||
signWindowsUpdateManifest,
|
||||
} from "./sign-windows-update-manifest.mjs";
|
||||
|
||||
const DEFAULT_REPOSITORY = "tianma-if/edgeever";
|
||||
const VERSION_BUMPS = new Set(["patch", "minor", "major"]);
|
||||
@@ -42,6 +46,7 @@ export const RELEASE_VALIDATIONS = [
|
||||
"test",
|
||||
"scripts/plan-native-release.test.mjs",
|
||||
"scripts/check-native-release-assets.test.mjs",
|
||||
"scripts/windows-update-metadata.test.mjs",
|
||||
"scripts/release.test.mjs",
|
||||
"scripts/validate-store-delivery.test.mjs",
|
||||
"scripts/store-delivery.test.mjs",
|
||||
@@ -360,7 +365,7 @@ export const buildIssueBody = ({ changesEn, changesZh, commitCoverageAudit }) =>
|
||||
"## Acceptance criteria",
|
||||
"",
|
||||
"- Required type checks, Web build, and native release planning tests pass.",
|
||||
"- The Draft Release contains audited macOS arm64 and x64 DMGs and a Play-signed Android arm64 APK.",
|
||||
"- The Draft Release contains audited macOS arm64/x64 DMGs, an unsigned Windows x64 Preview with an independently signed update manifest, and a Play-signed Android arm64 APK.",
|
||||
"- Post-publication native asset audits pass.",
|
||||
].join("\n");
|
||||
|
||||
@@ -1010,10 +1015,17 @@ const assertDraftAssets = ({
|
||||
.map((asset) => asset.name)
|
||||
.filter((name) =>
|
||||
/^EdgeEver-.*-mac-(?:arm64|x64)\.(?:dmg|zip)(?:\.blockmap)?$/.test(name) ||
|
||||
name === "latest-mac.yml"
|
||||
/^EdgeEver-.*-windows-x64\.exe$/.test(name) ||
|
||||
[
|
||||
"latest-mac.yml",
|
||||
"latest.yml",
|
||||
"latest-windows.json",
|
||||
"latest-windows.json.sig",
|
||||
"SHA256SUMS-windows.txt",
|
||||
].includes(name)
|
||||
);
|
||||
if (
|
||||
previousDesktopNames.length !== 9 ||
|
||||
previousDesktopNames.length !== 14 ||
|
||||
!previousDesktopNames.every((name) =>
|
||||
reusedAssetMatches(previousAssets, assets, name)
|
||||
)
|
||||
@@ -1023,6 +1035,44 @@ const assertDraftAssets = ({
|
||||
}
|
||||
};
|
||||
|
||||
const signDraftWindowsUpdate = ({ repository, tag }) => {
|
||||
const temporaryDirectory = mkdtempSync(join(tmpdir(), "edgeever-windows-update-signing-"));
|
||||
const manifestPath = join(temporaryDirectory, "latest-windows.json");
|
||||
const signaturePath = `${manifestPath}.sig`;
|
||||
try {
|
||||
run("gh", [
|
||||
"release",
|
||||
"download",
|
||||
tag,
|
||||
"--repo",
|
||||
repository,
|
||||
"--pattern",
|
||||
"latest-windows.json",
|
||||
"--dir",
|
||||
temporaryDirectory,
|
||||
]);
|
||||
signWindowsUpdateManifest({
|
||||
manifestPath,
|
||||
signaturePath,
|
||||
privateKeyPath: process.env.EDGE_EVER_WINDOWS_UPDATE_SIGNING_KEY,
|
||||
});
|
||||
run("gh", [
|
||||
"release",
|
||||
"upload",
|
||||
tag,
|
||||
signaturePath,
|
||||
"--repo",
|
||||
repository,
|
||||
"--clobber",
|
||||
]);
|
||||
console.log(`[release] signed Windows update manifest for ${tag}`);
|
||||
} finally {
|
||||
if (temporaryDirectory.startsWith(`${tmpdir()}${sep}edgeever-windows-update-signing-`)) {
|
||||
rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sha256File = (path) => new Promise((resolveHash, rejectHash) => {
|
||||
const hash = createHash("sha256");
|
||||
const stream = createReadStream(path);
|
||||
@@ -1221,6 +1271,13 @@ const releaseMain = async (options) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (desktopPlan.rebuild) {
|
||||
assertWindowsUpdateSigningKey({
|
||||
privateKeyPath: process.env.EDGE_EVER_WINDOWS_UPDATE_SIGNING_KEY,
|
||||
});
|
||||
console.log("[release] Windows update signing key matches the pinned desktop trust anchor");
|
||||
}
|
||||
|
||||
let issueNumber;
|
||||
let releaseSha;
|
||||
if (resumedDraft) {
|
||||
@@ -1372,6 +1429,23 @@ const releaseMain = async (options) => {
|
||||
androidReleaseReady,
|
||||
]);
|
||||
|
||||
if (desktopPlan.rebuild) {
|
||||
signDraftWindowsUpdate({ repository: options.repository, tag });
|
||||
}
|
||||
const windowsUpdateAuditRunId = await dispatchReleaseWorkflow({
|
||||
repository: options.repository,
|
||||
workflow: RELEASE_WORKFLOWS.desktop,
|
||||
tag,
|
||||
headSha: releaseSha,
|
||||
});
|
||||
checkpoint.windowsUpdateAuditRunId = windowsUpdateAuditRunId;
|
||||
persistCheckpoint();
|
||||
await waitForRun({
|
||||
repository: options.repository,
|
||||
runId: windowsUpdateAuditRunId,
|
||||
label: "Draft signed Windows update audit",
|
||||
});
|
||||
|
||||
const draft = ghJson([
|
||||
"release",
|
||||
"view",
|
||||
|
||||
@@ -44,6 +44,25 @@ describe("release automation", () => {
|
||||
expect(publication).toBeGreaterThan(signatureGate);
|
||||
});
|
||||
|
||||
test("blocks publication until the offline-signed Windows update passes an independent audit", () => {
|
||||
const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8");
|
||||
const signing = releaseSource.indexOf("signDraftWindowsUpdate({");
|
||||
const audit = releaseSource.indexOf('label: "Draft signed Windows update audit"');
|
||||
const publication = releaseSource.indexOf('"--draft=false"');
|
||||
expect(signing).toBeGreaterThanOrEqual(0);
|
||||
expect(audit).toBeGreaterThan(signing);
|
||||
expect(publication).toBeGreaterThan(audit);
|
||||
});
|
||||
|
||||
test("checks the offline Windows key before creating release state", () => {
|
||||
const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8");
|
||||
const releaseMain = releaseSource.indexOf("const releaseMain = async");
|
||||
const keyCheck = releaseSource.indexOf("assertWindowsUpdateSigningKey({", releaseMain);
|
||||
const issueCreation = releaseSource.indexOf('"issue",\n "create"', releaseMain);
|
||||
expect(keyCheck).toBeGreaterThan(releaseMain);
|
||||
expect(issueCreation).toBeGreaterThan(keyCheck);
|
||||
});
|
||||
|
||||
test("starts Play delivery as soon as Android preparation finishes", () => {
|
||||
const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8");
|
||||
const androidReady = releaseSource.lastIndexOf("const androidReleaseReady");
|
||||
@@ -337,6 +356,7 @@ describe("release automation", () => {
|
||||
expect(body).toContain("- 并行检查。");
|
||||
expect(body).toContain("## Commit coverage audit");
|
||||
expect(body).toContain("Play-signed Android arm64 APK");
|
||||
expect(body).toContain("unsigned Windows x64 Preview");
|
||||
expect(body).toContain("- Change 1: `aaaaaaaa`");
|
||||
expect(body).toContain("- Excluded `bbbbbbbb`: test-only coverage");
|
||||
});
|
||||
|
||||
@@ -12,6 +12,11 @@ const TARGETS = {
|
||||
jobName: "macOS x64",
|
||||
packageSteps: ["Package desktop installer"],
|
||||
},
|
||||
{
|
||||
name: "Windows x64 Preview",
|
||||
jobName: "Windows x64 unsigned Preview",
|
||||
packageSteps: ["Package unsigned Windows installer"],
|
||||
},
|
||||
],
|
||||
android: [
|
||||
{
|
||||
|
||||
@@ -22,7 +22,11 @@ function job(name, startedAt, completedAt, packageDurationSeconds) {
|
||||
completed_at: packageStart,
|
||||
},
|
||||
{
|
||||
name: name.startsWith("macOS") ? "Package desktop installer" : "Build signed release APK for GitHub Release",
|
||||
name: name.startsWith("macOS")
|
||||
? "Package desktop installer"
|
||||
: name.startsWith("Windows")
|
||||
? "Package unsigned Windows installer"
|
||||
: "Build signed release APK for GitHub Release",
|
||||
conclusion: "success",
|
||||
started_at: packageStart,
|
||||
completed_at: packageEnd,
|
||||
@@ -38,18 +42,24 @@ describe("native build timing reports", () => {
|
||||
expect(formatDuration(123_000)).toBe("2m 03s");
|
||||
});
|
||||
|
||||
test("reports both desktop architectures from Actions timestamps", () => {
|
||||
test("reports both macOS architectures and Windows x64 from Actions timestamps", () => {
|
||||
const report = createTimingReport({
|
||||
jobs: [
|
||||
job("macOS arm64", "2026-08-13T00:00:00Z", "2026-08-13T00:06:00Z", 180),
|
||||
job("macOS x64", "2026-08-13T00:00:00Z", "2026-08-13T00:09:00Z", 360),
|
||||
job("Windows x64 unsigned Preview", "2026-08-13T00:00:00Z", "2026-08-13T00:07:00Z", 240),
|
||||
],
|
||||
}, "desktop", { runId: "123" });
|
||||
|
||||
expect(report.runId).toBe("123");
|
||||
expect(report.targets.map((target) => target.name)).toEqual(["macOS arm64", "macOS x64"]);
|
||||
expect(report.targets.map((target) => target.name)).toEqual([
|
||||
"macOS arm64",
|
||||
"macOS x64",
|
||||
"Windows x64 Preview",
|
||||
]);
|
||||
expect(report.targets[1].packageDurationMs).toBe(360_000);
|
||||
expect(renderTimingMarkdown(report)).toContain("| macOS x64 | success | 9m 00s | 1m 00s | 6m 00s | 2m 00s |");
|
||||
expect(renderTimingMarkdown(report)).toContain("| Windows x64 Preview | success | 7m 00s | 1m 00s | 4m 00s | 2m 00s |");
|
||||
});
|
||||
|
||||
test("selects the release Android package step", () => {
|
||||
|
||||
@@ -43,28 +43,32 @@ function componentRow({ target, mode, candidate, duration, detail, url }) {
|
||||
|
||||
function nativeRows(payload, platform, requestedMode) {
|
||||
if (platform === "desktop") {
|
||||
const architectureJobs = ["macOS arm64", "macOS x64"]
|
||||
.map((name) => findJob(payload, name))
|
||||
.filter(Boolean);
|
||||
const architectureJobs = [
|
||||
{ name: "macOS arm64", packageStep: "Package desktop installer", detail: "package + notarize" },
|
||||
{ name: "macOS x64", packageStep: "Package desktop installer", detail: "package + notarize" },
|
||||
{ name: "Windows x64 unsigned Preview", packageStep: "Package unsigned Windows installer", detail: "package" },
|
||||
]
|
||||
.map((target) => ({ target, job: findJob(payload, target.name) }))
|
||||
.filter(({ job }) => Boolean(job));
|
||||
if (architectureJobs.length > 0) {
|
||||
return architectureJobs.map((job) => {
|
||||
const packageStep = findStep(job, "Package desktop installer");
|
||||
return architectureJobs.map(({ target, job }) => {
|
||||
const packageStep = findStep(job, target.packageStep);
|
||||
return componentRow({
|
||||
target: job.name,
|
||||
target: target.name === "Windows x64 unsigned Preview" ? "Windows x64 Preview" : target.name,
|
||||
mode: "rebuild",
|
||||
candidate: job,
|
||||
duration: jobDurationMs(job),
|
||||
detail: `package + notarize ${formatDuration(stepDurationMs(packageStep))}`,
|
||||
detail: `${target.detail} ${formatDuration(stepDurationMs(packageStep))}`,
|
||||
url: job.html_url ?? payload.run.html_url,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const reuseJob = findJob(payload, "Reuse macOS release assets");
|
||||
const reuseJob = findJob(payload, "Reuse desktop release assets");
|
||||
if (reuseJob) {
|
||||
return [
|
||||
componentRow({
|
||||
target: "macOS arm64 + x64",
|
||||
target: "macOS arm64 + x64 + Windows x64",
|
||||
mode: "reuse",
|
||||
candidate: reuseJob,
|
||||
duration: jobDurationMs(reuseJob),
|
||||
@@ -77,7 +81,7 @@ function nativeRows(payload, platform, requestedMode) {
|
||||
const planJob = findJob(payload, "Plan desktop release asset");
|
||||
return [
|
||||
componentRow({
|
||||
target: "macOS arm64 + x64",
|
||||
target: "macOS arm64 + x64 + Windows x64",
|
||||
mode: "already prepared",
|
||||
candidate: planJob ?? payload.run,
|
||||
duration: jobDurationMs(planJob) ?? runDurationMs(payload.run),
|
||||
|
||||
@@ -44,6 +44,9 @@ describe("release timing report", () => {
|
||||
job("macOS x64", start, 600, [
|
||||
step("Package desktop installer", start, 300),
|
||||
]),
|
||||
job("Windows x64 unsigned Preview", start, 420, [
|
||||
step("Package unsigned Windows installer", start, 240),
|
||||
]),
|
||||
]),
|
||||
desktopMode: "rebuild",
|
||||
mobile: payload("Mobile", start, 540, [
|
||||
@@ -83,6 +86,7 @@ describe("release timing report", () => {
|
||||
"Docker TCR (amd64 + arm64)",
|
||||
"macOS arm64",
|
||||
"macOS x64",
|
||||
"Windows x64 Preview",
|
||||
"Android arm64",
|
||||
"Google Play signed APK",
|
||||
]);
|
||||
@@ -96,6 +100,9 @@ describe("release timing report", () => {
|
||||
expect(markdown).toContain(
|
||||
"| macOS x64 | rebuild | success | 10m 00s | package + notarize 5m 00s |",
|
||||
);
|
||||
expect(markdown).toContain(
|
||||
"| Windows x64 Preview | rebuild | success | 7m 00s | package 4m 00s |",
|
||||
);
|
||||
expect(markdown).toContain(
|
||||
"| Google Play signed APK | build + deliver | success | 11m 00s | AAB build 3m 00s; Play upload 4m 00s |",
|
||||
);
|
||||
@@ -120,7 +127,7 @@ describe("release timing report", () => {
|
||||
const report = createReleaseTimingReport({
|
||||
...common,
|
||||
desktop: payload("Desktop", start, 40, [
|
||||
job("Reuse macOS release assets", start, 40),
|
||||
job("Reuse desktop release assets", start, 40),
|
||||
]),
|
||||
desktopMode: "reuse",
|
||||
mobile: payload("Mobile", start, 30, [
|
||||
@@ -130,7 +137,7 @@ describe("release timing report", () => {
|
||||
});
|
||||
|
||||
expect(report.rows.at(-2)).toMatchObject({
|
||||
target: "macOS arm64 + x64",
|
||||
target: "macOS arm64 + x64 + Windows x64",
|
||||
mode: "reuse",
|
||||
durationMs: 40_000,
|
||||
});
|
||||
@@ -146,7 +153,7 @@ describe("release timing report", () => {
|
||||
const report = createReleaseTimingReport({
|
||||
release: { tag: "v1.41.2", sha: "ghi", publishedAt: start },
|
||||
desktop: payload("Desktop", start, 40, [
|
||||
job("Reuse macOS release assets", start, 40),
|
||||
job("Reuse desktop release assets", start, 40),
|
||||
]),
|
||||
desktopMode: "reuse",
|
||||
mobile: payload("Mobile", start, 30, [
|
||||
|
||||
@@ -4,10 +4,16 @@ const publishIndex = process.argv.indexOf("--publish");
|
||||
const publishMode = publishIndex >= 0 ? process.argv[publishIndex + 1] ?? "never" : "never";
|
||||
const environment = { ...process.env };
|
||||
const requestedArch = environment.EDGE_EVER_DESKTOP_ARCH;
|
||||
const requestedTarget = environment.EDGE_EVER_DESKTOP_TARGET || (
|
||||
process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux"
|
||||
);
|
||||
|
||||
if (requestedArch && !["arm64", "x64"].includes(requestedArch)) {
|
||||
throw new Error(`EDGE_EVER_DESKTOP_ARCH must be arm64 or x64, received: ${requestedArch}`);
|
||||
}
|
||||
if (!["mac", "win", "linux"].includes(requestedTarget)) {
|
||||
throw new Error(`EDGE_EVER_DESKTOP_TARGET must be mac, win, or linux, received: ${requestedTarget}`);
|
||||
}
|
||||
|
||||
// electron-builder treats an explicitly present empty CSC_LINK/WIN_CSC_LINK
|
||||
// as a path. Remove empty optional signing variables so unsigned CI builds
|
||||
@@ -35,7 +41,7 @@ const builderArgs = [
|
||||
publishMode,
|
||||
];
|
||||
if (requestedArch) {
|
||||
builderArgs.push("--mac", `--${requestedArch}`);
|
||||
builderArgs.push(`--${requestedTarget}`, `--${requestedArch}`);
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, builderArgs, {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createPrivateKey, createPublicKey, sign } from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
WINDOWS_UPDATE_KEY_ID,
|
||||
WINDOWS_UPDATE_PUBLIC_KEY_PEM,
|
||||
WINDOWS_UPDATE_SIGNATURE_NAME,
|
||||
verifyWindowsUpdateMetadata,
|
||||
} from "../apps/desktop/src/main/windows-update-trust.mjs";
|
||||
|
||||
const normalizedPublicKey = (key) => (key?.type === "public" ? key : createPublicKey(key)).export({
|
||||
type: "spki",
|
||||
format: "pem",
|
||||
}).trim();
|
||||
|
||||
export const assertWindowsUpdateSigningKey = ({
|
||||
privateKeyPath,
|
||||
expectedPublicKey = WINDOWS_UPDATE_PUBLIC_KEY_PEM,
|
||||
}) => {
|
||||
if (!privateKeyPath) {
|
||||
throw new Error("EDGE_EVER_WINDOWS_UPDATE_SIGNING_KEY must point to the offline Ed25519 private key");
|
||||
}
|
||||
const privateKey = createPrivateKey(readFileSync(privateKeyPath, "utf8"));
|
||||
if (privateKey.asymmetricKeyType !== "ed25519") {
|
||||
throw new Error("Windows update signing key must be Ed25519");
|
||||
}
|
||||
const derivedPublicKey = normalizedPublicKey(createPublicKey(privateKey));
|
||||
if (derivedPublicKey !== normalizedPublicKey(expectedPublicKey)) {
|
||||
throw new Error("Windows update signing key does not match the public key pinned in the desktop client");
|
||||
}
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
export const signWindowsUpdateManifest = ({
|
||||
manifestPath,
|
||||
signaturePath,
|
||||
privateKeyPath,
|
||||
expectedPublicKey = WINDOWS_UPDATE_PUBLIC_KEY_PEM,
|
||||
}) => {
|
||||
const privateKey = assertWindowsUpdateSigningKey({ privateKeyPath, expectedPublicKey });
|
||||
const manifestBytes = readFileSync(manifestPath);
|
||||
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
||||
if (manifest?.schemaVersion !== 1 || manifest.keyId !== WINDOWS_UPDATE_KEY_ID) {
|
||||
throw new Error("Windows update manifest key metadata is invalid");
|
||||
}
|
||||
const envelope = {
|
||||
schemaVersion: 1,
|
||||
keyId: WINDOWS_UPDATE_KEY_ID,
|
||||
signature: sign(null, manifestBytes, privateKey).toString("base64"),
|
||||
};
|
||||
const signatureBytes = Buffer.from(`${JSON.stringify(envelope)}\n`);
|
||||
verifyWindowsUpdateMetadata({
|
||||
manifestBytes,
|
||||
signatureBytes,
|
||||
expectedVersion: manifest.version,
|
||||
updateInfo: {
|
||||
version: manifest.version,
|
||||
files: [{
|
||||
url: manifest.file?.name,
|
||||
size: manifest.file?.size,
|
||||
sha512: manifest.file?.sha512,
|
||||
}],
|
||||
},
|
||||
trustedPublicKeys: { [WINDOWS_UPDATE_KEY_ID]: expectedPublicKey },
|
||||
});
|
||||
writeFileSync(signaturePath, signatureBytes, { mode: 0o644 });
|
||||
return envelope;
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
const [manifestValue, signatureValue] = process.argv.slice(2);
|
||||
if (!manifestValue) {
|
||||
throw new Error("Usage: node scripts/sign-windows-update-manifest.mjs <manifest> [signature]");
|
||||
}
|
||||
const manifestPath = resolve(manifestValue);
|
||||
const signaturePath = resolve(signatureValue || `${manifestPath}.sig`);
|
||||
if (!signaturePath.endsWith(WINDOWS_UPDATE_SIGNATURE_NAME)) {
|
||||
throw new Error(`Signature filename must end with ${WINDOWS_UPDATE_SIGNATURE_NAME}`);
|
||||
}
|
||||
signWindowsUpdateManifest({
|
||||
manifestPath,
|
||||
signaturePath,
|
||||
privateKeyPath: process.env.EDGE_EVER_WINDOWS_UPDATE_SIGNING_KEY,
|
||||
});
|
||||
process.stdout.write(`${signaturePath}\n`);
|
||||
};
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
run();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
||||
import { existsSync, openSync, closeSync, readFileSync, readSync, readdirSync, rmSync, statSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { listPackage } from "@electron/asar";
|
||||
import { assertMacIcnsComplete } from "./desktop-icns.mjs";
|
||||
@@ -33,6 +33,22 @@ const verifyMachOArch = (path, arch, label) => {
|
||||
assert.deepEqual(result.stdout.trim().split(/\s+/), [expectedArch], `${label} must contain only ${expectedArch}`);
|
||||
};
|
||||
|
||||
const verifyPeX64 = (path, label) => {
|
||||
const descriptor = openSync(path, "r");
|
||||
try {
|
||||
const dosHeader = Buffer.alloc(64);
|
||||
assert.equal(readSync(descriptor, dosHeader, 0, dosHeader.length, 0), dosHeader.length, `${label} DOS header is incomplete`);
|
||||
assert.equal(dosHeader.toString("ascii", 0, 2), "MZ", `${label} is not a PE executable`);
|
||||
const peOffset = dosHeader.readUInt32LE(0x3c);
|
||||
const peHeader = Buffer.alloc(6);
|
||||
assert.equal(readSync(descriptor, peHeader, 0, peHeader.length, peOffset), peHeader.length, `${label} PE header is incomplete`);
|
||||
assert.equal(peHeader.toString("ascii", 0, 4), "PE\0\0", `${label} PE signature is invalid`);
|
||||
assert.equal(peHeader.readUInt16LE(4), 0x8664, `${label} must target Windows x64`);
|
||||
} finally {
|
||||
closeSync(descriptor);
|
||||
}
|
||||
};
|
||||
|
||||
for (const sidecarPath of files.filter((path) => /[\\/]resources[\\/]sidecar[\\/]edgeever-sidecar(?:\.exe)?$/i.test(path))) {
|
||||
const bundleRoot = sidecarPath.replace(/[\\/]resources[\\/]sidecar[\\/]edgeever-sidecar(?:\.exe)?$/i, "");
|
||||
assert.ok(existsSync(join(bundleRoot, "resources", "web", "index.html")), `Desktop bundle is missing the Web renderer: ${bundleRoot}`);
|
||||
@@ -69,9 +85,24 @@ if (requestedPlatform === "darwin") {
|
||||
// disposable unpacked bundle once its contents have passed verification.
|
||||
rmSync(unpackedApp, { recursive: true, force: true });
|
||||
} else if (requestedPlatform === "win32") {
|
||||
const installer = matchingPrefix(`EdgeEver-${version}-windows-`).some((path) => path.endsWith(".exe"));
|
||||
const unpacked = existsSync(join(outputDirectory, "win-arm64-unpacked", "EdgeEver.exe"));
|
||||
assert.ok(installer || unpacked, "Windows package must contain the current NSIS installer or an unpacked executable");
|
||||
assert.equal(requestedArch, "x64", `Windows release package must target x64, received: ${requestedArch}`);
|
||||
const installer = join(outputDirectory, `EdgeEver-${version}-windows-x64.exe`);
|
||||
const unpackedDirectory = join(outputDirectory, "win-unpacked");
|
||||
const executable = join(unpackedDirectory, "EdgeEver.exe");
|
||||
const resources = join(unpackedDirectory, "resources");
|
||||
const sidecar = join(resources, "sidecar", "edgeever-sidecar.exe");
|
||||
assert.ok(existsSync(installer), `Windows package must contain the current x64 NSIS installer: ${installer}`);
|
||||
assert.ok(existsSync(executable), `Windows package must contain the unpacked x64 executable: ${executable}`);
|
||||
assert.ok(existsSync(sidecar), `Windows package must contain the x64 sidecar: ${sidecar}`);
|
||||
assert.ok(existsSync(join(resources, "web", "index.html")), "Windows app bundle is missing the Web renderer");
|
||||
assert.ok(existsSync(join(resources, "migrations")), "Windows app bundle is missing migrations");
|
||||
verifyPeX64(executable, "Electron executable");
|
||||
verifyPeX64(sidecar, "Rust sidecar");
|
||||
const asarPath = join(resources, "app.asar");
|
||||
assert.ok(existsSync(asarPath), `Windows app bundle is missing app.asar: ${asarPath}`);
|
||||
const asarFiles = new Set(listPackage(asarPath));
|
||||
assert.ok(asarFiles.has("/src/preload/index.cjs"), "Windows app bundle must contain the sandbox-compatible CommonJS preload");
|
||||
assert.ok(!asarFiles.has("/src/preload/index.mjs"), "Windows app bundle must not contain the unsupported ESM preload");
|
||||
} else if (requestedPlatform === "linux") {
|
||||
assert.ok(matchingPrefix(`EdgeEver-${version}-linux-`).some((path) => path.endsWith(".AppImage")), "Linux package must contain the current AppImage");
|
||||
const unpacked = files.find((path) => path.endsWith("/resources/sidecar/edgeever-sidecar") && path.includes("linux-") && path.includes("-unpacked"));
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
WINDOWS_UPDATE_MANIFEST_NAME,
|
||||
WINDOWS_UPDATE_SIGNATURE_NAME,
|
||||
verifyDownloadedWindowsUpdate,
|
||||
verifyWindowsUpdateMetadata,
|
||||
} from "../apps/desktop/src/main/windows-update-trust.mjs";
|
||||
|
||||
export const verifyWindowsUpdateRelease = async (directory) => {
|
||||
const manifestBytes = readFileSync(join(directory, WINDOWS_UPDATE_MANIFEST_NAME));
|
||||
const signatureBytes = readFileSync(join(directory, WINDOWS_UPDATE_SIGNATURE_NAME));
|
||||
const untrustedManifest = JSON.parse(manifestBytes.toString("utf8"));
|
||||
const expectedNames = [
|
||||
untrustedManifest.file?.name,
|
||||
"latest.yml",
|
||||
WINDOWS_UPDATE_MANIFEST_NAME,
|
||||
WINDOWS_UPDATE_SIGNATURE_NAME,
|
||||
"SHA256SUMS-windows.txt",
|
||||
].sort();
|
||||
const actualNames = readdirSync(directory).sort();
|
||||
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
|
||||
throw new Error(`Windows update audit expected exactly: ${expectedNames.join(", ")}`);
|
||||
}
|
||||
const manifest = verifyWindowsUpdateMetadata({
|
||||
manifestBytes,
|
||||
signatureBytes,
|
||||
expectedVersion: untrustedManifest.version,
|
||||
updateInfo: {
|
||||
version: untrustedManifest.version,
|
||||
files: [{
|
||||
url: untrustedManifest.file?.name,
|
||||
size: untrustedManifest.file?.size,
|
||||
sha512: untrustedManifest.file?.sha512,
|
||||
}],
|
||||
},
|
||||
});
|
||||
await verifyDownloadedWindowsUpdate({
|
||||
path: join(directory, manifest.file.name),
|
||||
manifest,
|
||||
});
|
||||
|
||||
const latestYml = readFileSync(join(directory, "latest.yml"), "utf8");
|
||||
const expectedLatestYml = [
|
||||
`version: ${manifest.version}`,
|
||||
"files:",
|
||||
` - url: ${manifest.file.name}`,
|
||||
` sha512: ${manifest.file.sha512}`,
|
||||
` size: ${manifest.file.size}`,
|
||||
`path: ${manifest.file.name}`,
|
||||
`sha512: ${manifest.file.sha512}`,
|
||||
`releaseDate: '${manifest.releaseDate}'`,
|
||||
"",
|
||||
].join("\n");
|
||||
if (latestYml !== expectedLatestYml) {
|
||||
throw new Error("latest.yml does not exactly match the signed manifest");
|
||||
}
|
||||
const checksum = readFileSync(join(directory, "SHA256SUMS-windows.txt"), "utf8");
|
||||
if (checksum !== `${manifest.file.sha256} ${manifest.file.name}\n`) {
|
||||
throw new Error("SHA256SUMS-windows.txt does not match the signed manifest");
|
||||
}
|
||||
return manifest;
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const [directoryValue] = process.argv.slice(2);
|
||||
if (!directoryValue) {
|
||||
throw new Error("Usage: node scripts/verify-windows-update-release.mjs <directory>");
|
||||
}
|
||||
const manifest = await verifyWindowsUpdateRelease(resolve(directoryValue));
|
||||
process.stdout.write(`Verified ${manifest.file.name} with ${manifest.keyId}\n`);
|
||||
};
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
await run();
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { generateKeyPairSync } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createWindowsUpdateMetadata } from "./create-windows-update-metadata.mjs";
|
||||
import {
|
||||
assertWindowsUpdateSigningKey,
|
||||
signWindowsUpdateManifest,
|
||||
} from "./sign-windows-update-manifest.mjs";
|
||||
import { verifyWindowsUpdateRelease } from "./verify-windows-update-release.mjs";
|
||||
|
||||
describe("Windows update release metadata", () => {
|
||||
test("creates installer metadata consumed by electron-updater and the trust gate", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "edgeever-windows-metadata-"));
|
||||
try {
|
||||
await writeFile(join(directory, "EdgeEver-1.49.0-windows-x64.exe"), "payload");
|
||||
const manifest = await createWindowsUpdateMetadata({ directory, version: "1.49.0" });
|
||||
expect(manifest.file).toMatchObject({
|
||||
name: "EdgeEver-1.49.0-windows-x64.exe",
|
||||
size: 7,
|
||||
sha256: "239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5",
|
||||
});
|
||||
expect(await readFile(join(directory, "latest.yml"), "utf8"))
|
||||
.toContain(`sha512: ${manifest.file.sha512}`);
|
||||
expect(await readFile(join(directory, "SHA256SUMS-windows.txt"), "utf8"))
|
||||
.toBe(`${manifest.file.sha256} ${manifest.file.name}\n`);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("signs only with the private key matching the expected public key", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "edgeever-windows-signing-"));
|
||||
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
||||
const { publicKey: otherPublicKey } = generateKeyPairSync("ed25519");
|
||||
const manifestPath = join(directory, "latest-windows.json");
|
||||
const signaturePath = `${manifestPath}.sig`;
|
||||
const privateKeyPath = join(directory, "private.pem");
|
||||
try {
|
||||
await writeFile(manifestPath, `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
keyId: "edgeever-windows-update-2026-01",
|
||||
version: "1.49.0",
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
releaseDate: "2026-08-30T00:00:00.000Z",
|
||||
file: {
|
||||
name: "EdgeEver-1.49.0-windows-x64.exe",
|
||||
size: 7,
|
||||
sha512: "cLM86ckEfjD5F+fqE+Qvd2cAjD9PnJuvSeQ5D8YlVJ6WJe7jm5RUUHTooYJM8/I4RjsRvAPZc0jg/CmZyh//fw==",
|
||||
sha256: "239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5",
|
||||
},
|
||||
})}\n`);
|
||||
await writeFile(privateKeyPath, privateKey.export({ type: "pkcs8", format: "pem" }));
|
||||
expect(assertWindowsUpdateSigningKey({
|
||||
privateKeyPath,
|
||||
expectedPublicKey: publicKey,
|
||||
}).asymmetricKeyType).toBe("ed25519");
|
||||
expect(signWindowsUpdateManifest({
|
||||
manifestPath,
|
||||
signaturePath,
|
||||
privateKeyPath,
|
||||
expectedPublicKey: publicKey,
|
||||
}).signature).toHaveLength(88);
|
||||
expect(() => signWindowsUpdateManifest({
|
||||
manifestPath,
|
||||
signaturePath,
|
||||
privateKeyPath,
|
||||
expectedPublicKey: otherPublicKey,
|
||||
})).toThrow("does not match");
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("audits the complete production release asset set", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "edgeever-windows-release-audit-"));
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
keyId: "edgeever-windows-update-2026-01",
|
||||
version: "1.49.0",
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
releaseDate: "2026-08-30T00:00:00.000Z",
|
||||
file: {
|
||||
name: "EdgeEver-1.49.0-windows-x64.exe",
|
||||
size: 7,
|
||||
sha512: "cLM86ckEfjD5F+fqE+Qvd2cAjD9PnJuvSeQ5D8YlVJ6WJe7jm5RUUHTooYJM8/I4RjsRvAPZc0jg/CmZyh//fw==",
|
||||
sha256: "239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5",
|
||||
},
|
||||
};
|
||||
try {
|
||||
await writeFile(join(directory, manifest.file.name), "payload");
|
||||
await writeFile(join(directory, "latest-windows.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
await writeFile(join(directory, "latest-windows.json.sig"), `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
keyId: manifest.keyId,
|
||||
signature: "Itc3UD0G6Hn6BWdk9qlJ4SoqAUDn8efi1VRVUWDtA3Ip5+gwpAj7HxElxM6D/GXcAlHFxnVv19soEWxlfZLCDA==",
|
||||
})}\n`);
|
||||
await writeFile(join(directory, "latest.yml"), [
|
||||
`version: ${manifest.version}`,
|
||||
"files:",
|
||||
` - url: ${manifest.file.name}`,
|
||||
` sha512: ${manifest.file.sha512}`,
|
||||
` size: ${manifest.file.size}`,
|
||||
`path: ${manifest.file.name}`,
|
||||
`sha512: ${manifest.file.sha512}`,
|
||||
`releaseDate: '${manifest.releaseDate}'`,
|
||||
"",
|
||||
].join("\n"));
|
||||
await writeFile(
|
||||
join(directory, "SHA256SUMS-windows.txt"),
|
||||
`${manifest.file.sha256} ${manifest.file.name}\n`,
|
||||
);
|
||||
await expect(verifyWindowsUpdateRelease(directory)).resolves.toEqual(manifest);
|
||||
await writeFile(join(directory, manifest.file.name), "tampered");
|
||||
await expect(verifyWindowsUpdateRelease(directory)).rejects.toThrow("size does not match");
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user