feat(ci): publish steward image to the registry + remote-deploy compose
Add the rule-46 image-publish lane: a `publish` job gated on [lint, unit, integration] that builds the runtime Dockerfile in ci-builder and pushes git.fabledsword.com/bvandeusen/steward:<sha> always, :dev on dev and :latest on main. Authenticates with the REGISTRY_TOKEN repo secret (the injected GITHUB_TOKEN lacks write:package). Steward is the first family app to implement the publish lane; mechanics mirror CI-runner's build-ci-python.yml. Add compose.deploy.yml for a persistent remote instance that pulls :dev (no source bind-mounts, persistent app_data+pgdata volumes, bundled Postgres), plus a README "Remote dev instance" runbook and the POSTGRES_PASSWORD env key. Fix .env.example's database URL var to the canonical STEWARD_DATABASE_URL (single underscore) and document the publish lane in ci-requirements.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <user>
|
||||
|
||||
# 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://<server>: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 `:<commit-sha>` for rollback). To pin a specific commit, change the `image:` tag in `compose.deploy.yml` to `:<sha>`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start — Bare Metal
|
||||
|
||||
+13
-2
@@ -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:<sha>` 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
|
||||
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user