diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c176597..e5b9ee6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,21 +41,17 @@ jobs: github_environment: ${{ steps.detect.outputs.github_environment }} db_schema: ${{ steps.detect.outputs.db_schema }} service_suffix: ${{ steps.detect.outputs.service_suffix }} - bot_secret_id: ${{ steps.detect.outputs.bot_secret_id }} - db_secret_name: ${{ steps.detect.outputs.db_secret_name }} ref: ${{ steps.detect.outputs.ref }} steps: - name: Determine target environment id: detect run: | - # Determine environment from input (manual) or branch (auto) if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then target_env="${{ inputs.environment }}" ref="${{ inputs.ref }}" else - # Auto-detect from branch - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + if [[ "${{ github.event.workflow_run.head_branch }}" == "main" ]]; then target_env="prod" else target_env="dev" @@ -63,28 +59,21 @@ jobs: ref="${{ github.event.workflow_run.head_sha }}" fi - # Set derived values if [[ "$target_env" == "prod" ]]; then github_environment="Production" db_schema="public" service_suffix="prod" - bot_secret_id="telegram-bot-token" - db_secret_name="DATABASE_URL" else github_environment="Development" db_schema="test" service_suffix="dev" - bot_secret_id="telegram-bot-token-test" - db_secret_name="DATABASE_URL_TEST" fi - echo "target_env=$target_env" >> "$GITHUB_OUTPUT" + echo "target_env=$target_env" >> "$GITHUB_OUTPUT" echo "github_environment=$github_environment" >> "$GITHUB_OUTPUT" - echo "db_schema=$db_schema" >> "$GITHUB_OUTPUT" + echo "db_schema=$db_schema" >> "$GITHUB_OUTPUT" echo "service_suffix=$service_suffix" >> "$GITHUB_OUTPUT" - echo "bot_secret_id=$bot_secret_id" >> "$GITHUB_OUTPUT" - echo "db_secret_name=$db_secret_name" >> "$GITHUB_OUTPUT" - echo "ref=$ref" >> "$GITHUB_OUTPUT" + echo "ref=$ref" >> "$GITHUB_OUTPUT" echo "Target environment: $target_env" echo "GitHub Environment: $github_environment" @@ -106,10 +95,10 @@ jobs: - name: Evaluate trigger and required secrets id: check env: - GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} - GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }} - DATABASE_URL: ${{ secrets[needs.detect-environment.outputs.db_secret_name] }} + GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }} + GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} run: | eligible_event=false if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then @@ -118,9 +107,9 @@ jobs: eligible_event=true fi - secrets_ok=false + vars_ok=false if [[ -n "$GCP_PROJECT_ID" && -n "$GCP_WORKLOAD_IDENTITY_PROVIDER" && -n "$GCP_SERVICE_ACCOUNT" ]]; then - secrets_ok=true + vars_ok=true fi db_secret_ok=false @@ -129,8 +118,8 @@ jobs: fi echo "eligible_event=$eligible_event" >> "$GITHUB_OUTPUT" - echo "secrets_ok=$secrets_ok" >> "$GITHUB_OUTPUT" - echo "db_secret_ok=$db_secret_ok" >> "$GITHUB_OUTPUT" + echo "secrets_ok=$vars_ok" >> "$GITHUB_OUTPUT" + echo "db_secret_ok=$db_secret_ok" >> "$GITHUB_OUTPUT" deploy: name: Deploy Cloud Run @@ -144,7 +133,7 @@ jobs: ARTIFACT_REPOSITORY: ${{ vars.ARTIFACT_REPOSITORY || 'household-bot' }} SERVICE_SUFFIX: ${{ needs.detect-environment.outputs.service_suffix }} DB_SCHEMA: ${{ needs.detect-environment.outputs.db_schema }} - BOT_SECRET_ID: ${{ needs.detect-environment.outputs.bot_secret_id }} + BOT_SECRET_ID: ${{ vars.TELEGRAM_BOT_TOKEN_SECRET_ID }} steps: - name: Checkout deployment ref @@ -163,25 +152,40 @@ jobs: - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - name: Run database migrations env: - DATABASE_URL: ${{ secrets[needs.detect-environment.outputs.db_secret_name] }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} DB_SCHEMA: ${{ needs.detect-environment.outputs.db_schema }} run: bun run db:migrate - name: Setup gcloud uses: google-github-actions/setup-gcloud@v2 + - name: Resolve image tags + id: images + run: | + # For workflow_run, use the SHA from the triggering commit (which CI already built & pushed). + # For workflow_dispatch, the user-supplied ref may be a branch or tag name — resolve to SHA. + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + deploy_sha=$(git rev-parse HEAD) + else + deploy_sha="${{ github.event.workflow_run.head_sha }}" + fi + + repo="${GCP_REGION}-docker.pkg.dev/${{ vars.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}" + echo "bot_image=${repo}/bot:${deploy_sha}" >> "$GITHUB_OUTPUT" + echo "mini_image=${repo}/miniapp:${deploy_sha}" >> "$GITHUB_OUTPUT" + - name: Load Telegram bot token for command sync id: telegram-token run: | set +e token="$(gcloud secrets versions access latest \ --secret "${BOT_SECRET_ID}" \ - --project "${{ secrets.GCP_PROJECT_ID }}" 2>/dev/null)" + --project "${{ vars.GCP_PROJECT_ID }}" 2>/dev/null)" status=$? set -e @@ -198,35 +202,12 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - - name: Configure Artifact Registry auth - run: | - gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet - - - name: Resolve image tags - id: images - run: | - bot_image="${GCP_REGION}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}/bot:${GITHUB_SHA}" - mini_image="${GCP_REGION}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}/miniapp:${GITHUB_SHA}" - - echo "bot_image=$bot_image" >> "$GITHUB_OUTPUT" - echo "mini_image=$mini_image" >> "$GITHUB_OUTPUT" - - - name: Build and push bot image - run: | - docker build -f apps/bot/Dockerfile -t "${{ steps.images.outputs.bot_image }}" . - docker push "${{ steps.images.outputs.bot_image }}" - - - name: Build and push mini app image - run: | - docker build -f apps/miniapp/Dockerfile -t "${{ steps.images.outputs.mini_image }}" . - docker push "${{ steps.images.outputs.mini_image }}" - - name: Deploy bot service run: | gcloud run deploy "household-${SERVICE_SUFFIX}-bot-api" \ --image "${{ steps.images.outputs.bot_image }}" \ --region "${GCP_REGION}" \ - --project "${{ secrets.GCP_PROJECT_ID }}" \ + --project "${{ vars.GCP_PROJECT_ID }}" \ --set-env-vars "DB_SCHEMA=${DB_SCHEMA}" \ --allow-unauthenticated \ --quiet @@ -236,7 +217,7 @@ jobs: gcloud run deploy "household-${SERVICE_SUFFIX}-mini-app" \ --image "${{ steps.images.outputs.mini_image }}" \ --region "${GCP_REGION}" \ - --project "${{ secrets.GCP_PROJECT_ID }}" \ + --project "${{ vars.GCP_PROJECT_ID }}" \ --allow-unauthenticated \ --quiet @@ -253,7 +234,7 @@ jobs: run: | SERVICE_URL=$(gcloud run services describe "household-${SERVICE_SUFFIX}-bot-api" \ --region "${GCP_REGION}" \ - --project "${{ secrets.GCP_PROJECT_ID }}" \ + --project "${{ vars.GCP_PROJECT_ID }}" \ --format 'value(status.url)') export TELEGRAM_WEBHOOK_URL="$SERVICE_URL/webhook/telegram" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ada797c..3dadc73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - main + - dev permissions: contents: read @@ -32,7 +33,6 @@ jobs: - build - db-check - db-migrations - steps: - name: Checkout uses: actions/checkout@v4 @@ -58,38 +58,20 @@ jobs: - 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 - ;; + 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 @@ -107,8 +89,9 @@ jobs: terraform -chdir=infra/terraform init -backend=false terraform -chdir=infra/terraform validate - images: - name: Docker / build ${{ matrix.service }} + images-pr: + name: Docker / build (PR) + if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -117,7 +100,6 @@ jobs: service: - bot - miniapp - steps: - name: Checkout uses: actions/checkout@v4 @@ -125,17 +107,95 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build container image + - name: Build only + uses: docker/build-push-action@v6 + with: + context: . + file: apps/${{ matrix.service }}/Dockerfile + push: false + tags: household-${{ matrix.service }}:ci + 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: | - case "${{ matrix.service }}" in - bot) - docker build -f apps/bot/Dockerfile -t household-bot:ci . - ;; - miniapp) - docker build -f apps/miniapp/Dockerfile -t household-miniapp:ci . - ;; - *) - echo "Unknown service: ${{ matrix.service }}" - exit 1 - ;; - esac + 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" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: apps/${{ matrix.service }}/Dockerfile + push: true + tags: ${{ steps.image.outputs.name }} + 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."