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
