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.
This commit is contained in:
Linus Rath
2026-09-26 17:30:47 +02:00
parent 6ff94d439e
commit eb7b6f8dab
3 changed files with 31 additions and 16 deletions
+9 -5
View File
@@ -7,10 +7,10 @@ name: Build Static Lite
#
# Two targets are built side by side:
# - static: bulwark-lite-<version>.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
+4 -1
View File
@@ -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}'`);
+18 -10
View File
@@ -776,7 +776,10 @@ export function parseLiteTarget(argv = [], env = {}) {
return target;
}
/** Release asset name; `releases/latest/download/<this>` is the stable resourceUrl. */
/**
* Release asset name. It carries no version, so `releases/latest/download/<this>`
* 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