{"repo": "bladerunneryao/dachjob.ai", "files": {".github/workflows/deploy-gcp.yml": {"bdc6599203806f03c96ca10ca789f823bcd5286b": {"t": 1786068540.6954734, "n": 33252, "h": "dbe9934d2f33c899", "c": "name: Deploy to GCP\n\non:\n workflow_dispatch:\n inputs:\n environment:\n description: Deployment environment\n required: true\n type: choice\n default: dev\n options:\n - dev\n - staging\n - prod\n branch:\n description: Optional branch override. Leave empty to deploy the selected workflow branch.\n required: false\n default: \"\"\n target:\n description: Deployment target\n required: true\n type: choice\n default: all\n options:\n - all\n - api\n - frontend\n - worker\n - terraform\n terraform_apply:\n description: Apply Terraform after plan\n required: false\n type: boolean\n default: false\n run_migrations:\n description: Run database migrations after API deploy\n required: false\n type: boolean\n default: true\n worker_mode:\n description: Worker mode for this deploy\n required: true\n type: choice\n default: disabled\n options:\n - disabled\n - enabled\n scale_down_worker_when_disabled:\n description: Scale existing GKE worker deployment to zero when worker mode is disabled\n required: false\n type: boolean\n default: true\n smoke_test_mode:\n description: Post-deploy smoke test depth\n required: false\n type: choice\n default: minimal\n options:\n - minimal\n - full\n\nconcurrency:\n group: deploy-gcp-${{ github.event.inputs.environment || 'dev' }}\n cancel-in-progress: false\n\nenv:\n FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true\n PROJECT_ID: dachjob-ai-platform\n PROJECT_NUMBER: \"936971254595\"\n REGION: europe-west1\n GAR_LOCATION: europe-west1\n ENVIRONMENT: dev\n API_SERVICE_NAME: dachjob-dev-api\n FRONTEND_SERVICE_NAME: dachjob-dev-frontend\n MIGRATION_JOB_NAME: dachjob-dev-migrate\n API_BASE_URL: https://dachjob-dev-api-qxugiew36a-ew.a.run.app\n FRONTEND_URL: https://dachjob-dev-frontend-qxugiew36a-ew.a.run.app\n GKE_CLUSTER_NAME: dachjob-dev-cluster\n GKE_NAMESPACE: celery-worker\n WORKLOAD_IDENTITY_PROVIDER: projects/936971254595/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider\n DEPLOYER_SERVICE_ACCOUNT: dachjob-dev-terraform-sa@dachjob-ai-platform.iam.gserviceaccount.com\n API_SERVICE_ACCOUNT: dachjob-dev-api-sa@dachjob-ai-platform.iam.gserviceaccount.com\n FRONTEND_SERVICE_ACCOUNT: dachjob-dev-frontend-sa@dachjob-ai-platform.iam.gserviceaccount.com\n WORKER_SERVICE_ACCOUNT: dachjob-dev-worker-sa@dachjob-ai-platform.iam.gserviceaccount.com\n CLOUD_SQL_CONNECTION_NAME: dachjob-ai-platform:europe-west1:dachjob-dev-postgres\n VPC_CONNECTOR: projects/dachjob-ai-platform/locations/europe-west1/connectors/dachjob-dev-vpc-connector\n REDIS_HOST: 10.36.0.3\n REDIS_PORT: \"6379\"\n REDIS_URL: redis://10.36.0.3:6379/0\n REDIS_ENABLED: \"true\"\n GCS_BUCKET: dachjob-dev-artifacts\n GCP_TF_DIR: infra/terraform/live/gcp/dev\n\njobs:\n validate:\n name: Validate\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n defaults:\n run:\n working-directory: app/backend\n outputs:\n environment: ${{ steps.resolve.outputs.environment }}\n steps:\n - name: Resolve environment\n id: resolve\n working-directory: .\n run: |\n environment=\"${{ github.event.inputs.environment || 'dev' }}\"\n echo \"environment=${environment}\" >> \"${GITHUB_OUTPUT}\"\n\n - name: Checkout\n uses: actions/checkout@v6\n with:\n ref: ${{ github.event.inputs.branch || github.ref_name }}\n persist-credentials: false\n\n - name: Setup backend\n uses: ./.github/actions/setup-backend\n\n - name: Run pre-commit\n run: pre-commit run --all-files\n\n - name: Run unit tests\n run: pytest tests/ --ignore=tests/test_provider_smoke.py -v\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Fetch GCP API keys and run provider smoke\n run: |\n set +x\n export GEMINI_API_KEY=\"$(gcloud secrets versions access latest --secret=dachjob-dev-gemini-api-key --project=\"${PROJECT_ID}\")\"\n export DEEPSEEK_API_KEY=\"$(gcloud secrets versions access latest --secret=dachjob-dev-deepseek-api-key --project=\"${PROJECT_ID}\" 2>/dev/null || true)\"\n export OPENROUTER_API_KEY=\"$(gcloud secrets versions access latest --secret=dachjob-dev-openrouter-api-key --project=\"${PROJECT_ID}\" 2>/dev/null || true)\"\n if ! pytest tests/test_provider_smoke.py -v; then\n echo \"::warning::LLM provider smoke failed. Continuing deploy because provider quota/credits are external to the deployment pipeline.\"\n fi\n\n prepare:\n name: Prepare deployment\n needs: validate\n\n runs-on: ubuntu-latest\n permissions:\n contents: read\n outputs:\n environment: ${{ steps.inputs.outputs.environment }}\n branch: ${{ steps.inputs.outputs.branch }}\n target: ${{ steps.inputs.outputs.target }}\n image_tag: ${{ steps.tag.outputs.image_tag }}\n commit_sha: ${{ steps.tag.outputs.commit_sha }}\n deploy_api: ${{ steps.inputs.outputs.deploy_api }}\n deploy_frontend: ${{ steps.inputs.outputs.deploy_frontend }}\n deploy_worker: ${{ steps.inputs.outputs.deploy_worker }}\n run_terraform: ${{ steps.inputs.outputs.run_terraform }}\n terraform_apply: ${{ steps.inputs.outputs.terraform_apply }}\n run_migrations: ${{ steps.inputs.outputs.run_migrations }}\n worker_enabled: ${{ steps.inputs.outputs.worker_enabled }}\n scale_down_worker: ${{ steps.inputs.outputs.scale_down_worker }}\n steps:\n - name: Resolve inputs\n id: inputs\n run: |\n environment=\"${{ needs.validate.outputs.environment || 'dev' }}\"\n branch=\"${{ github.event.inputs.branch || github.ref_name }}\"\n target=\"${{ github.event.inputs.target || 'all' }}\"\n terraform_apply=\"${{ github.event.inputs.terraform_apply || 'false' }}\"\n run_migrations=\"${{ github.event.inputs.run_migrations || 'true' }}\"\n worker_mode=\"${{ github.event.inputs.worker_mode || 'disabled' }}\"\n scale_down_worker=\"${{ github.event.inputs.scale_down_worker_when_disabled || 'true' }}\"\n\n echo \"environment=${environment}\" >> \"${GITHUB_OUTPUT}\"\n echo \"branch=${branch}\" >> \"${GITHUB_OUTPUT}\"\n echo \"target=${target}\" >> \"${GITHUB_OUTPUT}\"\n echo \"terraform_apply=${terraform_apply}\" >> \"${GITHUB_OUTPUT}\"\n echo \"run_migrations=${run_migrations}\" >> \"${GITHUB_OUTPUT}\"\n echo \"worker_enabled=${worker_mode}\" >> \"${GITHUB_OUTPUT}\"\n echo \"scale_down_worker=${scale_down_worker}\" >> \"${GITHUB_OUTPUT}\"\n\n [[ \"${target}\" == \"all\" || \"${target}\" == \"api\" ]] && echo \"deploy_api=true\" >> \"${GITHUB_OUTPUT}\" || echo \"deploy_api=false\" >> \"${GITHUB_OUTPUT}\"\n [[ \"${target}\" == \"all\" || \"${target}\" == \"frontend\" ]] && echo \"deploy_frontend=true\" >> \"${GITHUB_OUTPUT}\" || echo \"deploy_frontend=false\" >> \"${GITHUB_OUTPUT}\"\n if [[ \"${worker_mode}\" == \"disabled\" && \"${target}\" == \"worker\" ]]; then\n echo \"Error: target=worker requires worker_mode=enabled\" >&2\n exit 1\n fi\n if [[ \"${worker_mode}\" == \"enabled\" && (\"${target}\" == \"all\" || \"${target}\" == \"worker\") ]]; then\n echo \"deploy_worker=true\" >> \"${GITHUB_OUTPUT}\"\n else\n echo \"deploy_worker=false\" >> \"${GITHUB_OUTPUT}\"\n fi\n [[ \"${target}\" == \"all\" || \"${target}\" == \"terraform\" ]] && echo \"run_terraform=true\" >> \"${GITHUB_OUTPUT}\" || echo \"run_terraform=false\" >> \"${GITHUB_OUTPUT}\"\n\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ steps.inputs.outputs.branch }}\n persist-credentials: false\n\n - name: Create image tag\n id: tag\n run: |\n commit_sha=\"$(git rev-parse HEAD)\"\n short_sha=\"$(git rev-parse --short=12 HEAD)\"\n echo \"image_tag=${short_sha}-${GITHUB_RUN_NUMBER}\" >> \"${GITHUB_OUTPUT}\"\n echo \"commit_sha=${commit_sha}\" >> \"${GITHUB_OUTPUT}\"\n\n terraform:\n name: Terraform (GCP)\n needs: prepare\n if: ${{ needs.prepare.outputs.run_terraform == 'true' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n env:\n TF_VAR_project_id: dachjob-ai-platform\n TF_VAR_region: europe-west1\n TF_VAR_environment: ${{ needs.prepare.outputs.environment }}\n TF_VAR_billing_account_id: ${{ vars.GCP_BILLING_ACCOUNT_ID || secrets.GCP_BILLING_ACCOUNT_ID || '' }}\n TF_VAR_notification_email: ${{ vars.GCP_NOTIFICATION_EMAIL || vars.NOTIFICATION_EMAIL || secrets.GCP_NOTIFICATION_EMAIL || '' }}\n GCP_TF_BACKEND_BUCKET: ${{ vars.GCP_TF_BACKEND_BUCKET || format('dachjob-{0}-terraform-state', needs.prepare.outputs.environment) }}\n GCP_TF_BACKEND_PREFIX: ${{ vars.GCP_TF_BACKEND_PREFIX || needs.prepare.outputs.environment }}\n outputs:\n api_url: ${{ steps.tf_outputs.outputs.api_url }}\n frontend_url: ${{ steps.tf_outputs.outputs.frontend_url }}\n steps:\n - name: Set TF_VAR_cors_origins from workflow env\n run: echo \"TF_VAR_cors_origins=${FRONTEND_URL}\" >> \"$GITHUB_ENV\"\n\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Setup Terraform\n uses: hashicorp/setup-terraform@v4\n with:\n terraform_version: \"1.10.0\"\n\n - name: Terraform init\n working-directory: ${{ env.GCP_TF_DIR }}\n run: terraform init -backend-config=\"bucket=${GCP_TF_BACKEND_BUCKET}\" -backend-config=\"prefix=${GCP_TF_BACKEND_PREFIX}\"\n\n - name: Terraform format check\n run: terraform fmt -check -recursive\n\n - name: Terraform validate\n working-directory: ${{ env.GCP_TF_DIR }}\n run: terraform validate\n\n - name: Validate Terraform inputs\n run: |\n missing=()\n [[ -n \"${TF_VAR_project_id}\" ]] || missing+=(\"TF_VAR_project_id\")\n [[ -n \"${TF_VAR_billing_account_id}\" ]] || missing+=(\"TF_VAR_billing_account_id\")\n [[ -n \"${TF_VAR_notification_email}\" ]] || missing+=(\"TF_VAR_notification_email\")\n if (( ${#missing[@]} > 0 )); then\n printf 'Missing required Terraform inputs: %s\\n' \"${missing[*]}\" >&2\n printf 'Configure repository/environment variables or secrets before running Terraform.\\n' >&2\n exit 1\n fi\n\n - name: Terraform plan\n working-directory: ${{ env.GCP_TF_DIR }}\n run: terraform plan -input=false -lock-timeout=300s -out=tfplan\n\n - name: Terraform apply\n if: ${{ needs.prepare.outputs.terraform_apply == 'true' }}\n working-directory: ${{ env.GCP_TF_DIR }}\n run: terraform apply -input=false -auto-approve tfplan\n\n - name: Capture Terraform outputs\n id: tf_outputs\n working-directory: ${{ env.GCP_TF_DIR }}\n run: |\n echo \"api_url=$(terraform output -raw api_url 2>/dev/null || echo '')\" >> \"${GITHUB_OUTPUT}\"\n echo \"frontend_url=$(terraform output -raw frontend_url 2>/dev/null || echo '')\" >> \"${GITHUB_OUTPUT}\"\n\n build-api:\n name: Build API image\n needs: prepare\n if: ${{ needs.prepare.outputs.deploy_api == 'true' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n outputs:\n image: ${{ steps.meta.outputs.image }}\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n id: auth\n uses: google-github-actions/auth@v3\n with:\n token_format: access_token\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Login to Artifact Registry\n uses: docker/login-action@v4\n with:\n registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev\n username: oauth2accesstoken\n password: ${{ steps.auth.outputs.access_token }}\n\n - name: Set image metadata\n id: meta\n run: |\n tag=\"${{ needs.prepare.outputs.image_tag }}\"\n gcp_image=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-api/api:${tag}\"\n gcp_latest=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-api/api:latest\"\n echo \"image=${gcp_image}\" >> \"${GITHUB_OUTPUT}\"\n echo \"latest=${gcp_latest}\" >> \"${GITHUB_OUTPUT}\"\n {\n echo \"tags<> \"${GITHUB_OUTPUT}\"\n\n - name: Build and push API image\n uses: ./.github/actions/docker-build-push\n with:\n context: .\n file: app/backend/Dockerfile\n platforms: linux/amd64\n push: true\n build-args: |\n GIT_BRANCH=${{ needs.prepare.outputs.branch }}\n GIT_COMMIT_SHA=${{ needs.prepare.outputs.commit_sha }}\n tags: |\n ${{ steps.meta.outputs.tags }}\n\n - name: Clean old Artifact Registry images (keep last 3)\n if: success()\n run: |\n set +e\n repo=\"dachjob-dev-api\"\n location=\"${{ env.GAR_LOCATION }}\"\n project=\"${{ env.PROJECT_ID }}\"\n\n gcloud artifacts docker images list \\\n \"${location}-docker.pkg.dev/${project}/${repo}\" \\\n --include-tags --format 'value(version)' --sort-by \"~create_time\" \\\n --limit 100 | tail -n +4 | while read -r digest; do\n [[ -z \"${digest}\" ]] && continue\n gcloud artifacts docker images delete \\\n \"${location}-docker.pkg.dev/${project}/${repo}@${digest}\" \\\n --quiet --delete-tags 2>/dev/null || true\n done\n\n build-frontend:\n name: Build frontend image\n needs: prepare\n if: ${{ needs.prepare.outputs.deploy_frontend == 'true' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n outputs:\n image: ${{ steps.meta.outputs.image }}\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n id: auth\n uses: google-github-actions/auth@v3\n with:\n token_format: access_token\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Login to Artifact Registry\n uses: docker/login-action@v4\n with:\n registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev\n username: oauth2accesstoken\n password: ${{ steps.auth.outputs.access_token }}\n\n - name: Set image metadata\n id: meta\n run: |\n tag=\"${{ needs.prepare.outputs.image_tag }}\"\n gcp_image=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-frontend/frontend:${tag}\"\n gcp_latest=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-frontend/frontend:latest\"\n echo \"image=${gcp_image}\" >> \"${GITHUB_OUTPUT}\"\n echo \"latest=${gcp_latest}\" >> \"${GITHUB_OUTPUT}\"\n {\n echo \"tags<> \"${GITHUB_OUTPUT}\"\n\n - name: Build and push frontend image\n uses: ./.github/actions/docker-build-push\n with:\n context: .\n file: app/frontend/Dockerfile\n platforms: linux/amd64\n push: true\n build-args: |\n NEXT_PUBLIC_API_BASE_URL=${{ env.API_BASE_URL }}\n tags: |\n ${{ steps.meta.outputs.tags }}\n\n - name: Clean old Artifact Registry images (keep last 3)\n if: success()\n run: |\n set +e\n repo=\"dachjob-dev-frontend\"\n location=\"${{ env.GAR_LOCATION }}\"\n project=\"${{ env.PROJECT_ID }}\"\n\n gcloud artifacts docker images list \\\n \"${location}-docker.pkg.dev/${project}/${repo}\" \\\n --include-tags --format 'value(version)' --sort-by \"~create_time\" \\\n --limit 100 | tail -n +4 | while read -r digest; do\n [[ -z \"${digest}\" ]] && continue\n gcloud artifacts docker images delete \\\n \"${location}-docker.pkg.dev/${project}/${repo}@${digest}\" \\\n --quiet --delete-tags 2>/dev/null || true\n done\n\n build-worker:\n name: Build worker image\n needs: prepare\n if: ${{ needs.prepare.outputs.deploy_worker == 'true' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n outputs:\n image: ${{ steps.meta.outputs.image }}\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n id: auth\n uses: google-github-actions/auth@v3\n with:\n token_format: access_token\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Login to Artifact Registry\n uses: docker/login-action@v4\n with:\n registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev\n username: oauth2accesstoken\n password: ${{ steps.auth.outputs.access_token }}\n\n - name: Set image metadata\n id: meta\n run: |\n tag=\"${{ needs.prepare.outputs.image_tag }}\"\n gcp_image=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-worker/worker:${tag}\"\n gcp_latest=\"${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/dachjob-dev-worker/worker:latest\"\n echo \"image=${gcp_image}\" >> \"${GITHUB_OUTPUT}\"\n echo \"latest=${gcp_latest}\" >> \"${GITHUB_OUTPUT}\"\n {\n echo \"tags<> \"${GITHUB_OUTPUT}\"\n\n - name: Build and push worker image\n uses: ./.github/actions/docker-build-push\n with:\n context: .\n file: app/backend/Dockerfile\n platforms: linux/amd64\n push: true\n build-args: |\n GIT_BRANCH=${{ needs.prepare.outputs.branch }}\n GIT_COMMIT_SHA=${{ needs.prepare.outputs.commit_sha }}\n tags: |\n ${{ steps.meta.outputs.tags }}\n\n - name: Clean old Artifact Registry images (keep last 3)\n if: success()\n run: |\n set +e\n repo=\"dachjob-dev-worker\"\n location=\"${{ env.GAR_LOCATION }}\"\n project=\"${{ env.PROJECT_ID }}\"\n\n gcloud artifacts docker images list \\\n \"${location}-docker.pkg.dev/${project}/${repo}\" \\\n --include-tags --format 'value(version)' --sort-by \"~create_time\" \\\n --limit 100 | tail -n +4 | while read -r digest; do\n [[ -z \"${digest}\" ]] && continue\n gcloud artifacts docker images delete \\\n \"${location}-docker.pkg.dev/${project}/${repo}@${digest}\" \\\n --quiet --delete-tags 2>/dev/null || true\n done\n\n deploy-api:\n name: Deploy API (GCP)\n needs:\n - prepare\n - terraform\n - build-api\n if: ${{ always() && needs.prepare.outputs.deploy_api == 'true' && (needs.terraform.result == 'success' || needs.terraform.result == 'skipped') && needs.build-api.result == 'success' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n outputs:\n url: ${{ steps.url.outputs.url }}\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Setup gcloud\n uses: google-github-actions/setup-gcloud@v3\n\n - name: Deploy API to Cloud Run\n run: |\n gcloud run deploy \"${API_SERVICE_NAME}\" \\\n --project=\"${PROJECT_ID}\" \\\n --region=\"${REGION}\" \\\n --image=\"${{ needs.build-api.outputs.image }}\" \\\n --service-account=\"${API_SERVICE_ACCOUNT}\" \\\n --port=8000 \\\n --allow-unauthenticated \\\n --add-cloudsql-instances=\"${CLOUD_SQL_CONNECTION_NAME}\" \\\n --vpc-connector=\"${VPC_CONNECTOR}\" \\\n --vpc-egress=private-ranges-only \\\n --set-env-vars=\"APP_ENV=production,CLOUD_SQL_CONNECTION_NAME=${CLOUD_SQL_CONNECTION_NAME},DATABASE_USER=postgres,DATABASE_NAME=dachjob,REDIS_URL=${REDIS_URL},REDIS_HOST=${REDIS_HOST},REDIS_PORT=${REDIS_PORT},REDIS_ENABLED=${REDIS_ENABLED},S3_ENDPOINT_URL=https://storage.googleapis.com,S3_BUCKET_NAME=${GCS_BUCKET},STORAGE_BUCKET_NAME=${GCS_BUCKET},GOOGLE_CLOUD_PROJECT=${PROJECT_ID},LLM_PROVIDER=vertex_ai,VERTEX_AI_PROJECT_ID=${PROJECT_ID},VERTEX_AI_LOCATION=global,CORS_ORIGINS=${FRONTEND_URL},WORKER_ENABLED=${{ needs.prepare.outputs.worker_enabled == 'enabled' && 'true' || 'false' }},WORKER_FALLBACK_TO_SYNC=true,LOG_LEVEL=INFO,LOG_JSON=true,ERROR_LOG_TO_FILE=true,ERROR_LOG_DIR=/tmp/dachjob-error-logs,STORAGE_PROVIDER=gcs\" \\\n --update-secrets=\"DATABASE_PASSWORD=dachjob-dev-db-password:latest,JWT_SECRET=dachjob-dev-jwt-secret-key:latest,SECRET_KEY=dachjob-dev-jwt-secret-key:latest,GEMINI_API_KEY=dachjob-dev-gemini-api-key:latest,RESEND_API_KEY=dachjob-dev-smtp-password:latest,S3_ACCESS_KEY_ID=dachjob-dev-s3-access-key:latest,S3_SECRET_ACCESS_KEY=dachjob-dev-s3-secret-key:latest\"\n\n - name: Deploy migration job\n if: ${{ needs.prepare.outputs.run_migrations == 'true' }}\n run: |\n gcloud run jobs deploy \"${MIGRATION_JOB_NAME}\" \\\n --project=\"${PROJECT_ID}\" \\\n --region=\"${REGION}\" \\\n --image=\"${{ needs.build-api.outputs.image }}\" \\\n --service-account=\"${API_SERVICE_ACCOUNT}\" \\\n --set-cloudsql-instances=\"${CLOUD_SQL_CONNECTION_NAME}\" \\\n --vpc-connector=\"${VPC_CONNECTOR}\" \\\n --vpc-egress=private-ranges-only \\\n --command=alembic \\\n --args=-c,app/db/migrations/alembic.ini,upgrade,head \\\n --set-env-vars=\"APP_ENV=production,CLOUD_SQL_CONNECTION_NAME=${CLOUD_SQL_CONNECTION_NAME},DATABASE_USER=postgres,DATABASE_NAME=dachjob,REDIS_URL=${REDIS_URL},REDIS_ENABLED=${REDIS_ENABLED},S3_ENDPOINT_URL=https://storage.googleapis.com,S3_BUCKET_NAME=${GCS_BUCKET},STORAGE_BUCKET_NAME=${GCS_BUCKET},GOOGLE_CLOUD_PROJECT=${PROJECT_ID},LLM_PROVIDER=vertex_ai,VERTEX_AI_PROJECT_ID=${PROJECT_ID},VERTEX_AI_LOCATION=global,STORAGE_PROVIDER=gcs\" \\\n --update-secrets=\"DATABASE_PASSWORD=dachjob-dev-db-password:latest,JWT_SECRET=dachjob-dev-jwt-secret-key:latest,SECRET_KEY=dachjob-dev-jwt-secret-key:latest,GEMINI_API_KEY=dachjob-dev-gemini-api-key:latest\"\n\n - name: Run migration job\n if: ${{ needs.prepare.outputs.run_migrations == 'true' }}\n run: |\n gcloud run jobs execute \"${MIGRATION_JOB_NAME}\" \\\n --project=\"${PROJECT_ID}\" \\\n --region=\"${REGION}\" \\\n --wait\n\n - name: Capture service URL\n id: url\n run: |\n url=\"$(gcloud run services describe \"${API_SERVICE_NAME}\" --project=\"${PROJECT_ID}\" --region=\"${REGION}\" --format='value(status.url)')\"\n echo \"url=${url}\" >> \"${GITHUB_OUTPUT}\"\n\n - name: Health check API\n run: |\n for attempt in {1..30}; do\n if curl -fsS \"${{ steps.url.outputs.url }}/api/health\"; then\n exit 0\n fi\n sleep 5\n done\n exit 1\n\n deploy-frontend:\n name: Deploy frontend (GCP)\n needs:\n - prepare\n - terraform\n - build-frontend\n if: ${{ always() && needs.prepare.outputs.deploy_frontend == 'true' && (needs.terraform.result == 'success' || needs.terraform.result == 'skipped') && needs.build-frontend.result == 'success' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n outputs:\n url: ${{ steps.url.outputs.url }}\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Setup gcloud\n uses: google-github-actions/setup-gcloud@v3\n\n - name: Deploy frontend to Cloud Run\n run: |\n gcloud run deploy \"${FRONTEND_SERVICE_NAME}\" \\\n --project=\"${PROJECT_ID}\" \\\n --region=\"${REGION}\" \\\n --image=\"${{ needs.build-frontend.outputs.image }}\" \\\n --service-account=\"${FRONTEND_SERVICE_ACCOUNT}\" \\\n --port=3000 \\\n --allow-unauthenticated \\\n --set-env-vars=\"NODE_ENV=production,NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL},INTERNAL_API_BASE_URL=${API_BASE_URL}\"\n\n - name: Capture service URL\n id: url\n run: |\n url=\"$(gcloud run services describe \"${FRONTEND_SERVICE_NAME}\" --project=\"${PROJECT_ID}\" --region=\"${REGION}\" --format='value(status.url)')\"\n echo \"url=${url}\" >> \"${GITHUB_OUTPUT}\"\n\n - name: Health check frontend\n run: |\n for attempt in {1..30}; do\n if curl -fsS \"${{ steps.url.outputs.url }}\" >/dev/null; then\n exit 0\n fi\n sleep 5\n done\n exit 1\n\n deploy-worker:\n name: Deploy worker (GCP)\n needs:\n - prepare\n - terraform\n - build-worker\n if: ${{ always() && needs.prepare.outputs.deploy_worker == 'true' && (needs.terraform.result == 'success' || needs.terraform.result == 'skipped') && needs.build-worker.result == 'success' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n steps:\n - name: Checkout selected branch\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.prepare.outputs.branch }}\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Setup gcloud\n uses: google-github-actions/setup-gcloud@v3\n\n - name: Get GKE credentials\n uses: google-github-actions/get-gke-credentials@v3\n with:\n cluster_name: ${{ env.GKE_CLUSTER_NAME }}\n location: ${{ env.REGION }}\n project_id: ${{ env.PROJECT_ID }}\n\n - name: Deploy worker to GKE\n run: |\n gcloud iam service-accounts add-iam-policy-binding \"${WORKER_SERVICE_ACCOUNT}\" \\\n --project=\"${PROJECT_ID}\" \\\n --role=\"roles/iam.workloadIdentityUser\" \\\n --member=\"serviceAccount:${PROJECT_ID}.svc.id.goog[${GKE_NAMESPACE}/worker]\" >/dev/null\n\n set +x\n database_password=\"$(gcloud secrets versions access latest --secret=dachjob-dev-db-password --project=\"${PROJECT_ID}\")\"\n jwt_secret_key=\"$(gcloud secrets versions access latest --secret=dachjob-dev-jwt-secret-key --project=\"${PROJECT_ID}\")\"\n gemini_api_key=\"$(gcloud secrets versions access latest --secret=dachjob-dev-gemini-api-key --project=\"${PROJECT_ID}\" 2>/dev/null || true)\"\n database_url=\"postgresql+psycopg://postgres:${database_password}@localhost/dachjob?host=/cloudsql/${CLOUD_SQL_CONNECTION_NAME}\"\n\n kubectl create namespace \"${GKE_NAMESPACE}\" --dry-run=client -o yaml | kubectl apply -f -\n kubectl create secret generic worker-secrets \\\n --namespace=\"${GKE_NAMESPACE}\" \\\n --from-literal=database_password=\"${database_password}\" \\\n --from-literal=database_url=\"${database_url}\" \\\n --from-literal=jwt_secret_key=\"${jwt_secret_key}\" \\\n --from-literal=gemini_api_key=\"${gemini_api_key}\" \\\n --dry-run=client -o yaml | kubectl apply -f -\n\n kubectl apply -f infra/k8s/celery-worker/deployment.yaml\n kubectl set image deployment/celery-worker \\\n worker=\"${{ needs.build-worker.outputs.image }}\" \\\n --namespace=\"${GKE_NAMESPACE}\"\n kubectl rollout status deployment/celery-worker --namespace=\"${GKE_NAMESPACE}\" --timeout=300s\n kubectl get pods --namespace=\"${GKE_NAMESPACE}\" -o wide\n\n smoke-test:\n name: Smoke test\n needs:\n - prepare\n - deploy-api\n - deploy-frontend\n if: ${{ always() && needs.deploy-api.result == 'success' && needs.deploy-frontend.result == 'success' }}\n runs-on: ubuntu-latest\n environment: ${{ needs.prepare.outputs.environment }}\n permissions:\n contents: read\n id-token: write\n steps:\n - name: Checkout\n uses: actions/checkout@v6\n with:\n persist-credentials: false\n\n - name: Authenticate to Google Cloud\n uses: google-github-actions/auth@v3\n with:\n workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}\n service_account: ${{ env.DEPLOYER_SERVICE_ACCOUNT }}\n\n - name: Fetch test credentials from GCP Secret Manager\n run: |\n SMOKE_TEST_PASSWORD=\"$(gcloud secrets versions access latest --secret=dachjob-dev-test-ci-password --project=\"${PROJECT_ID}\" 2>/dev/null || echo '')\"\n echo \"SMOKE_TEST_PASSWORD=${SMOKE_TEST_PASSWORD}\" >> \"${GITHUB_ENV}\"\n\n - name: Run smoke test\n uses: ./.github/actions/smoke-test\n with:\n mode: ${{ github.event.inputs.smoke_test_mode || 'minimal' }}\n api-base-url: ${{ needs.deploy-api.outputs.url || env.API_BASE_URL }}\n frontend-url: ${{ needs.deploy-frontend.outputs.url || env.FRONTEND_URL }}\n email: smoke+gcp-${{ github.run_id }}-${{ github.run_attempt }}@example.com\n\n summary:\n name: Deployment summary\n needs:\n - prepare\n - terraform\n - deploy-api\n - deploy-frontend\n - deploy-worker\n - smoke-test\n if: ${{ always() }}\n runs-on: ubuntu-latest\n steps:\n - name: Write summary\n run: |\n echo \"## GCP Deployment Summary\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Item | Value |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| --- | --- |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Environment | ${{ needs.prepare.outputs.environment || 'dev' }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Branch | ${{ needs.prepare.outputs.branch }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Target | ${{ needs.prepare.outputs.target }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Image tag | ${{ needs.prepare.outputs.image_tag }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Terraform | ${{ needs.terraform.result }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| API | ${{ needs.deploy-api.result }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Frontend | ${{ needs.deploy-frontend.result }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Worker | ${{ needs.deploy-worker.result || 'skipped' }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Smoke test | ${{ needs.smoke-test.result }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| API URL | ${{ needs.deploy-api.outputs.url || env.API_BASE_URL }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n echo \"| Frontend URL | ${{ needs.deploy-frontend.outputs.url || env.FRONTEND_URL }} |\" >> \"${GITHUB_STEP_SUMMARY}\"\n"}}}}