release: prepare 0.6.2 Linux and Windows packages
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Desktop builds and releases
|
||||
|
||||
For this fork’s Linux and Windows 0.6.0 release on Gitea, see [Gitea releases](gitea-release.md).
|
||||
For this fork’s Linux and Windows 0.6.2 release on Gitea, see [Gitea releases](gitea-release.md).
|
||||
The three-platform GitHub process below documents the upstream reference.
|
||||
|
||||
## Platforms
|
||||
|
||||
+61
-2
@@ -2,7 +2,8 @@
|
||||
|
||||
The release destination is
|
||||
https://git.sorokin-oleg-edu.xyz/sorokin.oleg.edu/learnharness.
|
||||
Version **0.6.1 is Linux x64 AppImage-only**; follow the dedicated procedure below.
|
||||
Version **0.6.2 targets Linux x64 AppImage and Windows x64 NSIS**; follow the
|
||||
dedicated procedure below. Version 0.6.1 was Linux x64 AppImage-only.
|
||||
The historical **0.6.0 release targets Linux x64 and Windows x64**, with each platform built
|
||||
and verified on its native operating system. The upstream GitHub workflow and three-platform packaging
|
||||
configuration remain available as reference in `.github/workflows/release.yml`
|
||||
@@ -19,7 +20,65 @@ Gitea's `latest` alias resolves the most recent published, non-prerelease releas
|
||||
Drafts do not become updates. The repository and published downloads must be
|
||||
public; no API credential is embedded in the application.
|
||||
|
||||
## 0.6.1: Linux x64 AppImage-only upload and verification
|
||||
## 0.6.2: Linux AppImage and Windows installer
|
||||
|
||||
Build both platforms from the same release commit with Node 26. Use a clean
|
||||
checkout on each native operating system so local credentials, environment
|
||||
files, application data, and older packages cannot enter the standalone payload.
|
||||
The Windows installer retains the existing unsigned per-user NSIS configuration.
|
||||
|
||||
After dependency installation, run the typecheck, deterministic tests, lint,
|
||||
native renderer tests, and standalone runtime smoke checks. Include
|
||||
`tests/desktop-logger.test.ts` under each platform's bundled Electron Node to
|
||||
exercise the broken-pipe and retention regressions in the shipped runtime.
|
||||
Curate the standalone payload with `node scripts/build-server.mjs`, then package:
|
||||
|
||||
```sh
|
||||
# Native Linux checkout
|
||||
npx electron-builder --config electron-builder.gitea.yml --linux AppImage --x64 --publish never
|
||||
|
||||
# Native Windows checkout
|
||||
npx electron-builder --config electron-builder.gitea.yml --win --x64 --publish never
|
||||
```
|
||||
|
||||
Verify each packaged updater with `scripts/gitea-release.mjs verify-package` and
|
||||
run the provider, server, and browser-helper smoke checks against that package's
|
||||
own standalone tree and executable. Copy only these six assets into a staging
|
||||
directory (the checksum command creates the final file):
|
||||
|
||||
- `LearnHarness-0.6.2-x86_64.AppImage`
|
||||
- `LearnHarness-0.6.2-x64-setup.exe`
|
||||
- `LearnHarness-0.6.2-x64-setup.exe.blockmap`
|
||||
- `latest-linux.yml`
|
||||
- `latest.yml`
|
||||
- `SHA256SUMS`
|
||||
|
||||
```sh
|
||||
node scripts/gitea-release.mjs checksums /path/to/staging --appimage-windows
|
||||
node scripts/gitea-release.mjs verify /path/to/staging --appimage-windows
|
||||
```
|
||||
|
||||
This explicit scope preserves the historical Linux defaults and AppImage-only
|
||||
mode. It rejects extra staged files, checks both installer headers and updater
|
||||
feeds against the actual bytes, regenerates the Windows blockmap with the pinned
|
||||
packager to verify every chunk and full installer coverage, and verifies the
|
||||
combined SHA-256 manifest. It includes no Debian, RPM, or macOS packages.
|
||||
|
||||
Commit and push the release source and its matching annotated `v0.6.2` tag.
|
||||
Supply the Gitea API token privately, then prepare and verify the draft:
|
||||
|
||||
```sh
|
||||
node scripts/gitea-release.mjs upload /path/to/staging --appimage-windows
|
||||
node scripts/gitea-release.mjs verify-remote /path/to/staging --appimage-windows
|
||||
```
|
||||
|
||||
The uploader reads notes from `docs/release-0.6.2.md`, requires the remote tag,
|
||||
keeps the release a stable draft, and downloads all six attachments to compare
|
||||
their SHA-256 hashes with the validated local files. It refuses published or
|
||||
ambiguous releases and mismatched existing attachments. Publication follows
|
||||
human review of the completed draft.
|
||||
|
||||
## Historical 0.6.1: Linux x64 AppImage-only upload and verification
|
||||
|
||||
This release has exactly three attachments:
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# LearnHarness 0.6.2
|
||||
|
||||
## English
|
||||
|
||||
This release fixes a critical desktop logging bug that could consume hundreds of
|
||||
gigabytes of disk space when the application's output pipe closed.
|
||||
|
||||
- Stops the repeated `EPIPE` exception loop after a launcher or terminal closes
|
||||
its output pipe. Uncaught errors are recorded directly to the log file.
|
||||
- Keeps desktop diagnostics within two files of 5 MiB each, for a maximum of
|
||||
10 MiB. Existing oversized logs are trimmed on startup, retaining recent data.
|
||||
- Handles output backpressure, full disks, and log rotation failures without
|
||||
starting another logging failure loop.
|
||||
|
||||
Packages: Linux x64 AppImage and Windows x64 NSIS installer. Both receive updates
|
||||
from this fork's Gitea releases. The Windows installer remains unsigned.
|
||||
|
||||
## Русский
|
||||
|
||||
Выпуск исправляет критическую ошибку журналирования: после закрытия канала
|
||||
вывода приложение могло занять сотни гигабайт на диске.
|
||||
|
||||
- Устранён цикл повторных исключений `EPIPE` после закрытия канала вывода
|
||||
терминалом или программой запуска. Необработанные ошибки записываются
|
||||
непосредственно в файл журнала.
|
||||
- Журналы ограничены двумя файлами по 5 МиБ, всего до 10 МиБ. При запуске
|
||||
прежние слишком большие журналы сокращаются с сохранением последних данных.
|
||||
- Переполнение канала вывода, заполненный диск и ошибки ротации журналов
|
||||
больше не вызывают новый цикл ошибок журналирования.
|
||||
|
||||
Пакеты: AppImage для Linux x64 и установщик NSIS для Windows x64. Обе версии
|
||||
получают обновления из выпусков этого форка в Gitea. Установщик Windows
|
||||
по-прежнему не подписан.
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "learnharness",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "learnharness",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "1.4.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "learnharness",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": {
|
||||
"name": "Amos Blomqvist",
|
||||
|
||||
+56
-24
@@ -1,4 +1,4 @@
|
||||
/** Prepare and verify this fork's Linux release; publication is always a separate human action. */
|
||||
/** Prepare and verify this fork's desktop release; publication is a separate human action. */
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
@@ -9,6 +9,7 @@ import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const yaml = require("js-yaml");
|
||||
@@ -22,6 +23,13 @@ export function expectedLinuxAssets(version, { appimageOnly = false } = {}) {
|
||||
assert.match(version, /^\d+\.\d+\.\d+$/, "release version must be a stable semver");
|
||||
return [`LearnHarness-${version}-x86_64.AppImage`, ...(!appimageOnly ? [`LearnHarness-${version}-amd64.deb`] : []), "latest-linux.yml", "SHA256SUMS"];
|
||||
}
|
||||
export function expectedReleaseAssets(version, { appimageOnly = false, appimageWindows = false } = {}) {
|
||||
assert(!(appimageOnly && appimageWindows), "release scopes are mutually exclusive");
|
||||
const linux = expectedLinuxAssets(version, { appimageOnly: appimageOnly || appimageWindows });
|
||||
if (!appimageWindows) return linux;
|
||||
const exe = `LearnHarness-${version}-x64-setup.exe`;
|
||||
return [linux[0], exe, `${exe}.blockmap`, "latest-linux.yml", "latest.yml", "SHA256SUMS"];
|
||||
}
|
||||
export async function fileDigest(file, algorithm, encoding = "hex") {
|
||||
const hash = createHash(algorithm);
|
||||
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
|
||||
@@ -29,7 +37,7 @@ export async function fileDigest(file, algorithm, encoding = "hex") {
|
||||
}
|
||||
|
||||
export async function writeChecksums(directory, version, options = {}) {
|
||||
const names = expectedLinuxAssets(version, options).filter((name) => name !== "SHA256SUMS");
|
||||
const names = expectedReleaseAssets(version, options).filter((name) => name !== "SHA256SUMS");
|
||||
const lines = [];
|
||||
for (const name of names) {
|
||||
const file = path.join(directory, name);
|
||||
@@ -48,7 +56,11 @@ export async function verifyPackagedUpdater(directory) {
|
||||
}
|
||||
|
||||
export async function verifyLocal(directory, version, options = {}) {
|
||||
const names = expectedLinuxAssets(version, options);
|
||||
const names = expectedReleaseAssets(version, options);
|
||||
if (options.appimageWindows) {
|
||||
const staged = (await fsp.readdir(directory, { withFileTypes: true })).filter((entry) => !entry.isDirectory()).map((entry) => entry.name);
|
||||
assert.deepEqual(staged.sort(), [...names].sort(), "release directory must contain exactly the six approved assets");
|
||||
}
|
||||
const inventory = new Map();
|
||||
for (const name of names) {
|
||||
const file = path.join(directory, name);
|
||||
@@ -56,22 +68,36 @@ export async function verifyLocal(directory, version, options = {}) {
|
||||
assert(stat.isFile() && stat.size > 0, `${name} must be a nonempty regular file`);
|
||||
inventory.set(name, { name, size: stat.size, sha256: await fileDigest(file, "sha256") });
|
||||
}
|
||||
for (const name of names.filter((name) => /\.(AppImage|deb)$/.test(name))) {
|
||||
const magic = name.endsWith(".AppImage") ? Buffer.from([0x7f, 0x45, 0x4c, 0x46]) : Buffer.from("!<arch>\n");
|
||||
for (const name of names.filter((name) => /\.(AppImage|deb|exe)$/.test(name))) {
|
||||
const magic = name.endsWith(".AppImage") ? Buffer.from([0x7f, 0x45, 0x4c, 0x46]) : Buffer.from(name.endsWith(".exe") ? "MZ" : "!<arch>\n");
|
||||
const handle = await fsp.open(path.join(directory, name), "r");
|
||||
try { const header = Buffer.alloc(magic.length); await handle.read(header, 0, header.length, 0); assert(header.equals(magic), `${name} has an invalid installer header`); } finally { await handle.close(); }
|
||||
}
|
||||
const feed = yaml.load(await fsp.readFile(path.join(directory, "latest-linux.yml"), "utf8"));
|
||||
assert.equal(feed?.version, version, "update metadata version differs from the package");
|
||||
assert(Array.isArray(feed.files), "update metadata must contain files");
|
||||
const installers = names.filter((name) => /\.(AppImage|deb)$/.test(name));
|
||||
assert.deepEqual(feed.files.map((entry) => entry.url).sort(), [...installers].sort(), "update feed must reference exactly the Linux installers by filename");
|
||||
for (const entry of feed.files) {
|
||||
assert.equal(entry.size, inventory.get(entry.url)?.size, `${entry.url} update size differs`);
|
||||
assert.equal(entry.sha512, await fileDigest(path.join(directory, entry.url), "sha512", "base64"), `${entry.url} update checksum differs`);
|
||||
for (const feedName of ["latest-linux.yml", ...(options.appimageWindows ? ["latest.yml"] : [])]) {
|
||||
const feed = yaml.load(await fsp.readFile(path.join(directory, feedName), "utf8"));
|
||||
assert.equal(feed?.version, version, "update metadata version differs from the package");
|
||||
assert(Array.isArray(feed.files), "update metadata must contain files");
|
||||
const installers = names.filter((name) => (feedName === "latest.yml" ? /\.exe$/ : /\.(AppImage|deb)$/).test(name));
|
||||
assert.deepEqual(feed.files.map((entry) => entry.url).sort(), [...installers].sort(), "update feed must reference exactly the platform installers by filename");
|
||||
for (const entry of feed.files) {
|
||||
assert.equal(entry.size, inventory.get(entry.url)?.size, `${entry.url} update size differs`);
|
||||
assert.equal(entry.sha512, await fileDigest(path.join(directory, entry.url), "sha512", "base64"), `${entry.url} update checksum differs`);
|
||||
}
|
||||
assert.equal(feed.path, feed.files[0].url, "legacy updater path differs");
|
||||
assert.equal(feed.sha512, feed.files[0].sha512, "legacy updater checksum differs");
|
||||
}
|
||||
if (options.appimageWindows) {
|
||||
// Regenerate with the pinned packager's streaming chunker: this checks
|
||||
// every block checksum and full EXE coverage, rather than just its shape.
|
||||
const { buildBlockMap } = require("app-builder-lib/out/targets/blockmap/blockmap.js");
|
||||
const exe = names.find((name) => name.endsWith(".exe"));
|
||||
const temporary = await fsp.mkdtemp(path.join(os.tmpdir(), "lh-blockmap-verify-"));
|
||||
try {
|
||||
const regenerated = path.join(temporary, "installer.blockmap");
|
||||
await buildBlockMap(path.join(directory, exe), "gzip", regenerated);
|
||||
assert.deepEqual(JSON.parse(gunzipSync(await fsp.readFile(path.join(directory, `${exe}.blockmap`))).toString()), JSON.parse(gunzipSync(await fsp.readFile(regenerated)).toString()), "Windows blockmap differs from the installer bytes");
|
||||
} finally { await fsp.rm(temporary, { recursive: true, force: true }); }
|
||||
}
|
||||
assert.equal(feed.path, feed.files[0].url, "legacy updater path differs");
|
||||
assert.equal(feed.sha512, feed.files[0].sha512, "legacy updater checksum differs");
|
||||
const checksums = (await fsp.readFile(path.join(directory, "SHA256SUMS"), "utf8")).trim().split("\n");
|
||||
assert.deepEqual(checksums, names.filter((name) => name !== "SHA256SUMS").map((name) => `${inventory.get(name).sha256} ${name}`), "SHA256SUMS does not match the release bytes");
|
||||
return [...inventory.values()];
|
||||
@@ -157,24 +183,30 @@ async function upload(directory, version, options = {}) {
|
||||
async function main() {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
const appimageOnly = args.includes("--appimage-only");
|
||||
assert(!appimageOnly || ["checksums", "verify", "upload", "verify-remote"].includes(command), "--appimage-only is supported only by checksums, verify, upload and verify-remote");
|
||||
assert(args.filter((arg) => arg === "--appimage-only").length <= 1, "duplicate --appimage-only option");
|
||||
assert(args.every((arg) => !arg.startsWith("--") || arg === "--appimage-only"), "unknown release option");
|
||||
const positional = args.filter((arg) => arg !== "--appimage-only");
|
||||
const appimageWindows = args.includes("--appimage-windows");
|
||||
const scopeFlags = ["--appimage-only", "--appimage-windows"];
|
||||
for (const flag of scopeFlags) {
|
||||
assert(!args.includes(flag) || ["checksums", "verify", "upload", "verify-remote"].includes(command), `${flag} is supported only by checksums, verify, upload and verify-remote`);
|
||||
assert(args.filter((arg) => arg === flag).length <= 1, `duplicate ${flag} option`);
|
||||
}
|
||||
assert(!(appimageOnly && appimageWindows), "release scopes are mutually exclusive");
|
||||
assert(args.every((arg) => !arg.startsWith("--") || scopeFlags.includes(arg)), "unknown release option");
|
||||
const positional = args.filter((arg) => !scopeFlags.includes(arg));
|
||||
assert(positional.length <= 1, "expected at most one release directory");
|
||||
const directory = positional[0] ?? "dist";
|
||||
const { version } = JSON.parse(await fsp.readFile(path.join(ROOT, "package.json"), "utf8"));
|
||||
if (command === "checksums") await writeChecksums(directory, version, { appimageOnly });
|
||||
const options = { appimageOnly, appimageWindows };
|
||||
if (command === "checksums") await writeChecksums(directory, version, options);
|
||||
else if (command === "verify-package") await verifyPackagedUpdater(directory);
|
||||
else if (command === "verify") console.log(JSON.stringify(await verifyLocal(directory, version, { appimageOnly }), null, 2));
|
||||
else if (command === "upload") await upload(directory, version, { appimageOnly });
|
||||
else if (command === "verify") console.log(JSON.stringify(await verifyLocal(directory, version, options), null, 2));
|
||||
else if (command === "upload") await upload(directory, version, options);
|
||||
else if (command === "verify-remote") {
|
||||
const inventory = await verifyLocal(directory, version, { appimageOnly });
|
||||
const inventory = await verifyLocal(directory, version, options);
|
||||
const release = await selectDraft(`v${version}`);
|
||||
assert(release, "draft not found");
|
||||
await verifyRemote(release, inventory, authorization());
|
||||
console.log(`Verified draft: ${release.html_url}`);
|
||||
} else throw new Error("Usage: node scripts/gitea-release.mjs checksums|verify|upload|verify-remote [dist-directory] [--appimage-only], or verify-package [unpacked-app-directory]");
|
||||
} else throw new Error("Usage: node scripts/gitea-release.mjs checksums|verify|upload|verify-remote [dist-directory] [--appimage-only|--appimage-windows], or verify-package [unpacked-app-directory]");
|
||||
}
|
||||
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
||||
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
||||
|
||||
@@ -6,7 +6,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { after, test } from "node:test";
|
||||
import { createRequire } from "node:module";
|
||||
import { expectedLinuxAssets, writeChecksums, verifyLocal, verifyRemote, verifyPackagedUpdater, UPDATE_URL } from "../scripts/gitea-release.mjs";
|
||||
import { gzipSync, gunzipSync } from "node:zlib";
|
||||
import { expectedLinuxAssets, expectedReleaseAssets, writeChecksums, verifyLocal, verifyRemote, verifyPackagedUpdater, UPDATE_URL } from "../scripts/gitea-release.mjs";
|
||||
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "lh-gitea-gate-"));
|
||||
after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
@@ -186,7 +187,7 @@ function mockedReleaseCommand(directory: string, command: string, options: strin
|
||||
const server = "https://git.sorokin-oleg-edu.xyz";
|
||||
const api = "/api/v1/repos/sorokin.oleg.edu/learnharness";
|
||||
const calls = { api: [], uploads: [], downloads: [], edits: [] };
|
||||
const files = (await fs.readdir(directory)).filter(name => name.endsWith(".AppImage") || name.endsWith(".deb") || ["latest-linux.yml", "SHA256SUMS"].includes(name));
|
||||
const files = (await fs.readdir(directory)).filter(name => /\.(AppImage|deb|exe|blockmap)$/.test(name) || ["latest-linux.yml", "latest.yml", "SHA256SUMS"].includes(name));
|
||||
const assets = await Promise.all(files.map(async name => ({ name, size: (await fs.stat(path.join(directory, name))).size, browser_download_url: server + "/sorokin.oleg.edu/learnharness/releases/download/" + tag + "/" + name })));
|
||||
const release = { id: 1, tag_name: tag, draft: scenario !== "published", prerelease: scenario === "prerelease", assets: scenario === "new" ? [] : [...assets], html_url: server + "/draft" };
|
||||
if (scenario === "missing-asset") release.assets.pop();
|
||||
@@ -334,3 +335,85 @@ test("packaged updater verification refuses inherited GitHub or incorrect generi
|
||||
await fs.writeFile(file, JSON.stringify({ provider: "generic", url: UPDATE_URL }));
|
||||
await verifyPackagedUpdater(directory);
|
||||
});
|
||||
|
||||
const desktopOptions = { appimageWindows: true };
|
||||
async function desktopFixture(name: string, fixtureVersion = "0.6.2") {
|
||||
const directory = await fixture(name, true, fixtureVersion);
|
||||
const exe = `LearnHarness-${fixtureVersion}-x64-setup.exe`;
|
||||
const bytes = Buffer.from(`MZ${"synthetic installer content ".repeat(2000)}`);
|
||||
await fs.writeFile(path.join(directory, exe), bytes);
|
||||
const require = createRequire(import.meta.url);
|
||||
await require("app-builder-lib/out/targets/blockmap/blockmap.js").buildBlockMap(path.join(directory, exe), "gzip", path.join(directory, `${exe}.blockmap`));
|
||||
const entry = { url: exe, size: bytes.length, sha512: createHash("sha512").update(bytes).digest("base64") };
|
||||
await fs.writeFile(path.join(directory, "latest.yml"), JSON.stringify({ version: fixtureVersion, files: [entry], path: exe, sha512: entry.sha512 }));
|
||||
await writeChecksums(directory, fixtureVersion, desktopOptions);
|
||||
return directory;
|
||||
}
|
||||
|
||||
test("AppImage and Windows release has exactly the six approved assets and exclusive scopes", async () => {
|
||||
assert.deepEqual(expectedReleaseAssets("0.6.2", desktopOptions), [
|
||||
"LearnHarness-0.6.2-x86_64.AppImage", "LearnHarness-0.6.2-x64-setup.exe", "LearnHarness-0.6.2-x64-setup.exe.blockmap",
|
||||
"latest-linux.yml", "latest.yml", "SHA256SUMS",
|
||||
]);
|
||||
assert.throws(() => expectedReleaseAssets("0.6.2", { ...desktopOptions, appimageOnly: true }), /mutually exclusive/);
|
||||
assert.deepEqual(expectedReleaseAssets(version), names);
|
||||
const directory = await desktopFixture("desktop-healthy");
|
||||
assert.equal((await verifyLocal(directory, "0.6.2", desktopOptions)).length, 6);
|
||||
await fs.writeFile(path.join(directory, "stale.deb"), "stale asset");
|
||||
await assert.rejects(verifyLocal(directory, "0.6.2", desktopOptions), /exactly the six approved/);
|
||||
});
|
||||
|
||||
for (const feedName of ["latest-linux.yml", "latest.yml"]) {
|
||||
for (const mutation of ["version", "path", "hash", "size", "duplicate", "legacy"] as const) {
|
||||
test(`combined release refuses ${feedName} ${mutation} drift despite fresh SHA256SUMS`, async () => {
|
||||
const directory = await desktopFixture(`desktop-${feedName}-${mutation}`);
|
||||
const file = path.join(directory, feedName);
|
||||
const feed = JSON.parse(await fs.readFile(file, "utf8"));
|
||||
if (mutation === "version") feed.version = "0.6.1";
|
||||
if (mutation === "path") feed.files[0].url = "../" + feed.files[0].url;
|
||||
if (mutation === "hash") feed.files[0].sha512 = "bad";
|
||||
if (mutation === "size") feed.files[0].size++;
|
||||
if (mutation === "duplicate") feed.files.push({ ...feed.files[0] });
|
||||
if (mutation === "legacy") feed.path = "unexpected.exe";
|
||||
await fs.writeFile(file, JSON.stringify(feed));
|
||||
await writeChecksums(directory, "0.6.2", desktopOptions);
|
||||
await assert.rejects(verifyLocal(directory, "0.6.2", desktopOptions), /metadata version|reference exactly|update checksum|update size|legacy updater path/);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const mutation of ["checksum", "coverage", "corrupt"] as const) {
|
||||
test(`combined release checks Windows blockmap ${mutation} against actual EXE bytes`, async () => {
|
||||
const directory = await desktopFixture(`desktop-blockmap-${mutation}`);
|
||||
const file = path.join(directory, "LearnHarness-0.6.2-x64-setup.exe.blockmap");
|
||||
const map = JSON.parse(gunzipSync(await fs.readFile(file)).toString());
|
||||
if (mutation === "checksum") map.files[0].checksums[0] = "bad";
|
||||
if (mutation === "coverage") map.files[0].sizes[0]--;
|
||||
await fs.writeFile(file, mutation === "corrupt" ? "not gzip" : gzipSync(JSON.stringify(map)));
|
||||
await writeChecksums(directory, "0.6.2", desktopOptions);
|
||||
await assert.rejects(verifyLocal(directory, "0.6.2", desktopOptions), /Windows blockmap differs|incorrect header/);
|
||||
});
|
||||
}
|
||||
|
||||
test("combined release uploads and downloads exactly six assets and keeps the release a draft", async () => {
|
||||
const pkg = JSON.parse(await fs.readFile(new URL("../package.json", import.meta.url), "utf8"));
|
||||
const directory = await desktopFixture("desktop-api", pkg.version);
|
||||
const expected = expectedReleaseAssets(pkg.version, desktopOptions).sort();
|
||||
for (const [command, scenario] of [["upload", "new"], ["upload", "draft"], ["verify-remote", "draft"]]) {
|
||||
const result = mockedReleaseCommand(directory, command!, ["--appimage-windows"], scenario);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.calls.downloads.sort(), expected);
|
||||
assert.deepEqual(result.calls.uploads.sort(), scenario === "new" ? expected : []);
|
||||
assert(result.calls.edits.every(edit => edit.draft));
|
||||
}
|
||||
for (const scenario of ["extra-asset", "missing-asset", "corrupt", "published"]) {
|
||||
const result = mockedReleaseCommand(directory, "verify-remote", ["--appimage-windows"], scenario);
|
||||
assert.equal(result.status, 1, result.stderr);
|
||||
assert(result.calls.api.every(call => call.startsWith("GET ")));
|
||||
}
|
||||
for (const options of [["--appimage-windows", "--appimage-only"], ["--appimage-windows", "--appimage-windows"]]) {
|
||||
const result = mockedReleaseCommand(directory, "upload", options);
|
||||
assert.equal(result.status, 1);
|
||||
assert.deepEqual(result.calls.api, []);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user