Add deploy workflow and backup tooling

CI deployment (.gitea/workflows/deploy.yml):
- Two jobs (build, deploy) gated on the repo variable
  DEPLOY_ENABLED=true so the workflow exists but does nothing
  until secrets and host are configured.
- Build pushes two image tags per run: rolling :main + the short
  SHA on main, or vX.Y.Z + :latest on tag pushes. Immutable per
  commit/tag tags make rollback trivial.
- Deploy SSHes to DEPLOY_HOST, runs docker compose pull && up -d
  in DEPLOY_PATH, then polls HEALTH_URL for up to a minute. A
  failed health check fails the workflow, which is the alert.
- Required secrets and the rollback procedure are documented in
  docs/operations.md.

Backup tooling (scripts/):
- backup.sh: SQLite online .backup snapshot + tarball of the
  per-tenant data dir + info.txt header, all wrapped into a
  single librenotes-YYYYMMDD-HHMMSS.tar.gz. Optional BACKUP_REMOTE
  triggers an rclone copy for off-site storage.
- backup-prune.sh: enforces retention "30 daily + 12 monthly".
  Sorts archives by filename (date is in the name so lex order
  matches chronological) and keeps the newest 30 plus the newest
  archive for each of the most recent 12 months.
- backup-restore-test.sh: extracts the most recent archive into
  a tmpdir, runs sqlite3 .schema (proves DB readability), and
  asserts the notes tar has at least one entry. Failure is the
  alert. Wired into a separate weekly timer.
- librenotes-backup.{service,timer}: systemd units for the daily
  03:17 UTC run with 5min jitter; ProtectSystem=strict, only
  /var/backups/librenotes is writable.
- librenotes-backup-verify.{service,timer}: weekly Monday
  04:00 UTC restore test.

Closes #26 and #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 22:49:40 +02:00
co-authored by Claude Opus 4.7
parent 635a03098b
commit bcccba92f7
9 changed files with 438 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
name: Deploy
on:
push:
branches: [main]
tags: ["v*"]
# Required repository secrets:
# REGISTRY registry hostname, e.g. registry.librete.ch
# REGISTRY_USER robot account
# REGISTRY_PASS robot token
# DEPLOY_HOST deployment SSH target, e.g. root@librenot.es
# DEPLOY_KEY private SSH key (PEM, no passphrase)
# DEPLOY_PATH remote directory containing the compose stack
# HEALTH_URL public URL to verify post-deploy, e.g.
# https://librenot.es/healthz
#
# Tag pushes deploy the tag (vX.Y.Z); main-branch pushes deploy
# the rolling :main image. Set image to immutable tag so rollback
# is just `docker compose -f ... up -d` with the previous tag.
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
if: ${{ vars.DEPLOY_ENABLED == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- name: Compute tags
id: tags
run: |
BASE="${{ secrets.REGISTRY }}/librenotes"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF##refs/tags/}"
echo "tags=${BASE}:${TAG},${BASE}:latest" >> "$GITHUB_OUTPUT"
echo "version=${TAG}" >> "$GITHUB_OUTPUT"
else
echo "tags=${BASE}:main,${BASE}:${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "version=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
fi
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.tags.outputs.tags }}
build-args: |
VERSION=${{ steps.tags.outputs.version }}
BUILDTIME=${{ github.event.head_commit.timestamp }}
deploy:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 10
if: ${{ vars.DEPLOY_ENABLED == 'true' }}
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
ssh-keyscan -H "${{ secrets.DEPLOY_HOST#*@ }}" >> ~/.ssh/known_hosts || true
- name: Pull and restart on deploy host
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
ssh -i ~/.ssh/id_deploy "$DEPLOY_HOST" \
"cd $DEPLOY_PATH && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans"
- name: Verify health
env:
HEALTH_URL: ${{ secrets.HEALTH_URL }}
run: |
# Give the new container ~30s to come up, then poll for
# a 200 from /healthz. Failure aborts the workflow which
# is the alert.
for i in $(seq 1 12); do
if curl -fsS "$HEALTH_URL" >/dev/null; then
echo "deploy verified"
exit 0
fi
sleep 5
done
echo "deploy verification failed"
exit 1