From eb7b6f8dab9832d78aff10d849e09b3b5b44f115 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:30:47 +0200 Subject: [PATCH] fix: point Stalwart installs at a tagged Lite release and publish checksums The Stalwart bundle's README told admins to use `releases/latest/download/bulwark-lite-stalwart.zip` as `resourceUrl`. Stalwart re-downloads that URL on its own schedule and checks no hash or signature, so whatever the newest release held would run as script on the Stalwart origin next to the web admin. The README now installs from the tagged release, explains updating by moving `resourceUrl` to the next tag, and describes the floating URL as an opt-in with that risk spelled out. Release runs upload a `.sha256` next to each Lite zip. --- .github/workflows/static-lite.yml | 14 +++++++++----- lib/__tests__/lite-scripts.test.ts | 5 ++++- scripts/lite/lib.mjs | 28 ++++++++++++++++++---------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/.github/workflows/static-lite.yml b/.github/workflows/static-lite.yml index ca07462ed..5eba3ef9b 100644 --- a/.github/workflows/static-lite.yml +++ b/.github/workflows/static-lite.yml @@ -7,10 +7,10 @@ name: Build Static Lite # # Two targets are built side by side: # - static: bulwark-lite-.zip for any static web host; -# - stalwart: bulwark-lite-stalwart.zip, a Stalwart `Application` bundle. Its -# release asset name carries no version, so -# releases/latest/download/bulwark-lite-stalwart.zip is a stable -# resourceUrl that follows new releases. +# - stalwart: bulwark-lite-stalwart.zip, a Stalwart `Application` bundle. +# Its LITE-README points `resourceUrl` at the tagged release. +# +# Each zip is published with a `.sha256` next to it. # # scripts/lite/prepare.mjs deletes the server-only trees in place; that is # safe here because the Actions checkout is disposable (CI=true is the guard). @@ -121,12 +121,16 @@ jobs: path: zips merge-multiple: true + - name: Checksum zips + working-directory: zips + run: for zip in *.zip; do sha256sum "$zip" > "$zip.sha256"; done + - name: Upload release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} TAG_NAME: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG_NAME" zips/*.zip --clobber + run: gh release upload "$TAG_NAME" zips/*.zip zips/*.zip.sha256 --clobber smoke: # Boots the demo export on a plain static server and drives it with diff --git a/lib/__tests__/lite-scripts.test.ts b/lib/__tests__/lite-scripts.test.ts index 8f301e8f4..ba7cc3448 100644 --- a/lib/__tests__/lite-scripts.test.ts +++ b/lib/__tests__/lite-scripts.test.ts @@ -625,7 +625,10 @@ describe('Stalwart target: the entry document', () => { it('documents the install, reserved prefixes, updates and the redirect URI', () => { const readme = buildStalwartReadme({ version: '1.10.0', commit: 'abc1234', locales: ['en'], buildId: 'b' }); - expect(readme).toContain(`releases/latest/download/${STALWART_ZIP_NAME}`); + // A tagged release by default: Stalwart verifies nothing it downloads. + expect(readme).toContain(`resourceUrl='https://github.com/bulwarkmail/webmail/releases/download/v`); + expect(readme).not.toContain(`resourceUrl='https://github.com/bulwarkmail/webmail/releases/latest`); + expect(readme).toContain(`sha256sum -c ${STALWART_ZIP_NAME}.sha256`); expect(readme).toContain('stalwart-cli create Application'); // The CLI wants JSON (verified against stalwart-cli 1.0.12), and nothing is mounted before UpdateApps. expect(readme).toContain(`--field 'urlPrefix={"/webmail":true}'`); diff --git a/scripts/lite/lib.mjs b/scripts/lite/lib.mjs index 047a265b6..b5e70faed 100644 --- a/scripts/lite/lib.mjs +++ b/scripts/lite/lib.mjs @@ -776,7 +776,10 @@ export function parseLiteTarget(argv = [], env = {}) { return target; } -/** Release asset name; `releases/latest/download/` is the stable resourceUrl. */ +/** + * Release asset name. It carries no version, so `releases/latest/download/` + * exists too, but the README points `resourceUrl` at a tagged release. + */ export const STALWART_ZIP_NAME = "bulwark-lite-stalwart.zip"; /** The mount prefix the docs and the one-line install use. */ @@ -1359,7 +1362,8 @@ export function collectZipEntries(dir) { /** LITE-README.md for the Stalwart bundle. */ export function buildStalwartReadme({ version, commit, locales, buildId, demoMode = false }) { - const url = `https://github.com/bulwarkmail/webmail/releases/latest/download/${STALWART_ZIP_NAME}`; + const url = `https://github.com/bulwarkmail/webmail/releases/download/v${version}/${STALWART_ZIP_NAME}`; + const latestUrl = `https://github.com/bulwarkmail/webmail/releases/latest/download/${STALWART_ZIP_NAME}`; return `# Bulwark Lite ${version} (${commit}) for Stalwart Bulwark Webmail as a Stalwart \`Application\`: Stalwart downloads this zip, @@ -1425,11 +1429,18 @@ admin's own \`admin\` and \`account\`. ## Updates -Stalwart keeps the downloaded zip for \`autoUpdateFrequency\` (default 90 -days) and downloads \`resourceUrl\` again at the first restart or -\`UpdateApps\` after that, so the \`latest/download\` URL above follows new -releases on that schedule. \`UpdateApps\` always downloads again: run it to -update right away. A failed download keeps the previous bundle online. +The resource URL above names one release, so what Stalwart serves only +changes when you change it: to update, point \`resourceUrl\` at the new +release's tag and run \`UpdateApps\` (it always downloads again; a failed +download keeps the previous bundle online). Every release also carries +\`${STALWART_ZIP_NAME}.sha256\`; check a download with +\`sha256sum -c ${STALWART_ZIP_NAME}.sha256\` before you switch. + +The bundle runs as script on your Stalwart origin, next to the web admin. +Stalwart checks no hash or signature when it downloads, so a floating URL +such as \`${latestUrl}\` would run whatever the newest release holds, at the +next restart or \`UpdateApps\` after \`autoUpdateFrequency\` (default 90 +days). Use it only if you accept that in exchange for automatic updates. Open tabs move to the new build on their next navigation (one full page load); a freshly opened tab gets it immediately. Nothing stays stuck on old @@ -1437,9 +1448,6 @@ files although Stalwart sends a year-long cache header for every file but index.html: every file the app fetches without a content hash carries the build id. -To pin a version, point \`resourceUrl\` at a tagged release instead: -\`https://github.com/bulwarkmail/webmail/releases/download/v${version}/${STALWART_ZIP_NAME}\`. - ## Sign-in Passwords (and TOTP codes) go to Stalwart's \`/api/auth\` and are exchanged at