agilentics / boiler
name: Deploy to App Engine (integration)

# The integration counterpart to deploy.yml. Same project, same Cloud SQL
# instance, same code - a different App Engine service, a different database and
# a different set of secrets.
#
# This is where main lands. Every merge deploys here automatically, so
# integration always holds the real merged trunk rather than a ref somebody
# remembered to pick. A `v*` tag then promotes an int-tested commit to production
# (deploy.yml).
#
# Making this manual is the tempting mistake: on the theory that a person will
# put a candidate here ahead of production, nobody ever is that person, and int
# ends up sitting *behind* production rather than ahead of it.
#
# workflow_dispatch stays for putting an arbitrary ref on int - trying a branch
# out before it merges is still a reasonable thing to want.
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy-int:
    runs-on: ubuntu-latest

    env:
      CLOUD_SQL_INSTANCE: myapp-000000:us-central1:myapp-postgres
      # Committed in app-int.yaml already, so not secrets. Named here so the
      # migration steps below cannot drift from what the service will read.
      INT_DB_USER: myapp
      INT_DB_NAME: myapp_int

    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_CREDENTIALS }}

      - name: Set up Python 3.10
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: pip

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Tests
        run: pytest -q -m "not e2e"

      - name: Download Cloud SQL Auth Proxy
        run: |
          curl -fsSL -o cloud-sql-proxy \
            https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.11.4/cloud-sql-proxy.linux.amd64
          chmod +x cloud-sql-proxy

      - name: Start Cloud SQL Auth Proxy
        run: ./cloud-sql-proxy --port 5432 --health-check "$CLOUD_SQL_INSTANCE" &

      - name: Wait for proxy to be ready
        run: |
          for i in $(seq 1 30); do
            if curl -fsS http://localhost:9090/readiness >/dev/null 2>&1; then
              echo "Proxy ready after ${i}s"
              exit 0
            fi
            sleep 1
          done
          echo "Cloud SQL Auth Proxy did not become ready in 30s" >&2
          exit 1

      # myapp_int, never myapp. The database name is the only thing standing
      # between an int deploy and a production migration, which is why it is a
      # literal here rather than a secret that could be set once and forgotten.
      - name: Run database migrations
        run: alembic upgrade head
        env:
          DB_USER: ${{ env.INT_DB_USER }}
          DB_PASS: ${{ secrets.DB_PASS }}
          DB_NAME: ${{ env.INT_DB_NAME }}
          DB_PORT: 5432

      - name: Check the schema matches the models
        run: alembic check
        env:
          DB_USER: ${{ env.INT_DB_USER }}
          DB_PASS: ${{ secrets.DB_PASS }}
          DB_NAME: ${{ env.INT_DB_NAME }}
          DB_PORT: 5432

      # env_secrets_int.yaml, which app-int.yaml includes - NOT env_secrets.yaml.
      # Two files rather than one so that a mistake here cannot render
      # production's secrets into the int service, or the other way round.
      #
      # Two keys, and each absence below is a deliberate behaviour that
      # app-int.yaml documents: no SMTP_PASS (mail is logged, not sent), no
      # GOOGLE_CLIENT_* (password sign-in only, until int's redirect URI is
      # registered on the OAuth client - a credential is bound to its registered
      # URIs, so production's client does not cover the int hostname).
      #
      # Written through python with a quoted heredoc and json-quoted values, the
      # way deploy.yml does it and for the reason its comment gives.
      - name: Render secret env file
        env:
          DB_PASS: ${{ secrets.DB_PASS }}
          SECRET_KEY: ${{ secrets.INT_SECRET_KEY }}
        run: |
          python - <<'PY'
          import json, os

          KEYS = ["DB_PASS", "SECRET_KEY"]

          with open("env_secrets_int.yaml", "w", encoding="utf-8") as f:
              f.write("env_variables:\n")
              for key in KEYS:
                  f.write("  %s: %s\n" % (key, json.dumps(os.environ.get(key, ""))))

          print("rendered:", {k: len(os.environ.get(k, "")) for k in KEYS})
          PY

      - uses: google-github-actions/deploy-appengine@v2
        with:
          project_id: myapp-000000
          deliverables: app-int.yaml