Fix the desktop download buttons; release v5.5.4
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

The buttons still led to a dead GitHub page. 5.5.3 updated one of the two places
these links live — the platforms menu on the connection screen keeps its own
DOWNLOADS table, and it was missed, so it stayed on 0.1.0.

Why it looked like a working link that did nothing: the stale entries used
/releases/latest/download/<file>, and GitHub resolves `latest` by redirecting to
the newest tag. Once 0.3.0 shipped, a link written for 0.1.0 resolved to
/releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe — a file that never
existed under that tag. The browser navigated to GitHub and downloaded nothing.

Both places now build their URLs from a DESKTOP_VERSION constant with the tag
pinned, so the version is written once per file and a link cannot silently
become invalid when a new release goes out.

Adds tests/desktop-download-links.test.mjs, which fails the build if this drifts
again: every source must derive URLs from that constant, /latest/ and hardcoded
versions in filenames are rejected, and each generated URL is fetched to prove
the asset exists. SKIP_NETWORK=1 skips the fetches offline.
This commit is contained in:
lockbitchat
2026-07-27 04:53:44 -04:00
parent fcee4216e8
commit 60bf037ef9
13 changed files with 176 additions and 46 deletions
+104
View File
@@ -0,0 +1,104 @@
// Desktop download links must actually download something.
//
// They have broken twice, both times the same way:
//
// 1. The version is written in more than one place — a `DOWNLOADS` table in
// app.jsx and a `DownloadApps` component — so bumping one left the other
// behind, and nobody noticed because the stale link still *looked* valid.
//
// 2. Links used /releases/latest/download/<file>. GitHub resolves `latest` by
// redirecting to the newest tag, so once 0.3.0 shipped, a link written for
// 0.1.0 turned into
// /releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe
// — a file that never existed under that tag. The button navigated to
// GitHub and downloaded nothing.
//
// So: one version across every file, tags pinned, and — unless SKIP_NETWORK=1 —
// every generated URL is fetched to prove the asset is really there.
//
// Run with: node tests/desktop-download-links.test.mjs
import fs from 'fs';
import path from 'path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'url';
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
const SOURCES = ['src/app.jsx', 'src/components/ui/DownloadApps.jsx'];
// ---------- every source agrees on one version ----------
const versions = SOURCES.map((f) => {
const m = /(?:SB_)?DESKTOP_VERSION\s*=\s*'([^']+)'/.exec(read(f));
assert.ok(m, `${f} must declare a DESKTOP_VERSION constant instead of inlining the version`);
return { file: f, version: m[1] };
});
const unique = [...new Set(versions.map((v) => v.version))];
assert.equal(
unique.length, 1,
`every source must use the same desktop version, found: ${versions.map((v) => `${v.file}=${v.version}`).join(', ')}`
);
const VERSION = unique[0];
assert.match(VERSION, /^\d+\.\d+\.\d+$/, 'desktop version must be semver');
// ---------- no source may resolve through /latest/ ----------
// Comments explain *why* these forms are avoided and would otherwise match, so
// check the code only.
const stripComments = (src) =>
src
.replace(/\/\*[\s\S]*?\*\//g, '')
.split('\n')
.map((line) => line.replace(/(^|\s)\/\/.*$/, '$1'))
.join('\n');
for (const f of SOURCES) {
const code = stripComments(read(f));
assert.doesNotMatch(
code, /releases\/latest\/download/,
`${f} must pin the release tag: a stale /latest/ link resolves to the newest tag ` +
'with an old filename and 404s'
);
// And the version must never be written into a URL by hand.
const hardcoded = code.match(/SecureBit\.Chat_\d+\.\d+\.\d+_/g);
assert.equal(
hardcoded, null,
`${f} hardcodes a version in a filename (${hardcoded && hardcoded[0]}); build it from the constant`
);
}
// ---------- the URLs the app will actually open ----------
const urls = [
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64.dmg`,
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64-setup.exe`,
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_amd64.AppImage`
];
if (process.env.SKIP_NETWORK === '1') {
console.log(`Desktop download link tests passed (v${VERSION}, network checks skipped)`);
process.exit(0);
}
let failed = 0;
for (const url of urls) {
try {
// Range keeps it to a few bytes: we only care that the asset resolves.
const res = await fetch(url, { headers: { Range: 'bytes=0-99' }, redirect: 'follow' });
const ok = res.status === 200 || res.status === 206;
if (!ok) failed++;
console.log(` ${ok ? 'ok ' : 'FAIL'} ${res.status} ${url.split('/').pop()}`);
} catch (err) {
failed++;
console.log(` FAIL ${url.split('/').pop()}${err.message}`);
}
}
assert.equal(
failed, 0,
'every desktop download link must resolve to a real release asset; ' +
'publish the release before bumping the version, or set SKIP_NETWORK=1 to skip this check'
);
console.log(`Desktop download link tests passed (v${VERSION}, ${urls.length} assets reachable)`);