All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
latest.json only had linux-x86_64, built from the .deb. An .rpm install looks up linux-x86_64-rpm first, then fell back on linux-x86_64, downloaded the .deb and refused it (InvalidUpdaterFormat). - scripts/build-latest-json.sh writes linux-x86_64-deb, linux-x86_64-rpm, linux-x86_64 (the .deb, as a fallback) and windows-x86_64 from the release assets. It fails unless it finds exactly one .deb, one .rpm and one -setup.exe, each with a non-empty .sig. The JSON comes from jq alone, notes read with --rawfile, file names encoded with @uri like the upload step. - scripts/build-latest-json.test.mjs (vitest) runs it under bash -e -o pipefail on a temporary assets dir; check-frontend installs jq. - release.yml calls the script, writes the notes output under a random delimiter, checks the manifest with jq before the registry DELETE, and names the .rpm instead of an AppImage in the release notes. - verify-release.sh requires the four keys, the extension of each URL and the minisign file: of each signature, and exits 1 on any failure. - Docs: release skill, qa-update-cycle section 3 (rpm procedure, keys, reason and removal condition of the fallback), architecture, README, CHANGELOG, STATE. Resolves #320 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
6.8 KiB
JavaScript
196 lines
6.8 KiB
JavaScript
/**
|
|
* scripts/build-latest-json.sh — the updater manifest of a release (#320).
|
|
*
|
|
* release.yml runs this script to write latest.json, which every installed
|
|
* app reads to find its update. tauri-plugin-updater looks up
|
|
* "{os}-{arch}-{installer}" first, then "{os}-{arch}". Before #320 the
|
|
* manifest only had linux-x86_64 (the .deb), so an .rpm install downloaded
|
|
* the .deb and refused it.
|
|
*
|
|
* Each test runs the script as release.yml does, under bash -e -o pipefail
|
|
* (the shell of the runner's steps), on a temporary assets directory. The
|
|
* bundle names are the real ones of v0.17.1: they contain a space, which the
|
|
* URLs must percent-encode. Expected values are literals, never rebuilt from
|
|
* the inputs.
|
|
*
|
|
* Plain .mjs on purpose: tsconfig only covers src/, so a .ts file here would
|
|
* look type-checked without being so. Needs bash and jq on PATH;
|
|
* check-frontend.yml installs jq for this file.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "build-latest-json.sh");
|
|
const BASE = "https://git.example.test/owner/Repo/releases/download/v0.17.1";
|
|
|
|
const DEB = "Simpl Resultat_0.17.1_amd64.deb";
|
|
const RPM = "Simpl Resultat-0.17.1-1.x86_64.rpm";
|
|
const EXE = "Simpl Resultat_0.17.1_x64-setup.exe";
|
|
|
|
// One distinct signature per bundle, to tell which .sig each key carries.
|
|
const SIG = {
|
|
[DEB]: "RWRkZWItc2lnbmF0dXJl",
|
|
[RPM]: "RWRycG0tc2lnbmF0dXJl",
|
|
[EXE]: "RWRleGUtc2lnbmF0dXJl",
|
|
};
|
|
|
|
let work;
|
|
let assets;
|
|
let notesFile;
|
|
|
|
function writeAsset(name, sig = SIG[name]) {
|
|
writeFileSync(join(assets, name), `bytes of ${name}\n`);
|
|
if (sig !== null) writeFileSync(join(assets, `${name}.sig`), sig);
|
|
}
|
|
|
|
function writeAllAssets() {
|
|
for (const name of [DEB, RPM, EXE]) writeAsset(name);
|
|
}
|
|
|
|
function run(args = [assets, "0.17.1", notesFile, BASE]) {
|
|
return spawnSync("bash", ["-e", "-o", "pipefail", SCRIPT, ...args], { encoding: "utf8" });
|
|
}
|
|
|
|
function expectFailure(result, message) {
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toContain(message);
|
|
}
|
|
|
|
beforeAll(() => {
|
|
const jq = spawnSync("jq", ["--version"], { encoding: "utf8" });
|
|
if (jq.status !== 0) {
|
|
throw new Error("jq not found: build-latest-json.sh needs it (check-frontend.yml installs it)");
|
|
}
|
|
});
|
|
|
|
beforeEach(() => {
|
|
work = mkdtempSync(join(tmpdir(), "build-latest-json-"));
|
|
assets = join(work, "release-assets");
|
|
mkdirSync(assets);
|
|
notesFile = join(work, "notes.md");
|
|
writeFileSync(notesFile, "### Fixed\n\n- Something (#320).");
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(work, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("build-latest-json.sh", () => {
|
|
it("writes the four keys, each with its own URL and its own asset's signature", () => {
|
|
writeAllAssets();
|
|
|
|
const result = run();
|
|
|
|
expect(result.status).toBe(0);
|
|
const manifest = JSON.parse(result.stdout);
|
|
expect(manifest.version).toBe("0.17.1");
|
|
expect(manifest.notes).toBe("### Fixed\n\n- Something (#320).");
|
|
expect(manifest.pub_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
expect(manifest.platforms).toStrictEqual({
|
|
"linux-x86_64": {
|
|
signature: "RWRkZWItc2lnbmF0dXJl",
|
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
|
|
},
|
|
"linux-x86_64-deb": {
|
|
signature: "RWRkZWItc2lnbmF0dXJl",
|
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
|
|
},
|
|
"linux-x86_64-rpm": {
|
|
signature: "RWRycG0tc2lnbmF0dXJl",
|
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat-0.17.1-1.x86_64.rpm",
|
|
},
|
|
"windows-x86_64": {
|
|
signature: "RWRleGUtc2lnbmF0dXJl",
|
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_x64-setup.exe",
|
|
},
|
|
});
|
|
// The step log names the bundle picked for each kind.
|
|
expect(result.stderr).toContain("build-latest-json: .rpm <- Simpl Resultat-0.17.1-1.x86_64.rpm");
|
|
});
|
|
|
|
it.each([
|
|
[".deb", DEB],
|
|
[".rpm", RPM],
|
|
["-setup.exe", EXE],
|
|
])("fails without a %s bundle, writing nothing on stdout", (kind, missing) => {
|
|
for (const name of [DEB, RPM, EXE]) {
|
|
if (name !== missing) writeAsset(name);
|
|
}
|
|
|
|
expectFailure(run(), `expected exactly one ${kind} in ${assets}, found 0`);
|
|
});
|
|
|
|
it("fails on two .deb bundles", () => {
|
|
writeAllAssets();
|
|
writeAsset("Simpl Resultat_0.17.0_amd64.deb", "RWRvbGQtZGViLXNpZ25hdHVyZQ==");
|
|
|
|
expectFailure(run(), `expected exactly one .deb in ${assets}, found 2`);
|
|
});
|
|
|
|
it("serves Windows the NSIS -setup.exe only, not another .exe", () => {
|
|
writeAllAssets();
|
|
writeAsset("simpl-result.exe", "RWRhcHAtZXhlLXNpZ25hdHVyZQ==");
|
|
|
|
const result = run();
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(JSON.parse(result.stdout).platforms["windows-x86_64"]).toStrictEqual({
|
|
signature: "RWRleGUtc2lnbmF0dXJl",
|
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_x64-setup.exe",
|
|
});
|
|
});
|
|
|
|
it("fails on an empty .sig", () => {
|
|
writeAllAssets();
|
|
writeFileSync(join(assets, `${RPM}.sig`), "");
|
|
|
|
expectFailure(run(), `missing or empty signature: ${assets}/${RPM}.sig`);
|
|
});
|
|
|
|
it("fails on a .sig holding only line breaks", () => {
|
|
writeAllAssets();
|
|
writeFileSync(join(assets, `${EXE}.sig`), "\n\n");
|
|
|
|
expectFailure(run(), `blank signature: ${assets}/${EXE}.sig`);
|
|
});
|
|
|
|
it("fails on a missing .sig", () => {
|
|
writeAllAssets();
|
|
unlinkSync(join(assets, `${DEB}.sig`));
|
|
|
|
expectFailure(run(), `missing or empty signature: ${assets}/${DEB}.sig`);
|
|
});
|
|
|
|
it("keeps notes with quotes, backslashes and a CHANGELOG_EOF line intact", () => {
|
|
writeAllAssets();
|
|
const notes =
|
|
'### Fixed\n\n- A "quoted" word, a back\\slash and C:\\Users\\path.\n' +
|
|
"CHANGELOG_EOF\n" +
|
|
'"version": "9.9.9", "platforms": {}\n';
|
|
writeFileSync(notesFile, notes);
|
|
|
|
const result = run();
|
|
|
|
expect(result.status).toBe(0);
|
|
const manifest = JSON.parse(result.stdout);
|
|
expect(manifest.notes).toBe(
|
|
'### Fixed\n\n- A "quoted" word, a back\\slash and C:\\Users\\path.\n' +
|
|
"CHANGELOG_EOF\n" +
|
|
'"version": "9.9.9", "platforms": {}\n',
|
|
);
|
|
expect(manifest.version).toBe("0.17.1");
|
|
expect(Object.keys(manifest.platforms)).toHaveLength(4);
|
|
});
|
|
|
|
it("fails without its four arguments", () => {
|
|
writeAllAssets();
|
|
|
|
expectFailure(run([assets, "0.17.1", notesFile]), "usage: build-latest-json.sh");
|
|
});
|
|
});
|