chore: keep all agentbbs services on latest software (#69)
Some checks are pending
CI / build (push) Waiting to run
deploy / deploy (push) Waiting to run
test / test (push) Waiting to run

* ci: add mailu-update workflow to keep the mail stack current

The deploy/mailu compose stack pins the floating series tags
(ghcr.io/mailu/*:2024.06); patch releases within the series only land when
someone runs `docker compose pull`, so the box drifts behind on security fixes.

Add a scheduled (weekly) + on-demand workflow that SSHes to the droplet
(reusing deploy.yml's DEPLOY_* secrets), backs up DKIM keys + the admin DB,
pulls the latest images for the pinned series, recreates the containers, and
health-checks the Mailu front on 127.0.0.1:8080. Shares deploy.yml's
concurrency group so it never races a code deploy. Stays within the pinned
series on purpose — crossing to a future series stays a deliberate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: bump Forgejo to 11.0.15 + add Dependabot to keep deps current

Audit of every version pin in the repo: Ergo (2.18.0), Go (1.26 → latest
patch via setup-go), the Ubuntu pod base (24.04 LTS), and the GitHub Action
majors are all already current. Only Forgejo was stale — bump 11.0.1 →
11.0.15 (latest patch of the 11.x LTS line; a 15.x major stays a deliberate,
tested upgrade because of DB migrations).

Add .github/dependabot.yml so github-actions, Go modules, and the Docker
image tags (Mailu compose + pod Containerfile) get review-gated update PRs
weekly. Shell-string pins (FORGEJO_VERSION/ERGO_VERSION in setup.sh) can't be
watched by Dependabot; noted inline. Mailu runtime patch level is handled by
the mailu-update workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* mailu: document RECIPIENT_DELIMITER=+ in mailu.env.example

Plus-addressing (chovy+tag@ -> chovy@) is a hard prerequisite for qaaas.dev's
packages/mail but was missing from the example, so tagged mail bounces as an
unknown recipient until an operator sets it by hand. Add it with a note that it
governs DELIVERY only, not login (Mailu auths the exact address; base <name>@
is the single login and already receives all +tagged mail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-30 20:06:06 -07:00 committed by GitHub
parent d19c5c4c3e
commit 807ecf2530
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 190 additions and 1 deletions

42
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,42 @@
# Keep agentbbs's pinned dependencies current automatically. Dependabot opens
# (review-gated) PRs when a newer version ships — nothing auto-merges, so a bad
# bump can't silently reach production.
#
# Coverage:
# - github-actions : the action pins in .github/workflows/* (checkout, setup-go…)
# - gomod : Go module dependencies (go.mod / go.sum)
# - docker : image tags in the Mailu compose stack and the pod Containerfile
#
# NOT covered (Dependabot can't watch shell-string pins): FORGEJO_VERSION and
# ERGO_VERSION in setup.sh — bump those by hand, or switch to Renovate (which can
# watch them via a custom regex manager). The Mailu *runtime* patch level is kept
# current separately by .github/workflows/mailu-update.yml.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
github-actions:
patterns: ["*"]
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
groups:
go-modules:
patterns: ["*"]
# Mailu compose stack — bumps ghcr.io/mailu/* image tags.
- package-ecosystem: docker
directory: /deploy/mailu
schedule:
interval: weekly
# Pod base image (docker.io/library/ubuntu).
- package-ecosystem: docker
directory: /pods
schedule:
interval: weekly

137
.github/workflows/mailu-update.yml vendored Normal file
View file

@ -0,0 +1,137 @@
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

View file

@ -37,6 +37,16 @@ BIND_ADDRESS4=127.0.0.1
SUBNET=192.168.203.0/24
MESSAGE_SIZE_LIMIT=52428800 # 50 MB
# --- Addressing --------------------------------------------------------------
# Plus-addressing (subaddressing): deliver mail sent to <name>+<tag>@ into the
# <name>@ mailbox (keeping the +tag in the To: header for filtering) instead of
# bouncing it as an unknown recipient. Required by qaaas.dev's packages/mail,
# which mints throwaway addresses like chovy+run-42@bbs.profullstack.com off the
# single chovy@ mailbox. NOTE: this affects DELIVERY only — it does NOT let
# <name>+<tag>@ be used as a LOGIN (Mailu authenticates the exact address; log
# into webmail as the base <name>@ and all +tagged mail is already there).
RECIPIENT_DELIMITER=+
# --- Gateway (the BBS reads/sends on behalf of members) ----------------------
# A Dovecot master user lets the agentbbs gateway open any member's mailbox with
# one secret (login "<name>*<master>"). Created by deploy/mailu/provision-mailbox.sh.

View file

@ -52,7 +52,7 @@ IRC_NETWORK="${IRC_NETWORK:-ProfullstackBBS}" # IRC network name shown to clien
ERGO_DATA="${ERGO_DATA:-/var/lib/ergo}" # Ergo state dir (ircd.db, tls/)
FORGEJO="${FORGEJO:-1}" # set 0 to skip the AgentGit Forgejo backend (git.${DOMAIN#*.})
GIT_DOMAIN="${GIT_DOMAIN:-git.${DOMAIN#*.}}" # AgentGit host (default: git.<root-of-DOMAIN>, e.g. git.profullstack.com)
FORGEJO_VERSION="${FORGEJO_VERSION:-11.0.1}" # Forgejo release to install
FORGEJO_VERSION="${FORGEJO_VERSION:-11.0.15}" # Forgejo release to install (latest 11.0.x LTS patch)
MAIL_STACK="${MAIL_STACK:-1}" # set 0 to skip the co-located Mailu mail stack (mail.${DOMAIN#*.}). NOT named MAIL: that is a reserved env var (the mail-spool path, e.g. /var/mail/root) which PAM sets under sudo, so a CI deploy inherited MAIL=/var/mail/root and silently dropped the mail Caddy route + §9e provisioning.
MAIL_DOMAIN="${MAIL_DOMAIN:-mail.${DOMAIN#*.}}" # mail host (default: mail.<root-of-DOMAIN>, e.g. mail.profullstack.com)
FORGEJO_HTTP_ADDR="${FORGEJO_HTTP_ADDR:-127.0.0.1:3000}" # Forgejo loopback HTTP (Caddy fronts it)