release: v0.3.0 (WIP — do not merge yet) #35
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, '-') }} |