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
+11
View File
@@ -1,5 +1,16 @@
# Changelog
## v5.5.4 — Fix the desktop download buttons
### Fixed
- **The download 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 has its own `DOWNLOADS` table, and it was missed. It still pointed at 0.1.0.
- The stale 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 became `/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, which is why it looked like a working link that simply did nothing.
### Added
- `tests/desktop-download-links.test.mjs`, which fails the build if any source drifts: it requires every file to derive its URLs from one `DESKTOP_VERSION` constant, forbids `/releases/latest/download/` and hardcoded versions in filenames, and fetches each generated URL to prove the release asset is really there. Set `SKIP_NETWORK=1` to skip the fetches offline.
## v5.5.3 — Desktop downloads point at 0.3.0
### Fixed
+1 -1
View File
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-5.5.3-3ecf8e.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-5.5.4-3ecf8e.svg)](CHANGELOG.md)
[![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app)
[![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model)
+1 -1
View File
@@ -18910,7 +18910,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [
React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"),
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.5.3")
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.5.4")
]),
React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted")
])
+1 -1
View File
File diff suppressed because one or more lines are too long
Vendored
+6 -4
View File
@@ -1805,10 +1805,12 @@ var EnhancedConnectionSetup = ({
}, disabled: !hasInvite || connectionStatus === "connecting", style: { width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "9px", padding: "14px", borderRadius: "13px", border: "none", background: hasInvite && connectionStatus !== "connecting" ? C_ORANGE : "rgba(255,255,255,0.05)", color: hasInvite && connectionStatus !== "connecting" ? "#1a0f04" : "#56565e", fontFamily: "inherit", fontSize: "15px", fontWeight: 700, cursor: hasInvite && connectionStatus !== "connecting" ? "pointer" : "not-allowed", boxShadow: hasInvite && connectionStatus !== "connecting" ? "0 8px 24px rgba(240,137,42,0.28)" : "none" } }, connectionStatus === "connecting" ? "Processing\u2026" : "Connect")
]);
}
const SB_DESKTOP_VERSION = "0.3.0";
const SB_DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${SB_DESKTOP_VERSION}`;
const DOWNLOADS = {
mac: { name: "macOS", format: ".dmg \xB7 Apple Silicon & Intel", icon: "fab fa-apple", url: "https://github.com/SecureBitChat/securebit-desktop/releases/download/v0.1.0/SecureBit.Chat_0.1.0_x64.dmg" },
win: { name: "Windows", format: ".exe \xB7 64-bit installer", icon: "fab fa-windows", url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe" },
linux: { name: "Linux", format: ".AppImage", icon: "fab fa-linux", url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage" }
mac: { name: "macOS", format: ".dmg \xB7 Apple Silicon & Intel", icon: "fab fa-apple", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64.dmg` },
win: { name: "Windows", format: ".exe \xB7 64-bit installer", icon: "fab fa-windows", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
linux: { name: "Linux", format: ".AppImage", icon: "fab fa-linux", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
};
const detectOS = () => {
const ua = (navigator.userAgent || "") + " " + (navigator.platform || "");
@@ -3589,7 +3591,7 @@ var EnhancedSecureP2PChat = () => {
} catch (error) {
}
}
handleMessage(" SecureBit.chat Enhanced Security Edition v5.5.3 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
handleMessage(" SecureBit.chat Enhanced Security Edition v5.5.4 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
manager.setFileTransferCallbacks(
// Progress callback — drives the voice-note upload/download ring.
(progress) => {
+2 -2
View File
File diff suppressed because one or more lines are too long
+22 -22
View File
@@ -24,7 +24,7 @@
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785141222839">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785142410213">
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -90,7 +90,7 @@
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785141222839">
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785142410213">
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
@@ -99,7 +99,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785141222839">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785142410213">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -183,7 +183,7 @@
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
both execute in document order after parsing, so React still runs before the
app modules below, but the parser / first paint is no longer blocked. -->
<script defer src="config/ice-servers.js?v=1785141222839"></script>
<script defer src="config/ice-servers.js?v=1785142410213"></script>
<script defer src="libs/react/react.production.min.js"></script>
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
@@ -191,8 +191,8 @@
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
<script defer src="libs/prism/prism.js"></script>
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
<link rel="stylesheet" href="assets/tailwind.css?v=1785141222839">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785141222839">
<link rel="stylesheet" href="assets/tailwind.css?v=1785142410213">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785142410213">
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
@@ -200,31 +200,31 @@
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="src/styles/main.css?v=1785141222839">
<link rel="stylesheet" href="src/styles/animations.css?v=1785141222839">
<link rel="stylesheet" href="src/styles/components.css?v=1785141222839">
<link rel="stylesheet" href="src/styles/main.css?v=1785142410213">
<link rel="stylesheet" href="src/styles/animations.css?v=1785142410213">
<link rel="stylesheet" href="src/styles/components.css?v=1785142410213">
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="src/scripts/load-async-css.js?v=1785141222839"></script>
<script defer src="src/scripts/load-async-css.js?v=1785142410213"></script>
<noscript>
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="libs/prism/prism.css">
</noscript>
<script defer src="src/scripts/fa-check.js?v=1785141222839"></script>
<script defer src="src/scripts/fa-check.js?v=1785142410213"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="src/utils/updateManager.js?v=1785141222839"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1785141222839"></script>
<script type="module" src="dist/qr-local.js?v=1785141222839"></script>
<script type="module" src="src/components/QRScanner.js?v=1785141222839"></script>
<script defer src="src/utils/updateManager.js?v=1785142410213"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1785142410213"></script>
<script type="module" src="dist/qr-local.js?v=1785142410213"></script>
<script type="module" src="src/components/QRScanner.js?v=1785142410213"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1785141222839"></script>
<script type="module" src="dist/app.js?v=1785141222839"></script>
<script type="module" src="dist/app-boot.js?v=1785142410213"></script>
<script type="module" src="dist/app.js?v=1785142410213"></script>
<script defer src="src/scripts/pwa-register.js?v=1785141222839"></script>
<script src="./src/pwa/install-prompt.js?v=1785141222839" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1785141222839" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1785141222839"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785141222839">
<script defer src="src/scripts/pwa-register.js?v=1785142410213"></script>
<script src="./src/pwa/install-prompt.js?v=1785142410213" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1785142410213" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1785142410213"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785142410213">
</body>
</html>
+7 -7
View File
@@ -1,10 +1,10 @@
{
"version": "1785141222839",
"buildVersion": "1785141222839",
"appVersion": "5.5.3",
"buildTime": "2026-07-27T08:33:42.891Z",
"buildId": "1785141222839-6152a77",
"gitHash": "6152a77",
"version": "1785142410213",
"buildVersion": "1785142410213",
"appVersion": "5.5.4",
"buildTime": "2026-07-27T08:53:30.254Z",
"buildId": "1785142410213-fcee421",
"gitHash": "fcee421",
"generated": true,
"generatedAt": "2026-07-27T08:33:42.893Z"
"generatedAt": "2026-07-27T08:53:30.255Z"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "securebit-chat",
"version": "5.5.3",
"version": "5.5.4",
"description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html",
"scripts": {
@@ -11,7 +11,7 @@
"dev": "npm run build && python -m http.server 8000",
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
"serve": "npx http-server -p 8000",
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/security-level-shape.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs"
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs"
},
"keywords": [
"p2p",
+17 -4
View File
@@ -1578,10 +1578,23 @@ import {
}
// Desktop downloads (real GitHub release assets) + OS detection.
//
// Keep SB_DESKTOP_VERSION in sync with the constant of the same name in
// src/components/ui/DownloadApps.jsx tests/desktop-download-links.test.mjs
// fails the build if the two ever disagree.
//
// The tag is pinned rather than using /releases/latest/download/. Release
// filenames carry the version, and GitHub resolves `latest` by redirecting
// to the newest tag so a stale `latest` link becomes
// /download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe, a file that never
// existed, and the download 404s. A pinned tag keeps serving a real
// installer instead.
const SB_DESKTOP_VERSION = '0.3.0';
const SB_DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${SB_DESKTOP_VERSION}`;
const DOWNLOADS = {
mac: { name: 'macOS', format: '.dmg · Apple Silicon & Intel', icon: 'fab fa-apple', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/download/v0.1.0/SecureBit.Chat_0.1.0_x64.dmg' },
win: { name: 'Windows', format: '.exe · 64-bit installer', icon: 'fab fa-windows', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe' },
linux: { name: 'Linux', format: '.AppImage', icon: 'fab fa-linux', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage' }
mac: { name: 'macOS', format: '.dmg · Apple Silicon & Intel', icon: 'fab fa-apple', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64.dmg` },
win: { name: 'Windows', format: '.exe · 64-bit installer', icon: 'fab fa-windows', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
linux: { name: 'Linux', format: '.AppImage', icon: 'fab fa-linux', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
};
const detectOS = () => {
const ua = (navigator.userAgent || '') + ' ' + (navigator.platform || '');
@@ -3533,7 +3546,7 @@ import {
}
}
handleMessage(' SecureBit.chat Enhanced Security Edition v5.5.3 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
handleMessage(' SecureBit.chat Enhanced Security Edition v5.5.4 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
// Setup file transfer callbacks (id-bound to THIS session's manager).
manager.setFileTransferCallbacks(
+1 -1
View File
@@ -559,7 +559,7 @@ const EnhancedMinimalHeader = ({
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.5.3')
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.5.4')
]),
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
])
+1 -1
View File
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
const SW_BUILD_VERSION = '1785141222839';
const SW_BUILD_VERSION = '1785142410213';
// Load version from meta.json on install
async function getAppVersion() {
+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)`);