name: CI on: push: branches: - main - dev pull_request: branches: - main - dev permissions: contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: quality: name: Quality / ${{ matrix.task }} runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: task: - format - lint - typecheck - test - build - db-check - db-migrations steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version-file: .bun-version - name: Restore Bun cache uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Run quality gate run: | case "${{ matrix.task }}" in format) bun run format:check ;; lint) bun run lint ;; typecheck) bun run typecheck ;; test) bun run test ;; build) bun run build ;; db-check) bun run db:check ;; db-migrations) bun run db:migrations:check ;; *) echo "Unknown task: ${{ matrix.task }}"; exit 1 ;; esac terraform: name: Terraform / validate runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.8.5 - name: Terraform format check run: terraform -chdir=infra/terraform fmt -check -recursive - name: Terraform validate run: | terraform -chdir=infra/terraform init -backend=false terraform -chdir=infra/terraform validate images-pr: name: Docker / build (PR) if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: service: - bot - miniapp steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build only uses: docker/build-push-action@v6 with: context: . file: apps/${{ matrix.service }}/Dockerfile push: false tags: household-${{ matrix.service }}:ci platforms: linux/amd64 provenance: false cache-from: type=gha cache-to: type=gha,mode=max images-push: name: Docker / build & push ${{ matrix.service }} if: github.event_name == 'push' runs-on: ubuntu-latest timeout-minutes: 20 environment: ${{ github.ref_name == 'main' && 'Production' || 'Development' }} strategy: fail-fast: false matrix: service: - bot - miniapp permissions: contents: read id-token: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - name: Configure Artifact Registry auth run: | gcloud auth configure-docker "${{ vars.GCP_REGION || 'europe-west1' }}-docker.pkg.dev" --quiet - name: Resolve image name id: image env: GCP_REGION: ${{ vars.GCP_REGION || 'europe-west1' }} ARTIFACT_REPOSITORY: ${{ vars.ARTIFACT_REPOSITORY || 'household-bot' }} run: | repo="${GCP_REGION}-docker.pkg.dev/${{ vars.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}" echo "name=${repo}/${{ matrix.service }}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" echo "cache_ref=${repo}/${{ matrix.service }}:cache" >> "$GITHUB_OUTPUT" echo "latest=${repo}/${{ matrix.service }}:latest" >> "$GITHUB_OUTPUT" - name: Build and push uses: docker/build-push-action@v6 with: context: . file: apps/${{ matrix.service }}/Dockerfile push: true tags: | ${{ steps.image.outputs.name }} ${{ steps.image.outputs.latest }} platforms: linux/amd64 provenance: false cache-from: type=registry,ref=${{ steps.image.outputs.cache_ref }} cache-to: type=registry,ref=${{ steps.image.outputs.cache_ref }},mode=max # Gate job: CD triggers on this workflow's conclusion. # By depending on all jobs, a failure in any job marks CI as failed, # which causes CD's `workflow_run` trigger to see conclusion != 'success' # and skip deployment. ci: name: CI complete runs-on: ubuntu-latest needs: [quality, terraform, images-pr, images-push] if: always() steps: - name: Check all jobs passed run: | results='${{ toJSON(needs) }}' echo "Job results: $results" failed=false for result in $(echo "$results" | jq -r '.[].result'); do # 'skipped' is expected — images-pr skips on push, images-push skips on PR if [[ "$result" != "success" && "$result" != "skipped" ]]; then echo "Job failed with result: $result" failed=true fi done if [[ "$failed" == "true" ]]; then echo "One or more CI jobs failed — blocking deployment." exit 1 fi echo "All CI jobs passed."