Overhaul deployment to Gitea Actions CI/CD, remove update.sh
Production no longer builds from source or self-updates via cron/git pull:
setup.sh now only provisions the server once (Docker, /opt/anouma, a
restricted `anouma-deploy` SSH user whose key can only ever run
deploy.sh, generated secrets). All future deployments run through
.gitea/workflows/ci.yml (lint/typecheck/test/build on every push) and
release.yml (on a vX.Y.Z tag: build the image, push it to the Gitea
registry, then SSH-trigger deploy.sh on the server), which pulls,
migrates, restarts, healthchecks, backs up the database first, and
automatically rolls back the code on a failed healthcheck.
docker-compose.yml's app service now runs a registry image
(${ANOUMA_IMAGE}) instead of building locally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
# No live database is needed here — every Payload-backed route in this
|
||||
# app is force-dynamic, so `next build` never touches Postgres. These
|
||||
# are dummy values only so a real config error (a missing secret) is
|
||||
# never silently masked by CI happening to run without one.
|
||||
env:
|
||||
NODE_ENV: production
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
DATABASE_URI: postgresql://ci:ci@localhost:5432/ci
|
||||
PAYLOAD_SECRET: ci-only-not-a-real-secret
|
||||
MEETING_SESSION_SECRET: ci-only-not-a-real-secret
|
||||
CRON_SECRET: ci-only-not-a-real-secret
|
||||
NEXT_PUBLIC_SERVER_URL: http://localhost:3000
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies (sharp needs libc6-compat on alpine)
|
||||
run: apk add --no-cache libc6-compat
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
env:
|
||||
NODE_ENV: production
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
DATABASE_URI: postgresql://ci:ci@localhost:5432/ci
|
||||
PAYLOAD_SECRET: ci-only-not-a-real-secret
|
||||
MEETING_SESSION_SECRET: ci-only-not-a-real-secret
|
||||
CRON_SECRET: ci-only-not-a-real-secret
|
||||
NEXT_PUBLIC_SERVER_URL: http://localhost:3000
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: apk add --no-cache libc6-compat
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npx tsc --noEmit
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
|
||||
build-and-deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
# No container here — this job needs the Docker CLI + an SSH client
|
||||
# directly on the runner host, not inside a Node container.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine image tags
|
||||
id: image
|
||||
# Gitea Actions' runner is protocol-compatible with GitHub Actions,
|
||||
# so step commands use the same $GITHUB_OUTPUT / $GITHUB_ENV
|
||||
# mechanism (the `gitea.*` vs `github.*` expression contexts are
|
||||
# just aliases of each other for `${{ }}` templating).
|
||||
run: |
|
||||
echo "registry=${REGISTRY:-git.maro.run}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo=${REGISTRY_REPO:-maro/anouma}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
REGISTRY: git.maro.run
|
||||
REGISTRY_REPO: maro/anouma
|
||||
|
||||
- name: Log in to registry
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ steps.image.outputs.registry }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
IMAGE="${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}"
|
||||
VERSION="${{ steps.image.outputs.version }}"
|
||||
docker build -t "$IMAGE:$VERSION" -t "$IMAGE:latest" .
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
IMAGE="${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}"
|
||||
VERSION="${{ steps.image.outputs.version }}"
|
||||
docker push "$IMAGE:$VERSION"
|
||||
docker push "$IMAGE:latest"
|
||||
|
||||
- name: Deploy to production
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
# Optional: pin the exact host key instead of trusting it on
|
||||
# first connect (see DEPLOYMENT.md). Safe to leave unset.
|
||||
DEPLOY_HOST_KEY: ${{ secrets.DEPLOY_HOST_KEY }}
|
||||
VERSION: ${{ steps.image.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
key_file="$(mktemp)"
|
||||
known_hosts_file="$(mktemp)"
|
||||
trap 'rm -f "$key_file" "$known_hosts_file"' EXIT
|
||||
|
||||
printf '%s\n' "$DEPLOY_SSH_KEY" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
|
||||
if [ -n "${DEPLOY_HOST_KEY:-}" ]; then
|
||||
printf '%s\n' "$DEPLOY_HOST_KEY" > "$known_hosts_file"
|
||||
else
|
||||
ssh-keyscan -p "${DEPLOY_PORT:-22}" "$DEPLOY_HOST" > "$known_hosts_file" 2>/dev/null
|
||||
fi
|
||||
|
||||
# The forced command in the server's authorized_keys (see
|
||||
# setup.sh) ignores this and always runs deploy.sh itself — the
|
||||
# string below only ever reaches it via $SSH_ORIGINAL_COMMAND.
|
||||
ssh -i "$key_file" -o UserKnownHostsFile="$known_hosts_file" -p "${DEPLOY_PORT:-22}" \
|
||||
"${DEPLOY_USER}@${DEPLOY_HOST}" "deploy ${VERSION}"
|
||||
Reference in New Issue
Block a user