@slingr/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +202 -0
- package/README.md +490 -319
- package/bin/dev.cmd +2 -2
- package/bin/dev.js +5 -5
- package/bin/run.cmd +2 -2
- package/bin/run.js +4 -4
- package/bin/slingr +1 -0
- package/dist/commands/build.d.ts +20 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +206 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create-app.d.ts +0 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +38 -57
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/debug.d.ts +28 -0
- package/dist/commands/debug.d.ts.map +1 -0
- package/dist/commands/debug.js +474 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/ds.d.ts +14 -1
- package/dist/commands/ds.d.ts.map +1 -1
- package/dist/commands/ds.js +450 -121
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/gql.d.ts +1 -1
- package/dist/commands/gql.d.ts.map +1 -1
- package/dist/commands/gql.js +190 -184
- package/dist/commands/gql.js.map +1 -1
- package/dist/commands/infra/down.d.ts.map +1 -1
- package/dist/commands/infra/down.js +8 -7
- package/dist/commands/infra/down.js.map +1 -1
- package/dist/commands/infra/up.d.ts.map +1 -1
- package/dist/commands/infra/up.js +8 -7
- package/dist/commands/infra/up.js.map +1 -1
- package/dist/commands/infra/update.d.ts +1 -0
- package/dist/commands/infra/update.d.ts.map +1 -1
- package/dist/commands/infra/update.js +33 -69
- package/dist/commands/infra/update.js.map +1 -1
- package/dist/commands/run.d.ts +29 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +628 -130
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +34 -71
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sync-metadata.d.ts +15 -0
- package/dist/commands/sync-metadata.d.ts.map +1 -0
- package/dist/commands/sync-metadata.js +225 -0
- package/dist/commands/sync-metadata.js.map +1 -0
- package/dist/commands/users.d.ts +30 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +472 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/views.d.ts +11 -0
- package/dist/commands/views.d.ts.map +1 -0
- package/dist/commands/views.js +73 -0
- package/dist/commands/views.js.map +1 -0
- package/dist/projectStructure.d.ts +2 -2
- package/dist/projectStructure.d.ts.map +1 -1
- package/dist/projectStructure.js +281 -69
- package/dist/projectStructure.js.map +1 -1
- package/dist/scripts/generate-metadata.d.ts +13 -0
- package/dist/scripts/generate-metadata.d.ts.map +1 -0
- package/dist/scripts/generate-metadata.js +412 -0
- package/dist/scripts/generate-metadata.js.map +1 -0
- package/dist/scripts/generate-metadata.ts +498 -0
- package/dist/scripts/generate-schema.d.ts +1 -1
- package/dist/scripts/generate-schema.js +168 -74
- package/dist/scripts/generate-schema.js.map +1 -1
- package/dist/scripts/generate-schema.ts +258 -143
- package/dist/templates/.env.template +23 -0
- package/dist/templates/.firebaserc.template +5 -0
- package/dist/templates/.github/copilot-instructions.md.template +652 -17
- package/dist/templates/backend/Dockerfile.template +30 -0
- package/dist/templates/config/datasource.ts.template +12 -9
- package/dist/templates/config/jest.config.ts +30 -30
- package/dist/templates/config/jest.setup.ts +1 -1
- package/dist/templates/config/tsconfig.json.template +50 -29
- package/dist/templates/dataSources/mysql.ts.template +16 -13
- package/dist/templates/dataSources/postgres.ts.template +15 -13
- package/dist/templates/dataset-generator-script.ts.template +139 -139
- package/dist/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/dist/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/dist/templates/docker-compose.prod-test.yml.template +32 -0
- package/dist/templates/docker-compose.yml.template +24 -0
- package/dist/templates/docs/app-description.md.template +33 -33
- package/dist/templates/firebase.json.template +68 -0
- package/dist/templates/frontend/.umirc.ts.template +23 -0
- package/dist/templates/frontend/package.json.template +45 -0
- package/dist/templates/frontend/public/config.json +6 -0
- package/dist/templates/frontend/public/logo.svg +6 -0
- package/dist/templates/frontend/src/app.tsx.template +44 -0
- package/dist/templates/frontend/src/global.less.template +117 -0
- package/dist/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/dist/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/dist/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/dist/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/dist/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/dist/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/dist/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/dist/templates/frontend/tsconfig.json.template +50 -0
- package/dist/templates/gql/codegen.yml.template +25 -25
- package/dist/templates/gql/index.ts.template +17 -24
- package/dist/templates/gql/operations.graphql.template +30 -30
- package/dist/templates/ops/README.md.template +1045 -0
- package/dist/templates/ops/cloudbuild.yaml.template +161 -0
- package/dist/templates/ops/scripts/_utils.js.template +217 -0
- package/dist/templates/ops/scripts/deploy.js.template +145 -0
- package/dist/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/dist/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/dist/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/dist/templates/package.json.template +50 -38
- package/dist/templates/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/prompt-analysis.md.template +110 -110
- package/dist/templates/prompt-script-generation.md.template +258 -258
- package/dist/templates/src/Address.ts.template +28 -31
- package/dist/templates/src/App.ts.template +17 -61
- package/dist/templates/src/Company.ts.template +41 -47
- package/dist/templates/src/Models.test.ts.template +654 -654
- package/dist/templates/src/Person.test.ts.template +289 -289
- package/dist/templates/src/Person.ts.template +90 -105
- package/dist/templates/src/actions/index.ts.template +11 -11
- package/dist/templates/src/auth/permissions.ts.template +34 -0
- package/dist/templates/src/data/App.ts.template +48 -0
- package/dist/templates/src/data/User.ts.template +35 -0
- package/dist/templates/src/types/gql.d.ts.template +17 -17
- package/dist/templates/vscode/extensions.json +4 -3
- package/dist/templates/vscode/settings.json +17 -11
- package/dist/templates/workspace-package.json.template +21 -0
- package/dist/utils/buildCache.d.ts +12 -0
- package/dist/utils/buildCache.d.ts.map +1 -0
- package/dist/utils/buildCache.js +102 -0
- package/dist/utils/buildCache.js.map +1 -0
- package/dist/utils/checkFramework.d.ts +27 -0
- package/dist/utils/checkFramework.d.ts.map +1 -0
- package/dist/utils/checkFramework.js +104 -0
- package/dist/utils/checkFramework.js.map +1 -0
- package/dist/utils/datasourceParser.d.ts +11 -0
- package/dist/utils/datasourceParser.d.ts.map +1 -1
- package/dist/utils/datasourceParser.js +154 -56
- package/dist/utils/datasourceParser.js.map +1 -1
- package/dist/utils/dockerManager.d.ts +25 -0
- package/dist/utils/dockerManager.d.ts.map +1 -0
- package/dist/utils/dockerManager.js +281 -0
- package/dist/utils/dockerManager.js.map +1 -0
- package/dist/utils/infraFileParser.d.ts +26 -0
- package/dist/utils/infraFileParser.d.ts.map +1 -0
- package/dist/utils/infraFileParser.js +75 -0
- package/dist/utils/infraFileParser.js.map +1 -0
- package/dist/utils/jsonlLoader.d.ts +91 -12
- package/dist/utils/jsonlLoader.d.ts.map +1 -1
- package/dist/utils/jsonlLoader.js +674 -63
- package/dist/utils/jsonlLoader.js.map +1 -1
- package/dist/utils/model-analyzer.d.ts.map +1 -1
- package/dist/utils/model-analyzer.js +67 -13
- package/dist/utils/model-analyzer.js.map +1 -1
- package/dist/utils/userManagement.d.ts +57 -0
- package/dist/utils/userManagement.d.ts.map +1 -0
- package/dist/utils/userManagement.js +288 -0
- package/dist/utils/userManagement.js.map +1 -0
- package/dist/utils/viewsGenerator.d.ts +15 -0
- package/dist/utils/viewsGenerator.d.ts.map +1 -0
- package/dist/utils/viewsGenerator.js +311 -0
- package/dist/utils/viewsGenerator.js.map +1 -0
- package/oclif.manifest.json +445 -20
- package/package.json +29 -26
- package/src/templates/.env.template +23 -0
- package/src/templates/.firebaserc.template +5 -0
- package/src/templates/.github/copilot-instructions.md.template +652 -17
- package/src/templates/backend/Dockerfile.template +30 -0
- package/src/templates/config/datasource.ts.template +12 -9
- package/src/templates/config/jest.config.ts +30 -30
- package/src/templates/config/jest.setup.ts +1 -1
- package/src/templates/config/tsconfig.json.template +50 -29
- package/src/templates/dataSources/mysql.ts.template +16 -13
- package/src/templates/dataSources/postgres.ts.template +15 -13
- package/src/templates/dataset-generator-script.ts.template +139 -139
- package/src/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/src/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/src/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/src/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/src/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/src/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/src/templates/docker-compose.prod-test.yml.template +32 -0
- package/src/templates/docker-compose.yml.template +24 -0
- package/src/templates/docs/app-description.md.template +33 -33
- package/src/templates/firebase.json.template +68 -0
- package/src/templates/frontend/.umirc.ts.template +23 -0
- package/src/templates/frontend/package.json.template +45 -0
- package/src/templates/frontend/public/config.json +6 -0
- package/src/templates/frontend/public/logo.svg +6 -0
- package/src/templates/frontend/src/app.tsx.template +44 -0
- package/src/templates/frontend/src/global.less.template +117 -0
- package/src/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/src/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/src/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/src/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/src/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/src/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/src/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/src/templates/frontend/tsconfig.json.template +50 -0
- package/src/templates/gql/codegen.yml.template +25 -25
- package/src/templates/gql/index.ts.template +17 -24
- package/src/templates/gql/operations.graphql.template +30 -30
- package/src/templates/ops/README.md.template +1045 -0
- package/src/templates/ops/cloudbuild.yaml.template +161 -0
- package/src/templates/ops/scripts/_utils.js.template +217 -0
- package/src/templates/ops/scripts/deploy.js.template +145 -0
- package/src/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/src/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/src/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/src/templates/package.json.template +50 -38
- package/src/templates/pnpm-workspace.yaml.template +3 -0
- package/src/templates/prompt-analysis.md.template +110 -110
- package/src/templates/prompt-script-generation.md.template +258 -258
- package/src/templates/src/Address.ts.template +28 -31
- package/src/templates/src/App.ts.template +17 -61
- package/src/templates/src/Company.ts.template +41 -47
- package/src/templates/src/Models.test.ts.template +654 -654
- package/src/templates/src/Person.test.ts.template +289 -289
- package/src/templates/src/Person.ts.template +90 -105
- package/src/templates/src/actions/index.ts.template +11 -11
- package/src/templates/src/auth/permissions.ts.template +34 -0
- package/src/templates/src/data/App.ts.template +48 -0
- package/src/templates/src/data/User.ts.template +35 -0
- package/src/templates/src/types/gql.d.ts.template +17 -17
- package/src/templates/vscode/extensions.json +4 -3
- package/src/templates/vscode/settings.json +17 -11
- package/src/templates/workspace-package.json.template +21 -0
- package/dist/templates/src/index.ts +0 -66
- package/src/templates/src/index.ts +0 -66
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
# GCP Deployment Guide — Slingr Framework Apps
|
|
2
|
+
|
|
3
|
+
A step-by-step guide for deploying **{{APP_NAME}}** (a Slingr framework app) to Google Cloud Platform.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [GCP Deployment Guide — Slingr Framework Apps](#gcp-deployment-guide--slingr-framework-apps)
|
|
10
|
+
- [Table of Contents](#table-of-contents)
|
|
11
|
+
- [Architecture Overview](#architecture-overview)
|
|
12
|
+
- [Prerequisites](#prerequisites)
|
|
13
|
+
- [Tools](#tools)
|
|
14
|
+
- [GCP Setup](#gcp-setup)
|
|
15
|
+
- [Required GCP IAM Permissions](#required-gcp-iam-permissions)
|
|
16
|
+
- [File Structure Convention](#file-structure-convention)
|
|
17
|
+
- [Script Execution Order](#script-execution-order)
|
|
18
|
+
- [Adapting the Scripts for Your App](#adapting-the-scripts-for-your-app)
|
|
19
|
+
- [Variable mapping](#variable-mapping)
|
|
20
|
+
- [Step 1 — Backend Dockerfile](#step-1--backend-dockerfile)
|
|
21
|
+
- [Build context](#build-context)
|
|
22
|
+
- [Step 2 — Local Production Smoke Testing](#step-2--local-production-smoke-testing)
|
|
23
|
+
- [Quick start](#quick-start)
|
|
24
|
+
- [Manual step-by-step](#manual-step-by-step)
|
|
25
|
+
- [What to verify](#what-to-verify)
|
|
26
|
+
- [Iterating quickly](#iterating-quickly)
|
|
27
|
+
- [Step 3 — One-Time GCP Infrastructure Setup](#step-3--one-time-gcp-infrastructure-setup)
|
|
28
|
+
- [What it creates](#what-it-creates)
|
|
29
|
+
- [After running](#after-running)
|
|
30
|
+
- [Step 4 — Secrets Management](#step-4--secrets-management)
|
|
31
|
+
- [Rotating secrets later](#rotating-secrets-later)
|
|
32
|
+
- [Step 5 — Cloud Build Trigger (Automated Deploys)](#step-5--cloud-build-trigger-automated-deploys)
|
|
33
|
+
- [Connect the GitHub repository (one-time)](#connect-the-github-repository-one-time)
|
|
34
|
+
- [Create the trigger](#create-the-trigger)
|
|
35
|
+
- [cloudbuild.yaml structure](#cloudbuildyaml-structure)
|
|
36
|
+
- [Step 6 — Manual Deployment](#step-6--manual-deployment)
|
|
37
|
+
- [Step 7 — DNS \& HTTPS Setup (Optional)](#step-7--dns--https-setup-optional)
|
|
38
|
+
- [What setup-domain.js does](#what-setup-domainjs-does)
|
|
39
|
+
- [After running setup-domain.js](#after-running-setup-domainjs)
|
|
40
|
+
- [Manual equivalent (no script)](#manual-equivalent-no-script)
|
|
41
|
+
- [Networking](#networking)
|
|
42
|
+
- [What was set up](#what-was-set-up)
|
|
43
|
+
- [Cloud SQL connectivity](#cloud-sql-connectivity)
|
|
44
|
+
- [Verifying the networking setup](#verifying-the-networking-setup)
|
|
45
|
+
- [Frontend Hosting](#frontend-hosting)
|
|
46
|
+
- [URL routing](#url-routing)
|
|
47
|
+
- [Manual frontend deploy](#manual-frontend-deploy)
|
|
48
|
+
- [CDN cache invalidation](#cdn-cache-invalidation)
|
|
49
|
+
- [Database Schema Sync](#database-schema-sync)
|
|
50
|
+
- [Default Admin User](#default-admin-user)
|
|
51
|
+
- [Logging \& Observability](#logging--observability)
|
|
52
|
+
- [View backend logs](#view-backend-logs)
|
|
53
|
+
- [Stream live logs](#stream-live-logs)
|
|
54
|
+
- [Useful log filters](#useful-log-filters)
|
|
55
|
+
- [Cloud Monitoring — uptime check](#cloud-monitoring--uptime-check)
|
|
56
|
+
- [Cloud Monitoring — error rate alert](#cloud-monitoring--error-rate-alert)
|
|
57
|
+
- [IAM \& Service Accounts Reference](#iam--service-accounts-reference)
|
|
58
|
+
- [Runtime service account (`<app>-backend`)](#runtime-service-account-app-backend)
|
|
59
|
+
- [Cloud Build service account (CI/CD)](#cloud-build-service-account-cicd)
|
|
60
|
+
- [Ongoing Operations](#ongoing-operations)
|
|
61
|
+
- [Roll back to a previous revision](#roll-back-to-a-previous-revision)
|
|
62
|
+
- [Scale to zero immediately](#scale-to-zero-immediately)
|
|
63
|
+
- [Connect to Cloud SQL from your local machine](#connect-to-cloud-sql-from-your-local-machine)
|
|
64
|
+
- [Troubleshooting](#troubleshooting)
|
|
65
|
+
- [Backend fails to connect to Cloud SQL](#backend-fails-to-connect-to-cloud-sql)
|
|
66
|
+
- [Cloud Run service fails to start](#cloud-run-service-fails-to-start)
|
|
67
|
+
- [Schema sync job exits non-zero](#schema-sync-job-exits-non-zero)
|
|
68
|
+
- [Docker build fails: `cannot find module '@slingr/framework-backend'`](#docker-build-fails-cannot-find-module-slingr-frameworkbackend)
|
|
69
|
+
- [Docker push fails: Unauthenticated request](#docker-push-fails-unauthenticated-request)
|
|
70
|
+
- [Cloud SQL Proxy is not recognized on local machine](#cloud-sql-proxy-is-not-recognized-on-local-machine)
|
|
71
|
+
- [Frontend not updating after deploy](#frontend-not-updating-after-deploy)
|
|
72
|
+
- [Load Balancer returns 404 for frontend routes](#load-balancer-returns-404-for-frontend-routes)
|
|
73
|
+
- [SSL certificate stuck in PROVISIONING](#ssl-certificate-stuck-in-provisioning)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Architecture Overview
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ Users (browser) │
|
|
82
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
83
|
+
│ HTTP/HTTPS
|
|
84
|
+
┌─────────────▼──────────────┐
|
|
85
|
+
│ Cloud HTTP(S) Load Balancer │
|
|
86
|
+
│ /graphql → Cloud Run NEG │
|
|
87
|
+
│ /auth/* → Cloud Run NEG │
|
|
88
|
+
│ /** → GCS bucket CDN │
|
|
89
|
+
└──────┬──────────────┬───────┘
|
|
90
|
+
│ │
|
|
91
|
+
┌─────────────▼──┐ ┌──────▼──────────────┐
|
|
92
|
+
│ Cloud Run │ │ GCS Bucket (CDN) │
|
|
93
|
+
│ <app>-backend │ │ React SPA static │
|
|
94
|
+
└────────┬────────┘ └─────────────────────┘
|
|
95
|
+
│ Cloud SQL Auth Proxy (IAM, encrypted)
|
|
96
|
+
┌────────▼────────┐
|
|
97
|
+
│ Cloud SQL │ PostgreSQL 15
|
|
98
|
+
│ <app>-db │
|
|
99
|
+
└─────────────────┘
|
|
100
|
+
|
|
101
|
+
Secrets → Secret Manager (DB password, JWT secret)
|
|
102
|
+
Images → Artifact Registry (Docker)
|
|
103
|
+
CI/CD → Cloud Build (triggered on push to a branch)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Why this architecture:**
|
|
107
|
+
|
|
108
|
+
| Service | Choice | Reason |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Backend | Cloud Run | Serverless, scales to zero, $0 when idle |
|
|
111
|
+
| Frontend | GCS + Cloud LB | CDN-backed static hosting, no extra IAM roles |
|
|
112
|
+
| Database | Cloud SQL (PostgreSQL 15) | Managed, automated backups, point-in-time recovery |
|
|
113
|
+
| DB connectivity | Cloud SQL Auth Proxy | Native Cloud Run integration, no VPC connector needed |
|
|
114
|
+
| Secrets | Secret Manager | Audit trail, rotation support, IAM-scoped access |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Prerequisites
|
|
119
|
+
|
|
120
|
+
### Tools
|
|
121
|
+
|
|
122
|
+
| Tool | Min Version | Install | Purpose |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| **gcloud CLI** | >= 450.0.0 | [Install guide](https://cloud.google.com/sdk/docs/install) | GCP resource management |
|
|
125
|
+
| **Docker** | >= 24.0 | [Install guide](https://docs.docker.com/get-docker/) | Build backend container image |
|
|
126
|
+
| **Node.js** | >= 20.0 | [Install guide](https://nodejs.org/) | Build frontend, run CLI commands |
|
|
127
|
+
| **pnpm** | >= 8.0 | `npm install -g pnpm` | Monorepo workspace dependency management |
|
|
128
|
+
| **Cloud SQL Auth Proxy** | latest | `gcloud components install cloud-sql-proxy` | Local DB connection for seeding |
|
|
129
|
+
| **gsutil** | bundled | Included in gcloud CLI | Upload frontend files to GCS |
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Verify all dependencies
|
|
133
|
+
gcloud --version # >= 450.0.0
|
|
134
|
+
docker --version # >= 24.0
|
|
135
|
+
node --version # >= 20.0
|
|
136
|
+
pnpm --version # >= 8.0
|
|
137
|
+
cloud-sql-proxy --version
|
|
138
|
+
gsutil --version
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### GCP Setup
|
|
142
|
+
|
|
143
|
+
- GCP project created with billing enabled
|
|
144
|
+
- Required IAM roles on the project (see next section)
|
|
145
|
+
- `gcloud init` — sets your authenticated account and default project on this machine
|
|
146
|
+
- `gcloud auth application-default login` — sets Application Default Credentials for local SDK usage
|
|
147
|
+
- `gcloud auth configure-docker REGION-docker.pkg.dev` — authenticates Docker to push images to Artifact Registry
|
|
148
|
+
|
|
149
|
+
> **Important:** `setup-gcp.js` and `setup-secrets.js` are **project-level** bootstrap steps — run once per GCP project. `gcloud init`, `gcloud auth ...`, and installing the gcloud CLI are **machine-level** prerequisites. If you switch machines, install `gcloud` and authenticate again before running any scripts.
|
|
150
|
+
|
|
151
|
+
If the project was already provisioned from another machine:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# 1. Install Google Cloud CLI on this machine
|
|
155
|
+
# https://cloud.google.com/sdk/docs/install
|
|
156
|
+
|
|
157
|
+
# 2. Authenticate this machine
|
|
158
|
+
gcloud init
|
|
159
|
+
gcloud auth application-default login
|
|
160
|
+
|
|
161
|
+
# 3. Install Cloud SQL Auth Proxy
|
|
162
|
+
gcloud components install cloud-sql-proxy
|
|
163
|
+
cloud-sql-proxy --version
|
|
164
|
+
|
|
165
|
+
# 4. Authenticate Docker to Artifact Registry
|
|
166
|
+
gcloud auth configure-docker us-central1-docker.pkg.dev
|
|
167
|
+
|
|
168
|
+
# 5. Deploy (run from your app root)
|
|
169
|
+
node ops/scripts/deploy.js
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Required GCP IAM Permissions
|
|
173
|
+
|
|
174
|
+
The human (or CI service account) running the setup and deployment scripts must have the following roles on the GCP project. The simplest option is `roles/owner`. For least-privilege setups, grant the specific roles:
|
|
175
|
+
|
|
176
|
+
| Role | Required for |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `roles/resourcemanager.projectIamAdmin` | Granting IAM bindings to service accounts in `setup-gcp.js` |
|
|
179
|
+
| `roles/iam.serviceAccountAdmin` | Creating the backend service account |
|
|
180
|
+
| `roles/compute.admin` | Creating the Load Balancer (NEG, URL map, forwarding rules, backend services) |
|
|
181
|
+
| `roles/cloudsql.admin` | Creating the Cloud SQL instance, database, and setting user passwords |
|
|
182
|
+
| `roles/artifactregistry.admin` | Creating the Artifact Registry repository |
|
|
183
|
+
| `roles/run.admin` | Deploying Cloud Run services and jobs |
|
|
184
|
+
| `roles/secretmanager.admin` | Creating and versioning secrets in Secret Manager |
|
|
185
|
+
| `roles/storage.admin` | Creating and configuring the GCS bucket |
|
|
186
|
+
| `roles/serviceusage.serviceUsageAdmin` | Enabling GCP APIs |
|
|
187
|
+
| `roles/cloudbuild.builds.editor` | Creating Cloud Build triggers |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## File Structure Convention
|
|
192
|
+
|
|
193
|
+
Each app should have an `ops/` directory mirroring this layout:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
apps/<your-app>/
|
|
197
|
+
├── backend/
|
|
198
|
+
│ └── Dockerfile # Multi-stage build (see Step 1)
|
|
199
|
+
├── frontend/
|
|
200
|
+
│ └── src/services/GraphQLClientService.ts
|
|
201
|
+
└── ops/
|
|
202
|
+
├── README.md # This guide
|
|
203
|
+
├── cloudbuild.yaml # Cloud Build CI/CD pipeline
|
|
204
|
+
├── nginx/
|
|
205
|
+
│ └── prod-test.conf # nginx config for local smoke testing
|
|
206
|
+
└── scripts/
|
|
207
|
+
├── setup-gcp.js # One-time infrastructure setup
|
|
208
|
+
├── setup-secrets.js # Secret Manager credential management
|
|
209
|
+
├── setup-domain.js # Custom domain + HTTPS setup (optional)
|
|
210
|
+
├── deploy.js # Manual deployment script
|
|
211
|
+
└── test-prod-local.js # Local production smoke test
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The scripts in `ops/scripts/` have been pre-configured for **{{APP_NAME}}**. No manual variable substitution is needed.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Script Execution Order
|
|
219
|
+
|
|
220
|
+
The scripts are designed to be run in this order. Each step builds on the previous one.
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
1. (dockerfile) Step 1: Backend Dockerfile — copy and adapt the Dockerfile
|
|
224
|
+
2. test-prod-local.js Step 2: Verify the full production stack runs correctly in Docker locally
|
|
225
|
+
3. setup-gcp.js Step 3: Create all GCP infrastructure (run once per project)
|
|
226
|
+
4. setup-secrets.js Step 4: Store DB password and JWT secret in Secret Manager
|
|
227
|
+
5. (trigger creation) Step 5: Create the Cloud Build trigger for automated deploys
|
|
228
|
+
6. deploy.js Step 6: First manual deployment to GCP
|
|
229
|
+
7. setup-domain.js Step 7: (Optional) Add a custom domain and HTTPS
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> **`setup-domain.js` is optional and can be run any time after `setup-gcp.js`** — It requires that the Load Balancer already exists (created by `setup-gcp.js`) but is otherwise independent of deployment state.
|
|
233
|
+
|
|
234
|
+
> **You do not need a domain to deploy.** Without `setup-domain.js`, the app is accessible over HTTP on the Load Balancer IP that `setup-gcp.js` prints at the end. HTTPS with a custom domain is an optional step for production environments.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## GCP Resource Naming Reference
|
|
239
|
+
|
|
240
|
+
The scripts use `{{APP_NAME}}` as the base identifier for all GCP resource names. Here is the full mapping for reference:
|
|
241
|
+
|
|
242
|
+
### Variable mapping
|
|
243
|
+
|
|
244
|
+
| Variable | Description | Value for {{APP_NAME}} |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `APP_SHORT` | Short identifier used as a prefix for every GCP resource name. Keep it lowercase, hyphen-separated, and unique within the project. | `<your-app-short>` |
|
|
247
|
+
| `DB_INSTANCE_NAME` | Name of the Cloud SQL instance. One instance can host multiple databases. | `{{APP_NAME}}-db` |
|
|
248
|
+
| `DB_NAME` | Database name. PostgreSQL database inside the instance. Used as `DB_NAME` env var at runtime. | `{{APP_NAME}}` |
|
|
249
|
+
| `DB_USER` | PostgreSQL user the backend connects as. | `postgres` | superuser is used for simplicity |
|
|
250
|
+
| `AR_REPO_NAME` | Shared Docker registry across all apps in the project. | `slingr-apps` || `APP_SHORT` | Short identifier used as a prefix for every GCP resource name. Keep it lowercase, hyphen-separated, and unique within the project. | `<your-app-short>` |
|
|
251
|
+
| `BACKEND_SA_NAME` | Backend service account | Runtime identity for Cloud Run. Granted minimum roles. | `{{APP_NAME}}-backend` |
|
|
252
|
+
| `BACKEND_SERVICE` | Cloud Run service | The deployed backend service. Also used as the Docker image name. | `{{APP_NAME}}-backend` |
|
|
253
|
+
| `FRONTEND_BUCKET` | GCS bucket | Hosts the React SPA static files. Globally unique. | `PROJECT_ID-{{APP_NAME}}-frontend` |
|
|
254
|
+
| `NEG_NAME` | Serverless NEG | Connects the Load Balancer to Cloud Run. | `{{APP_NAME}}-backend-neg` |
|
|
255
|
+
| `URL_MAP_NAME` | Path-based routing (`/graphql` → Cloud Run, `/**` → GCS). | `{{APP_NAME}}-url-map` |
|
|
256
|
+
| `HTTP_PROXY_NAME` | Routes HTTP requests into the URL map. | `{{APP_NAME}}-http-proxy` |
|
|
257
|
+
| `FWD_RULE_NAME` | Port 80, assigns public IP. | `{{APP_NAME}}-http-rule` |
|
|
258
|
+
| `DB_SECRET_NAME` | Cloud SQL `postgres` password in Secret Manager. | `{{APP_NAME}}-db-password` |
|
|
259
|
+
| `JWT_SECRET_NAME` | JWT signing key in Secret Manager. | `{{APP_NAME}}-jwt-secret` |
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Step 1 — Backend Dockerfile
|
|
265
|
+
|
|
266
|
+
The `backend/Dockerfile` has been generated for your app. It uses a two-stage build:
|
|
267
|
+
|
|
268
|
+
1. **Stage 1 (builder):** Installs dependencies (`npm install`), compiles TypeScript (`npm run build`).
|
|
269
|
+
2. **Stage 2 (runtime):** Copies only `dist/` and `node_modules/`. Exposes port 8080. Starts with `node dist/src/App.js`.
|
|
270
|
+
|
|
271
|
+
### Build context
|
|
272
|
+
|
|
273
|
+
The build context is the **app root directory** (the folder containing `backend/` and `frontend/`):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# Run from your app root
|
|
277
|
+
docker build \
|
|
278
|
+
-f backend/Dockerfile \
|
|
279
|
+
-t REGION-docker.pkg.dev/PROJECT_ID/slingr-apps/{{APP_NAME}}-backend:latest \
|
|
280
|
+
.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The Cloud Build pipeline and `deploy.js` enforce this automatically.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Step 2 — Local Production Smoke Testing
|
|
288
|
+
|
|
289
|
+
Before touching GCP, run the full production stack locally. This catches environment-specific bugs that only appear in production builds — minified bundles, URL routing, Docker runtime config — without creating any cloud resources.
|
|
290
|
+
|
|
291
|
+
The local stack mirrors the GCP topology exactly:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
Browser
|
|
295
|
+
└─► nginx :8080
|
|
296
|
+
├─ /graphql → backend:3000 (Node.js container)
|
|
297
|
+
├─ /auth/* → backend:3000
|
|
298
|
+
└─ /** → React SPA (frontend/dist/)
|
|
299
|
+
backend:3000
|
|
300
|
+
└─ mainDs-db:5432 (PostgreSQL 15)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Quick start
|
|
304
|
+
|
|
305
|
+
Run from the **app root** (the directory containing `backend/`, `frontend/`, and `ops/`):
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
node ops/scripts/test-prod-local.js
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Then open **http://localhost:8080** in your browser.
|
|
312
|
+
|
|
313
|
+
The script performs these steps in order:
|
|
314
|
+
|
|
315
|
+
1. **Framework build** — skipped in standalone mode (framework is an npm dependency); runs `pnpm run build:all` only when inside the framework monorepo
|
|
316
|
+
2. `npx slingr sync-metadata` — regenerates `frontend/generated/` (view registry, GraphQL types)
|
|
317
|
+
3. `npm run build` (in `frontend/`) — produces a minified production bundle in `frontend/dist/`
|
|
318
|
+
4. `docker compose up --build` — builds the backend image and starts all three containers
|
|
319
|
+
|
|
320
|
+
In monorepo mode, skip rebuilding the framework if only app code changed:
|
|
321
|
+
```bash
|
|
322
|
+
SKIP_FRAMEWORK_BUILD=1 node ops/scripts/test-prod-local.js
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Manual step-by-step
|
|
326
|
+
|
|
327
|
+
Run from the **app root**:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# 1. (Monorepo only) Build the framework and CLI from source
|
|
331
|
+
# Skip this in standalone mode — the framework is in node_modules
|
|
332
|
+
# pnpm run build:all
|
|
333
|
+
|
|
334
|
+
# 2. Regenerate view registry and GraphQL types
|
|
335
|
+
npx slingr sync-metadata
|
|
336
|
+
|
|
337
|
+
# 3. Build the production frontend bundle
|
|
338
|
+
(cd frontend && npm run build)
|
|
339
|
+
|
|
340
|
+
# 4. Start the full stack
|
|
341
|
+
docker compose \
|
|
342
|
+
-f docker-compose.yml \
|
|
343
|
+
-f docker-compose.prod-test.yml \
|
|
344
|
+
up --build
|
|
345
|
+
|
|
346
|
+
# Open http://localhost:8080
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
To tear it down and clean up volumes:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
docker compose \
|
|
353
|
+
-f docker-compose.yml \
|
|
354
|
+
-f docker-compose.prod-test.yml \
|
|
355
|
+
down -v
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### What to verify
|
|
359
|
+
|
|
360
|
+
| Check | How |
|
|
361
|
+
|---|---|
|
|
362
|
+
| App loads at `http://localhost:8080` | No blank page, no console errors on startup |
|
|
363
|
+
| Login works | Authenticate with the seeded user (`sys@app.com`) |
|
|
364
|
+
| Toolbar buttons navigate correctly | Click every toolbar button — a `View class "X" is not registered` error means class name minification is breaking the view registry |
|
|
365
|
+
| Dashboard loads without errors | Open the dashboard — a `Failed to construct 'URL': Invalid URL` error means `GraphQLClientService.baseUrl` is a relative path that `graphql-request` cannot handle |
|
|
366
|
+
| Create / update forms submit | Submit a create form — `Runtime Object type "ExpectedErrorType" is not a possible type` means the GraphQL union type is missing `ExpectedErrorType` in the schema |
|
|
367
|
+
| Backend logs look clean | `docker compose logs backend` — no startup crashes |
|
|
368
|
+
|
|
369
|
+
### Iterating quickly
|
|
370
|
+
|
|
371
|
+
After fixing a frontend issue (run from app root):
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
(cd frontend && npm run build)
|
|
375
|
+
|
|
376
|
+
docker compose \
|
|
377
|
+
-f docker-compose.yml \
|
|
378
|
+
-f docker-compose.prod-test.yml \
|
|
379
|
+
restart frontend
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
After fixing a backend issue (run from app root):
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
docker compose \
|
|
386
|
+
-f docker-compose.yml \
|
|
387
|
+
-f docker-compose.prod-test.yml \
|
|
388
|
+
up --build backend
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Step 3 — One-Time GCP Infrastructure Setup
|
|
394
|
+
|
|
395
|
+
Run once per GCP project. The script is idempotent — safe to re-run.
|
|
396
|
+
|
|
397
|
+
Run from the **app root**:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
node ops/scripts/setup-gcp.js
|
|
401
|
+
|
|
402
|
+
# Override project or region:
|
|
403
|
+
PROJECT_ID=my-project REGION=europe-west1 node ops/scripts/setup-gcp.js
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### What it creates
|
|
407
|
+
|
|
408
|
+
| Resource | Example name | Notes |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| APIs enabled | Cloud Run, Cloud SQL, Artifact Registry, Cloud Build, Secret Manager, Cloud Storage, Compute Engine | ~1 minute on first run |
|
|
411
|
+
| Artifact Registry repo | `slingr-apps` | Shared Docker registry across all apps in the project |
|
|
412
|
+
| Cloud SQL instance | `<app>-db` | PostgreSQL 15, `db-f1-micro` tier |
|
|
413
|
+
| Cloud SQL database | `<your_db_name>` | Created inside the instance |
|
|
414
|
+
| Cloud SQL user | `postgres` | Password set interactively |
|
|
415
|
+
| Backend service account | `<app>-backend@PROJECT.iam.gserviceaccount.com` | Runtime identity for Cloud Run |
|
|
416
|
+
| IAM bindings (backend SA) | `roles/cloudsql.client`, `roles/secretmanager.secretAccessor` | Minimum required at runtime |
|
|
417
|
+
| IAM bindings (Cloud Build SA) | `roles/run.admin`, `roles/iam.serviceAccountUser`, `roles/secretmanager.secretAccessor`, `roles/artifactregistry.writer`, `roles/cloudsql.client`, `roles/storage.objectAdmin` | Required for automated CI/CD |
|
|
418
|
+
| GCS bucket | `PROJECT_ID-<app>-frontend` | Public, SPA routing via `index.html` |
|
|
419
|
+
| Serverless NEG | `<app>-backend-neg` | Routes Load Balancer traffic to Cloud Run |
|
|
420
|
+
| Backend service | `<app>-backend-service` | Wraps the NEG |
|
|
421
|
+
| Backend bucket | `<app>-frontend-bucket` | CDN-backed, wraps the GCS bucket |
|
|
422
|
+
| URL map | `<app>-url-map` | `/graphql` and `/auth/*` → Cloud Run; `/**` → GCS |
|
|
423
|
+
| HTTP proxy + forwarding rule | `<app>-http-proxy`, `<app>-http-rule` | Port 80; see [Step 7](#step-7--dns--https-setup-optional) to add HTTPS |
|
|
424
|
+
|
|
425
|
+
### After running
|
|
426
|
+
|
|
427
|
+
The script prints the Load Balancer IP. Note it — this is the app's public IP until you configure a custom domain.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Step 4 — Secrets Management
|
|
432
|
+
|
|
433
|
+
Run after `setup-gcp.js`. Can be re-run at any time to rotate credentials.
|
|
434
|
+
|
|
435
|
+
Run from the **app root**:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
node ops/scripts/setup-secrets.js
|
|
439
|
+
|
|
440
|
+
# Override project:
|
|
441
|
+
PROJECT_ID=my-project node ops/scripts/setup-secrets.js
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The script prompts for:
|
|
445
|
+
|
|
446
|
+
| Secret name | What to enter |
|
|
447
|
+
|---|---|
|
|
448
|
+
| `<app>-db-password` | The password you set on the Cloud SQL `postgres` user during `setup-gcp.js` |
|
|
449
|
+
| `<app>-jwt-secret` | Any long random string — press Enter to auto-generate |
|
|
450
|
+
|
|
451
|
+
Both secrets are created in Secret Manager and the backend service account is granted access to each.
|
|
452
|
+
|
|
453
|
+
### Rotating secrets later
|
|
454
|
+
|
|
455
|
+
Re-run the script — it adds a new secret version automatically. Cloud Run picks up the new version on the next cold start. To force immediate pickup:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
gcloud run services update <app>-backend \
|
|
459
|
+
--region=REGION \
|
|
460
|
+
--no-traffic \
|
|
461
|
+
--project=PROJECT_ID
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Step 5 — Cloud Build Trigger (Automated Deploys)
|
|
467
|
+
|
|
468
|
+
### Connect the GitHub repository (one-time)
|
|
469
|
+
|
|
470
|
+
Before creating the trigger, connect your repo to Cloud Build:
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
https://console.cloud.google.com/cloud-build/triggers/connect
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Or via CLI:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
gcloud builds connections create github <CONNECTION_NAME> --region=$REGION
|
|
480
|
+
gcloud builds repositories create <REPO_NAME> \
|
|
481
|
+
--remote-uri=https://github.com/<ORG>/<REPO>.git \
|
|
482
|
+
--connection=<CONNECTION_NAME> \
|
|
483
|
+
--region=$REGION
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Create the trigger
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
PROJECT_ID="your-project-id"
|
|
490
|
+
REGION="us-central1"
|
|
491
|
+
GITHUB_ORG="your-github-org"
|
|
492
|
+
APP="{{APP_NAME}}"
|
|
493
|
+
DB_NAME="{{APP_NAME}}"
|
|
494
|
+
|
|
495
|
+
gcloud builds triggers create github \
|
|
496
|
+
--project="$PROJECT_ID" \
|
|
497
|
+
--name="${APP}-deploy-develop" \
|
|
498
|
+
--repo-name="deployment-support" \
|
|
499
|
+
--repo-owner="$GITHUB_ORG" \
|
|
500
|
+
--branch-pattern="^develop$" \
|
|
501
|
+
--build-config="ops/cloudbuild.yaml" \
|
|
502
|
+
--substitutions="\
|
|
503
|
+
_PROJECT_ID=$PROJECT_ID,\
|
|
504
|
+
_REGION=$REGION,\
|
|
505
|
+
_AR_REPO=slingr-apps,\
|
|
506
|
+
_BACKEND_SERVICE=${APP}-backend,\
|
|
507
|
+
_BACKEND_SA=${APP}-backend@${PROJECT_ID}.iam.gserviceaccount.com,\
|
|
508
|
+
_CLOUDSQL_INSTANCE=${PROJECT_ID}:${REGION}:${APP}-db,\
|
|
509
|
+
_DB_SECRET_NAME=${APP}-db-password,\
|
|
510
|
+
_JWT_SECRET_NAME=${APP}-jwt-secret"
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### cloudbuild.yaml structure
|
|
514
|
+
|
|
515
|
+
The `cloudbuild.yaml` uses substitution variables so the same file works for any project. The pipeline:
|
|
516
|
+
|
|
517
|
+
1. **pull-backend-cache** — pulls `:latest` from Artifact Registry (cache for subsequent builds)
|
|
518
|
+
2. **build-backend** — builds the Docker image from the app root with `--cache-from`
|
|
519
|
+
3. **push-backend** — pushes `:COMMIT_SHA` and `:latest` tags
|
|
520
|
+
4. **schema-sync** — creates/updates and executes the `<app>-migrate` Cloud Run Job (`DB_SYNCHRONIZE=true`)
|
|
521
|
+
5. **deploy-backend** — deploys the Cloud Run service (`DB_SYNCHRONIZE=false`); runs after schema-sync
|
|
522
|
+
6. **build-frontend** — installs app dependencies, runs `slingr sync-metadata`, builds the SPA (runs in parallel with schema-sync after push-backend)
|
|
523
|
+
7. **deploy-frontend** — syncs `frontend/dist/` to GCS bucket
|
|
524
|
+
|
|
525
|
+
> **Steps 4 and 6 run in parallel** (`waitFor: [push-backend]`), reducing total pipeline time.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Step 6 — Manual Deployment
|
|
530
|
+
|
|
531
|
+
Run from the **app root**:
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
# Deploy HEAD
|
|
535
|
+
node ops/scripts/deploy.js
|
|
536
|
+
|
|
537
|
+
# Deploy a specific tag
|
|
538
|
+
node ops/scripts/deploy.js v1.2.3
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The script:
|
|
542
|
+
|
|
543
|
+
| Step | What happens |
|
|
544
|
+
|---|---|
|
|
545
|
+
| 0 | (Monorepo only) Builds framework + CLI from source; generates metadata (`slingr sync-metadata`); compiles backend + frontend |
|
|
546
|
+
| 1 | `docker build` with `--cache-from :latest` |
|
|
547
|
+
| 2 | `docker push` (`:SHA` and `:latest`) |
|
|
548
|
+
| 3 | Creates/updates and executes the `<app>-migrate` Cloud Run Job |
|
|
549
|
+
| 4 | Seeds default admin user via Cloud SQL Proxy (skipped if user already exists) |
|
|
550
|
+
| 5 | `gcloud run deploy <app>-backend` |
|
|
551
|
+
| 6 | `gsutil -m rsync -r -d frontend/dist gs://PROJECT_ID-<app>-frontend` |
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Step 7 — DNS & HTTPS Setup (Optional)
|
|
556
|
+
|
|
557
|
+
> **You need to own a domain before running this step.** Google does not provide a domain — you purchase one from a registrar (Namecheap, GoDaddy, Google/Squarespace Domains, etc.), typically for $10–20/year. Without a domain, the app continues to work over HTTP on the Load Balancer IP. HTTPS with a custom domain is an optional production step.
|
|
558
|
+
|
|
559
|
+
> **This step can be done any time after `setup-gcp.js`** — even weeks after the first deployment. The Load Balancer created in Step 3 is the only prerequisite.
|
|
560
|
+
|
|
561
|
+
By default, `setup-gcp.js` creates an HTTP (port 80) Load Balancer with an ephemeral IP. Run `setup-domain.js` to add a static IP, SSL certificate, and HTTPS:
|
|
562
|
+
|
|
563
|
+
Run from the **app root**:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
DOMAIN=app.yourdomain.com node ops/scripts/setup-domain.js
|
|
567
|
+
|
|
568
|
+
# Override project or region:
|
|
569
|
+
PROJECT_ID=my-project REGION=us-central1 DOMAIN=app.yourdomain.com \
|
|
570
|
+
node ops/scripts/setup-domain.js
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### What setup-domain.js does
|
|
574
|
+
|
|
575
|
+
The script is idempotent — safe to re-run:
|
|
576
|
+
|
|
577
|
+
1. **Reserves a static global IP** (`<app>-ip`) — replaces the ephemeral IP on the forwarding rule
|
|
578
|
+
2. **Creates a Google-managed SSL certificate** (`<app>-cert`) for your domain — Google provisions and auto-renews it at no extra cost
|
|
579
|
+
3. **Creates an HTTPS target proxy** (`<app>-https-proxy`) pointing at the existing URL map
|
|
580
|
+
4. **Creates an HTTPS forwarding rule** (`<app>-https-rule`) on port 443 using the static IP
|
|
581
|
+
5. **Prints the static IP** and the DNS record you need to configure
|
|
582
|
+
|
|
583
|
+
### After running setup-domain.js
|
|
584
|
+
|
|
585
|
+
1. **Configure DNS at your domain registrar:**
|
|
586
|
+
Add an `A` record pointing your domain to the static IP printed by the script.
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
Type: A
|
|
590
|
+
Name: app (or @ for root domain)
|
|
591
|
+
Value: <static IP from script output>
|
|
592
|
+
TTL: 300
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
2. **Wait for certificate provisioning** — Google-managed certificates are provisioned automatically after DNS propagates. This takes **10–60 minutes**.
|
|
596
|
+
|
|
597
|
+
3. **Verify the certificate is active:**
|
|
598
|
+
```bash
|
|
599
|
+
gcloud compute ssl-certificates describe <app>-cert --global \
|
|
600
|
+
--format='value(managed.status, managed.domainStatus)'
|
|
601
|
+
```
|
|
602
|
+
Status should be `ACTIVE`. If it shows `PROVISIONING`, wait and check again.
|
|
603
|
+
|
|
604
|
+
4. **Test your HTTPS endpoint:**
|
|
605
|
+
```bash
|
|
606
|
+
curl -I https://app.yourdomain.com/graphql
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Manual equivalent (no script)
|
|
610
|
+
|
|
611
|
+
If you prefer running the steps manually:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
# 1. Reserve a static IP
|
|
615
|
+
gcloud compute addresses create <app>-ip --global
|
|
616
|
+
|
|
617
|
+
# 2. Get the static IP
|
|
618
|
+
gcloud compute addresses describe <app>-ip --global --format='value(address)'
|
|
619
|
+
|
|
620
|
+
# 3. Create a Google-managed SSL certificate
|
|
621
|
+
gcloud compute ssl-certificates create <app>-cert \
|
|
622
|
+
--domains=app.yourdomain.com \
|
|
623
|
+
--global
|
|
624
|
+
|
|
625
|
+
# 4. Create HTTPS target proxy
|
|
626
|
+
gcloud compute target-https-proxies create <app>-https-proxy \
|
|
627
|
+
--url-map=<app>-url-map \
|
|
628
|
+
--ssl-certificates=<app>-cert \
|
|
629
|
+
--global
|
|
630
|
+
|
|
631
|
+
# 5. Create HTTPS forwarding rule
|
|
632
|
+
gcloud compute forwarding-rules create <app>-https-rule \
|
|
633
|
+
--load-balancing-scheme=EXTERNAL_MANAGED \
|
|
634
|
+
--global \
|
|
635
|
+
--target-https-proxy=<app>-https-proxy \
|
|
636
|
+
--ports=443 \
|
|
637
|
+
--address=<app>-ip
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Networking
|
|
643
|
+
|
|
644
|
+
The networking layer is fully provisioned by `setup-gcp.js`. No manual network configuration is required after running that script.
|
|
645
|
+
|
|
646
|
+
### What was set up
|
|
647
|
+
|
|
648
|
+
```
|
|
649
|
+
Internet → Cloud HTTP(S) Load Balancer (global, external)
|
|
650
|
+
│
|
|
651
|
+
├── URL map: /graphql, /auth/* → Backend Service
|
|
652
|
+
│ └── Serverless NEG → Cloud Run service
|
|
653
|
+
│
|
|
654
|
+
└── URL map: /** (default) → Backend Bucket
|
|
655
|
+
└── GCS Bucket (CDN-backed)
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
| Component | Name | Purpose |
|
|
659
|
+
|---|---|---|
|
|
660
|
+
| Forwarding rule | `<app>-http-rule` (port 80) | Accepts incoming HTTP traffic |
|
|
661
|
+
| HTTP target proxy | `<app>-http-proxy` | Routes requests to the URL map |
|
|
662
|
+
| URL map | `<app>-url-map` | Path-based routing rules |
|
|
663
|
+
| Serverless NEG | `<app>-backend-neg` | Connects the LB to the Cloud Run service |
|
|
664
|
+
| Backend service | `<app>-backend-service` | Wraps the NEG for the URL map |
|
|
665
|
+
| Backend bucket | `<app>-frontend-bucket` | Wraps the GCS bucket with CDN |
|
|
666
|
+
| GCS bucket | `PROJECT_ID-<app>-frontend` | Serves the React SPA static files |
|
|
667
|
+
|
|
668
|
+
### Cloud SQL connectivity
|
|
669
|
+
|
|
670
|
+
Cloud Run connects to Cloud SQL through the **Cloud SQL Auth Proxy** — a built-in sidecar that Google manages automatically. No VPC Connector, no firewall rules, and no open database ports are needed. The connection is:
|
|
671
|
+
|
|
672
|
+
- **Authenticated** via the backend service account IAM role (`roles/cloudsql.client`)
|
|
673
|
+
- **Encrypted** in transit automatically
|
|
674
|
+
- **Socket-based** inside the container: `DB_HOST=/cloudsql/PROJECT:REGION:INSTANCE`
|
|
675
|
+
|
|
676
|
+
### Verifying the networking setup
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
# Check the Load Balancer forwarding rules
|
|
680
|
+
gcloud compute forwarding-rules list --global
|
|
681
|
+
|
|
682
|
+
# Inspect the URL map routing rules
|
|
683
|
+
gcloud compute url-maps describe <app>-url-map --global
|
|
684
|
+
|
|
685
|
+
# Get the Load Balancer's public IP
|
|
686
|
+
gcloud compute forwarding-rules describe <app>-http-rule \
|
|
687
|
+
--global --format='value(IPAddress)'
|
|
688
|
+
|
|
689
|
+
# Check the serverless NEG
|
|
690
|
+
gcloud compute network-endpoint-groups describe <app>-backend-neg \
|
|
691
|
+
--region=REGION
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Frontend Hosting
|
|
697
|
+
|
|
698
|
+
The React SPA is built with UmiJS (`npm run build` → `frontend/dist/`) and served from a GCS bucket behind the Cloud Load Balancer.
|
|
699
|
+
|
|
700
|
+
### URL routing
|
|
701
|
+
|
|
702
|
+
| Path pattern | Destination |
|
|
703
|
+
|---|---|
|
|
704
|
+
| `/graphql` | Cloud Run backend (via serverless NEG) |
|
|
705
|
+
| `/auth/*` | Cloud Run backend (via serverless NEG) |
|
|
706
|
+
| `/**` (default) | GCS bucket CDN |
|
|
707
|
+
|
|
708
|
+
API calls use relative paths (`/graphql`, `/auth/...`). The LB routes them to the backend — no CORS issues, no hardcoded URLs.
|
|
709
|
+
|
|
710
|
+
### Manual frontend deploy
|
|
711
|
+
|
|
712
|
+
Run from the **app root**:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
cd frontend
|
|
716
|
+
npm ci && npm run build
|
|
717
|
+
|
|
718
|
+
gsutil -m rsync -r -d dist gs://YOUR_PROJECT_ID-<app>-frontend
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### CDN cache invalidation
|
|
722
|
+
|
|
723
|
+
After deploying new frontend files, force immediate CDN propagation:
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
gcloud compute url-maps invalidate-cdn-cache <app>-url-map \
|
|
727
|
+
--path="/*" --global
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## Database Schema Sync
|
|
733
|
+
|
|
734
|
+
TypeORM's `synchronize: true` is **disabled in production** to prevent accidental table drops. Schema updates are handled by a dedicated Cloud Run Job before each deployment:
|
|
735
|
+
|
|
736
|
+
```
|
|
737
|
+
Cloud Run Job: <app>-migrate
|
|
738
|
+
Image: same backend image, new tag
|
|
739
|
+
Env: DB_SYNCHRONIZE=true, NODE_ENV=migration
|
|
740
|
+
Runs once → exits after schema is in sync
|
|
741
|
+
→ Production service deploys after job exits 0
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
The job is created on first run and updated on every subsequent deploy. On the very first deployment to a fresh database, the schema-sync job creates all tables — no manual SQL needed.
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
## Default Admin User
|
|
749
|
+
|
|
750
|
+
`deploy.js` seeds a default admin user on the first deployment (idempotent — skipped if the user already exists). Configure the credentials in `deploy.js`:
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
SEED_USER_EMAIL="sys@app.com"
|
|
754
|
+
SEED_USER_PASS="123456788"
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
The seed runs locally via the Cloud SQL Auth Proxy, using the DB password fetched from Secret Manager. **Change the password immediately after first login.**
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Logging & Observability
|
|
762
|
+
|
|
763
|
+
Cloud Run services automatically ship all stdout/stderr to **Cloud Logging** — no configuration is required. Logs are available immediately after the first request.
|
|
764
|
+
|
|
765
|
+
### View backend logs
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
gcloud logging read \
|
|
769
|
+
'resource.type="cloud_run_revision" AND resource.labels.service_name="<app>-backend"' \
|
|
770
|
+
--project=PROJECT_ID \
|
|
771
|
+
--limit=50 \
|
|
772
|
+
--format='table(timestamp, severity, textPayload)'
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Stream live logs
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
gcloud beta run services logs tail <app>-backend --region=REGION
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Useful log filters
|
|
782
|
+
|
|
783
|
+
| What to find | Filter |
|
|
784
|
+
|---|---|
|
|
785
|
+
| All backend logs | `resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend"` |
|
|
786
|
+
| Errors only | `resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend" severity>=ERROR` |
|
|
787
|
+
| Schema sync job | `resource.type="cloud_run_job" resource.labels.job_name="<app>-migrate"` |
|
|
788
|
+
| DB connection errors | `resource.type="cloud_run_revision" textPayload:"connection refused" OR textPayload:"authentication failed"` |
|
|
789
|
+
| Specific request | `resource.type="cloud_run_revision" httpRequest.requestUrl:"/graphql"` |
|
|
790
|
+
|
|
791
|
+
Copy these filters into the [Cloud Console Logs Explorer](https://console.cloud.google.com/logs/query).
|
|
792
|
+
|
|
793
|
+
### Cloud Monitoring — uptime check
|
|
794
|
+
|
|
795
|
+
Create an uptime check to monitor backend availability and receive alerts if the service goes down:
|
|
796
|
+
|
|
797
|
+
```bash
|
|
798
|
+
gcloud monitoring uptime create \
|
|
799
|
+
--display-name="<app>-backend uptime" \
|
|
800
|
+
--resource-type=uptime-url \
|
|
801
|
+
--uri="http://<LB_IP>/graphql" \
|
|
802
|
+
--check-interval=300 \
|
|
803
|
+
--project=PROJECT_ID
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
> Replace `<LB_IP>` with your Load Balancer IP (or custom domain if configured). Attach a notification channel in [Cloud Console → Monitoring → Alerting](https://console.cloud.google.com/monitoring/alerting).
|
|
807
|
+
|
|
808
|
+
### Cloud Monitoring — error rate alert
|
|
809
|
+
|
|
810
|
+
Create a log-based metric and alert policy to notify when backend errors spike:
|
|
811
|
+
|
|
812
|
+
```bash
|
|
813
|
+
# Create a log-based metric for backend errors
|
|
814
|
+
gcloud logging metrics create <app>-backend-errors \
|
|
815
|
+
--description="Count of ERROR+ logs from the backend" \
|
|
816
|
+
--log-filter='resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend" severity>=ERROR' \
|
|
817
|
+
--project=PROJECT_ID
|
|
818
|
+
|
|
819
|
+
# Create an alert policy (fires if >5 errors in 5 minutes)
|
|
820
|
+
gcloud alpha monitoring policies create \
|
|
821
|
+
--policy='{
|
|
822
|
+
"displayName": "<app>-backend error spike",
|
|
823
|
+
"conditions": [{
|
|
824
|
+
"displayName": "Error log count",
|
|
825
|
+
"conditionThreshold": {
|
|
826
|
+
"filter": "metric.type=\"logging.googleapis.com/user/<app>-backend-errors\"",
|
|
827
|
+
"comparison": "COMPARISON_GT",
|
|
828
|
+
"thresholdValue": 5,
|
|
829
|
+
"duration": "300s",
|
|
830
|
+
"aggregations": [{"alignmentPeriod": "300s", "perSeriesAligner": "ALIGN_RATE"}]
|
|
831
|
+
}
|
|
832
|
+
}],
|
|
833
|
+
"alertStrategy": {"autoClose": "604800s"},
|
|
834
|
+
"combiner": "OR",
|
|
835
|
+
"enabled": true
|
|
836
|
+
}' \
|
|
837
|
+
--project=PROJECT_ID
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
> Attach a notification channel (email, Slack, PagerDuty) in [Cloud Console → Monitoring → Alerting](https://console.cloud.google.com/monitoring/alerting) after creating the policy.
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## IAM & Service Accounts Reference
|
|
845
|
+
|
|
846
|
+
### Runtime service account (`<app>-backend`)
|
|
847
|
+
|
|
848
|
+
| Role | Purpose |
|
|
849
|
+
|---|---|
|
|
850
|
+
| `roles/cloudsql.client` | Connect to Cloud SQL via Auth Proxy at runtime |
|
|
851
|
+
| `roles/secretmanager.secretAccessor` | Read DB password and JWT secret at container startup |
|
|
852
|
+
|
|
853
|
+
### Cloud Build service account (CI/CD)
|
|
854
|
+
|
|
855
|
+
| Role | Purpose |
|
|
856
|
+
|---|---|
|
|
857
|
+
| `roles/run.admin` | Deploy Cloud Run services and jobs |
|
|
858
|
+
| `roles/iam.serviceAccountUser` | Act as the backend SA when deploying Cloud Run |
|
|
859
|
+
| `roles/secretmanager.secretAccessor` | Read secrets referenced in `cloudbuild.yaml` |
|
|
860
|
+
| `roles/artifactregistry.writer` | Push Docker images to Artifact Registry |
|
|
861
|
+
| `roles/cloudsql.client` | Execute the schema-sync Cloud Run Job |
|
|
862
|
+
| `roles/storage.objectAdmin` | Sync frontend files to the GCS bucket |
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## Ongoing Operations
|
|
867
|
+
|
|
868
|
+
### Roll back to a previous revision
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
# List recent revisions
|
|
872
|
+
gcloud run revisions list --service=<app>-backend --region=REGION
|
|
873
|
+
|
|
874
|
+
# Send 100% traffic to a previous revision
|
|
875
|
+
gcloud run services update-traffic <app>-backend \
|
|
876
|
+
--to-revisions=<app>-backend-00005-xyz=100 --region=REGION
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### Scale to zero immediately
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
gcloud run services update <app>-backend \
|
|
883
|
+
--min-instances=0 --region=REGION
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Connect to Cloud SQL from your local machine
|
|
887
|
+
|
|
888
|
+
```bash
|
|
889
|
+
# Start proxy
|
|
890
|
+
cloud-sql-proxy PROJECT:REGION:<app>-db --port=5432 &
|
|
891
|
+
|
|
892
|
+
# Connect
|
|
893
|
+
psql -h 127.0.0.1 -U postgres -d <your_db_name>
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Troubleshooting
|
|
899
|
+
|
|
900
|
+
### Backend fails to connect to Cloud SQL
|
|
901
|
+
|
|
902
|
+
**Symptom:** `connect ENOENT /cloudsql/PROJECT:REGION:INSTANCE/.s.PGSQL.5433`
|
|
903
|
+
|
|
904
|
+
The socket file is always `.s.PGSQL.5432`. If the error shows `5433`, `DB_PORT` is wrong:
|
|
905
|
+
|
|
906
|
+
```bash
|
|
907
|
+
gcloud run services update <app>-backend \
|
|
908
|
+
--region=REGION \
|
|
909
|
+
--update-env-vars=DB_PORT=5432 \
|
|
910
|
+
--project=PROJECT_ID
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**Other checks:**
|
|
914
|
+
|
|
915
|
+
1. Verify `--add-cloudsql-instances` is set:
|
|
916
|
+
```bash
|
|
917
|
+
gcloud run services describe <app>-backend --region=REGION \
|
|
918
|
+
--format='value(spec.template.metadata.annotations)'
|
|
919
|
+
```
|
|
920
|
+
2. Verify the backend SA has `roles/cloudsql.client`:
|
|
921
|
+
```bash
|
|
922
|
+
gcloud projects get-iam-policy PROJECT_ID --flatten="bindings[].members" \
|
|
923
|
+
--filter="bindings.members:<app>-backend@*"
|
|
924
|
+
```
|
|
925
|
+
3. Check `DB_HOST` matches the instance connection name exactly: `PROJECT:REGION:INSTANCE`
|
|
926
|
+
|
|
927
|
+
### Cloud Run service fails to start
|
|
928
|
+
|
|
929
|
+
**Symptom:** Deployment succeeds but health check fails → rollback
|
|
930
|
+
|
|
931
|
+
1. Check startup logs: `gcloud beta run services logs tail <app>-backend --region=REGION`
|
|
932
|
+
2. Verify all secrets exist: `gcloud secrets list --project=PROJECT_ID`
|
|
933
|
+
|
|
934
|
+
**Symptom:** `password authentication failed for user "postgres"`
|
|
935
|
+
|
|
936
|
+
The DB password and the Secret Manager secret are out of sync:
|
|
937
|
+
|
|
938
|
+
```bash
|
|
939
|
+
NEW_PASS="$(openssl rand -base64 24)"
|
|
940
|
+
|
|
941
|
+
gcloud sql users set-password postgres \
|
|
942
|
+
--instance=<app>-db \
|
|
943
|
+
--password="$NEW_PASS" \
|
|
944
|
+
--project=PROJECT_ID
|
|
945
|
+
|
|
946
|
+
echo -n "$NEW_PASS" | gcloud secrets versions add <app>-db-password \
|
|
947
|
+
--data-file=- --project=PROJECT_ID
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
Then redeploy.
|
|
951
|
+
|
|
952
|
+
### Schema sync job exits non-zero
|
|
953
|
+
|
|
954
|
+
**Symptom:** `<app>-migrate` job execution fails
|
|
955
|
+
|
|
956
|
+
1. Check job logs:
|
|
957
|
+
```bash
|
|
958
|
+
gcloud logging read \
|
|
959
|
+
'resource.type="cloud_run_job" resource.labels.job_name="<app>-migrate"' \
|
|
960
|
+
--limit=20 --format='value(textPayload)'
|
|
961
|
+
```
|
|
962
|
+
2. TypeORM exits 0 even when schema is already up to date — non-zero indicates a real error
|
|
963
|
+
3. Verify `DB_HOST` socket path is correct in the job's env vars
|
|
964
|
+
|
|
965
|
+
### Docker build fails: `cannot find module '@slingr/framework-backend'`
|
|
966
|
+
|
|
967
|
+
The Docker build context must be the **app root**, not `backend/`:
|
|
968
|
+
|
|
969
|
+
```bash
|
|
970
|
+
# Run from your app root (the directory containing backend/ and frontend/)
|
|
971
|
+
docker build -f backend/Dockerfile .
|
|
972
|
+
# ^ app root
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
### Docker push fails: Unauthenticated request
|
|
976
|
+
|
|
977
|
+
**Symptom:** `Unauthenticated requests do not have permission "artifactregistry.repositories.uploadArtifacts"`
|
|
978
|
+
|
|
979
|
+
Configure `gcloud` as the Docker credential helper (runs once per machine):
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
gcloud auth configure-docker us-central1-docker.pkg.dev
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
If the error persists, ensure you are logged in with an account that has `roles/artifactregistry.writer`:
|
|
986
|
+
|
|
987
|
+
```bash
|
|
988
|
+
gcloud auth login
|
|
989
|
+
gcloud auth configure-docker us-central1-docker.pkg.dev
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
### Cloud SQL Proxy is not recognized on local machine
|
|
993
|
+
|
|
994
|
+
**Symptom:** `"cloud-sql-proxy" is not recognized as an internal or external command`
|
|
995
|
+
|
|
996
|
+
Install the component and open a new terminal:
|
|
997
|
+
|
|
998
|
+
```bash
|
|
999
|
+
gcloud components install cloud-sql-proxy
|
|
1000
|
+
cloud-sql-proxy --version
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
On Windows, restart your terminal (or VS Code) after install so `PATH` is refreshed.
|
|
1004
|
+
|
|
1005
|
+
### Frontend not updating after deploy
|
|
1006
|
+
|
|
1007
|
+
**Symptom:** Old files served despite successful `gsutil rsync`
|
|
1008
|
+
|
|
1009
|
+
Force CDN cache invalidation:
|
|
1010
|
+
|
|
1011
|
+
```bash
|
|
1012
|
+
gcloud compute url-maps invalidate-cdn-cache <app>-url-map \
|
|
1013
|
+
--path="/*" --global
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### Load Balancer returns 404 for frontend routes
|
|
1017
|
+
|
|
1018
|
+
**Symptom:** Navigating directly to `/dashboard` returns 404
|
|
1019
|
+
|
|
1020
|
+
The GCS bucket must have its 404 page set to `index.html`:
|
|
1021
|
+
|
|
1022
|
+
```bash
|
|
1023
|
+
gcloud storage buckets describe gs://PROJECT_ID-<app>-frontend \
|
|
1024
|
+
--format='value(website)'
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
If missing:
|
|
1028
|
+
|
|
1029
|
+
```bash
|
|
1030
|
+
gcloud storage buckets update gs://PROJECT_ID-<app>-frontend \
|
|
1031
|
+
--web-main-page-suffix=index.html \
|
|
1032
|
+
--web-error-page=index.html
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### SSL certificate stuck in PROVISIONING
|
|
1036
|
+
|
|
1037
|
+
**Symptom:** `gcloud compute ssl-certificates describe <app>-cert --global` shows `PROVISIONING` for more than 2 hours
|
|
1038
|
+
|
|
1039
|
+
1. Verify the DNS A record points to the correct static IP:
|
|
1040
|
+
```bash
|
|
1041
|
+
dig +short app.yourdomain.com
|
|
1042
|
+
gcloud compute addresses describe <app>-ip --global --format='value(address)'
|
|
1043
|
+
```
|
|
1044
|
+
2. Ensure the forwarding rules use the **static IP** (not the old ephemeral one). `setup-domain.js` handles this automatically.
|
|
1045
|
+
3. Certificate provisioning requires the domain to resolve to the LB IP globally. Use [dnschecker.org](https://dnschecker.org) to verify propagation across regions.
|