From 2da091d6e9df841476fe0812c5b3baee163e196c Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sun, 25 Oct 2026 17:41:43 +0300 Subject: [PATCH] fix: replace inherited updater provider in Gitea packages --- docs/gitea-release.md | 2 +- electron-builder.gitea.yml | 9 +++++---- package.json | 2 +- scripts/gitea-release.mjs | 10 +++++++++- tests/gitea-release.test.ts | 22 ++++++++++++++++++---- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/gitea-release.md b/docs/gitea-release.md index cec19d8..65c5bdb 100644 --- a/docs/gitea-release.md +++ b/docs/gitea-release.md @@ -37,7 +37,7 @@ npm run desktop:release:linux The release script builds the standalone server, initializes providers offline, curates the payload, boots it under Electron's bundled Node, packages AppImage -and Debian installers, creates SHA-256 checksums, and verifies update metadata +and Debian installers, checks the packaged Gitea updater configuration, creates SHA-256 checksums, and verifies update metadata against the actual installer bytes. Run the packaged-runtime gates too: diff --git a/electron-builder.gitea.yml b/electron-builder.gitea.yml index b54e987..8248688 100644 --- a/electron-builder.gitea.yml +++ b/electron-builder.gitea.yml @@ -1,7 +1,8 @@ # Official packages for this fork. Upstream packaging stays in electron-builder.yml. extends: ./electron-builder.yml publish: - - provider: generic - # Gitea resolves the latest published, non-prerelease release here. - # Drafts remain private and do not become an update before publication. - url: https://server2.sorokin-oleg-edu.xyz/replicated/learnharness/releases/download/latest + # Use an object: electron-builder concatenates inherited publish arrays. + provider: generic + # Gitea resolves the latest published, non-prerelease release here. + # Drafts remain private and do not become an update before publication. + url: https://server2.sorokin-oleg-edu.xyz/replicated/learnharness/releases/download/latest diff --git a/package.json b/package.json index ebe3997..53d35ab 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "NODE_ENV=test tsx --test tests/*.test.ts tests/*.test.tsx", "lint": "eslint .", "typecheck": "tsc --noEmit", - "desktop:release:linux": "npm run build && node scripts/smoke-provider-runtime.mjs && node scripts/build-server.mjs && node scripts/smoke-server.mjs --electron && electron-builder --config electron-builder.gitea.yml --linux --x64 --publish never && node scripts/gitea-release.mjs checksums dist && node scripts/gitea-release.mjs verify dist", + "desktop:release:linux": "rm -rf dist .next/standalone && npm run build && node scripts/smoke-provider-runtime.mjs && node scripts/build-server.mjs && node scripts/smoke-server.mjs --electron && electron-builder --config electron-builder.gitea.yml --linux --x64 --publish never && node scripts/gitea-release.mjs verify-package dist/linux-unpacked && node scripts/gitea-release.mjs checksums dist && node scripts/gitea-release.mjs verify dist", "desktop:dev": "concurrently -k \"next dev\" \"LH_DEV_URL=http://localhost:3000 electron .\"", "dev:fresh": "bash scripts/dev-fresh.sh", "desktop:dist": "rm -rf dist .next/standalone && next build && node scripts/smoke-provider-runtime.mjs && node scripts/build-server.mjs && node scripts/smoke-server.mjs && DEVELOPER_DIR=${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer} APPLE_KEYCHAIN_PROFILE=LH_NOTARY APPLE_TEAM_ID=JS6XN22HY6 electron-builder --mac --arm64" diff --git a/scripts/gitea-release.mjs b/scripts/gitea-release.mjs index 5253237..92b6a97 100644 --- a/scripts/gitea-release.mjs +++ b/scripts/gitea-release.mjs @@ -35,6 +35,13 @@ export async function writeChecksums(directory, version) { await fsp.writeFile(path.join(directory, "SHA256SUMS"), lines.join("\n") + "\n"); } +export async function verifyPackagedUpdater(directory) { + const config = yaml.load(await fsp.readFile(path.join(directory, "resources", "app-update.yml"), "utf8")); + assert.equal(config.provider, "generic", "packaged updater must use the Gitea generic provider"); + assert.equal(config.url, UPDATE_URL, "packaged updater must use the public Gitea release feed"); + assert(!config.owner && !config.repo, "packaged updater must not retain an upstream GitHub destination"); +} + export async function verifyLocal(directory, version) { const names = expectedLinuxAssets(version); const inventory = new Map(); @@ -145,6 +152,7 @@ async function main() { const [command, directory = "dist"] = process.argv.slice(2); const { version } = JSON.parse(await fsp.readFile(path.join(ROOT, "package.json"), "utf8")); if (command === "checksums") await writeChecksums(directory, version); + else if (command === "verify-package") await verifyPackagedUpdater(directory); else if (command === "verify") console.log(JSON.stringify(await verifyLocal(directory, version), null, 2)); else if (command === "upload") await upload(directory, version); else if (command === "verify-remote") { @@ -153,7 +161,7 @@ async function main() { 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]"); + } else throw new Error("Usage: node scripts/gitea-release.mjs checksums|verify|upload|verify-remote [dist-directory], 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; }); diff --git a/tests/gitea-release.test.ts b/tests/gitea-release.test.ts index 437debc..5ee838c 100644 --- a/tests/gitea-release.test.ts +++ b/tests/gitea-release.test.ts @@ -5,7 +5,7 @@ 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, UPDATE_URL } from "../scripts/gitea-release.mjs"; +import { expectedLinuxAssets, 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 })); @@ -39,6 +39,7 @@ test("updater cannot point at traversal paths, other versions, or substituted in ["traversal", (feed: any) => { feed.files[0].url = "../outside.AppImage"; }, /reference exactly/], ["version", (feed: any) => { feed.version = "0.3.7"; }, /metadata version/], ["hash", (feed: any) => { feed.files[0].sha512 = "bad"; }, /update checksum/], + ["duplicate", (feed: any) => { feed.files.push({ ...feed.files[1] }); }, /reference exactly/], ] as const) { const directory = await fixture(name); const file = path.join(directory, "latest-linux.yml"); @@ -73,11 +74,24 @@ test("remote verification downloads and hashes every draft attachment, and refus test("release configuration preserves packaging while replacing the upstream update origin", async () => { const require = createRequire(import.meta.url); const yaml = require("js-yaml"); - const config = yaml.load(await fs.readFile(new URL("../electron-builder.gitea.yml", import.meta.url), "utf8")); - assert.equal(config.extends, "./electron-builder.yml"); - assert.deepEqual(config.publish, [{ provider: "generic", url: UPDATE_URL }]); + const { getConfig } = require("app-builder-lib/out/util/config/config.js"); + const config = await getConfig(path.resolve(import.meta.dirname, ".."), "electron-builder.gitea.yml", null); + assert.deepEqual(config.publish, { provider: "generic", url: UPDATE_URL }); + assert.equal(config.appId, "com.learnharness.LearnHarness"); + assert.deepEqual(config.linux.target.map((target: any) => target.target), ["AppImage", "deb"]); const workflow = yaml.load(await fs.readFile(new URL("../.gitea/workflows/release.yml", import.meta.url), "utf8")); assert(workflow.on.workflow_dispatch); assert(!workflow.on.pull_request, "untrusted pull requests must not run release publishing"); assert.equal(workflow.permissions.releases, "write"); }); +test("packaged updater verification refuses inherited GitHub or incorrect generic destinations", async () => { + const directory = path.join(root, "package"); + await fs.mkdir(path.join(directory, "resources"), { recursive: true }); + const file = path.join(directory, "resources", "app-update.yml"); + await fs.writeFile(file, JSON.stringify({ provider: "github", owner: "amosblomqvist", repo: "learnharness" })); + await assert.rejects(verifyPackagedUpdater(directory), /Gitea generic provider/); + await fs.writeFile(file, JSON.stringify({ provider: "generic", url: "https://other.example" })); + await assert.rejects(verifyPackagedUpdater(directory), /public Gitea release feed/); + await fs.writeFile(file, JSON.stringify({ provider: "generic", url: UPDATE_URL })); + await verifyPackagedUpdater(directory); +});