Auto Deploy Backend to Development #1300
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: Auto Deploy Backend to Development | |
| on: | |
| workflow_run: | |
| workflows: ["Release Eligibility"] | |
| branches: [main] | |
| types: [completed] | |
| # Share the development backend mutation domain with manual deploys, traffic | |
| # repair, backend-listen, and LLM Gateway's shared backend-secrets updates. | |
| concurrency: | |
| group: deploy-backend-stack-development | |
| cancel-in-progress: false | |
| env: | |
| SERVICE: backend | |
| REGION: us-central1 | |
| # Cloud Run tags are service-scoped. Reusing this tag atomically moves each | |
| # service's candidate URL without accumulating stale no-traffic routes. | |
| CANDIDATE_TAG: candidate | |
| jobs: | |
| firestore_readiness: | |
| # A workflow_run can access deployment credentials, so never run source | |
| # code until the first completed proof identifies the exact current commit | |
| # on this repository's main branch. Every later checkout and release vector | |
| # uses that admitted SHA rather than a mutable default branch ref. | |
| if: >- | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push' && | |
| github.event.workflow_run.run_attempt == 1 && | |
| github.event.workflow_run.head_branch == 'main' && | |
| github.event.workflow_run.head_repository.full_name == github.repository | |
| environment: development | |
| permissions: | |
| contents: 'read' | |
| runs-on: ubuntu-latest-m | |
| outputs: | |
| admitted_sha: ${{ steps.admitted_source.outputs.admitted_sha }} | |
| steps: | |
| # This checks out current main solely to run the guard script. Source | |
| # selected by the workflow_run is not checked out or executed until the | |
| # guard establishes it is still this exact main commit. | |
| - name: Checkout current main for automatic source admission | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| - name: Verify Release Eligibility proof is current main | |
| id: admitted_source | |
| env: | |
| RELEASE_SHA: ${{ github.event.workflow_run.head_sha }} | |
| RELEASE_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| main_sha="$(git rev-parse --verify 'origin/main^{commit}')" | |
| checkout_sha="$(git rev-parse --verify HEAD)" | |
| python3 .github/scripts/verify_auto_backend_release_admission.py \ | |
| --sha "$RELEASE_SHA" \ | |
| --main-sha "$main_sha" \ | |
| --checkout-sha "$checkout_sha" \ | |
| --run-attempt "$RELEASE_RUN_ATTEMPT" | |
| printf 'admitted_sha=%s\n' "$RELEASE_SHA" >> "$GITHUB_OUTPUT" | |
| - name: Require read-only Firestore credentials | |
| env: | |
| GCP_FIRESTORE_READONLY_CREDENTIALS: ${{ secrets.GCP_FIRESTORE_READONLY_CREDENTIALS }} | |
| run: | | |
| if [ -z "$GCP_FIRESTORE_READONLY_CREDENTIALS" ]; then | |
| echo "::error title=Missing Firestore read-only credentials::Set GCP_FIRESTORE_READONLY_CREDENTIALS in the development environment before deploying." | |
| exit 1 | |
| fi | |
| - name: Checkout admitted Firestore source | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ steps.admitted_source.outputs.admitted_sha }} | |
| - name: Google Auth for read-only Firestore inventory | |
| uses: 'google-github-actions/auth@v3' | |
| with: | |
| credentials_json: ${{ secrets.GCP_FIRESTORE_READONLY_CREDENTIALS }} | |
| - name: Set up gcloud | |
| uses: google-github-actions/setup-gcloud@v3 | |
| - name: Verify serving Firestore indexes | |
| id: firestore_readiness | |
| env: | |
| FIRESTORE_PROPOSAL_PATH: ${{ runner.temp }}/firestore-schema-proposal-${{ github.run_id }}-${{ github.run_attempt }}.json | |
| FIRESTORE_SOURCE_COMMIT: ${{ steps.admitted_source.outputs.admitted_sha }} | |
| run: | | |
| python3 backend/scripts/reconcile_firestore_indexes.py \ | |
| --project "${{ vars.RUNTIME_GCP_PROJECT_ID }}" \ | |
| --check-only \ | |
| --proposal-output "$FIRESTORE_PROPOSAL_PATH" \ | |
| --source-commit "$FIRESTORE_SOURCE_COMMIT" \ | |
| --proposal-ttl-seconds 3600 | |
| - name: Validate blocked Firestore schema proposal | |
| id: validate_firestore_proposal | |
| if: ${{ failure() && steps.firestore_readiness.outcome == 'failure' }} | |
| env: | |
| FIRESTORE_PROPOSAL_PATH: ${{ runner.temp }}/firestore-schema-proposal-${{ github.run_id }}-${{ github.run_attempt }}.json | |
| FIRESTORE_SOURCE_COMMIT: ${{ steps.admitted_source.outputs.admitted_sha }} | |
| run: | | |
| python3 backend/scripts/reconcile_firestore_indexes.py \ | |
| --project "${{ vars.RUNTIME_GCP_PROJECT_ID }}" \ | |
| --validate-proposal "$FIRESTORE_PROPOSAL_PATH" \ | |
| --source-commit "$FIRESTORE_SOURCE_COMMIT" \ | |
| --proposal-ttl-seconds 3600 | |
| - name: Preserve blocked Firestore schema proposal | |
| if: ${{ failure() && steps.firestore_readiness.outcome == 'failure' && steps.validate_firestore_proposal.outcome == 'success' }} | |
| uses: actions/upload-artifact@v7 | |
| env: | |
| FIRESTORE_PROPOSAL_PATH: ${{ runner.temp }}/firestore-schema-proposal-${{ github.run_id }}-${{ github.run_attempt }}.json | |
| with: | |
| name: firestore-schema-proposal-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ env.FIRESTORE_PROPOSAL_PATH }} | |
| if-no-files-found: error | |
| retention-days: 1 | |
| deploy: | |
| needs: firestore_readiness | |
| environment: development | |
| permissions: | |
| contents: 'read' | |
| id-token: 'write' | |
| runs-on: ubuntu-latest-m | |
| steps: | |
| # To workaround "no space left on device" issue of GitHub-hosted runner | |
| - name: Delete huge unnecessary tools folder | |
| run: rm -rf /opt/hostedtoolcache | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.firestore_readiness.outputs.admitted_sha }} | |
| - name: Google Auth | |
| id: auth | |
| uses: 'google-github-actions/auth@v3' | |
| with: | |
| credentials_json: ${{ secrets.GCP_CREDENTIALS }} | |
| - name: Set up gcloud | |
| uses: google-github-actions/setup-gcloud@v3 | |
| - name: Verify serving sync ledger fence mode before deploy | |
| env: | |
| SYNC_LEDGER_FENCE_MODE: ${{ vars.SYNC_LEDGER_FENCE_MODE || 'legacy' }} | |
| run: | | |
| python3 backend/scripts/verify_sync_ledger_fence_transition.py \ | |
| --project=${{ vars.GCP_PROJECT_ID }} \ | |
| --region=${{ env.REGION }} \ | |
| --desired-mode="$SYNC_LEDGER_FENCE_MODE" \ | |
| --allow-tagged-no-percent-targets | |
| - name: Login to GCR | |
| run: gcloud auth configure-docker | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Compute short SHA | |
| id: image-tag | |
| run: | | |
| SHORT_SHA="$(git rev-parse --short=7 HEAD)" | |
| echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "revision_suffix=${SHORT_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" | |
| - name: Install Python deps for deploy scripts | |
| run: python3 -m pip install -q pyyaml | |
| - name: Get GKE credentials for gateway serving gate | |
| uses: google-github-actions/get-gke-credentials@v3 | |
| with: | |
| cluster_name: ${{ vars.GKE_CLUSTER }} | |
| location: ${{ env.REGION }} | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| - name: Verify LLM gateway control plane before promotion | |
| id: gateway-serving | |
| run: | | |
| python3 backend/scripts/verify-llm-gateway-serving.py \ | |
| --environment=dev \ | |
| --project="${{ vars.GCP_PROJECT_ID }}" \ | |
| --region="${{ env.REGION }}" \ | |
| --github-output "$GITHUB_OUTPUT" | |
| - name: Preflight Cloud Run deploy | |
| run: | | |
| python3 backend/scripts/preflight-cloud-run-deploy.py \ | |
| --env dev \ | |
| --project ${{ vars.GCP_PROJECT_ID }} \ | |
| --region ${{ env.REGION }} \ | |
| --check-secrets \ | |
| --check-traffic \ | |
| --repair-traffic | |
| - name: Validate backend runtime env before deploy | |
| env: | |
| SYNC_LEDGER_FENCE_MODE: ${{ vars.SYNC_LEDGER_FENCE_MODE || 'legacy' }} | |
| run: | | |
| python3 backend/scripts/validate-backend-runtime-env.py --env dev --check-workflows --check-rendered-cloud-run | |
| - name: Build runtime image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./backend/Dockerfile | |
| push: false | |
| load: true | |
| tags: | | |
| gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:latest | |
| gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| cache-from: type=registry,ref=gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:buildcache | |
| cache-to: type=registry,ref=gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:buildcache,mode=max | |
| - name: Verify built runtime image before publish | |
| run: | | |
| image=gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| python3 backend/scripts/runtime_image_contracts.py smoke --dockerfile backend/Dockerfile --image "$image" | |
| - name: Push verified runtime image | |
| run: | | |
| docker push gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:latest | |
| docker push gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| - name: Probe LLM gateway from the Cloud Run VPC before promotion | |
| run: | | |
| bash backend/scripts/probe-llm-gateway-from-cloud-run.sh \ | |
| --project "${{ vars.GCP_PROJECT_ID }}" \ | |
| --region "${{ env.REGION }}" \ | |
| --image "gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }}" \ | |
| --gateway-url "${{ steps.gateway-serving.outputs.gateway_url }}" \ | |
| --network "${{ vars.CLOUD_RUN_VPC_NETWORK }}" \ | |
| --subnet "${{ vars.CLOUD_RUN_VPC_SUBNET }}" \ | |
| --vpc-egress private-ranges-only \ | |
| --name-suffix "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| - name: Render backend runtime env from the gated gateway endpoint | |
| id: runtime-env | |
| env: | |
| CLOUD_RUN_VPC_NETWORK: ${{ vars.CLOUD_RUN_VPC_NETWORK }} | |
| CLOUD_RUN_VPC_SUBNET: ${{ vars.CLOUD_RUN_VPC_SUBNET }} | |
| GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }} | |
| OMI_LLM_GATEWAY_URL: ${{ steps.gateway-serving.outputs.gateway_url }} | |
| SYNC_LEDGER_FENCE_MODE: ${{ vars.SYNC_LEDGER_FENCE_MODE || 'legacy' }} | |
| run: | | |
| python3 backend/scripts/render_backend_runtime_env.py --env dev >> "$GITHUB_OUTPUT" | |
| - name: Migrate legacy public Cloud Run bindings | |
| run: >- | |
| python3 backend/scripts/preflight-cloud-run-deploy.py | |
| --env dev | |
| --project="${{ vars.GCP_PROJECT_ID }}" | |
| --region="${{ env.REGION }}" | |
| --migrate-legacy-public-binding backend | |
| --migrate-legacy-public-binding backend-sync | |
| --migrate-legacy-public-binding backend-sync-backfill | |
| --migrate-legacy-public-binding backend-integration | |
| - name: Check development Cloud Run runtime bindings | |
| run: >- | |
| python3 backend/scripts/preflight-cloud-run-deploy.py | |
| --env dev | |
| --project="${{ vars.GCP_PROJECT_ID }}" | |
| --region="${{ env.REGION }}" | |
| --check-runtime-bindings | |
| - name: Deploy ${{ env.SERVICE }} to Cloud Run | |
| id: deploy-backend | |
| uses: google-github-actions/deploy-cloudrun@v3 | |
| with: | |
| service: ${{ env.SERVICE }} | |
| region: ${{ env.REGION }} | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| no_traffic: true | |
| flags: >- | |
| --revision-suffix=${{ steps.image-tag.outputs.revision_suffix }} | |
| --tag=${{ env.CANDIDATE_TAG }} | |
| ${{ steps.runtime-env.outputs.cloud_run_flags }} | |
| env_vars: ${{ steps.runtime-env.outputs.backend_env_vars }} | |
| secrets: ${{ steps.runtime-env.outputs.backend_secrets }} | |
| - name: Capture ${{ env.SERVICE }} revision | |
| id: capture-backend-revision | |
| run: | | |
| echo "revision=backend-${{ steps.image-tag.outputs.revision_suffix }}" >> "$GITHUB_OUTPUT" | |
| - name: Deploy sync-backfill worker | |
| id: sync-backfill | |
| uses: ./.github/actions/sync-backfill-lifecycle | |
| with: | |
| mode: worker | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| region: ${{ env.REGION }} | |
| service: ${{ env.SERVICE }} | |
| image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| revision_suffix: ${{ steps.image-tag.outputs.revision_suffix }} | |
| cloud_run_flags: ${{ steps.runtime-env.outputs.cloud_run_flags }} | |
| backfill_env_vars: ${{ steps.runtime-env.outputs.backend_sync_backfill_env_vars }} | |
| backfill_secrets: ${{ steps.runtime-env.outputs.backend_sync_backfill_secrets }} | |
| candidate_tag: ${{ env.CANDIDATE_TAG }} | |
| - name: Deploy ${{ env.SERVICE }}-sync to Cloud Run | |
| id: deploy-backend-sync | |
| uses: google-github-actions/deploy-cloudrun@v3 | |
| with: | |
| service: ${{ env.SERVICE }}-sync | |
| region: ${{ env.REGION }} | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| no_traffic: true | |
| flags: >- | |
| --revision-suffix=${{ steps.image-tag.outputs.revision_suffix }} | |
| --tag=${{ env.CANDIDATE_TAG }} | |
| --remove-env-vars=HOSTED_PUSHER_API_URL | |
| ${{ steps.runtime-env.outputs.cloud_run_flags }} | |
| env_vars: |- | |
| ${{ steps.runtime-env.outputs.backend_sync_env_vars }} | |
| ${{ steps.sync-backfill.outputs.sync_backfill_env_vars }} | |
| secrets: ${{ steps.runtime-env.outputs.backend_sync_secrets }} | |
| - name: Capture ${{ env.SERVICE }}-sync revision | |
| id: capture-backend-sync-revision | |
| run: | | |
| echo "revision=backend-sync-${{ steps.image-tag.outputs.revision_suffix }}" >> "$GITHUB_OUTPUT" | |
| - name: Provision sync-backfill platform | |
| uses: ./.github/actions/sync-backfill-lifecycle | |
| with: | |
| mode: platform | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| region: ${{ env.REGION }} | |
| service: ${{ env.SERVICE }} | |
| provision_sync_ledger_ttl: 'true' | |
| provision_budget_alerts: 'false' | |
| - name: Deploy ${{ env.SERVICE }}-integration to Cloud Run | |
| id: deploy-backend-integration | |
| uses: google-github-actions/deploy-cloudrun@v3 | |
| with: | |
| service: ${{ env.SERVICE }}-integration | |
| region: ${{ env.REGION }} | |
| project_id: ${{ vars.GCP_PROJECT_ID }} | |
| image: gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }}:${{ steps.image-tag.outputs.short_sha }} | |
| no_traffic: true | |
| flags: >- | |
| --revision-suffix=${{ steps.image-tag.outputs.revision_suffix }} | |
| --tag=${{ env.CANDIDATE_TAG }} | |
| --remove-env-vars=HOSTED_PUSHER_API_URL | |
| ${{ steps.runtime-env.outputs.cloud_run_flags }} | |
| env_vars: ${{ steps.runtime-env.outputs.backend_integration_env_vars }} | |
| secrets: ${{ steps.runtime-env.outputs.backend_integration_secrets }} | |
| - name: Capture ${{ env.SERVICE }}-integration revision | |
| id: capture-backend-integration-revision | |
| run: | | |
| echo "revision=backend-integration-${{ steps.image-tag.outputs.revision_suffix }}" >> "$GITHUB_OUTPUT" | |
| - name: Wait for Cloud Run revisions to become ready | |
| run: | | |
| python3 backend/scripts/preflight-cloud-run-deploy.py \ | |
| --env dev \ | |
| --project ${{ vars.GCP_PROJECT_ID }} \ | |
| --region ${{ env.REGION }} \ | |
| --wait-revision-ready backend=${{ steps.capture-backend-revision.outputs.revision }} \ | |
| --wait-revision-ready backend-sync=${{ steps.capture-backend-sync-revision.outputs.revision }} \ | |
| --wait-revision-ready backend-sync-backfill=${{ steps.sync-backfill.outputs.revision }} \ | |
| --wait-revision-ready backend-integration=${{ steps.capture-backend-integration-revision.outputs.revision }} | |
| - name: Capture exact no-traffic candidate URLs | |
| id: candidate-urls | |
| run: | | |
| set -euo pipefail | |
| backend_url="$(python3 backend/scripts/resolve_cloud_run_tagged_url.py \ | |
| --project=${{ vars.GCP_PROJECT_ID }} \ | |
| --region=${{ env.REGION }} \ | |
| --service=backend \ | |
| --revision=${{ steps.capture-backend-revision.outputs.revision }} \ | |
| --tag=${{ env.CANDIDATE_TAG }})" | |
| backend_sync_url="$(python3 backend/scripts/resolve_cloud_run_tagged_url.py \ | |
| --project=${{ vars.GCP_PROJECT_ID }} \ | |
| --region=${{ env.REGION }} \ | |
| --service=backend-sync \ | |
| --revision=${{ steps.capture-backend-sync-revision.outputs.revision }} \ | |
| --tag=${{ env.CANDIDATE_TAG }})" | |
| backend_sync_backfill_url="$(python3 backend/scripts/resolve_cloud_run_tagged_url.py \ | |
| --project=${{ vars.GCP_PROJECT_ID }} \ | |
| --region=${{ env.REGION }} \ | |
| --service=backend-sync-backfill \ | |
| --revision=${{ steps.sync-backfill.outputs.revision }} \ | |
| --tag=${{ env.CANDIDATE_TAG }})" | |
| backend_integration_url="$(python3 backend/scripts/resolve_cloud_run_tagged_url.py \ | |
| --project=${{ vars.GCP_PROJECT_ID }} \ | |
| --region=${{ env.REGION }} \ | |
| --service=backend-integration \ | |
| --revision=${{ steps.capture-backend-integration-revision.outputs.revision }} \ | |
| --tag=${{ env.CANDIDATE_TAG }})" | |
| # Cloud Run verifies the service URL as the OIDC audience even when | |
| # the HTTP request itself uses a tagged candidate URL. | |
| backend_audience="$(gcloud run services describe backend --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.url)')" | |
| backend_sync_audience="$(gcloud run services describe backend-sync --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.url)')" | |
| backend_sync_backfill_audience="$(gcloud run services describe backend-sync-backfill --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.url)')" | |
| backend_integration_audience="$(gcloud run services describe backend-integration --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.url)')" | |
| test -n "$backend_audience" | |
| test -n "$backend_sync_audience" | |
| test -n "$backend_sync_backfill_audience" | |
| test -n "$backend_integration_audience" | |
| { | |
| echo "backend_url=${backend_url}" | |
| echo "backend_sync_url=${backend_sync_url}" | |
| echo "backend_sync_backfill_url=${backend_sync_backfill_url}" | |
| echo "backend_integration_url=${backend_integration_url}" | |
| echo "backend_audience=${backend_audience}" | |
| echo "backend_sync_audience=${backend_sync_audience}" | |
| echo "backend_sync_backfill_audience=${backend_sync_backfill_audience}" | |
| echo "backend_integration_audience=${backend_integration_audience}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Validate backend runtime env after deploy | |
| env: | |
| SYNC_LEDGER_FENCE_MODE: ${{ vars.SYNC_LEDGER_FENCE_MODE || 'legacy' }} | |
| run: | | |
| python3 backend/scripts/validate-backend-runtime-env.py --env dev --check-workflows --check-live-cloud-run | |
| - name: Gate candidate revisions with the acceptance manifest | |
| env: | |
| PROJECT_ID: ${{ vars.GCP_PROJECT_ID }} | |
| run: | | |
| set -euo pipefail | |
| ADMIN_KEY="$(gcloud secrets versions access latest --secret=ADMIN_KEY --project="$PROJECT_ID")" | |
| export ADMIN_KEY | |
| trap 'unset ADMIN_KEY' EXIT | |
| python3 backend/scripts/run_dev_candidate_acceptance.py \ | |
| --candidate backend=${{ steps.candidate-urls.outputs.backend_url }} \ | |
| --candidate backend-sync=${{ steps.candidate-urls.outputs.backend_sync_url }} \ | |
| --candidate backend-sync-backfill=${{ steps.candidate-urls.outputs.backend_sync_backfill_url }} \ | |
| --candidate backend-integration=${{ steps.candidate-urls.outputs.backend_integration_url }} \ | |
| --audience backend=${{ steps.candidate-urls.outputs.backend_audience }} \ | |
| --audience backend-sync=${{ steps.candidate-urls.outputs.backend_sync_audience }} \ | |
| --audience backend-sync-backfill=${{ steps.candidate-urls.outputs.backend_sync_backfill_audience }} \ | |
| --audience backend-integration=${{ steps.candidate-urls.outputs.backend_integration_audience }} \ | |
| --evidence-path artifacts/dev-backend-candidate-acceptance.json | |
| # This is the Cloud Run-only acceptance boundary. The development | |
| # candidate manifest must pass before shared GKE runtime state can change. | |
| - name: Accept no-traffic Cloud Run candidate | |
| run: | | |
| python3 backend/scripts/verify_backend_release_vector.py \ | |
| --candidate \ | |
| --cloud-run-only \ | |
| --commit-sha "${{ needs.firestore_readiness.outputs.admitted_sha }}" \ | |
| --short-sha "${{ steps.image-tag.outputs.short_sha }}" \ | |
| --deploy-run-id "${{ github.run_id }}" \ | |
| --deploy-run-attempt "${{ github.run_attempt }}" \ | |
| --project "${{ vars.GCP_PROJECT_ID }}" \ | |
| --region "${{ env.REGION }}" \ | |
| --environment dev \ | |
| --evidence-path artifacts/dev-backend-cloud-run-candidate-release-vector.json | |
| - name: Install Helm | |
| uses: azure/setup-helm@v5 | |
| - name: Apply non-secret backend runtime config | |
| env: | |
| ENVIRONMENT: ${{ vars.ENV }} | |
| CONVERSATION_SUMMARIZED_APP_IDS: ${{ vars.CONVERSATION_SUMMARIZED_APP_IDS }} | |
| GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }} | |
| MCP_AUTHORIZATION_SERVER_URL: ${{ vars.MCP_AUTHORIZATION_SERVER_URL }} | |
| MCP_OAUTH_CHATGPT_CLIENT_ID: ${{ vars.MCP_OAUTH_CHATGPT_CLIENT_ID }} | |
| MCP_OAUTH_CHATGPT_REDIRECT_URIS: ${{ vars.MCP_OAUTH_CHATGPT_REDIRECT_URIS }} | |
| MCP_OAUTH_PUBLIC_CLIENT_ID: ${{ vars.MCP_OAUTH_PUBLIC_CLIENT_ID }} | |
| MCP_OAUTH_PUBLIC_REDIRECT_URIS: ${{ vars.MCP_OAUTH_PUBLIC_REDIRECT_URIS }} | |
| MCP_RESOURCE_URL: ${{ vars.MCP_RESOURCE_URL }} | |
| RAPID_API_HOST: ${{ vars.RAPID_API_HOST }} | |
| REDIS_DB_HOST: ${{ vars.REDIS_DB_HOST }} | |
| STT_PRERECORDED_MODEL: parakeet,modulate-velma-2 | |
| STT_SERVICE_MODELS: parakeet,modulate-velma-2 | |
| TYPESENSE_HOST: ${{ vars.TYPESENSE_HOST }} | |
| TWILIO_ACCOUNT_SID: ${{ vars.TWILIO_ACCOUNT_SID }} | |
| TWILIO_API_KEY_SID: ${{ vars.TWILIO_API_KEY_SID }} | |
| TWILIO_TWIML_APP_SID: ${{ vars.TWILIO_TWIML_APP_SID }} | |
| X_OAUTH_CLIENT_ID: ${{ vars.X_OAUTH_CLIENT_ID }} | |
| X_OAUTH_REDIRECT_URI: ${{ vars.X_OAUTH_REDIRECT_URI }} | |
| run: backend/scripts/deploy-backend-config.sh | |
| - name: Deploy backend-secrets to GKE | |
| env: | |
| BACKEND_SECRETS_GSA: ${{ vars.BACKEND_SECRETS_GSA }} | |
| ENVIRONMENT: ${{ vars.ENV }} | |
| GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }} | |
| GKE_CLUSTER: ${{ vars.GKE_CLUSTER }} | |
| REGION: ${{ env.REGION }} | |
| run: backend/scripts/deploy-backend-secrets.sh | |
| - name: Deploy ${{ env.SERVICE }}-listen to GKE | |
| run: | | |
| helm -n ${{ vars.ENV }}-omi-backend upgrade --install \ | |
| ${{ vars.ENV }}-omi-backend-listen \ | |
| ./backend/charts/backend-listen \ | |
| -f ./backend/charts/backend-listen/${{ vars.ENV }}_omi_backend_listen_values.yaml \ | |
| --set image.repository=gcr.io/${{ vars.GCP_PROJECT_ID }}/${{ env.SERVICE }} \ | |
| --set gcpProjectId=${{ vars.GCP_PROJECT_ID }} \ | |
| --set runtimeGcpProjectId=${{ vars.RUNTIME_GCP_PROJECT_ID }} \ | |
| --set-string image.tag=${{ steps.image-tag.outputs.short_sha }} | |
| # See gcp_backend.yml: 300s cannot cover a full backend-listen roll; a | |
| # stalled rollout still fails on progressDeadlineSeconds. | |
| if ! kubectl -n ${{ vars.ENV }}-omi-backend rollout status deploy/${{ vars.ENV }}-omi-backend-listen --timeout=1800s; then | |
| python3 backend/scripts/deploy_status_report.py --env ${{ vars.ENV }} --include-gke --gke-service backend-listen || true | |
| exit 1 | |
| fi | |
| python3 backend/scripts/deploy_status_report.py --env ${{ vars.ENV }} --include-gke --gke-service backend-listen | |
| # The lock remains held until the accepted Cloud Run candidate and the | |
| # newly rolled GKE listener form one exact candidate release vector. | |
| - name: Verify exact candidate composition | |
| run: | | |
| python3 backend/scripts/verify_backend_release_vector.py \ | |
| --candidate \ | |
| --commit-sha "${{ needs.firestore_readiness.outputs.admitted_sha }}" \ | |
| --short-sha "${{ steps.image-tag.outputs.short_sha }}" \ | |
| --deploy-run-id "${{ github.run_id }}" \ | |
| --deploy-run-attempt "${{ github.run_attempt }}" \ | |
| --project "${{ vars.GCP_PROJECT_ID }}" \ | |
| --region "${{ env.REGION }}" \ | |
| --environment dev \ | |
| --evidence-path artifacts/dev-backend-deployment-composition.json | |
| - name: Capture Cloud Run pre-promotion traffic snapshot | |
| id: cloud-run-traffic-snapshot | |
| run: | | |
| python3 backend/scripts/cloud_run_traffic_snapshot.py capture \ | |
| --project "${{ vars.GCP_PROJECT_ID }}" \ | |
| --region "${{ env.REGION }}" \ | |
| --service backend \ | |
| --service backend-sync \ | |
| --service backend-sync-backfill \ | |
| --service backend-integration \ | |
| --output artifacts/dev-backend-cloud-run-pre-promotion-traffic-snapshot.json | |
| - name: Verify validated revisions are still current | |
| run: | | |
| test "$(gcloud run services describe backend --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.latestCreatedRevisionName)')" = "${{ steps.capture-backend-revision.outputs.revision }}" | |
| test "$(gcloud run services describe backend-sync --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.latestCreatedRevisionName)')" = "${{ steps.capture-backend-sync-revision.outputs.revision }}" | |
| test "$(gcloud run services describe backend-sync-backfill --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.latestCreatedRevisionName)')" = "${{ steps.sync-backfill.outputs.revision }}" | |
| test "$(gcloud run services describe backend-integration --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --format='value(status.latestCreatedRevisionName)')" = "${{ steps.capture-backend-integration-revision.outputs.revision }}" | |
| - name: Shift Cloud Run traffic to validated revisions | |
| id: shift-cloud-run-traffic | |
| run: | | |
| gcloud run services update-traffic backend --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --to-revisions=${{ steps.capture-backend-revision.outputs.revision }}=100 --quiet | |
| gcloud run services update-traffic backend-sync --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --to-revisions=${{ steps.capture-backend-sync-revision.outputs.revision }}=100 --quiet | |
| gcloud run services update-traffic backend-sync-backfill --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --to-revisions=${{ steps.sync-backfill.outputs.revision }}=100 --quiet | |
| gcloud run services update-traffic backend-integration --project=${{ vars.GCP_PROJECT_ID }} --region=${{ env.REGION }} --to-revisions=${{ steps.capture-backend-integration-revision.outputs.revision }}=100 --quiet | |
| # The no-traffic candidate gate proves readiness; this post-promotion | |
| # read verifies that every serving tier actually converged on the same | |
| # attempt-scoped release vector. | |
| - name: Verify serving backend release vector | |
| id: verify-serving-release-vector | |
| run: | | |
| python3 backend/scripts/verify_backend_release_vector.py \ | |
| --commit-sha "${{ needs.firestore_readiness.outputs.admitted_sha }}" \ | |
| --short-sha "${{ steps.image-tag.outputs.short_sha }}" \ | |
| --deploy-run-id "${{ github.run_id }}" \ | |
| --deploy-run-attempt "${{ github.run_attempt }}" \ | |
| --project "${{ vars.GCP_PROJECT_ID }}" \ | |
| --region "${{ env.REGION }}" \ | |
| --environment dev \ | |
| --evidence-path artifacts/dev-backend-serving-release-vector.json | |
| - name: Restore Cloud Run traffic snapshot after failed promotion | |
| if: ${{ failure() && steps.cloud-run-traffic-snapshot.outcome == 'success' && (steps.shift-cloud-run-traffic.outcome == 'failure' || steps.verify-serving-release-vector.outcome == 'failure') }} | |
| run: | | |
| python3 backend/scripts/cloud_run_traffic_snapshot.py restore \ | |
| --snapshot artifacts/dev-backend-cloud-run-pre-promotion-traffic-snapshot.json \ | |
| --evidence-path artifacts/dev-backend-cloud-run-traffic-restore.json | |
| - name: Cloud Run deploy status report | |
| if: always() | |
| run: | | |
| python3 backend/scripts/deploy_status_report.py \ | |
| --env dev \ | |
| --project ${{ vars.GCP_PROJECT_ID }} \ | |
| --region ${{ env.REGION }} \ | |
| --include-cloud-run \ | |
| --cloud-run-service backend \ | |
| --cloud-run-service backend-sync \ | |
| --cloud-run-service backend-sync-backfill \ | |
| --cloud-run-service backend-integration \ | |
| --expect-cloud-run-traffic backend=${{ steps.capture-backend-revision.outputs.revision }} \ | |
| --expect-cloud-run-traffic backend-sync=${{ steps.capture-backend-sync-revision.outputs.revision }} \ | |
| --expect-cloud-run-traffic backend-sync-backfill=${{ steps.sync-backfill.outputs.revision }} \ | |
| --expect-cloud-run-traffic backend-integration=${{ steps.capture-backend-integration-revision.outputs.revision }} \ | |
| --candidate-acceptance-manifest backend/deploy/dev_candidate_acceptance.json \ | |
| --candidate-acceptance-evidence artifacts/dev-backend-candidate-acceptance.json \ | |
| --candidate-status-output artifacts/dev-backend-candidate-status.json | |
| - name: Upload deployment acceptance evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dev-backend-deployment-acceptance-${{ github.run_id }} | |
| path: | | |
| artifacts/dev-backend-cloud-run-candidate-release-vector.json | |
| artifacts/dev-backend-deployment-composition.json | |
| artifacts/dev-backend-cloud-run-pre-promotion-traffic-snapshot.json | |
| artifacts/dev-backend-serving-release-vector.json | |
| artifacts/dev-backend-candidate-acceptance.json | |
| artifacts/dev-backend-candidate-status.json | |
| artifacts/dev-backend-cloud-run-traffic-restore.json | |
| if-no-files-found: warn | |
| - name: Show Output | |
| run: echo "Backend deployed to development environment" |