# Continuous integration on git.menzel.center (Gitea Actions). # # Two jobs: `check` runs the test suite, the linter and the frontend # typecheck; `build-and-push` only starts once `check` is green, so a red # suite never reaches the registry (and never reaches the self-update # checker, which would happily offer a broken release). # # Builds and pushes the three images to this instance's container registry # on every push to main: backend, frontend, and agent (which is built FROM # the backend image - see agent/Dockerfile - so it has to come after). Each # image gets both a ":latest" tag and a ":{APP_VERSION}" tag, the latter read # from backend/version.py (the single source of truth for the release # version) - self_update_service compares registry version *tags* against the # running APP_VERSION to decide whether an update is available, so without a # version tag it would never see one, no matter how far behind :latest is. # # Runs on ubuntu-latest, not the docker label: that label's image is a bare # docker:24-dind with no Node/bash, which breaks actions/checkout (a JS # action). ubuntu-latest has both a shell and the Docker CLI, talking to the # host daemon through the socket the runner passes in. name: CI on: push: branches: [main] env: REGISTRY: git.menzel.center/menzeljonas jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: token: ${{ secrets.CI_TOKEN }} - uses: actions/setup-python@v5 with: python-version: "3.12" # matches backend/Dockerfile - name: Install backend + test dependencies working-directory: backend run: pip install -r requirements-dev.txt - name: Lint (ruff) working-directory: backend run: ruff check . # The suite runs without a Docker daemon on purpose: it drives the app # through TestClient without the lifespan, so no background loops and no # socket. See backend/tests/conftest.py. - name: Test (pytest) working-directory: backend run: pytest - uses: actions/setup-node@v4 with: node-version: "20" cache: npm cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies working-directory: frontend run: npm ci - name: Typecheck (tsc) working-directory: frontend run: npx tsc --noEmit -p tsconfig.json build-and-push: needs: check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: token: ${{ secrets.CI_TOKEN }} - name: Read app version id: version run: | VERSION=$(grep -oP '(?<=APP_VERSION = ")[^"]+' backend/version.py) echo "Building version $VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Log in to the registry run: | echo "${{ secrets.CI_TOKEN }}" | docker login git.menzel.center -u menzeljonas --password-stdin - name: Build and push backend run: | docker build \ -t "$REGISTRY/stackpilot-backend:latest" \ -t "$REGISTRY/stackpilot-backend:${{ steps.version.outputs.version }}" \ ./backend docker push "$REGISTRY/stackpilot-backend:latest" docker push "$REGISTRY/stackpilot-backend:${{ steps.version.outputs.version }}" - name: Build and push frontend run: | docker build \ -t "$REGISTRY/stackpilot-frontend:latest" \ -t "$REGISTRY/stackpilot-frontend:${{ steps.version.outputs.version }}" \ ./frontend docker push "$REGISTRY/stackpilot-frontend:latest" docker push "$REGISTRY/stackpilot-frontend:${{ steps.version.outputs.version }}" # Uses the backend image just pushed above as its base (already in the # local Docker cache from that step, so this doesn't need to pull it). - name: Build and push agent run: | docker build \ --build-arg "BACKEND_IMAGE=$REGISTRY/stackpilot-backend:latest" \ -t "$REGISTRY/stackpilot-agent:latest" \ -t "$REGISTRY/stackpilot-agent:${{ steps.version.outputs.version }}" \ ./agent docker push "$REGISTRY/stackpilot-agent:latest" docker push "$REGISTRY/stackpilot-agent:${{ steps.version.outputs.version }}"