Skip to content

mailu-update

mailu-update #2

Workflow file for this run

name: mailu-update
# Keep the self-hosted Mailu stack on the latest patch of its pinned series.
#
# The Mailu compose stack (deploy/mailu) pins the FLOATING series tags
# (ghcr.io/mailu/*:2024.06). Upstream ships frequent patch releases within that
# series (2024.06.NN) with security fixes, but a running host only picks them up
# when someone runs `docker compose pull`. Left alone, the box drifts behind.
#
# This workflow SSHes to the droplet on a weekly schedule (and on demand), backs
# up the irreplaceable state (DKIM keys + admin DB), pulls the newest images for
# the pinned series, recreates the containers, and health-checks the result.
# It stays WITHIN the pinned series on purpose: crossing to a future series
# (e.g. a 2025.xx) can carry DB migrations and config changes, so that is a
# deliberate PR that edits the tags in deploy/mailu/*.yml — not an auto-pull.
#
# The agentbbs Go binary already redeploys on every push (deploy.yml) and the
# rootless podman pods rebuild from upstream base images, so this workflow is the
# missing piece: it covers the one long-lived docker-compose stack on the box.
#
# Reuses the same repo secrets as deploy.yml:
# DEPLOY_SSH_KEY private key whose public half is in the droplet admin user's
# authorized_keys
# DEPLOY_HOST bbs.profullstack.com (or the droplet IP)
# DEPLOY_USER admin SSH user (default: root)
# DEPLOY_PORT admin SSH port (default: 2202)
on:
schedule:
# 06:30 UTC every Monday. Off-peak; adjust as you like.
- cron: '30 6 * * 1'
workflow_dispatch:
inputs:
prune:
description: 'Prune dangling images after the update'
type: boolean
default: true
# Share deploy.yml's concurrency group so an image pull can never race a code
# deploy on the same host — whichever starts first runs to completion, the other
# queues behind it.
concurrency:
group: deploy-production
cancel-in-progress: false
permissions:
contents: read
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Configure SSH
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }}
run: |
test -n "$DEPLOY_SSH_KEY" || { echo "::error::DEPLOY_SSH_KEY secret is not set"; exit 1; }
test -n "$DEPLOY_HOST" || { echo "::error::DEPLOY_HOST secret is not set"; exit 1; }
install -d -m 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
ssh-keyscan -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Pull latest Mailu images, recreate, health-check
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER || 'root' }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }}
PRUNE: ${{ github.event_name == 'schedule' && 'true' || inputs.prune }}
run: |
ssh -i ~/.ssh/id_deploy -p "$DEPLOY_PORT" \
-o BatchMode=yes -o StrictHostKeyChecking=yes \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
"sudo -n env PRUNE=$(printf %q "$PRUNE") bash -s" <<'REMOTE'
set -euo pipefail
MAILU_DIR=/opt/agentbbs/deploy/mailu
cd "$MAILU_DIR"
if [ ! -f mailu.env ]; then
echo "::notice::mailu.env not present at ${MAILU_DIR} — stack not deployed, nothing to update"
exit 0
fi
echo "== images before =="
docker compose images
# Back up the irreplaceable bits before touching anything: the DKIM
# signing keys (data/dkim) and the admin database (data/data, sqlite).
# Mailboxes (data/mail) are large and are NOT touched by an image pull,
# so we deliberately skip them here — back those up separately.
ts="$(date -u +%Y%m%dT%H%M%SZ)"
install -d -m 700 backups
tar czf "backups/mailu-state-${ts}.tgz" \
$( [ -d data/dkim ] && echo data/dkim ) \
$( [ -d data/data ] && echo data/data ) 2>/dev/null || true
echo "::notice::backed up DKIM + admin DB to backups/mailu-state-${ts}.tgz"
# Keep only the 10 most recent state backups.
ls -1t backups/mailu-state-*.tgz 2>/dev/null | tail -n +11 | xargs -r rm -f
echo "== pulling latest patch for the pinned series =="
docker compose pull
echo "== recreating containers =="
docker compose up -d
# Give services a moment, then verify. Mailu's front serves HTTP on
# 127.0.0.1:8080 (Caddy fronts it); a reachable webmail means the
# stack came back up.
ok=0
code=""
for i in $(seq 1 30); do
code="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:8080/ || true)"
case "$code" in
2??|3??) ok=1; break ;;
esac
sleep 3
done
echo "== compose status =="
docker compose ps
if [ "$ok" != "1" ]; then
echo "::error::Mailu front did not answer on 127.0.0.1:8080 after update — check 'docker compose logs' in ${MAILU_DIR}. Restore from backups/mailu-state-${ts}.tgz if needed."
exit 1
fi
echo "::notice::Mailu front is answering (HTTP ${code}) after update"
echo "== images after =="
docker compose images
if [ "${PRUNE:-false}" = "true" ]; then
echo "== pruning dangling images =="
docker image prune -f
fi
REMOTE