lawn-estimator-dev / .github /workflows /deploy-cloudrun.yml
TempuraML's picture
feat(app): Cloud Run deploy workflow + infra runbook (B2)
234d00b
Raw
History Blame Contribute Delete
5.96 kB
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"