Spaces:
Running
Running
| 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" | |