name: Deploy to Cloud Run # Build the app image and deploy it to Google Cloud Run (Phase B2 of the HF -> GCP move). # # MANUAL for now (workflow_dispatch): during the migration the HF Space is still the live # prod surface (deploy.yml), so we deploy to Cloud Run on demand to test it, and only flip # the domain at cutover (B4). Once cut over, this can switch to `on: push` to main. # # Auth: the `GCP_SA_KEY` secret (gh-deployer SA JSON key) + the `GCP_PROJECT` / `GCP_REGION` # repo variables. Runtime config comes from GCP Secret Manager (the `run-svc` runtime SA has # secretAccessor); non-secret flags are plain env vars. HF_TOKEN is a BUILD-time secret only # (the Dockerfile's parcels bake) — never a runtime var. on: workflow_dispatch: inputs: allow_unauthenticated: description: "Expose the service publicly (the app does its own API-key auth)" type: boolean default: true concurrency: group: deploy-cloudrun cancel-in-progress: false # never interrupt a build/deploy mid-flight env: SERVICE: lawn-app REPO: lawn-app # Artifact Registry repo (created in B1) GCP_PROJECT: ${{ vars.GCP_PROJECT }} GCP_REGION: ${{ vars.GCP_REGION }} jobs: deploy: runs-on: ubuntu-latest permissions: contents: read # checkout steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 # The image bakes 3 models (~1.8 GB) + torch + the statewide parcels GPKG (~1.1 GB), # so the build needs more free disk than a stock runner has. Reclaim the preinstalled # toolchains we don't use before building. - name: Free up runner disk run: | sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ /opt/hostedtoolcache/CodeQL /usr/local/share/boost "$AGENT_TOOLSDIRECTORY" || true df -h / - name: Authenticate to Google Cloud env: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} run: | set -euo pipefail if [ -z "${GCP_SA_KEY:-}" ] || [ -z "${GCP_PROJECT:-}" ] || [ -z "${GCP_REGION:-}" ]; then echo "::error::Set the GCP_SA_KEY secret and GCP_PROJECT / GCP_REGION variables (see B1)." exit 1 fi printf '%s' "$GCP_SA_KEY" > "$RUNNER_TEMP/gcp-key.json" gcloud auth activate-service-account --key-file="$RUNNER_TEMP/gcp-key.json" gcloud config set project "$GCP_PROJECT" --quiet gcloud auth configure-docker "$GCP_REGION-docker.pkg.dev" --quiet rm -f "$RUNNER_TEMP/gcp-key.json" - name: Build & push image env: HF_TOKEN: ${{ secrets.HF_TOKEN }} # build-time only: the Dockerfile's parcels bake run: | set -euo pipefail IMAGE="$GCP_REGION-docker.pkg.dev/$GCP_PROJECT/$REPO/api:${GITHUB_SHA::12}" echo "IMAGE=$IMAGE" >> "$GITHUB_ENV" # HF_TOKEN is passed as a BuildKit secret (never a layer/env) for the parcels bake; # absent (e.g. a fork) -> the Dockerfile skips that bake. printf '%s' "${HF_TOKEN:-}" > "$RUNNER_TEMP/hf_token" docker buildx build --secret id=HF_TOKEN,src="$RUNNER_TEMP/hf_token" \ -t "$IMAGE" --load . rm -f "$RUNNER_TEMP/hf_token" docker push "$IMAGE" - name: Deploy to Cloud Run run: | set -euo pipefail AUTH_FLAG="--allow-unauthenticated" if [ "${{ inputs.allow_unauthenticated }}" != "true" ]; then AUTH_FLAG="--no-allow-unauthenticated"; fi # Every secret the app reads is a Secret Manager shell (created in B2). Mount ONLY the # ones the owner has actually filled (an enabled version exists) — unfilled ones stay # unset and the app falls back to its defaults, so the service deploys before every # secret is populated and each `gcloud secrets versions add` is picked up next deploy. SECRET_KEYS="DATABASE_URL ALLOWED_API_KEYS WIDGET_API_KEYS CLERK_SECRET_KEY \ CLERK_PUBLISHABLE_KEY GOOGLE_MAPS_API_KEY GOOGLE_GEOCODING_API_KEY \ GOOGLE_PLACES_SERVER_KEY RESEND_API_KEY SENDGRID_API_KEY EMAIL_FROM EMAIL_REPLY_TO \ LEAD_NOTIFY_EMAIL EMAIL_SENDING_DOMAINS TURNSTILE_SECRET_KEY TURNSTILE_SITE_KEY \ PLATFORM_ADMIN_EMAILS CORS_ALLOW_ORIGINS R2_BUCKET R2_ACCOUNT_ID R2_ACCESS_KEY_ID \ R2_SECRET_ACCESS_KEY" SECRETS="" for k in $SECRET_KEYS; do if gcloud secrets versions list "$k" --filter="state=ENABLED" --format='value(name)' 2>/dev/null | grep -q .; then SECRETS="${SECRETS:+$SECRETS,}$k=$k:latest" echo " mounting secret: $k" fi done # Deterministic: replace the secret set each deploy (--clear-secrets if none filled yet). if [ -n "$SECRETS" ]; then SECRET_FLAG=(--set-secrets "$SECRETS"); else SECRET_FLAG=(--clear-secrets); fi gcloud run deploy "$SERVICE" \ --image "$IMAGE" \ --region "$GCP_REGION" \ --service-account "run-svc@$GCP_PROJECT.iam.gserviceaccount.com" \ --cpu 4 --memory 16Gi \ --concurrency 1 \ --timeout 3600 \ --min-instances 0 --max-instances 4 \ --no-cpu-throttling \ --vpc-connector lawn-app-conn --vpc-egress all-traffic \ --port 8000 \ $AUTH_FLAG \ --set-env-vars "LAWN_DATA_DIR=/app/data,WARM_MODEL_ON_STARTUP=1,SAM_RESTRICT=1,ROW_TO_CURB=1,GREEN_RECLAIM=1,LAWN_CASCADE=1,PARCELS_FALLBACK=1" \ "${SECRET_FLAG[@]}" URL=$(gcloud run services describe "$SERVICE" --region "$GCP_REGION" --format='value(status.url)') echo "Deployed $SERVICE -> $URL" echo "### Cloud Run deployed :rocket:" >> "$GITHUB_STEP_SUMMARY" echo "- image: \`$IMAGE\`" >> "$GITHUB_STEP_SUMMARY" echo "- url: $URL" >> "$GITHUB_STEP_SUMMARY"