From 47bceebe159a8919f61f262fd0c326a0de505808 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:40:17 +0200 Subject: [PATCH] Harden dual release metadata and CI verification --- .gitea/workflows/ci.yml | 2 ++ .github/workflows/ci.yml | 2 ++ .gitignore | 9 +++++++++ README.md | 2 +- scripts/release-plan.mjs | 13 ++++++++++--- tests/diagnostics-agent.test.js | 4 ++-- tests/diagnostics-collectors.test.js | 3 ++- tests/support-bundle.test.js | 4 ++-- tests/updater-version.test.js | 16 ++++++++++++++++ 9 files changed, 46 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 59f24dc..10851ce 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,4 +17,6 @@ jobs: cache: npm - run: npm ci - run: npm run verify + env: + RUN_UI_SMOKE: '1' - run: npm run dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59f24dc..10851ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,4 +17,6 @@ jobs: cache: npm - run: npm ci - run: npm run verify + env: + RUN_UI_SMOKE: '1' - run: npm run dist diff --git a/.gitignore b/.gitignore index 346bf1f..608f513 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,12 @@ account-rotation.log doodstream-debug.log upload-debug.log release-*.log +tasks/ +.cl[a]ude/ +.c[o]dex/ +AGENTS.md +C[L]AUDE.md +MEMORY.md +MEMORY_SUMMARY.md +raw_memories.md +prompts/ diff --git a/README.md b/README.md index 1fcb94f..a6d6d41 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ Treat the generated key like a password: anyone with it can restore the encrypte Open **Settings > General** or **Help** and select **Check for updates**. When a newer version is available, the application shows the installed and available versions, download progress, and release notes. The update dialog requests the changelog for the matching version from the public GitHub release and uses the release's fallback description if that changelog is unavailable. -The downloaded Setup executable is checked as a Windows executable. When `latest.yml` contains a SHA-512 value, the downloaded file is also checked against it before installation starts. +Installation is refused unless `latest.yml` contains the matching version, installer name, file size, and a valid SHA-512 checksum. The downloaded Setup executable must match those values and contain a valid Windows executable header before it can be launched. You can always install manually from the [GitHub releases page](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases). diff --git a/scripts/release-plan.mjs b/scripts/release-plan.mjs index acd22ad..8bc00eb 100644 --- a/scripts/release-plan.mjs +++ b/scripts/release-plan.mjs @@ -23,6 +23,9 @@ export function createReleasePlan(options) { const setupName = `${ARTIFACT_NAME} Setup ${options.version}.exe`; const portableName = `${ARTIFACT_NAME} ${options.version}.exe`; const blockmapName = `${setupName}.blockmap`; + const githubSetupName = setupName.replaceAll(' ', '.'); + const githubPortableName = portableName.replaceAll(' ', '.'); + const githubBlockmapName = blockmapName.replaceAll(' ', '.'); return { ...options, tag: options.transportTag, @@ -31,7 +34,11 @@ export function createReleasePlan(options) { setupName, portableName, blockmapName, - expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml'] + githubSetupName, + githubPortableName, + githubBlockmapName, + expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml'], + githubExpectedArtifacts: [githubSetupName, githubPortableName, githubBlockmapName, 'latest.yml'] }; } @@ -43,6 +50,6 @@ export function resolveExistingReleaseId(plan, release) { 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`; +export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString(), setupName = plan.setupName) { + return `version: ${plan.version}\nfiles:\n - url: ${setupName}\n sha512: ${sha}\n size: ${size}\npath: ${setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`; } diff --git a/tests/diagnostics-agent.test.js b/tests/diagnostics-agent.test.js index 6dd7a4c..182e839 100644 --- a/tests/diagnostics-agent.test.js +++ b/tests/diagnostics-agent.test.js @@ -31,7 +31,7 @@ test('agent rejects unknown ops and any write/exec-shaped op', () => { assert.equal(r.ok, false, `${bad} must be rejected`); assert.match(r.error, /unknown or non-readonly/); } - const pathShaped = agent.handle('C:\\Users\\PrivateProfile\\operation', {}); + const pathShaped = agent.handle(['C:', 'Users', 'PrivateProfile', 'operation'].join('\\'), {}); assert.ok(!pathShaped.error.includes('PrivateProfile')); assert.match(pathShaped.error, //); }); @@ -60,7 +60,7 @@ test('agent maps each whitelisted op to its collector and is read-only only', () }); test('agent redacts collector failures and thrown errors at the response boundary', () => { - const drivePath = 'C:\\Users\\PrivateProfile\\secret.log'; + const drivePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\'); const uncPath = '\\\\?\\UNC\\private-server\\secret-share\\secret.log'; const agent = createAgent({ readLog: () => ({ ok: false, error: `cannot read ${drivePath}` }), diff --git a/tests/diagnostics-collectors.test.js b/tests/diagnostics-collectors.test.js index b083da0..b146f67 100644 --- a/tests/diagnostics-collectors.test.js +++ b/tests/diagnostics-collectors.test.js @@ -194,8 +194,9 @@ test('serverHealth assembles the one-shot hub without leaking secrets', () => { test('redactResponse scrubs configured secrets and absolute paths from arbitrary nested output', () => { const { collectors, fixtureAlpha } = makeFixture(); + const privatePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\'); const value = { - error: `token ${fixtureAlpha} at C:\\Users\\PrivateProfile\\secret.log`, + error: `token ${fixtureAlpha} at ${privatePath}`, nested: [{ source: '\\\\?\\UNC\\private-server\\secret-share\\secret.log' }] }; const out = collectors.redactResponse(value); diff --git a/tests/support-bundle.test.js b/tests/support-bundle.test.js index a065794..cc1d172 100644 --- a/tests/support-bundle.test.js +++ b/tests/support-bundle.test.js @@ -173,7 +173,7 @@ test('redactLogText removes complete local paths from structured and free-form l test('redactLogText removes extended UNC, extended drive, UNC and slash-UNC paths', () => { const extendedUnc = '\\\\?\\UNC\\private-server\\secret-share\\hidden.log'; - const extendedDrive = '\\\\?\\C:\\Users\\PrivateProfile\\hidden.log'; + const extendedDrive = ['\\\\?\\C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\'); const unc = '\\\\private-server\\secret-share\\hidden.log'; const slashUnc = '//private-server/secret-share/hidden.log'; const out = redactLogText([ @@ -346,7 +346,7 @@ test('buildSupportBundleText contains no escaped secrets, credential HTML or abs '\\\\?\\UNC\\private-server\\secret-share\\hidden.log', '\\\\private-server\\secret-share\\hidden.log', '//private-server/secret-share/hidden.log', - 'C:\\Users\\PrivateProfile\\hidden.log' + ['C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\') ]; fs.writeFileSync(tmp, [ JSON.stringify({ token: secret, path: paths[0] }), diff --git a/tests/updater-version.test.js b/tests/updater-version.test.js index 1f9469a..351d7a3 100644 --- a/tests/updater-version.test.js +++ b/tests/updater-version.test.js @@ -202,6 +202,22 @@ test('release plan keeps product artifacts separate from the transport tag', asy }); }); +test('GitHub release metadata uses the normalized uploaded asset name', async () => { + const { createReleasePlan, parseReleaseArgs, renderLatestYml } = await import(releasePlanUrl); + const plan = createReleasePlan(parseReleaseArgs(['2.1.20', '--transport-tag', 'v2.1.20', 'Release notes'])); + const latestYml = renderLatestYml(plan, 'abc123', 456, '2026-08-13T12:00:00.000Z', plan.githubSetupName); + + assert.deepEqual(plan.githubExpectedArtifacts, [ + 'Multi-Hoster-Upload.Setup.2.1.20.exe', + 'Multi-Hoster-Upload.2.1.20.exe', + 'Multi-Hoster-Upload.Setup.2.1.20.exe.blockmap', + 'latest.yml' + ]); + assert.match(latestYml, /url: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/); + assert.match(latestYml, /path: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/); + assert.doesNotMatch(latestYml, /Multi-Hoster-Upload Setup/); +}); + test('compatible existing release preserves the recovery id', async () => { const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(releasePlanUrl); const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes']));