diff --git a/.env.example b/.env.example index 8d36b78..84f9d65 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,11 @@ # These override values in config.yaml STEWARD_SECRET_KEY=change-me -STEWARD_DATABASE__URL=postgresql+asyncpg://steward:password@localhost/steward +STEWARD_DATABASE_URL=postgresql+asyncpg://steward:password@localhost/steward STEWARD_SMTP__PASSWORD= + +# --- compose.deploy.yml (remote instance) only --- +# Password for the bundled Postgres; the steward service builds its +# STEWARD_DATABASE_URL from it. Leave STEWARD_SECRET_KEY above unset to let the +# app generate + persist one on the /data volume. +POSTGRES_PASSWORD=change-me diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index ab6dea8..800746e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -90,3 +90,40 @@ jobs: pip install -e '.[dev,ansible]' fi pytest tests/integration -v -m integration --durations=10 + + # Image-publish lane — FabledRulebook rule 46: every push to dev/main publishes + # an immutable commit-SHA image (the rollback unit); dev moves :dev, main moves + # :latest. Gated on the three test lanes via `needs:` so a red commit never + # produces a :dev image. Mechanics mirror CI-runner's build-ci-python.yml. + publish: + needs: [lint, unit, integration] + runs-on: python-ci + container: + # ci-builder carries docker + buildx + git. The docker socket is + # auto-mounted by act_runner (valid_volumes) — do NOT add a + # container.options "-v /var/run/docker.sock:..." mount; that duplicate + # mount fails the job at "Set up job". + image: git.fabledsword.com/bvandeusen/ci-builder:latest + steps: + - uses: actions/checkout@v4 + - name: Registry login + # The injected GITHUB_TOKEN lacks write:package scope, so docker push + # 401s with it. REGISTRY_TOKEN is a repo Actions secret holding a + # Forgejo token scoped read:package + write:package. + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.fabledsword.com -u bvandeusen --password-stdin + - name: Build + push (commit-SHA always; :dev on dev, :latest on main) + run: | + set -euxo pipefail + IMAGE=git.fabledsword.com/bvandeusen/steward + docker build -t "$IMAGE:${{ github.sha }}" . + docker push "$IMAGE:${{ github.sha }}" + if [ "${{ github.ref }}" = "refs/heads/dev" ]; then + docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:dev" + docker push "$IMAGE:dev" + elif [ "${{ github.ref }}" = "refs/heads/main" ]; then + docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:latest" + docker push "$IMAGE:latest" + fi + - name: Prune dangling layers + if: always() + run: docker image prune -f diff --git a/README.md b/README.md index b7a6239..d7e2999 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,42 @@ docker compose up -d Open `http://localhost:5000`. On first run you'll be prompted to create the admin account. +This compose file builds the image locally and bind-mounts the source for live editing. To run a persistent instance off the published CI image instead, see **Remote dev instance** below. + +--- + +## Remote dev instance + +For a long-lived instance on a remote server that tracks the `dev` CI line, use `compose.deploy.yml`. It pulls the published image (`git.fabledsword.com/bvandeusen/steward:dev`) rather than building, runs no source bind-mounts, and persists data in named volumes. + +**One-time prerequisite — registry push token (on the Git host).** CI publishes the image, so the FabledSteward repo needs a push credential. The injected `GITHUB_TOKEN` lacks `write:package`, so create a Forgejo token scoped **`read:package` + `write:package`** and add it as the repo Actions secret **`REGISTRY_TOKEN`**. Until this exists, the `publish` CI job 401s and no `:dev` image is produced. + +**Standup (on the server):** + +```bash +# 1. Authenticate to the registry (read:package token is enough to pull) +docker login git.fabledsword.com -u + +# 2. Configure secrets +cp .env.example .env +# Set POSTGRES_PASSWORD. Leave STEWARD_SECRET_KEY unset to let the app +# generate + persist one on the /data volume. + +# 3. Pull + boot +docker compose -f compose.deploy.yml up -d +``` + +Open `http://:5000` and create the admin account on first run. + +**Update to the latest dev build:** + +```bash +docker compose -f compose.deploy.yml pull +docker compose -f compose.deploy.yml up -d +``` + +Every push to `dev` that goes CI-green publishes a fresh `:dev` (and an immutable `:` for rollback). To pin a specific commit, change the `image:` tag in `compose.deploy.yml` to `:`. + --- ## Quick Start — Bare Metal diff --git a/ci-requirements.md b/ci-requirements.md index 01a73e4..86c04bd 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -14,8 +14,12 @@ git.fabledsword.com/bvandeusen/ci-python:3.14 ## Per-job tool installs -- `uv pip install --system -e '.[dev]'` (pip fallback) — in `unit` and `integration` - jobs. Steward declares its deps in `pyproject.toml`; the `[dev]` extra adds +- `uv pip install --system -e '.[dev]'` (pip fallback) — in the `unit` job. +- `uv pip install --system -e '.[dev,ansible]'` (pip fallback) — in the + `integration` job, which boots the real app and exercises the Ansible runner, + so it needs the `ansible` extra (`ansible>=10,<13`) at import time. + + Steward declares its deps in `pyproject.toml`; the `[dev]` extra adds `pytest` + `pytest-asyncio`. No `requirements.txt` is tracked. ## Lanes @@ -30,6 +34,13 @@ git.fabledsword.com/bvandeusen/ci-python:3.14 (`create_app(testing=False)`), which runs core + every bundled plugin's migrations, then round-trips the DB. This is the guard that the folded-in plugin migration graph resolves. +- **publish** — builds the runtime `Dockerfile` and pushes to the package + registry. Runs in `ci-builder:latest` (docker + buildx, socket auto-mounted), + gated on `needs: [lint, unit, integration]` so a red commit never ships an + image. Publishes `git.fabledsword.com/bvandeusen/steward:` always, plus + `:dev` on `dev` and `:latest` on `main` (FabledRulebook rule 46). Needs the + `REGISTRY_TOKEN` repo Actions secret (`read:package` + `write:package`) — + the injected `GITHUB_TOKEN` can't push packages. ## Notes diff --git a/compose.deploy.yml b/compose.deploy.yml new file mode 100644 index 0000000..2e19a38 --- /dev/null +++ b/compose.deploy.yml @@ -0,0 +1,53 @@ +# Remote deployment — pulls the published :dev image instead of building locally. +# +# This is NOT the local-dev compose. `docker-compose.yml` bind-mounts ./steward +# + ./plugins and runs --debug for live editing; THIS file runs the image as the +# source of truth (no bind-mounts, no reloader) for a persistent remote instance +# that tracks the dev CI line. +# +# Standup + update runbook: see README → "Remote dev instance". + +services: + steward: + image: git.fabledsword.com/bvandeusen/steward:dev + container_name: steward + # Re-pull :dev on every `up` so restarting the stack picks up the latest + # green dev build without a manual `docker pull`. + pull_policy: always + ports: + - "5000:5000" + volumes: + # /data holds the auto-generated secret key, third-party plugins, and the + # Ansible playbook cache — all of which must survive image updates. No + # source bind-mounts: core + first-party plugins live inside the image. + - app_data:/data + environment: + - STEWARD_DATABASE_URL=postgresql+asyncpg://steward:${POSTGRES_PASSWORD:-steward}@db/steward + # Optional: if unset, the app generates a key and persists it to + # /data/secret.key, so sessions survive restarts via the app_data volume. + # Set STEWARD_SECRET_KEY in .env to pin it explicitly across volume resets. + - STEWARD_SECRET_KEY=${STEWARD_SECRET_KEY:-} + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: steward + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-steward} + POSTGRES_DB: steward + volumes: + - pgdata:/var/lib/postgresql/data + # No host port published — Postgres is reachable only on the compose network. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U steward"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + pgdata: + app_data: