Revert "ci: use GitHub Container Registry"
This reverts commit b29b017341.
This commit is contained in:
+7
-7
@@ -80,19 +80,19 @@ NPM_NETWORK=
|
||||
BACKUP_RETENTION_DAYS=14
|
||||
|
||||
# ── Container registry & deployment (used by deploy.sh) ────────────────
|
||||
# GitHub Container Registry. deploy.sh builds nothing — it only ever pulls
|
||||
# the image that GitHub Actions built and pushed.
|
||||
REGISTRY=ghcr.io
|
||||
REGISTRY_REPO=whitehai11/anouma
|
||||
# Gitea's built-in container registry. deploy.sh builds nothing — it only
|
||||
# ever pulls this image (see .gitea/workflows/release.yml, which is what
|
||||
# actually builds and pushes it).
|
||||
REGISTRY=git.maro.run
|
||||
REGISTRY_REPO=maro/anouma
|
||||
# Only needed if the registry/repository requires authentication to pull.
|
||||
# Use a GitHub token with at least read:packages as the password, not your
|
||||
# account password.
|
||||
# Use a Gitea access token as the password, not your account password.
|
||||
REGISTRY_USERNAME=
|
||||
REGISTRY_PASSWORD=
|
||||
# Set automatically by deploy.sh on every successful deploy — do not edit
|
||||
# by hand. Empty until the first release has been deployed.
|
||||
ANOUMA_IMAGE=
|
||||
# Informational only (shown in the setup summary) — actual automatic
|
||||
# deployment is controlled by whether release.yml + its GitHub secrets are
|
||||
# deployment is controlled by whether release.yml + its Gitea secrets are
|
||||
# configured, not by this flag.
|
||||
AUTO_DEPLOY=true
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
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.
|
||||
#
|
||||
# NODE_ENV is deliberately NOT set here: `npm ci` treats a production
|
||||
# NODE_ENV as `--omit=dev`, which would skip eslint/typescript/etc. and
|
||||
# break Lint/Typecheck/Test. It's set below, scoped to the Build step.
|
||||
env:
|
||||
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
|
||||
ANALYTICS_SALT_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 next typegen && npx tsc --noEmit
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build
|
||||
|
||||
# Builds once per commit that lands on main and has passed the checks
|
||||
# above, and pushes it to the registry tagged by commit SHA. release.yml
|
||||
# (triggered by a version tag) never rebuilds — it only promotes this
|
||||
# exact, already-tested image to the release tag + `latest` and deploys
|
||||
# it. This guarantees byte-for-byte that what gets deployed is what CI
|
||||
# verified, and makes tagging a commit that never passed CI on main fail
|
||||
# loudly (no image to promote) instead of silently rebuilding from
|
||||
# source and skipping the checks.
|
||||
build-image:
|
||||
needs: test
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
# The anouma-runner runs as a Docker container itself (gitea/runner:3),
|
||||
# with the host's docker.sock bind-mounted in but no `docker` CLI binary
|
||||
# of its own — a job with no `container:` here runs directly inside that
|
||||
# minimal runner image and gets "docker: command not found".
|
||||
#
|
||||
# A plain `docker:27-cli` image (Alpine, no Node.js) doesn't work either:
|
||||
# actions/checkout@v4 is a JS action and needs a Node runtime in the job
|
||||
# container, which that image doesn't have — it fails at Checkout with
|
||||
# exit 127. Using a Node image as the base guarantees Node is already
|
||||
# there (same reason the `test` job's node:22-alpine works), and the
|
||||
# Docker CLI is added via apt. The runner auto-mounts the same host
|
||||
# docker.sock into this container (sibling containers — see
|
||||
# container.docker_host in the runner's config.yaml), so `docker
|
||||
# build`/`push` operate on the host's real Docker daemon. release.yml's
|
||||
# promote-and-deploy job uses the same setup, for the same reasons.
|
||||
container:
|
||||
image: node:22-bookworm-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends docker.io
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Determine image name
|
||||
id: image
|
||||
run: |
|
||||
echo "registry=${REGISTRY:-git.maro.run}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo=${REGISTRY_REPO:-maro/anouma}" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
REGISTRY: git.maro.run
|
||||
REGISTRY_REPO: maro/anouma
|
||||
|
||||
- name: Log in to registry
|
||||
env:
|
||||
REGISTRY_IMAGE: ${{ steps.image.outputs.registry }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_IMAGE" -u "$REGISTRY_USERNAME" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
|
||||
# Tagged by commit SHA only — `latest` is reserved for whatever was
|
||||
# last actually released (see release.yml), not the newest main build.
|
||||
run: docker build -t "$IMAGE:$GITHUB_SHA" .
|
||||
|
||||
- name: Push image
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
|
||||
run: docker push "$IMAGE:$GITHUB_SHA"
|
||||
@@ -0,0 +1,120 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
# Deliberately does NOT rebuild or re-run lint/typecheck/test/build: the
|
||||
# image for this exact commit was already built and pushed by ci.yml's
|
||||
# build-image job when it landed on main (which itself only runs after
|
||||
# the `test` job passes). This job just promotes that already-verified
|
||||
# image to the release tag + `latest` and deploys it — if the tagged
|
||||
# commit was never pushed to main (or CI hasn't finished yet), the
|
||||
# "Pull the tested image" step below fails loudly instead of silently
|
||||
# rebuilding from source and skipping the checks.
|
||||
promote-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
# Node-based image (not the bare runner, not a Docker-only image) for
|
||||
# the same reasons as ci.yml's build-image job: the runner needs a
|
||||
# Docker CLI added via apt (it has none of its own), and actions/checkout
|
||||
# needs a Node runtime that a plain docker:*-cli image doesn't have.
|
||||
container:
|
||||
image: node:22-bookworm-slim
|
||||
steps:
|
||||
- name: Install Docker CLI and ssh client
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends docker.io openssh-client
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- 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).
|
||||
#
|
||||
# The tag name (github.ref_name / $GITHUB_REF_NAME) is
|
||||
# attacker-influenced: anyone with tag-push rights controls it, and
|
||||
# git allows shell metacharacters in tag names. It's read here via
|
||||
# the automatic $GITHUB_REF_NAME env var rather than `${{ github.ref_name }}`
|
||||
# so its value reaches bash as data (argv/envp), never spliced into
|
||||
# the script text — `${{ }}` expressions are substituted into the
|
||||
# YAML before bash ever sees the script, which is what makes
|
||||
# interpolating attacker-controlled values that way a script-injection
|
||||
# vector (arbitrary code execution in a job that holds the registry
|
||||
# and production deploy credentials).
|
||||
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
|
||||
env:
|
||||
REGISTRY_IMAGE: ${{ steps.image.outputs.registry }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_IMAGE" -u "$REGISTRY_USERNAME" --password-stdin
|
||||
|
||||
- name: Pull the tested image for this commit
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
|
||||
run: |
|
||||
docker pull "$IMAGE:$GITHUB_SHA" || {
|
||||
echo "::error::No image found for commit $GITHUB_SHA. This tag must point at a commit that was already pushed to main and built by ci.yml's build-image job — push to main first (and wait for CI to finish) before tagging a release." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Promote image to the release tag and latest
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
|
||||
VERSION: ${{ steps.image.outputs.version }}
|
||||
run: |
|
||||
docker tag "$IMAGE:$GITHUB_SHA" "$IMAGE:$VERSION"
|
||||
docker tag "$IMAGE:$GITHUB_SHA" "$IMAGE:latest"
|
||||
|
||||
- name: Push promoted tags
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
|
||||
VERSION: ${{ steps.image.outputs.version }}
|
||||
run: |
|
||||
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 }}
|
||||
shell: bash
|
||||
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}"
|
||||
@@ -46,18 +46,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
IMAGE: ghcr.io/whitehai11/anouma
|
||||
IMAGE: git.maro.run/maro/anouma
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: git.maro.run
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Build image
|
||||
run: docker build -t "$IMAGE:$GITHUB_SHA" .
|
||||
- name: Push image
|
||||
|
||||
@@ -6,20 +6,19 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
promote-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IMAGE: ghcr.io/whitehai11/anouma
|
||||
IMAGE: git.maro.run/maro/anouma
|
||||
steps:
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: git.maro.run
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Pull tested image
|
||||
run: docker pull "$IMAGE:$GITHUB_SHA"
|
||||
- name: Promote release image
|
||||
|
||||
+40
-19
@@ -1,10 +1,10 @@
|
||||
# ANOUMA — Deployment
|
||||
|
||||
Production deployment is fully CI/CD-driven via GitHub Actions. There is no `update.sh` and the production server never builds anything — it only ever pulls a tested image from GitHub Container Registry.
|
||||
Production deployment is fully CI/CD-driven via Gitea or GitHub Actions. There is no `update.sh` and the production server never builds anything — it only ever pulls a tested image from the registry.
|
||||
|
||||
```text
|
||||
Developer → git push main → GitHub Actions (CI)
|
||||
→ Tests → Docker Build → GHCR Push (tagged by commit SHA)
|
||||
Developer → git push main → Gitea Actions (CI)
|
||||
→ Tests → Docker Build → Registry Push (tagged by commit SHA)
|
||||
→ Release (git tag vX.Y.Z) → promote that exact image to vX.Y.Z + latest
|
||||
→ SSH → deploy.sh → docker compose pull → migrate → up -d → Healthcheck → LIVE
|
||||
```
|
||||
@@ -18,7 +18,7 @@ building from source and skipping the checks.
|
||||
## One-time server setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/whitehai11/anouma.git
|
||||
git clone https://git.maro.run/maro/anouma.git
|
||||
cd anouma
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
@@ -34,15 +34,15 @@ chmod +x setup.sh
|
||||
6. Creates a restricted `anouma-deploy` system user, generates a dedicated SSH keypair for it, and installs the public key with a **forced command** so that key can only ever run `deploy.sh` — never an interactive shell.
|
||||
7. Copies `docker-compose.yml` and `deploy.sh` into `/opt/anouma`.
|
||||
8. Starts Postgres (so it's ready for the first deploy).
|
||||
9. Prints the exact values to paste into **GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions**, including the deploy private key — shown only once, the first time the key is generated.
|
||||
9. Prints the exact values to paste into **Gitea → maro/anouma → Settings → Secrets → Actions**, including the deploy private key — shown only once, the first time the key is generated.
|
||||
|
||||
Run `./setup.sh --non-interactive` for a scripted install; any value that has no safe default and wasn't exported as an environment variable beforehand aborts with a clear message naming the missing variable, instead of silently guessing.
|
||||
|
||||
Re-running `./setup.sh` later (e.g. after pulling script updates) is always safe: existing secrets, the SSH key, the database and all volumes are left exactly as they are — only `docker-compose.yml` and `deploy.sh` themselves get refreshed, since those are generated artifacts, not data.
|
||||
|
||||
### After setup: add the GitHub secrets
|
||||
### After setup: add the Gitea secrets
|
||||
|
||||
Copy the deployment values `setup.sh` printed into **GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions**:
|
||||
Copy the block `setup.sh` printed into **Gitea → maro/anouma → Settings → Secrets → Actions**:
|
||||
|
||||
| Secret | Value |
|
||||
| --- | --- |
|
||||
@@ -51,7 +51,7 @@ Copy the deployment values `setup.sh` printed into **GitHub → whitehai11/anoum
|
||||
| `DEPLOY_USER` | `anouma-deploy` |
|
||||
| `DEPLOY_SSH_KEY` | the private key setup.sh printed |
|
||||
| `DEPLOY_HOST_KEY` | optional — pin the server's SSH host public key instead of trusting it on first connect (`ssh-keyscan -p <port> <host>` on a machine you already trust) |
|
||||
| `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | **not needed in GitHub Actions**; these belong in `/opt/anouma/.env` on the server for pulling the private GHCR image |
|
||||
| `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | registry push credentials (a Gitea access token as the password, not your account password) |
|
||||
|
||||
### First deploy
|
||||
|
||||
@@ -60,9 +60,28 @@ Nothing runs on the server yet at this point — no release has been built. Push
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push github v1.0.0 # GitHub Actions
|
||||
# alternativ: git push origin v1.0.0 # Gitea Actions
|
||||
```
|
||||
|
||||
The GitHub workflow takes it from there. Once it's live, seed the original content once: `ssh anouma-deploy@<host>` won't work (the key is restricted — see Security), so run it directly on the server instead: `cd /opt/anouma && docker compose run --rm app npm run seed`.
|
||||
The selected CI provider takes it from there (see below). Once it's live, seed the original content once: `ssh anouma-deploy@<host>` won't work (the key is restricted — see Security), so run it directly on the server instead: `cd /opt/anouma && docker compose run --rm app npm run seed`.
|
||||
|
||||
## Gitea Actions
|
||||
|
||||
### CI (`.gitea/workflows/ci.yml`)
|
||||
|
||||
Runs on every push and PR: `npm ci` → lint → typecheck → test → build, on the `ubuntu-latest`-labelled runner. No database is required — every Payload-backed route in this app is `force-dynamic`, so a production build never touches Postgres. (There's no automated test suite yet — `npm test` is an honest placeholder; add a real one, e.g. vitest, whenever the project needs it, and it'll run here automatically.)
|
||||
|
||||
On top of that, every push to `main` that passes the checks above also builds the Docker image and pushes it to the registry tagged by commit SHA (`git.maro.run/maro/anouma:<sha>`) — this is the one and only build of that commit; nothing later rebuilds it.
|
||||
|
||||
### Release (`.gitea/workflows/release.yml`)
|
||||
|
||||
Triggered by pushing a tag matching `v*` (e.g. `v1.2.0`):
|
||||
|
||||
1. Pulls the image CI already built and pushed for that commit's SHA — fails immediately with a clear error if it isn't there (the tagged commit was never on `main`, or CI for it hasn't finished yet).
|
||||
2. Retags that same image as `git.maro.run/maro/anouma:vX.Y.Z` **and** `:latest`, and pushes both.
|
||||
3. SSHes into the production server as `anouma-deploy` and runs `deploy.sh deploy vX.Y.Z`.
|
||||
|
||||
Production always runs a specific version tag — `latest` is pushed for convenience/reference only (and always points at the most recently *released* build, not just the newest commit on `main`), `deploy.sh` never uses it.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
@@ -72,26 +91,28 @@ The GitHub repository is `whitehai11/anouma`; the workflows live in
|
||||
|
||||
| Secret | Required | Value |
|
||||
| --- | --- | --- |
|
||||
| `REGISTRY_USERNAME` | yes | Gitea Container-Registry username, normally `maro` |
|
||||
| `REGISTRY_PASSWORD` | yes | Gitea access token/password with permission to push/pull `git.maro.run/maro/anouma` |
|
||||
| `DEPLOY_HOST` | yes | Public IP or hostname of the production server |
|
||||
| `DEPLOY_PORT` | yes | SSH port, normally `22` |
|
||||
| `DEPLOY_USER` | yes | `anouma-deploy` |
|
||||
| `DEPLOY_SSH_KEY` | yes | Private key generated by `setup.sh` for the restricted deploy user |
|
||||
| `DEPLOY_HOST_KEY` | no | Complete SSH host-key line from a trusted `ssh-keyscan`; safer than first-connect trust |
|
||||
|
||||
GitHub Actions authentifiziert sich mit dem automatisch bereitgestellten
|
||||
`GITHUB_TOKEN`; dafür musst du keine Registry-Secrets anlegen. Registry und
|
||||
Image sind im Workflow fest auf `ghcr.io/whitehai11/anouma` gesetzt. Runtime-Secrets
|
||||
Es werden keine zusätzlichen GitHub-Variables benötigt; Registry und Image
|
||||
sind im Workflow fest auf `git.maro.run/maro/anouma` gesetzt. Runtime-Secrets
|
||||
wie `PAYLOAD_SECRET`, `MEETING_SESSION_SECRET`, `LIVEKIT_API_SECRET` und
|
||||
`DATABASE_URI` gehören ausschließlich in `/opt/anouma/.env` auf dem Server und
|
||||
nicht in GitHub Actions.
|
||||
|
||||
Wichtig: Für Releases nur GitHub Actions verwenden. Gitea kann als reiner
|
||||
Repository-Spiegel bestehen bleiben, darf aber keine eigenen Build- oder
|
||||
Release-Workflows ausführen.
|
||||
Wichtig: Für Releases nur einen CI-Anbieter verwenden, damit nicht beide
|
||||
Systeme dasselbe Image parallel bauen oder deployen. Für dieses GitHub-Setup
|
||||
ist GitHub Actions die empfohlene Quelle; Gitea kann als Spiegel bestehen
|
||||
bleiben.
|
||||
|
||||
## `deploy.sh` (server-side, `/opt/anouma/deploy.sh`)
|
||||
|
||||
Not a manual tool — it's invoked exclusively by GitHub Actions over SSH. What it does for `deploy.sh deploy vX.Y.Z`:
|
||||
Not a manual tool — it's invoked exclusively by Gitea Actions over SSH. What it does for `deploy.sh deploy vX.Y.Z`:
|
||||
|
||||
```text
|
||||
Lock (refuse a second concurrent deploy)
|
||||
@@ -105,7 +126,7 @@ Lock (refuse a second concurrent deploy)
|
||||
→ success, or: automatic rollback to the previous version
|
||||
```
|
||||
|
||||
If the post-deploy healthcheck fails, `deploy.sh` automatically redeploys the **previous** version (recorded from `ANOUMA_IMAGE` before the attempt) and reports the failed deploy as exit code 1 — GitHub Actions shows the release as failed even though production recovered. Database migrations from the failed release are **not** reverted (write migrations to be forward-compatible); restore the pre-deploy backup manually if a migration truly needs undoing.
|
||||
If the post-deploy healthcheck fails, `deploy.sh` automatically redeploys the **previous** version (recorded from `ANOUMA_IMAGE` before the attempt) and reports the failed deploy as exit code 1 — Gitea Actions shows the release as failed even though production recovered. Database migrations from the failed release are **not** reverted (write migrations to be forward-compatible); restore the pre-deploy backup manually if a migration truly needs undoing.
|
||||
|
||||
A lock file (`/opt/anouma/.deploy.lock`) prevents two deploys from running at once; a stale lock from a killed process is detected and ignored automatically.
|
||||
|
||||
@@ -221,7 +242,7 @@ Neither `setup.sh` nor `deploy.sh` ever runs `docker system prune`, `docker volu
|
||||
- `anouma-deploy` **is** a member of the `docker` group, because running `docker compose` requires it — and Docker-group membership is well known to be effectively root-equivalent (a container can bind-mount the host filesystem). This is a deliberate, documented trade-off: the blast radius is contained by the two points above (no interactive access, and the only reachable code path is the fixed `deploy.sh` logic), not eliminated. For a stricter setup, consider rootless Docker or a dedicated deployment tool with finer-grained Docker API ACLs — out of scope here.
|
||||
- `.env` and the deploy SSH private key are `chmod 600`.
|
||||
- Secrets are never committed, never logged, and never printed more than once (`setup.sh` shows the private key only the first time it's generated; on a later run it prints "Existing deployment key found." instead).
|
||||
- `REGISTRY_PASSWORD`/`SMTP_PASSWORD`/etc. are masked (`********`) in `setup.sh`'s configuration summary. The GHCR token is needed only on the production server for pulling the private image; GitHub Actions uses its automatic `GITHUB_TOKEN`.
|
||||
- `REGISTRY_PASSWORD`/`SMTP_PASSWORD`/etc. are masked (`********`) in `setup.sh`'s configuration summary — only the one-time Gitea-secrets block shows real values, since that block exists specifically for you to copy them into Gitea.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -229,7 +250,7 @@ Neither `setup.sh` nor `deploy.sh` ever runs `docker system prune`, `docker volu
|
||||
| --- | --- |
|
||||
| `setup.sh` fails at "Datenbank wurde nicht rechtzeitig healthy" | `docker compose logs postgres` (from `/opt/anouma`) — usually a bad `POSTGRES_PASSWORD`/`DATABASE_URI` mismatch if you hand-edited `.env` |
|
||||
| Release workflow fails at "Pull the tested image" | The tagged commit was never pushed to `main`, or `main`'s CI/build-image run for it hasn't finished yet — push to `main`, wait for CI to go green, then tag |
|
||||
| Release workflow fails at "Deploy to production" | Check the GitHub repository secrets are all set correctly, and that the server's SSH port is reachable from the runner |
|
||||
| Release workflow fails at "Deploy to production" | Check the Gitea secrets are all set correctly, and that the server's SSH port is reachable from the runner |
|
||||
| App container unhealthy after a deploy | `ssh` directly to the server (not via the restricted key) and run `docker compose logs app` from `/opt/anouma` |
|
||||
| Video calls connect but no audio/video | Check LiveKit logs, the UDP 3478/50000–60000 firewall rules and the public `LIVEKIT_URL` |
|
||||
| Browser cannot connect to LiveKit | Check the LiveKit proxy host, trusted TLS certificate and that Websockets Support is enabled |
|
||||
|
||||
@@ -40,16 +40,16 @@ Der Versand ist idempotent (`reminder60Sent`/`reminder30Sent` je Anmeldung, `hos
|
||||
|
||||
## Setup (Produktion / Deployment)
|
||||
|
||||
Produktion läuft komplett über GitHub Actions CI/CD — der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus GitHub Container Registry. Einmalig auf dem Server:
|
||||
Produktion läuft komplett über Gitea Actions CI/CD — der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus der Registry. Einmalig auf dem Server:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/whitehai11/anouma.git
|
||||
git clone https://git.maro.run/maro/anouma.git
|
||||
cd anouma
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
`setup.sh` installiert bei Bedarf Docker, legt `/opt/anouma` an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für GitHub Actions ein und zeigt am Ende genau die Werte an, die als GitHub Repository Secrets hinterlegt werden müssen. Das Docker-Image landet unter `ghcr.io/whitehai11/anouma`. `./setup.sh --non-interactive` läuft ohne Rückfragen.
|
||||
`setup.sh` installiert bei Bedarf Docker, legt `/opt/anouma` an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für Gitea Actions ein und zeigt am Ende genau die Werte an, die als Gitea Secrets hinterlegt werden müssen. `./setup.sh --non-interactive` läuft ohne Rückfragen.
|
||||
|
||||
Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:
|
||||
|
||||
@@ -57,9 +57,9 @@ Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:
|
||||
git tag v1.2.0 && git push origin v1.2.0
|
||||
```
|
||||
|
||||
GitHub Actions testet, baut das Image, pusht es nach GHCR und deployt es per SSH auf den Server — inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.
|
||||
Gitea Actions testet, baut das Image, pusht es in die Registry und deployt es per SSH auf den Server — inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.
|
||||
|
||||
Für alle Details (Architektur, GitHub Secrets, CI/Release-Workflows, Reverse-Proxy- und LiveKit-Einrichtung, Environment-Variablen, Backups, Rollback, Security) siehe **[DEPLOYMENT.md](./DEPLOYMENT.md)**.
|
||||
Für alle Details (Architektur, Gitea Secrets, CI/Release-Workflows, Reverse-Proxy- und LiveKit-Einrichtung, Environment-Variablen, Backups, Rollback, Security) siehe **[DEPLOYMENT.md](./DEPLOYMENT.md)**.
|
||||
|
||||
## Lokale Entwicklung (ohne Docker für die App)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# ANOUMA — server-side deployment script.
|
||||
#
|
||||
# This is NOT a manual update tool. It is invoked exclusively by GitHub
|
||||
# This is NOT a manual update tool. It is invoked exclusively by Gitea
|
||||
# Actions over SSH as the restricted `anouma-deploy` user (see setup.sh,
|
||||
# which installs a forced-command SSH key that can only ever run this
|
||||
# script). The production server never builds an image — it only ever
|
||||
@@ -106,7 +106,7 @@ backup_database() {
|
||||
|
||||
registry_login() {
|
||||
if [ -n "${REGISTRY_USERNAME:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
|
||||
echo "$REGISTRY_PASSWORD" | docker login "${REGISTRY:-ghcr.io}" -u "$REGISTRY_USERNAME" --password-stdin >/dev/null 2>&1 \
|
||||
echo "$REGISTRY_PASSWORD" | docker login "${REGISTRY:-git.maro.run}" -u "$REGISTRY_USERNAME" --password-stdin >/dev/null 2>&1 \
|
||||
&& log_success "Registry login OK." \
|
||||
|| fail "Registry login failed — check REGISTRY_USERNAME/REGISTRY_PASSWORD in .env."
|
||||
fi
|
||||
@@ -116,7 +116,7 @@ registry_login() {
|
||||
# the rest of this script (and `docker compose`, via env_file) picks it up.
|
||||
set_image_version() {
|
||||
local version="$1"
|
||||
env_set ANOUMA_IMAGE "${REGISTRY:-ghcr.io}/${REGISTRY_REPO:-whitehai11/anouma}:${version}"
|
||||
env_set ANOUMA_IMAGE "${REGISTRY:-git.maro.run}/${REGISTRY_REPO:-maro/anouma}:${version}"
|
||||
set -a; source .env; set +a
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -30,10 +30,10 @@ services:
|
||||
|
||||
app:
|
||||
# Production never builds from source — the image is built once by
|
||||
# GitHub Actions on release and pushed to GHCR; this host only
|
||||
# Gitea Actions on release and pushed to the registry; this host only
|
||||
# ever pulls a specific, already-tested tag (see deploy.sh, which is the
|
||||
# only thing that ever changes ANOUMA_IMAGE in .env).
|
||||
image: ${ANOUMA_IMAGE:-ghcr.io/whitehai11/anouma:latest}
|
||||
image: ${ANOUMA_IMAGE:-git.maro.run/maro/anouma:latest}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- anouma-network
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# ANOUMA — production server installer.
|
||||
#
|
||||
# git clone https://github.com/whitehai11/anouma.git && cd anouma
|
||||
# git clone https://git.maro.run/maro/anouma.git && cd anouma
|
||||
# chmod +x setup.sh && ./setup.sh
|
||||
#
|
||||
# Prepares a fresh Ubuntu/Debian server for ANOUMA: installs Docker if
|
||||
# needed, creates /opt/anouma, writes a production .env with generated
|
||||
# secrets, creates a restricted `anouma-deploy` SSH user for GitHub Actions,
|
||||
# secrets, creates a restricted `anouma-deploy` SSH user for Gitea Actions,
|
||||
# and installs the server-side deploy.sh + docker-compose.yml.
|
||||
#
|
||||
# From then on, GitHub Actions owns all deployments (see
|
||||
# .github/workflows/release.yml) — there is no update.sh. Pushing a release
|
||||
# From then on, Gitea Actions owns all deployments (see
|
||||
# .gitea/workflows/release.yml) — there is no update.sh. Pushing a release
|
||||
# tag (vX.Y.Z) is the only way production gets updated.
|
||||
#
|
||||
# Safe to re-run: never regenerates an existing secret or SSH key, never
|
||||
@@ -151,7 +151,7 @@ command -v wget >/dev/null 2>&1 || log_info "wget nicht gefunden — wird hier n
|
||||
if systemctl is-active --quiet ssh 2>/dev/null || systemctl is-active --quiet sshd 2>/dev/null; then
|
||||
log_success "SSH-Server läuft."
|
||||
else
|
||||
log_warn "Kein laufender SSH-Server erkannt (ssh/sshd) — GitHub Actions kann ohne einen laufenden SSH-Server nicht deployen. Installiere/starte z. B. 'openssh-server'."
|
||||
log_warn "Kein laufender SSH-Server erkannt (ssh/sshd) — Gitea Actions kann ohne einen laufenden SSH-Server nicht deployen. Installiere/starte z. B. 'openssh-server'."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -317,8 +317,8 @@ GOOGLE_SITE_VERIFICATION=
|
||||
DOCKER_NETWORK=anouma-network
|
||||
NPM_NETWORK=
|
||||
BACKUP_RETENTION_DAYS=14
|
||||
REGISTRY=ghcr.io
|
||||
REGISTRY_REPO=whitehai11/anouma
|
||||
REGISTRY=git.maro.run
|
||||
REGISTRY_REPO=maro/anouma
|
||||
REGISTRY_USERNAME=
|
||||
REGISTRY_PASSWORD=
|
||||
ANOUMA_IMAGE=
|
||||
@@ -380,7 +380,7 @@ if [ -z "$(env_get NPM_NETWORK)" ] && [ "$NPM_DETECTED" = true ]; then
|
||||
fi
|
||||
|
||||
# --- registry --------------------------------------------------------------
|
||||
REGISTRY_HOST="$(env_get REGISTRY)"; REGISTRY_HOST="${REGISTRY_HOST:-ghcr.io}"
|
||||
REGISTRY_HOST="$(env_get REGISTRY)"; REGISTRY_HOST="${REGISTRY_HOST:-git.maro.run}"
|
||||
log_info "Prüfe Registry-Erreichbarkeit ($REGISTRY_HOST) …"
|
||||
REGISTRY_REACHABLE=false
|
||||
REGISTRY_HTTP_CODE="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 5 "https://${REGISTRY_HOST}/v2/" 2>/dev/null || echo "000")"
|
||||
@@ -405,7 +405,7 @@ if [ -z "$(env_get REGISTRY_USERNAME)" ]; then
|
||||
NEED_REGISTRY_AUTH="$(ask_yn "Werden Zugangsdaten für die Container Registry benötigt (privates Repository)?" "Y")"
|
||||
if [ "$NEED_REGISTRY_AUTH" = true ]; then
|
||||
REG_USER="$(ask "Registry Username" "" "REGISTRY_USERNAME")"
|
||||
REG_PASS="$(ask_secret "Registry Password/Token (GitHub Token mit read:packages)" "REGISTRY_PASSWORD")"
|
||||
REG_PASS="$(ask_secret "Registry Password/Token (Gitea Personal Access Token empfohlen)" "REGISTRY_PASSWORD")"
|
||||
if [ -z "$REG_USER" ] || [ -z "$REG_PASS" ]; then
|
||||
if [ "$NON_INTERACTIVE" = true ]; then
|
||||
fail "Registry-Zugangsdaten fehlen." "Setze die Umgebungsvariablen REGISTRY_USERNAME und REGISTRY_PASSWORD und starte setup.sh erneut, oder führe es interaktiv aus."
|
||||
@@ -421,7 +421,7 @@ else
|
||||
fi
|
||||
|
||||
if [ -z "$(env_get AUTO_DEPLOY)" ] || [ "$FRESH_INSTALL" = true ]; then
|
||||
AUTO_DEPLOY_ANSWER="$(ask_yn "Enable automatic deployment (GitHub Actions deployt automatisch bei neuen Releases)?" "Y")"
|
||||
AUTO_DEPLOY_ANSWER="$(ask_yn "Enable automatic deployment (Gitea Actions deployt automatisch bei neuen Releases)?" "Y")"
|
||||
env_set AUTO_DEPLOY "$AUTO_DEPLOY_ANSWER"
|
||||
fi
|
||||
|
||||
@@ -590,7 +590,7 @@ if [ -n "$EXISTING_IMAGE" ]; then
|
||||
log_warn "Anwendung konnte nicht (neu) gestartet werden — prüfe: $COMPOSE logs app"
|
||||
fi
|
||||
else
|
||||
log_info "Noch kein Release deployt — die Anwendung wird beim ersten Git-Tag automatisch über GitHub Actions gestartet."
|
||||
log_info "Noch kein Release deployt — die Anwendung wird beim ersten Git-Tag automatisch über Gitea Actions gestartet."
|
||||
if [ "$SKIP_SEED" = false ]; then
|
||||
log_info "Hinweis: Inhalte können nach dem ersten Deploy mit 'docker compose run --rm app npm run seed' eingespielt werden."
|
||||
fi
|
||||
@@ -635,7 +635,7 @@ echo ""
|
||||
echo "Registry:"
|
||||
if [ "$REGISTRY_REACHABLE" = true ]; then echo " ✓ ($(env_get REGISTRY))"; else echo " ⚠ nicht erreichbar geprüft ($(env_get REGISTRY))"; fi
|
||||
echo ""
|
||||
echo "GitHub Actions:"
|
||||
echo "Gitea Actions:"
|
||||
echo " Ready"
|
||||
echo ""
|
||||
echo "Automatic Deployment:"
|
||||
@@ -643,11 +643,11 @@ if [ "$(env_get AUTO_DEPLOY)" = "true" ]; then echo " Enabled"; else echo " Di
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "GITHUB DEPLOY SECRETS"
|
||||
echo "GITEA SECRETS"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
if [ "$SSH_KEY_FRESH" = true ]; then
|
||||
echo "Trage folgende Werte EINMALIG unter GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions ein."
|
||||
echo "Trage folgende Werte EINMALIG unter Gitea → maro/anouma → Settings → Secrets → Actions ein."
|
||||
echo "Der private Key wird nach diesem Lauf nicht erneut angezeigt."
|
||||
echo ""
|
||||
echo "DEPLOY_HOST:"
|
||||
@@ -664,13 +664,13 @@ if [ "$SSH_KEY_FRESH" = true ]; then
|
||||
$SUDO cat "$KEY_PATH"
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
echo "GHCR SERVER USERNAME (not a GitHub Secret):"
|
||||
echo "REGISTRY_USERNAME:"
|
||||
echo " $(env_get REGISTRY_USERNAME)"
|
||||
echo ""
|
||||
echo "GHCR SERVER TOKEN (not a GitHub Secret):"
|
||||
echo "REGISTRY_PASSWORD:"
|
||||
echo " $(env_get REGISTRY_PASSWORD)"
|
||||
else
|
||||
echo "Existing deployment key found — GitHub Secrets wurden bereits bei der ersten Einrichtung ausgegeben."
|
||||
echo "Existing deployment key found — Gitea Secrets wurden bereits bei der ersten Einrichtung ausgegeben."
|
||||
echo "Falls sie nicht mehr vorliegen: DEPLOY_SSH_KEY kann sicher eingesehen werden mit:"
|
||||
echo " sudo cat $KEY_PATH"
|
||||
fi
|
||||
@@ -681,7 +681,7 @@ echo "NEXT STEP"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Add these values to:"
|
||||
echo " GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions"
|
||||
echo " Gitea → maro/anouma → Settings → Secrets → Actions"
|
||||
echo ""
|
||||
echo "Danach reicht für jedes zukünftige Deployment:"
|
||||
echo " git tag v1.0.0 && git push origin v1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user