CI/CD & Deployment
Moonlight uses GitHub Actions for continuous integration and deployment to Google Cloud Platform.
CI Pipeline
Pull Request Checks
Every PR to main runs path-filtered checks via pr-check.yml:
| Component | Trigger Path | Checks |
|---|---|---|
| Server | moonlight-server/** | Install → Lint → Test → Build → Docker build verify |
| Frontend | moonlight-ui/** | Install → Lint → Build |
| SDK | moonlight-sdk/** | Install → Lint → Build |
| Docs | moonlight-docs/** | Install → Build |
Only modified components are checked, using dorny/paths-filter for change detection. PRs have concurrency control — new pushes to the same PR cancel in-progress runs.
Deployment Pipelines
Deployments trigger automatically on push to main (path-filtered) and deploy to the stage environment by default. Each workflow also supports workflow_dispatch for manual runs with environment selection (stage or prod).
Backend (backend-deploy.yml)
Trigger paths: moonlight-server/**, flyway/**
1. Authenticate to GCP (Workload Identity Federation)
2. Start Cloud SQL Proxy
3. Run Flyway migrations against Cloud SQL
4. Build Docker image (multi-stage, Node 22 Alpine)
5. Push to Artifact Registry
6. Deploy to Cloud Run
Cloud Run configuration:
- Port: 3000
- Memory: 512Mi
- CPU: 1
- Instances: 0–10 (scale to zero)
- VPC Connector: for Cloud SQL access
- Pub/Sub:
PUBSUB_PROJECT_IDenv, runtime SA hasroles/pubsub.publisherandroles/pubsub.subscriber - Secrets: DB password and JWT secret from GCP Secret Manager
Frontend (frontend-deploy.yml)
Trigger path: moonlight-ui/**
1. Install dependencies and build (Vite)
2. Authenticate to GCP
3. Upload to GCS bucket (gsutil rsync)
4. Set cache headers:
- Assets (JS/CSS): public, max-age=31536000, immutable
- HTML: no-cache, must-revalidate
Documentation (docs-deploy.yml)
Trigger path: moonlight-docs/**
1. Install dependencies and build (Docusaurus)
2. Authenticate to GCP
3. Upload to GCS bucket (gsutil rsync)
SDK (sdk-publish.yml)
Trigger path: moonlight-sdk/**
1. Install dependencies and build (tsup)
2. Publish to GitHub Packages (npm registry)
- Version: {base}-build.{run_number} (e.g., 0.1.0-build.42)
- Tag: dev
- Scope: @tkgreg
GCP Authentication
All CI/CD pipelines use Workload Identity Federation — no static service account keys are stored in GitHub. GitHub Actions authenticate via OIDC tokens that are exchanged for short-lived GCP credentials.
Environments
| Environment | GCP Project | Default Trigger | Purpose |
|---|---|---|---|
| stage | moonlight-stage | Push to main | Staging / QA |
| prod | moonlight-prod | Manual dispatch | Production |
Domains
| Service | Stage | Production |
|---|---|---|
| Frontend | stage.moon-light.app | moon-light.app |
| API | stage-api.moon-light.app | api.moon-light.app |
| Docs | stage-docs.moon-light.app | docs.moon-light.app |
All domains route through Cloudflare (CDN + DDoS protection) to a GCP HTTPS Load Balancer with host-based URL mapping.
GCP Resources
| Resource | Service | Purpose |
|---|---|---|
| Cloud Run | moonlight-server | Backend API |
| Cloud SQL | PostgreSQL 15 | Database |
| Pub/Sub | ingest, accounting-sync, scheduler-tick (+ *-dlq) | Asynchronous job dispatch |
| Cloud Scheduler | moonlight-tick-* jobs | Cron triggers publishing to scheduler-tick |
| Artifact Registry | moonlight | Docker images |
| Cloud Storage | moonlight-{env}-frontend | Frontend static files |
| Cloud Storage | moonlight-{env}-docs | Documentation site |
| Cloud Storage | moonlight-{env}-uploads | File uploads |
| VPC Connector | moonlight-vpc | Cloud Run → Cloud SQL |
| HTTPS Load Balancer | moonlight-lb | Domain routing + SSL |
| Secret Manager | — | DB password, JWT secret |
Region: asia-southeast1 (Singapore)
GitHub Repository Configuration
Secrets (per environment)
| Secret | Description |
|---|---|
GCP_WORKLOAD_IDENTITY_PROVIDER | WIF provider resource name |
GCP_SERVICE_ACCOUNT | GCP service account email |
Variables (per environment)
| Variable | Description |
|---|---|
GCP_PROJECT_ID | GCP project ID |
GCP_REGION | GCP region |
CLOUD_SQL_INSTANCE | Cloud SQL connection name |
VPC_CONNECTOR | VPC connector name |
FRONTEND_GCS_BUCKET | Frontend bucket name |
DOCS_GCS_BUCKET | Docs bucket name |
UPLOADS_BUCKET | File uploads bucket name |
PUBSUB_PROJECT_ID | GCP project that owns the Pub/Sub topology (defaults to GCP_PROJECT_ID) |
DB_PRIVATE_IP | Cloud SQL private IP |
API_URL | Public API URL |
FRONTEND_URL | Public frontend URL |
Infrastructure Setup (from scratch)
Infrastructure is provisioned via bash scripts in infra/:
# 1. Authenticate with GCP
gcloud auth login
# 2. Create all GCP resources (Cloud SQL, VPC, Pub/Sub topics + DLQs,
# Cloud Scheduler jobs, buckets, IAM bindings, etc.)
./infra/setup-gcp.sh stage
# 3. Set up domains (Load Balancer, SSL certificates, backend routing)
./infra/setup-domain.sh stage
# 4. Configure GitHub Actions environment (secrets and variables)
./infra/setup-github.sh stage
# 5. Add DNS records in Cloudflare
# All domains → A record → LB static IP (from setup-domain.sh output)
# SSL mode: Full
# Initially "DNS only" until Google cert is ACTIVE, then switch to "Proxied"
See infra/README.md for detailed infrastructure documentation.
Docker
Backend Dockerfile (moonlight-server/Dockerfile)
Multi-stage build:
- Build stage: Node 22 Alpine,
npm ci, compile TypeScript - Production stage: Node 22 Alpine, production deps only, non-root user (
app), healthcheck
Local Development (docker-compose.yaml)
| Service | Image | Port | Purpose |
|---|---|---|---|
moon_db | postgres | 9432 | Main database |
moon_test_db | postgres | 9433 | Test database |
flyway | flyway/flyway | — | Migration runner (main DB) |
flyway-test | flyway/flyway | — | Migration runner (test DB) |
pubsub | gcr.io/google.com/cloudsdktool/cloud-sdk:emulators | 8085 | Pub/Sub emulator (topics created on app boot) |
minio | minio/minio | 9000, 9001 | S3-compatible storage |
minio-init | minio/mc | — | Creates moonlight bucket |