name: CI on: push: branches: [master] pull_request: branches: [master] permissions: contents: read jobs: backend-tests: name: Backend Tests runs-on: ubuntu-latest services: postgres: image: postgres:17-alpine env: POSTGRES_USER: primora_test POSTGRES_PASSWORD: test POSTGRES_DB: primora_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.26' cache-dependency-path: apps/backend/go.sum - name: Run backend tests working-directory: apps/backend env: DATABASE_URL: postgres://primora_test:test@localhost:5432/primora_test?sslmode=disable run: go test -v ./... frontend-tests: name: Frontend Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run frontend tests run: npm run test --workspace @primora/frontend - name: TypeScript check run: npm run typecheck:frontend build: name: Build All Services runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.26' cache-dependency-path: apps/backend/go.sum - name: Install dependencies run: npm ci - name: Build frontend run: npm run build --workspace @primora/frontend - name: Build auth run: npm run build --workspace @primora/auth - name: Build backend working-directory: apps/backend run: go build -v ./cmd/server docker-build: name: Docker Build (${{ matrix.app }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: app: [backend, auth, frontend] steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build ${{ matrix.app }} image uses: docker/build-push-action@v6 with: context: . file: apps/${{ matrix.app }}/Dockerfile push: false load: true tags: primora-${{ matrix.app }}:test cache-from: type=gha,scope=${{ matrix.app }} cache-to: type=gha,mode=max,scope=${{ matrix.app }} - name: Smoke check ${{ matrix.app }} image contents run: | case "${{ matrix.app }}" in backend) docker run --rm --entrypoint sh primora-backend:test -c 'test -x /usr/local/bin/primora-backend && test -x /usr/local/bin/dbx-mcp' ;; auth) docker run --rm --entrypoint sh primora-auth:test -c 'test -f /workspace/apps/auth/dist/index.js' ;; frontend) docker run --rm --entrypoint sh primora-frontend:test -c 'test -f /usr/share/nginx/html/index.html' ;; esac # Trivy via its container image — the action's binary installer is # rate-limited by unauthenticated GitHub API calls on shared runners. - name: Scan ${{ matrix.app }} image run: >- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.65.0 image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 0 --format table primora-${{ matrix.app }}:test docker-publish: name: Publish edge images if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [docker-build, e2e-smoke] runs-on: ubuntu-latest permissions: packages: write contents: read strategy: fail-fast: false matrix: app: [backend, auth, frontend] steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 # ghcr.io requires lowercase repo paths; the owner login may be mixed-case. - name: Compute lowercase GHCR owner run: echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV" - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push edge + sha tags uses: docker/build-push-action@v6 with: context: . file: apps/${{ matrix.app }}/Dockerfile push: true tags: | ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:edge ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:sha-${{ github.sha }} cache-from: type=gha,scope=${{ matrix.app }} cache-to: type=gha,mode=max,scope=${{ matrix.app }} migrations: name: Migration Up/Down runs-on: ubuntu-latest services: postgres: image: postgres:17-alpine env: POSTGRES_USER: primora_test POSTGRES_PASSWORD: test POSTGRES_DB: primora_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.26' cache-dependency-path: apps/backend/go.sum - name: Install goose run: go install github.com/pressly/goose/v3/cmd/goose@v3.26.0 - name: Migrations up run: | goose -dir apps/backend/db/migrations \ postgres "postgres://primora_test:test@localhost:5432/primora_test?sslmode=disable" up - name: Migrations reset (all downs) run: | goose -dir apps/backend/db/migrations \ postgres "postgres://primora_test:test@localhost:5432/primora_test?sslmode=disable" reset - name: Migrations up again (idempotent re-apply) run: | goose -dir apps/backend/db/migrations \ postgres "postgres://primora_test:test@localhost:5432/primora_test?sslmode=disable" up e2e-smoke: name: Compose E2E Smoke runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Prepare env run: cp .env.example .env - name: Validate compose files run: >- docker compose -f docker-compose.yml -f docker-compose.dev.yml config -q - name: Start stack run: >- docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build - name: Run smoke checks run: ./scripts/smoke-e2e.sh - name: Dump logs on failure if: failure() run: >- docker compose -f docker-compose.yml -f docker-compose.dev.yml logs --tail=200 - name: Tear down if: always() run: >- docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.26' cache-dependency-path: apps/backend/go.sum - name: Install dependencies run: npm ci - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: apps/backend # Prebuilt binaries lag the repo's Go version — build the linter # with the toolchain above so it understands go 1.26. install-mode: goinstall