Add Docker packaging and Gitea Actions CI/CD pipeline.
Containerize the app for local and production deploys, and publish images to the Gitea container registry on main pushes and version tags. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.claude
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.log
|
||||||
|
server.log
|
||||||
|
|
||||||
|
# Secrets and local config (injected via compose env_file)
|
||||||
|
backend/.env
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Runtime artifacts (mounted as volumes in compose)
|
||||||
|
backend/uploads/
|
||||||
|
backend/outputs/
|
||||||
|
backend/.llm_cache/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Large local samples
|
||||||
|
samples/
|
||||||
|
*.pdf
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
name: Docker Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.scoutitsystems.com
|
||||||
|
# Gitea container registry: {registry}/{owner}/{image}
|
||||||
|
IMAGE: gitea.scoutitsystems.com/woogi/conflict-checker
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Image tags
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
short_sha="${GITEA_SHA:-${GITHUB_SHA}}"
|
||||||
|
short_sha="${short_sha:0:7}"
|
||||||
|
echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "tags<<EOF"
|
||||||
|
echo "${IMAGE}:latest"
|
||||||
|
echo "${IMAGE}:sha-${short_sha}"
|
||||||
|
if [ "${GITEA_REF_TYPE:-${GITHUB_REF_TYPE}}" = "tag" ]; then
|
||||||
|
ref_name="${GITEA_REF_NAME:-${GITHUB_REF_NAME}}"
|
||||||
|
echo "${IMAGE}:${ref_name}"
|
||||||
|
echo "${IMAGE}:${ref_name#v}"
|
||||||
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
IMAGE: ${{ env.IMAGE }}
|
||||||
|
GITEA_SHA: ${{ gitea.sha }}
|
||||||
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
GITEA_REF_TYPE: ${{ gitea.ref_type }}
|
||||||
|
GITHUB_REF_TYPE: ${{ github.ref_type }}
|
||||||
|
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build-and-push
|
||||||
|
if: gitea.ref_type == 'tag' || github.ref_type == 'tag'
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Publish Gitea release
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--experimental-fetch"
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
tag_name: ${{ gitea.ref_name }}
|
||||||
|
name: Conflict Checker ${{ gitea.ref_name }}
|
||||||
|
body: |
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login gitea.scoutitsystems.com
|
||||||
|
docker pull gitea.scoutitsystems.com/woogi/conflict-checker:${{ gitea.ref_name }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy on a host (with `docker-compose.prod.yml` and `backend/.env`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
IMAGE_TAG=${{ gitea.ref_name }} docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends poppler-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY backend backend
|
||||||
|
COPY frontend frontend
|
||||||
|
COPY cli cli
|
||||||
|
|
||||||
|
RUN mkdir -p backend/uploads backend/outputs backend/.llm_cache
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
EXPOSE 8099
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8099/health')"
|
||||||
|
|
||||||
|
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8099"]
|
||||||
@@ -33,12 +33,111 @@ and the conflict taxonomy.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
### Docker (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp backend/.env.example backend/.env # set AI_API_KEY (and SMTP if you want email)
|
||||||
|
docker compose up -d --build
|
||||||
|
# -> http://localhost:8099
|
||||||
|
```
|
||||||
|
|
||||||
|
Check health: `curl http://localhost:8099/health`
|
||||||
|
|
||||||
|
Logs: `docker compose logs -f app`
|
||||||
|
|
||||||
|
CLI inside the container (mount your PDF read-only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm \
|
||||||
|
-v "$(pwd)/samples/your_set.pdf:/data/set.pdf:ro" \
|
||||||
|
app python cli/run_check.py /data/set.pdf --out /app/backend/outputs/cli-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Persistent data lives in Docker volumes (`uploads`, `outputs`, `llm_cache`). To bind
|
||||||
|
mount host directories instead, replace the named volumes in `docker-compose.yml`.
|
||||||
|
|
||||||
|
For hybrid mode (local vLLM on the host), set `LOCAL_BASE_URL=http://host.docker.internal:8000/v1`
|
||||||
|
in `backend/.env`. Compose already maps `host.docker.internal` to the host gateway on Linux.
|
||||||
|
|
||||||
|
### Local Python
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .venv && . .venv/bin/activate
|
python3 -m venv .venv && . .venv/bin/activate
|
||||||
pip install -r requirements.txt # needs system poppler-utils for pdf2image
|
pip install -r requirements.txt # needs system poppler-utils for pdf2image
|
||||||
cp backend/.env.example backend/.env # then set AI_API_KEY to your OpenRouter key
|
cp backend/.env.example backend/.env # then set AI_API_KEY to your OpenRouter key
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Repository (Gitea)
|
||||||
|
|
||||||
|
Source of truth is Scout IT's Gitea instance:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Remote** | `https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git` |
|
||||||
|
| **Web UI** | https://gitea.scoutitsystems.com/woogi/Conflict_Checker |
|
||||||
|
|
||||||
|
Clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git
|
||||||
|
cd Conflict_Checker
|
||||||
|
```
|
||||||
|
|
||||||
|
This repo's git config uses a dedicated credential store at
|
||||||
|
`~/.config/git/gitea-credentials` (not your global GitHub credentials). If push/pull
|
||||||
|
prompts for auth, log in via the Gitea web UI and create a personal access token, then
|
||||||
|
store it there or run one authenticated push so the helper saves it.
|
||||||
|
|
||||||
|
## CI/CD (Gitea Actions → Container Registry)
|
||||||
|
|
||||||
|
Develop locally, push to Gitea, and let a runner build/publish the Docker image.
|
||||||
|
|
||||||
|
### One-time setup
|
||||||
|
|
||||||
|
1. **Enable Actions** on the repo: Settings → Actions → Enable Repository Actions.
|
||||||
|
2. **Runner** — an `act_runner` registered against `gitea.scoutitsystems.com`
|
||||||
|
with the `self-hosted` label and access to a Docker daemon (typically
|
||||||
|
`/var/run/docker.sock` mounted into the runner). This instance uses
|
||||||
|
`sits-docker-runner` (`self-hosted`, `linux`, `docker`).
|
||||||
|
3. **Registry secret** — create a Personal Access Token on Gitea with at least
|
||||||
|
`write:package` (and `write:release` if you use version tags). Add it as a
|
||||||
|
repository secret named **`REGISTRY_TOKEN`** (Settings → Actions → Secrets).
|
||||||
|
|
||||||
|
### What runs automatically
|
||||||
|
|
||||||
|
| Trigger | Result |
|
||||||
|
|---------|--------|
|
||||||
|
| Push to `main` | Image pushed as `:latest` and `:sha-<commit>` |
|
||||||
|
| Push tag `v*` (e.g. `v0.1.0`) | Image tagged with the release + Gitea Release created |
|
||||||
|
|
||||||
|
Image location:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gitea.scoutitsystems.com/woogi/conflict-checker:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow file: `.gitea/workflows/docker-release.yml`
|
||||||
|
|
||||||
|
### Local dev → deploy loop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Develop and test locally (venv or docker compose build)
|
||||||
|
git add -A && git commit -m "your change"
|
||||||
|
git push origin main # CI builds :latest
|
||||||
|
|
||||||
|
# 2. Cut a release when ready
|
||||||
|
git tag v0.1.0 && git push origin v0.1.0
|
||||||
|
|
||||||
|
# 3. On the deploy host — pull and run the published image
|
||||||
|
docker login gitea.scoutitsystems.com
|
||||||
|
IMAGE_TAG=v0.1.0 docker compose -f docker-compose.prod.yml up -d
|
||||||
|
# or: IMAGE_TAG=latest for the newest main-branch build
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `backend/.env` to the deploy host separately (never commit it). Use
|
||||||
|
`docker-compose.prod.yml` for production; keep `docker-compose.yml` for local
|
||||||
|
builds from source.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
CLI (the fast tuning loop — also dumps `assertions.json` / `clusters.json` for inspection):
|
CLI (the fast tuning loop — also dumps `assertions.json` / `clusters.json` for inspection):
|
||||||
@@ -51,9 +150,11 @@ python cli/run_check.py samples/your_set.pdf --out out/your_set
|
|||||||
Web UI (upload + view):
|
Web UI (upload + view):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvicorn backend.main:app --reload # open http://127.0.0.1:8000
|
uvicorn backend.main:app --reload --port 8099 # open http://127.0.0.1:8099
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or use Docker: `docker compose up -d` (see **Setup** above).
|
||||||
|
|
||||||
## Conflict categories
|
## Conflict categories
|
||||||
|
|
||||||
`dimensional_disagreement`, `elevation_disagreement`, `location_mismatch`,
|
`dimensional_disagreement`, `elevation_disagreement`, `location_mismatch`,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Production compose: pull the image built by Gitea Actions instead of building locally.
|
||||||
|
#
|
||||||
|
# docker login gitea.scoutitsystems.com
|
||||||
|
# IMAGE_TAG=latest docker compose -f docker-compose.prod.yml up -d
|
||||||
|
#
|
||||||
|
# Set IMAGE_TAG to a release tag (e.g. v0.1.0) or sha-abc1234 from CI.
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.scoutitsystems.com/woogi/conflict-checker:${IMAGE_TAG:-latest}
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8099}:8099"
|
||||||
|
env_file:
|
||||||
|
- backend/.env
|
||||||
|
environment:
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8099}
|
||||||
|
volumes:
|
||||||
|
- uploads:/app/backend/uploads
|
||||||
|
- outputs:/app/backend/outputs
|
||||||
|
- llm_cache:/app/backend/.llm_cache
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uploads:
|
||||||
|
outputs:
|
||||||
|
llm_cache:
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8099}:8099"
|
||||||
|
env_file:
|
||||||
|
- backend/.env
|
||||||
|
environment:
|
||||||
|
# Override in backend/.env for production (email links, etc.)
|
||||||
|
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8099}
|
||||||
|
volumes:
|
||||||
|
- uploads:/app/backend/uploads
|
||||||
|
- outputs:/app/backend/outputs
|
||||||
|
- llm_cache:/app/backend/.llm_cache
|
||||||
|
extra_hosts:
|
||||||
|
# Reach a vLLM box on the Docker host (hybrid text backend)
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uploads:
|
||||||
|
outputs:
|
||||||
|
llm_cache:
|
||||||
Reference in New Issue
Block a user