Skip to content

release: v0.3.0 (WIP — do not merge yet) #33

release: v0.3.0 (WIP — do not merge yet)

release: v0.3.0 (WIP — do not merge yet) #33

Workflow file for this run

name: CI
# Triggers:
# - on every PR / master push: backend pytest, frontend typecheck +
# lint, docker-compose syntax check. NO image build, NO ghcr push.
# - on push of a semver tag (``v*``): the same tests, then build +
# push all three images (backend / frontend / local-ai) to ghcr
# with the matching semver tag matrix and create a GitHub Release
# with auto-generated notes.
#
# Rationale for tag-only publishing: keeps the package registry clean
# (no accumulating ``:sha-XXX`` versions for every merge into master),
# matches the team's intent that ``:latest`` always points at a
# deliberate, named release.
on:
push:
branches: [main, master]
# Semver tags (``v0.2.0``, ``v0.3.0-rc1``, …) trigger a release run:
# builds the image with semver-derived tags AND creates a GitHub
# Release with auto-generated notes from commits since the previous
# tag. Pre-releases are detected by the ``-`` suffix and don't move
# the ``:latest`` image tag.
tags: ["v*"]
pull_request:
branches: [main, master]
# Cancel an in-flight run when a new commit comes in on the same branch
# / PR — saves Actions minutes on rapid pushes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
# ghcr.io requires lowercase image names; ${{ github.repository_owner }}
# preserves case, so we lowercase it at job time via a setup step.
BACKEND_IMAGE_NAME: wow-ai-log-analyzer-backend
FRONTEND_IMAGE_NAME: wow-ai-log-analyzer-frontend
LOCAL_AI_IMAGE_NAME: wow-ai-log-analyzer-local-ai
jobs:
backend-tests:
name: Backend (pytest)
runs-on: ubuntu-latest
# Tests run against the same engine as production (PostgreSQL),
# so dialect-specific features (JSONB ops, native UUID, …) are
# actually exercised. Redis is needed for the rate-limiter that
# runs on auth endpoints.
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test -d test"
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 5
env:
# GitHub-hosted services are exposed on localhost via the port
# mappings above. conftest reads TEST_DATABASE_URL first, then
# falls back to DATABASE_URL, then SQLite.
TEST_DATABASE_URL: postgresql+asyncpg://test:test@127.0.0.1:5432/test
REDIS_HOST: 127.0.0.1
REDIS_PORT: "6379"
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: latest
enable-cache: true
cache-dependency-glob: backend/uv.lock
- name: Sync dependencies
working-directory: backend
# ``--frozen`` requires backend/uv.lock to be committed (which
# it is); CI fails loudly if the lock and pyproject diverge.
run: uv sync --frozen
- name: Run pytest
working-directory: backend
run: uv run pytest -q
frontend-checks:
name: Frontend (typecheck + lint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
# ``npm ci`` requires package-lock.json (recommended for
# reproducibility). Falls back to ``npm install`` for the very
# first push before a lock has been committed.
run: |
if [ -f package-lock.json ]; then
npm ci
else
echo "::warning::package-lock.json not committed — using npm install. Run 'npm install' locally and commit the lockfile for reproducible builds."
npm install --no-audit --no-fund
fi
- name: TypeScript
working-directory: frontend
run: npm run typecheck
- name: ESLint
working-directory: frontend
run: npm run lint
compose-validate:
name: docker-compose syntax
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate compose file
# ``docker compose config`` substitutes env vars; we provide
# the example values so required-ish placeholders don't fail
# the parse step.
run: |
cp .env.example .env
docker compose config --quiet
# Three parallel image jobs (backend + frontend + local-ai). All
# deployment-agnostic since the runtime-config refactor (see
# docs/SETUP.md §8).
#
# Builds are gated on ``refs/tags/v*`` ONLY — pushing to master no
# longer creates ghcr package versions. Tests + lint still run on
# every push to master so regressions are caught early; images are
# only published when a deliberate semver tag is created. This
# prevents the registry from accumulating throwaway ``:sha-XXX``
# versions for every merge into master.
build-backend:
name: Build + push backend → ghcr.io
needs: [backend-tests, frontend-checks, compose-validate]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Lowercase repository owner
id: lowercase
run: |
echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tags
# Tag matrix (this job only runs on semver tag pushes):
#
# tag v0.2.0 → :0.2.0, :0.2, :0, :latest (stable)
# tag v0.2.0-rc1 → :0.2.0-rc1 (pre-release)
#
# ``flavor.latest=auto`` decides whether to add ``:latest`` —
# yes for plain semver, no for pre-releases.
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.owner }}/${{ env.BACKEND_IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build & push backend
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# ``scope`` keeps backend/frontend cache namespaces separate
# so they don't evict each other from the 10 GB GHA cache.
cache-from: type=gha,scope=backend
cache-to: type=gha,mode=max,scope=backend
build-frontend:
name: Build + push frontend → ghcr.io
needs: [backend-tests, frontend-checks, compose-validate]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Lowercase repository owner
id: lowercase
run: |
echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.owner }}/${{ env.FRONTEND_IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build & push frontend
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
build-local-ai:
name: Build + push local-ai → ghcr.io
needs: [backend-tests, frontend-checks, compose-validate]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Lowercase repository owner
id: lowercase
run: |
echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.owner }}/${{ env.LOCAL_AI_IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build & push local-ai
uses: docker/build-push-action@v5
with:
context: ./local-ai
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Image is mostly the upstream llama.cpp:server-cuda base
# (~2.3 GB compressed). ghcr deduplicates layers, so only
# the thin supervisor.py layer is uploaded after the first
# push. Cache scope keeps the upstream base layers warm
# across runs.
cache-from: type=gha,scope=local-ai
cache-to: type=gha,mode=max,scope=local-ai
release:
name: Create GitHub Release
needs: [build-backend, build-frontend, build-local-ai]
# Only on tag pushes — main-branch pushes don't need a release entry.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
# Full history so the release-notes generator can diff
# against the previous tag.
fetch-depth: 0
- name: Create release
uses: softprops/action-gh-release@v2
with:
# Auto-generate the release body from commits + merged PRs
# between the previous tag and this one (GitHub's standard
# changelog format with PRs/contributors grouped).
generate_release_notes: true
# Pre-release suffix (``-rc1``, ``-beta``, …) → mark as
# pre-release so users who pin :latest don't accidentally
# ship it. Stable semver tags become normal releases.
prerelease: ${{ contains(github.ref_name, '-') }}