From cad270dd2c4346edc312f3dbc3cf2af448762dd7 Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 7 Aug 2026 18:08:50 +0200 Subject: [PATCH] fix(release): reject incompatible bridge recovery Fail closed when an existing transport-tag release has a title that does not match the product version, preventing the new updater from re-entering the bridge loop. --- scripts/release_gitea.mjs | 10 +++++++++- tests/updater-version.test.js | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/scripts/release_gitea.mjs b/scripts/release_gitea.mjs index dbb1251..811500f 100644 --- a/scripts/release_gitea.mjs +++ b/scripts/release_gitea.mjs @@ -43,6 +43,14 @@ export function createReleasePlan(options) { }; } +export function resolveExistingReleaseId(plan, release) { + const existingTitle = typeof release?.name === 'string' ? release.name : ''; + if (existingTitle !== plan.releaseTitle) { + throw new Error(`Refusing recovery for ${plan.tag}: existing release title "${existingTitle}" does not match "${plan.releaseTitle}"`); + } + return release.id; +} + export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString()) { return `version: ${plan.version}\nfiles:\n - url: ${plan.setupName}\n sha512: ${sha}\n size: ${size}\npath: ${plan.setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`; } @@ -240,7 +248,7 @@ async function main(args = process.argv.slice(2)) { if (createStatus === 409 || createStatus === 422) { // Release already exists, find it const { data: releases } = await giteaApi('GET', `/api/v1/repos/Administrator/${PRODUCT_NAME}/releases/tags/${tag}`, token); - releaseId = releases.id; + releaseId = resolveExistingReleaseId(plan, releases); console.log(`Release already exists (id: ${releaseId})`); } else { releaseId = createData.id; diff --git a/tests/updater-version.test.js b/tests/updater-version.test.js index 227dfa7..a8a6413 100644 --- a/tests/updater-version.test.js +++ b/tests/updater-version.test.js @@ -59,3 +59,40 @@ test('release plan keeps product artifacts separate from the transport tag', () latestYml: "version: 2.0.1\nfiles:\n - url: Multi-Hoster-Upload Setup 2.0.1.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload Setup 2.0.1.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n" }); }); + +test('compatible existing release preserves the recovery id', async () => { + const moduleUrl = pathToFileURL(path.resolve(__dirname, '../scripts/release_gitea.mjs')).href; + const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(moduleUrl); + const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes'])); + const release = { + id: 81, + tag_name: 'v3.3.109', + name: 'Multi-Hoster-Upload v2.0.1', + body: 'Bridge notes', + draft: false, + prerelease: false, + assets: [] + }; + + assert.equal(resolveExistingReleaseId(plan, release), 81); +}); + +test('incompatible existing release title fails closed', async () => { + const moduleUrl = pathToFileURL(path.resolve(__dirname, '../scripts/release_gitea.mjs')).href; + const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(moduleUrl); + const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes'])); + const release = { + id: 81, + tag_name: 'v3.3.109', + name: 'Multi-Hoster-Upload v3.3.109', + body: 'Old transport release', + draft: false, + prerelease: false, + assets: [] + }; + + assert.throws( + () => resolveExistingReleaseId(plan, release), + /Refusing recovery for v3\.3\.109: existing release title "Multi-Hoster-Upload v3\.3\.109" does not match "Multi-Hoster-Upload v2\.0\.1"/ + ); +});