Files
webmail/.github/workflows/static-lite.yml
T
Linus Rath eb7b6f8dab 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.
2026-09-26 17:30:47 +02:00

221 lines
6.8 KiB
YAML

name: Build Static Lite
# Bulwark Lite: the static export of the webmail (see docs in README.md,
# "Static Lite build"). Runs the build on every push as a regression check,
# attaches the zips to releases, and lets maintainers build a custom variant
# (fixed server URL, sub-path mount, locale subset, demo) on demand.
#
# 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 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).
on:
release:
types: [published]
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
jmap_server_url:
description: "Default JMAP server URL baked into config.json (static: editable after download; stalwart: leave empty for the serving Stalwart)"
required: false
app_name:
description: "App name"
required: false
default: "Bulwark Webmail"
base_path:
description: "Sub-path mount for the static target, e.g. /webmail (baked into asset URLs; the stalwart target ignores it)"
required: false
locales:
description: "Comma-separated locale subset, e.g. en,de,fr (default: all)"
required: false
demo:
description: "Build the static demo (built-in demo account, no server)"
type: boolean
default: false
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
target: [static, stalwart]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build static export
env:
CI: "true"
GIT_COMMIT: ${{ github.sha }}
LITE_TARGET: ${{ matrix.target }}
# The Stalwart bundle learns its mount prefix at runtime.
NEXT_PUBLIC_BASE_PATH: ${{ matrix.target == 'static' && inputs.base_path || '' }}
LITE_JMAP_SERVER_URL: ${{ inputs.jmap_server_url }}
LITE_APP_NAME: ${{ inputs.app_name }}
LITE_LOCALES: ${{ inputs.locales }}
LITE_DEMO_MODE: ${{ inputs.demo }}
run: npm run build:lite
- name: Package zip
env:
REF_NAME: ${{ github.ref_name }}
run: |
if [ "${{ matrix.target }}" = "stalwart" ]; then
# postbuild already packed it (entries at the archive root, no
# wrapping folder); the name stays version-less on purpose.
ZIP="bulwark-lite-stalwart.zip"
else
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${REF_NAME#v}"
else
VERSION="$(cat VERSION)-${GITHUB_SHA::7}"
fi
ZIP="bulwark-lite-${VERSION}.zip"
(cd out && zip -qr "../${ZIP}" .)
fi
echo "ZIP=${ZIP}" >> "$GITHUB_ENV"
ls -la "${ZIP}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bulwark-lite-${{ matrix.target }}
path: ${{ env.ZIP }}
retention-days: 7
release:
# The only job with a write token, and it runs no checked-out code.
needs: build
if: github.event_name == 'release'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download zips
uses: actions/download-artifact@v4
with:
pattern: bulwark-lite-*
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 zips/*.zip.sha256 --clobber
smoke:
# Boots the demo export on a plain static server and drives it with
# Playwright: login, open a thread, switch surfaces, reload a deep link.
# No JMAP server involved, so it is cheap and deterministic.
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
# prepare.mjs prunes every test tree in the checkout; the smoke suite
# lives under scripts/lite/smoke, which it leaves alone.
- name: Build demo export
env:
CI: "true"
GIT_COMMIT: ${{ github.sha }}
LITE_DEMO_MODE: "true"
LITE_LOCALES: "en,de"
run: npm run build:lite
- name: Run smoke test
run: npm run test:lite-smoke
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: lite-smoke-report
path: playwright-report
retention-days: 7
smoke-stalwart:
# The Stalwart Application bundle, served by scripts/lite/serve.mjs
# --stalwart (an emulation of how Stalwart serves Applications) from the
# zip itself, under two prefixes.
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Build demo bundle
env:
CI: "true"
GIT_COMMIT: ${{ github.sha }}
LITE_TARGET: stalwart
LITE_DEMO_MODE: "true"
LITE_LOCALES: "en,de"
run: npm run build:lite
- name: Run smoke test
run: npm run test:lite-smoke:stalwart
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: lite-smoke-stalwart-report
path: playwright-report
retention-days: 7