Skip to main content

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:

ComponentTrigger PathChecks
Servermoonlight-server/**Install → Lint → Test → Build → Docker build verify
Frontendmoonlight-ui/**Install → Lint → Build
SDKmoonlight-sdk/**Install → Lint → Build
Docsmoonlight-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_ID env, runtime SA has roles/pubsub.publisher and roles/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

EnvironmentGCP ProjectDefault TriggerPurpose
stagemoonlight-stagePush to mainStaging / QA
prodmoonlight-prodManual dispatchProduction

Domains

ServiceStageProduction
Frontendstage.moon-light.appmoon-light.app
APIstage-api.moon-light.appapi.moon-light.app
Docsstage-docs.moon-light.appdocs.moon-light.app

All domains route through Cloudflare (CDN + DDoS protection) to a GCP HTTPS Load Balancer with host-based URL mapping.

GCP Resources

ResourceServicePurpose
Cloud Runmoonlight-serverBackend API
Cloud SQLPostgreSQL 15Database
Pub/Subingest, accounting-sync, scheduler-tick (+ *-dlq)Asynchronous job dispatch
Cloud Schedulermoonlight-tick-* jobsCron triggers publishing to scheduler-tick
Artifact RegistrymoonlightDocker images
Cloud Storagemoonlight-{env}-frontendFrontend static files
Cloud Storagemoonlight-{env}-docsDocumentation site
Cloud Storagemoonlight-{env}-uploadsFile uploads
VPC Connectormoonlight-vpcCloud Run → Cloud SQL
HTTPS Load Balancermoonlight-lbDomain routing + SSL
Secret ManagerDB password, JWT secret

Region: asia-southeast1 (Singapore)

GitHub Repository Configuration

Secrets (per environment)

SecretDescription
GCP_WORKLOAD_IDENTITY_PROVIDERWIF provider resource name
GCP_SERVICE_ACCOUNTGCP service account email

Variables (per environment)

VariableDescription
GCP_PROJECT_IDGCP project ID
GCP_REGIONGCP region
CLOUD_SQL_INSTANCECloud SQL connection name
VPC_CONNECTORVPC connector name
FRONTEND_GCS_BUCKETFrontend bucket name
DOCS_GCS_BUCKETDocs bucket name
UPLOADS_BUCKETFile uploads bucket name
PUBSUB_PROJECT_IDGCP project that owns the Pub/Sub topology (defaults to GCP_PROJECT_ID)
DB_PRIVATE_IPCloud SQL private IP
API_URLPublic API URL
FRONTEND_URLPublic 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:

  1. Build stage: Node 22 Alpine, npm ci, compile TypeScript
  2. Production stage: Node 22 Alpine, production deps only, non-root user (app), healthcheck

Local Development (docker-compose.yaml)

ServiceImagePortPurpose
moon_dbpostgres9432Main database
moon_test_dbpostgres9433Test database
flywayflyway/flywayMigration runner (main DB)
flyway-testflyway/flywayMigration runner (test DB)
pubsubgcr.io/google.com/cloudsdktool/cloud-sdk:emulators8085Pub/Sub emulator (topics created on app boot)
miniominio/minio9000, 9001S3-compatible storage
minio-initminio/mcCreates moonlight bucket