Compare commits

..

No commits in common. "main" and "feat/compose" have entirely different histories.

5 changed files with 0 additions and 186 deletions

View file

@ -1,19 +0,0 @@
.git
.gitignore
.venv
.python-version
.pytest_cache
.mypy_cache
.superpowers
__pycache__
**/__pycache__
*.py[cod]
*.egg-info
backups/
quartermaster.db
quartermaster.db-journal
tests/
docs/
CLAUDE.md
.dockerignore
Dockerfile

View file

@ -1,62 +0,0 @@
name: deploy
on:
push:
branches:
- main
jobs:
build-push-deploy:
runs-on: homelab
env:
REGISTRY: forgejo.labbity.unbiasedgeek.com
IMAGE: forgejo.labbity.unbiasedgeek.com/archeious/quartermaster/quartermaster
COMPOSE_PROJECT_NAME: quartermaster
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Forgejo registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: archeious
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.sha }}
${{ env.IMAGE }}:latest
- name: Deploy
run: |
set -euo pipefail
printf 'QUARTERMASTER_TAG=%s\n' '${{ github.sha }}' > .env
docker compose pull
docker compose up -d
- name: Smoke test
env:
SMOKE_PASSWORD: ${{ secrets.QUARTERMASTER_SMOKE_PASSWORD }}
run: |
set -eu
for attempt in 1 2 3 4 5 6 7 8 9 10; do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
-u "admin:$SMOKE_PASSWORD" \
https://quartermaster.unbiasedgeek.com/healthz || echo "000")
if [ "$code" = "200" ]; then
echo "smoke OK after $attempt attempt(s)"
exit 0
fi
echo "attempt $attempt: got $code, retrying"
sleep 3
done
echo "smoke FAILED — last code $code"
exit 1

View file

@ -1,34 +0,0 @@
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /usr/local/bin/
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PROJECT_ENVIRONMENT=/app/.venv \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --frozen --no-install-project
COPY src ./src
COPY alembic ./alembic
COPY alembic.ini ./
COPY scripts ./scripts
COPY README.md ./
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN uv sync --no-dev --frozen \
&& chmod +x /usr/local/bin/entrypoint.sh \
&& chown -R 1000:1000 /app
USER 1000:1000
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3).status == 200 else 1)"
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View file

@ -63,33 +63,6 @@ HTTP access logs appear as `event="http_request"` with `method`, `path`,
`month_closed`, `template_entry_updated`, `posting_added`, `month_closed`, `template_entry_updated`, `posting_added`,
`posting_deleted`) fire at the matching mutation sites. `posting_deleted`) fire at the matching mutation sites.
## Docker
A `Dockerfile` at the repo root produces a self-contained image that runs
`alembic upgrade head` (with the pre-upgrade backup hook) then
`uvicorn quartermaster.main:app` as a non-root user (`uid:gid 1000:1000`).
The image `EXPOSE`s port 8000 and declares a `HEALTHCHECK` against
`/healthz`.
Build and smoke-run locally against a tempfile database:
```sh
docker build -t quartermaster:dev .
mkdir -p /tmp/qm-data
docker run --rm -p 8000:8000 \
-e QUARTERMASTER_DB_URL=sqlite:////data/qm.db \
-v /tmp/qm-data:/data \
quartermaster:dev
```
Then `curl http://127.0.0.1:8000/healthz` should return
`{"status":"ok"}` with JSON access logs on the container's stdout.
In production on home-ctr-onyx the bind mount is `/mnt/quartermaster/`
and `QUARTERMASTER_DB_URL` points at the DB inside it; see the
compose file for the full wiring.
## Tests ## Tests
```sh ```sh
@ -167,34 +140,3 @@ editing the checked-in compose file.
path. Three would resolve relative to the working directory, and the path. Three would resolve relative to the working directory, and the
SQLite file would NOT land on the bind mount (on next restart the SQLite file would NOT land on the bind mount (on next restart the
database would be empty). database would be empty).
## CI/CD
Push to `main` triggers `.forgejo/workflows/deploy.yml` on the
`homelab` runner. That runner lives on home-ctr-onyx itself in
container mode with the host's Docker socket mounted — so the
workflow talks to the same Docker daemon that hosts the production
container and no SSH round-trip is needed.
The workflow: checks out the repo, builds the image, pushes it to
the Forgejo registry tagged with the commit SHA and `latest`,
writes `QUARTERMASTER_TAG=<git-sha>` to a `.env` file next to the
checked-out `compose.yml`, runs `docker compose pull && docker
compose up -d`, and finishes with one
`curl -fsS -u admin:… https://quartermaster.unbiasedgeek.com/healthz`
against the public URL — catching TLS, Traefik routing, and the
basic-auth middleware in a single probe.
The workflow reads two Forgejo Actions secrets (repo-scoped under
`archeious/quartermaster`):
* `REGISTRY_TOKEN` — archeious Forgejo personal token with
`write:package` scope; used as the docker-login password.
* `QUARTERMASTER_SMOKE_PASSWORD` — plaintext basic-auth password
for the `admin` user, delivered to the tenant out-of-band by the
platform team.
Rollback is manual for v1: `git checkout` the previous SHA, set
`QUARTERMASTER_TAG` in `.env` to that SHA, and `docker compose up -d`
from a clone of the repo (or let the previous commit be the `main`
tip and the deploy workflow will roll it out).

View file

@ -1,13 +0,0 @@
#!/usr/bin/env sh
set -eu
cd /app
alembic upgrade head
exec uvicorn quartermaster.main:app \
--host 0.0.0.0 \
--port 8000 \
--proxy-headers \
--forwarded-allow-ips='*' \
--log-config src/quartermaster/logconfig.json