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