Intial commit for deployment script p2

This commit is contained in:
2026-05-04 23:23:02 +05:00
parent 724e6a8ecd
commit 40fe69ed65
20 changed files with 2008 additions and 2 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
bin bin
obj obj
node_modules node_modules
package-lock.json .env
+38
View File
@@ -0,0 +1,38 @@
# Environment variables for Cloud Run deployment
# Copy this file to GCR/.env and fill in your values.
# GCR/.env is gitignored — never commit real credentials.
#
# IMPORTANT: Sensitive values (MongoDB connection string, API keys, passwords)
# should NOT be stored here. Use Google Cloud Secret Manager instead.
# See GCR/README.md for the Secret Manager setup workflow.
# ═══════════════════════════════════════════════════════════════════════════
# NON-SENSITIVE CONFIG — safe to commit to version control
# ═══════════════════════════════════════════════════════════════════════════
# Your GCP project ID (lowercase letters, digits, hyphens; 630 chars)
GCP_PROJECT_ID=htmx-demo
# Region to deploy the Cloud Run service and Artifact Registry repository
# Recommended regions: us-central1, us-east1, europe-west1, asia-east1
GCP_REGION=asia-east1
# Name of the Artifact Registry Docker repository (created by 02-setup-project.sh)
GCP_REPOSITORY=htmx-demo
# Cloud Run service name
SERVICE_NAME=htmx-demo-app
# Name of the MongoDB database to use (not sensitive, just a name)
MONGODB_DATABASE_NAME=HtmxAppDb
# ═══════════════════════════════════════════════════════════════════════════
# SENSITIVE CONFIG — stored in Google Cloud Secret Manager, never here!
# ═══════════════════════════════════════════════════════════════════════════
#
# MongoDB Connection String
# Store in Secret Manager as: mongodb-connection-string
# Create with: printf '%s' "mongodb+srv://user:pass@..." | \
# gcloud secrets create mongodb-connection-string --data-file=-
#
# The deploy script will inject this automatically as ConnectionStrings__DefaultConnection
+2
View File
@@ -0,0 +1,2 @@
# Never commit real credentials
.env
+98
View File
@@ -0,0 +1,98 @@
# ─────────────────────────────────────────────────────────────────────────────
# Stage 1 — npm install (Tailwind CLI)
# The .NET SDK stage needs node_modules present before running dotnet publish
# because the MSBuild Tailwind target calls `npx @tailwindcss/cli` during build.
#
# We use the official node:24-slim image here. This means the npm that ships
# with Node 24 (npm 10.x) is used as-is — we deliberately do NOT run
# `npm install -g npm@latest` anywhere. Running a global npm self-upgrade
# inside a Debian container is a known reliability hazard: npm replaces its
# own running binaries mid-flight, which can cause EBUSY / ENOENT failures
# that corrupt the install. The bundled npm is current enough.
# ─────────────────────────────────────────────────────────────────────────────
FROM node:24-slim AS npm-install
WORKDIR /npm
COPY Htmx.ApiDemo/package.json .
# npm ci requires package-lock.json; if it doesn't exist yet, run
# `npm install` locally first to generate it, then commit it to the repo.
COPY Htmx.ApiDemo/package-lock.json* ./
# ci is preferred over install in CI/Docker contexts: respects package-lock,
# clean installs, and is faster.
RUN npm ci
# ─────────────────────────────────────────────────────────────────────────────
# Stage 2 — AOT publish
# Uses the full .NET 10 SDK image. Node/npx must also be present here so the
# Tailwind MSBuild target can run. We install Node 24 from NodeSource using
# the official setup script and then immediately install nodejs via apt — no
# subsequent `npm install -g npm` step, for the same reason as above.
# ─────────────────────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS publish
# Install Node.js 24 (required by the Tailwind MSBuild target at publish time).
# We download the NodeSource setup script to a file first so we can inspect it
# if needed, then run it. Using `| bash -` directly is convenient but hides
# the script from audit — the two-step form is safer in CI/CD contexts.
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/nodesource_setup.sh && \
bash /tmp/nodesource_setup.sh && \
apt-get install -y --no-install-recommends nodejs && \
rm /tmp/nodesource_setup.sh && \
rm -rf /var/lib/apt/lists/*
# Intentionally no `npm install -g npm` — see Stage 1 note above.
WORKDIR /src
# Copy solution and project files first so NuGet restore is cached separately
COPY Htmx.slnx .
COPY Htmx.ApiDemo/Htmx.ApiDemo.csproj Htmx.ApiDemo/
COPY Htmx.SourceGenerator/Htmx.SourceGenerator.csproj Htmx.SourceGenerator/
RUN dotnet restore Htmx.ApiDemo/Htmx.ApiDemo.csproj
# Bring in the pre-installed node_modules from Stage 1.
# These were installed with `npm ci` on Node 24 — no npm upgrade was performed.
COPY --from=npm-install /npm/node_modules Htmx.ApiDemo/node_modules
# Copy the rest of the source
COPY . .
# AOT publish — output goes to /publish
RUN dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj \
-c Release \
--no-restore \
-o /publish
# ─────────────────────────────────────────────────────────────────────────────
# Stage 3 — Runtime image
# runtime-deps provides the native library dependencies (libc, libssl, libicu)
# that the AOT binary links against at runtime — no .NET runtime needed.
# ─────────────────────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0 AS runtime
# Run as non-root for security hardening (recommended by Cloud Run docs)
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
WORKDIR /app
COPY --from=publish /publish .
# Ensure the binary is executable
RUN chmod +x ./Htmx.ApiDemo
# Cloud Run injects PORT (default 8080).
# ASP.NET Core reads ASPNETCORE_HTTP_PORTS, not PORT directly, so we set it.
# The entrypoint script below maps $PORT → ASPNETCORE_HTTP_PORTS at startup.
COPY GCR/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Transfer ownership so the app can write temp files if needed
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
+384
View File
@@ -0,0 +1,384 @@
# Deploying to Google Cloud Run
This folder contains everything needed to deploy the Htmx app to Google Cloud Run — completely isolated from the application code.
## Folder structure
```
GCR/
├── .env.example ← copy to .env and fill in your values
├── Dockerfile ← multi-stage AOT build (Linux/amd64)
├── entrypoint.sh ← maps Cloud Run's PORT var to ASP.NET Core
├── docker-compose.yml ← Cloud Run service definition (used by gcloud)
├── run-all.sh ← smart Linux runner (checks + prompts)
├── run-all.ps1 ← smart Windows runner (checks + prompts)
└── scripts/
├── 00-install-gcloud.sh / .ps1 ← install Google Cloud CLI
├── 01-login.sh / .ps1 ← authenticate + configure Docker
├── 02-setup-project.sh / .ps1 ← one-time GCP project setup
├── 03-create-secrets.sh / .ps1 ← manage MongoDB secret
└── 04-deploy.sh / .ps1 ← build, push, and deploy
```
`.sh` scripts are for **Linux**. `.ps1` scripts are for **Windows** (PowerShell 5.1+).
### One-command flow (recommended)
Instead of running each step manually, use the root runner. It checks each step,
shows completed items, and prompts to run only missing steps.
**Linux:**
```bash
bash GCR/run-all.sh
# non-interactive (auto-run missing steps):
bash GCR/run-all.sh --yes
```
**Windows:**
```powershell
.\GCR\run-all.ps1
# non-interactive (auto-run missing steps):
.\GCR\run-all.ps1 -Yes
```
## Security on Untrusted Machines
Do **not** run these scripts on machines you don't control unless absolutely necessary.
Why this matters:
1. The scripts grant project-level IAM roles to your user, including `roles/secretmanager.admin`.
2. `gcloud auth login` stores local credentials/tokens that can be reused if the machine is compromised.
3. Docker auth is configured for Artifact Registry and may persist in local Docker config.
4. A local `GCR/.env` file contains project identifiers and deployment metadata.
Minimum cleanup if you ever used a shared/untrusted machine:
1. Revoke IAM roles from your user account in the GCP project.
2. Revoke local gcloud credentials and clear config.
3. Remove Docker credential entries for Artifact Registry.
4. Delete local `GCR/.env` and any temporary files.
Example role cleanup (Linux/macOS shell):
```bash
USER_EMAIL="your-user@company.com"
PROJECT_ID="your-project-id"
for ROLE in \
roles/run.developer \
roles/artifactregistry.writer \
roles/iam.serviceAccountUser \
roles/secretmanager.admin \
roles/secretmanager.secretAccessor \
roles/secretmanager.secretVersionAdder; do
gcloud projects remove-iam-policy-binding "$PROJECT_ID" \
--member="user:$USER_EMAIL" \
--role="$ROLE" \
--quiet
done
```
Credential cleanup:
```bash
gcloud auth revoke --all
gcloud config configurations delete default --quiet || true
```
Credential cleanup (Windows PowerShell):
```powershell
gcloud auth revoke --all
gcloud config configurations delete default --quiet
```
Prefer a dedicated personal/admin workstation, or use a tightly scoped CI service account instead of broad user credentials.
---
## Step 0 — Configure your .env file
Copy the example and fill it in:
```bash
# Linux
cp GCR/.env.example GCR/.env
```
```powershell
# Windows
Copy-Item GCR\.env.example GCR\.env
```
Open `GCR/.env` in any editor and set:
| Variable | Description | Example |
|---|---|---|
| `GCP_PROJECT_ID` | Your GCP project ID | `my-htmx-project` |
| `GCP_REGION` | Cloud Run region | `us-central1` |
| `GCP_REPOSITORY` | Artifact Registry repo name | `htmx` |
| `SERVICE_NAME` | Cloud Run service name | `htmx-app` |
| `MONGODB_DATABASE_NAME` | Database name | `HtmxAppDb` |
> **Security note:** `GCR/.env` is gitignored. Never commit it.
> **MongoDB note:** The app connects to MongoDB at startup. Cloud Run containers do not have access to `localhost:27017` — use MongoDB Atlas (cloud-hosted) or a MongoDB instance reachable over the internet/VPC.
> **Secrets note:** The MongoDB connection string is **not stored in .env**. It's stored securely in Google Cloud Secret Manager. See **Step 4** below.
---
## Step 1 — Install the Google Cloud CLI
Run **once** on a new machine.
**Linux:**
```bash
bash GCR/scripts/00-install-gcloud.sh
```
**Windows** (PowerShell, run as Administrator):
```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # first time only
.\GCR\scripts\00-install-gcloud.ps1
```
After installation, open a new terminal and verify:
```
gcloud version
```
---
## Step 2 — Log in
Authenticates your machine to GCP and configures Docker to push to Artifact Registry.
**Linux:**
```bash
bash GCR/scripts/01-login.sh
```
**Windows:**
```powershell
.\GCR\scripts\01-login.ps1
```
A browser window opens for Google sign-in. Use the account that owns or has access to your GCP project.
---
## Step 3 — Set up the GCP project (one time)
Enables APIs, creates the Artifact Registry repository, and grants your account the required IAM roles. If billing is not yet linked, the script prompts you to choose a billing account.
**Linux:**
```bash
bash GCR/scripts/02-setup-project.sh
```
**Windows:**
```powershell
.\GCR\scripts\02-setup-project.ps1
```
This is **safe to re-run** — all operations are idempotent.
### What it enables
| API | Purpose |
|---|---|
| `run.googleapis.com` | Cloud Run service |
| `artifactregistry.googleapis.com` | Docker image storage |
| `secretmanager.googleapis.com` | Available for future use |
| `cloudresourcemanager.googleapis.com` | IAM and project management |
### What IAM roles it grants (to your account)
| Role | Purpose |
|---|---|
| `roles/run.developer` | Deploy and manage Cloud Run services |
| `roles/artifactregistry.writer` | Push container images |
| `roles/iam.serviceAccountUser` | Run the service under a service account |
| `roles/secretmanager.admin` | Create/manage secrets and IAM policies (includes `secretmanager.secrets.create`) |
| `roles/secretmanager.secretAccessor` | Read secret payloads (for validation/access workflows) |
| `roles/secretmanager.secretVersionAdder` | Add/set new secret versions (rotate values safely) |
---
## Step 4 — Create secrets in Google Cloud Secret Manager
Store the MongoDB connection string securely:
**Linux:**
```bash
bash GCR/scripts/03-create-secrets.sh
```
**Windows:**
```powershell
.\GCR\scripts\03-create-secrets.ps1
```
The script prompts for your MongoDB connection string, creates the secret in Secret Manager, and grants Cloud Run permission to access it. The secret is referenced by name (`mongodb-connection-string`) in the deploy script — never stored in .env.
This is a **one-time setup**. Re-run only if you need to **update** the MongoDB connection string.
---
## Step 5 — Deploy
Builds the Docker image, pushes it to Artifact Registry, and deploys to Cloud Run.
If secrets are missing, the deploy script now performs a pre-check and prompts to run
the secrets setup script before continuing.
**Linux:**
```bash
bash GCR/scripts/04-deploy.sh
# or with a custom image tag:
bash GCR/scripts/04-deploy.sh v1.0.0
```
**Windows:**
```powershell
.\GCR\scripts\04-deploy.ps1
# or with a custom image tag:
.\GCR\scripts\04-deploy.ps1 -Tag v1.0.0
```
The script:
1. Checks for (or generates) `Htmx.ApiDemo/package-lock.json`
2. Builds the Docker image from the repo root using `GCR/Dockerfile`
3. Pushes the image to Artifact Registry
4. Deploys to Cloud Run using `GCR/docker-compose.yml`
5. Opens the service to public access (no authentication required)
6. Prints the live service URL
By default the image tag is the short git commit SHA (e.g. `a3f4b7c`). A timestamp is used if the directory is not a git repo.
---
## How configuration reaches the app
The app reads configuration from environment variables. Cloud Run injects them at container startup — no config files needed in the image.
| Environment variable | Maps to | Set by |
|---|---|---|
| `ConnectionStrings__DefaultConnection` | `appsettings.json``ConnectionStrings.DefaultConnection` | Secret Manager (via `--set-secrets`) → deploy script |
| `MongoDbName` | `appsettings.json``MongoDbName` | `GCR/.env` → deploy script → docker-compose.yml |
| `ASPNETCORE_ENVIRONMENT` | ASP.NET Core environment | `docker-compose.yml` (hardcoded `Production`) |
| `PORT` | Listening port | Injected by Cloud Run (default `8080`) |
**Secret Manager workflow:**
- Step 4 stores the MongoDB connection string in Cloud Run Secret Manager
- Step 5 (deploy script) injects it via `gcloud run services update --set-secrets=...`
- The container never sees the raw connection string; Cloud Run mounts it as an env var at runtime
- Each time you update the secret, Cloud Run automatically uses the latest version
The `GCR/entrypoint.sh` script translates Cloud Run's `PORT` variable into `ASPNETCORE_HTTP_PORTS` at container startup, since ASP.NET Core does not read `PORT` directly.
---
## Re-deploying after code changes
Just run Step 5 again. Each deployment gets a new image tag (git SHA), and Cloud Run creates a new immutable revision. Traffic is shifted to the new revision automatically.
---
## Updating configuration
### Non-sensitive config (MONGODB_DATABASE_NAME, etc.)
To change a non-sensitive value without rebuilding:
**Linux:**
```bash
source GCR/.env
gcloud run services update $SERVICE_NAME \
--region=$GCP_REGION \
--update-env-vars "MongoDbName=NewDatabaseName"
```
**Windows:**
```powershell
gcloud run services update htmx-app `
--region=us-central1 `
--update-env-vars "MongoDbName=NewDatabaseName"
```
### Sensitive config (MongoDB connection string)
To update the MongoDB connection string:
**Linux:**
```bash
source GCR/.env
# Read from stdin (paste the connection string and press Ctrl+D):
gcloud secrets versions add mongodb-connection-string \
--data-file=- \
--project=$GCP_PROJECT_ID
```
**Windows:**
```powershell
# Use a temp file to avoid adding trailing newlines to the secret
$connectionString = Read-Host -AsSecureString "Enter MongoDB connection string"
$connectionStringPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($connectionString))
$TempFile = [System.IO.Path]::GetTempFileName()
try {
[System.IO.File]::WriteAllText($TempFile, $connectionStringPlainText, [System.Text.Encoding]::UTF8)
gcloud secrets versions add mongodb-connection-string --data-file=$TempFile --project=your-project-id
} finally {
Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
}
```
Or re-run the creation script:
**Linux:**
```bash
bash GCR/scripts/03-create-secrets.sh
```
**Windows:**
```powershell
.\GCR\scripts\03-create-secrets.ps1
```
Cloud Run automatically uses the latest secret version on the next container start.
---
## Troubleshooting
### Docker build fails on `npm ci`
`npm ci` requires `Htmx.ApiDemo/package-lock.json` to exist. Generate it locally:
```bash
cd Htmx.ApiDemo && npm install
```
Then commit `package-lock.json` to the repository.
### `gcloud: command not found` after install
Close and reopen your terminal. The installer adds `gcloud` to `PATH`, but the current shell session won't see it until restarted.
### `PERMISSION_DENIED` errors during deploy
Run `02-setup-project` again — it grants the required IAM roles. It is safe to re-run.
### Cloud Run container crashes on startup
View logs in the GCP console (Cloud Run → your service → Logs tab), or:
```bash
gcloud run services logs read $SERVICE_NAME --region=$GCP_REGION --limit=50
```
The most common causes:
- MongoDB connection string is wrong or unreachable from Cloud Run
- `ASPNETCORE_ENVIRONMENT` is `Production` but `appsettings.Production.json` overrides something unexpectedly
### Service URL returns 404 for all routes
The service is running but no route matched. Confirm the app started correctly by checking logs for `Now listening on`.
+30
View File
@@ -0,0 +1,30 @@
# docker-compose.yml — Cloud Run Compose deployment
#
# Usage:
# gcloud run services replace docker-compose.yml --region=REGION
#
# Or via the deploy script:
# ./GCR/scripts/03-deploy.sh
#
# Environment variables are substituted from your shell or a .env file.
# Copy GCR/.env.example to GCR/.env and fill in your values before deploying.
#
# Cloud Run Compose reference:
# https://docs.cloud.google.com/run/docs/deploy-run-compose
services:
app:
# IMAGE_URI is set by the deploy script after pushing to Artifact Registry.
# Format: REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/SERVICE_NAME:TAG
image: ${IMAGE_URI}
environment:
# Non-sensitive config only — sensitive values (like MongoDB connection string)
# are injected via Secret Manager (--set-secrets) by the deploy script.
MongoDbName: ${MONGODB_DATABASE_NAME:-HtmxAppDb}
ASPNETCORE_ENVIRONMENT: Production
# Cloud Run only honours the first port entry; the container must listen on
# the port Cloud Run advertises via the PORT env var (default 8080).
ports:
- target: 8080
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# Translate Cloud Run's injected PORT env var into what ASP.NET Core reads.
# Cloud Run sets PORT (default 8080). ASP.NET Core reads ASPNETCORE_HTTP_PORTS.
export ASPNETCORE_HTTP_PORTS="${PORT:-8080}"
exec ./Htmx.ApiDemo "$@"
+185
View File
@@ -0,0 +1,185 @@
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
param(
[switch]$Yes
)
$RootDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$EnvFile = Join-Path $RootDir ".env"
function Confirm-Run {
param(
[string]$Label,
[string]$ScriptPath
)
if ($Yes) {
Write-Host "[x] $Label not done yet. Running $ScriptPath (-Yes enabled)..."
& $ScriptPath
return
}
$answer = Read-Host "[x] $Label not done yet. Run now? [y/N]"
if ($answer -match '^[Yy]$') {
& $ScriptPath
}
}
function Get-EnvConfig {
if (-not (Test-Path $EnvFile)) {
throw "GCR/.env not found. Copy GCR/.env.example to GCR/.env first."
}
$cfg = @{}
foreach ($line in Get-Content $EnvFile) {
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
if ($line -match '^([^=]+)=(.*)$') {
$cfg[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
foreach ($key in @('GCP_PROJECT_ID', 'GCP_REGION', 'GCP_REPOSITORY', 'SERVICE_NAME')) {
if (-not $cfg[$key]) {
throw "$key is not set in GCR/.env"
}
}
return $cfg
}
function Test-GcloudInstalled {
return [bool](Get-Command gcloud -ErrorAction SilentlyContinue)
}
function Test-Login {
param([hashtable]$Cfg)
$activeAccount = (gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>$null | Select-Object -First 1)
$currentProject = (gcloud config get-value project 2>$null)
$currentRegion = (gcloud config get-value run/region 2>$null)
$dockerCfg = if ($env:DOCKER_CONFIG) { Join-Path $env:DOCKER_CONFIG "config.json" } else { Join-Path $HOME ".docker\config.json" }
$dockerOk = $false
if (Test-Path $dockerCfg) {
$dockerOk = (Select-String -Path $dockerCfg -Pattern "\"$($Cfg['GCP_REGION'])-docker.pkg.dev\"" -SimpleMatch -Quiet)
}
return (-not [string]::IsNullOrWhiteSpace($activeAccount)) -and
($currentProject.Trim() -eq $Cfg['GCP_PROJECT_ID']) -and
($currentRegion.Trim() -eq $Cfg['GCP_REGION']) -and
$dockerOk
}
function Test-ProjectSetup {
param([hashtable]$Cfg)
$billingEnabled = (gcloud billing projects describe $Cfg['GCP_PROJECT_ID'] --format="value(billingEnabled)" 2>$null)
if ($billingEnabled -ne 'True') { return $false }
try {
gcloud artifacts repositories describe $Cfg['GCP_REPOSITORY'] --location=$Cfg['GCP_REGION'] --project=$Cfg['GCP_PROJECT_ID'] 2>$null | Out-Null
} catch {
return $false
}
foreach ($api in @('run.googleapis.com', 'artifactregistry.googleapis.com', 'secretmanager.googleapis.com', 'cloudresourcemanager.googleapis.com')) {
$enabled = gcloud services list --enabled --project=$Cfg['GCP_PROJECT_ID'] --format="value(config.name)" 2>$null | Select-String -Pattern "^$([regex]::Escape($api))$"
if (-not $enabled) { return $false }
}
return $true
}
function Test-SecretsSetup {
param([hashtable]$Cfg)
try {
gcloud secrets describe mongodb-connection-string --project=$Cfg['GCP_PROJECT_ID'] 2>$null | Out-Null
} catch {
return $false
}
$serviceAccount = "serviceAccount:$($Cfg['GCP_PROJECT_ID'])@appspot.gserviceaccount.com"
$binding = gcloud secrets get-iam-policy mongodb-connection-string `
--project=$Cfg['GCP_PROJECT_ID'] `
--flatten="bindings[].members" `
--filter="bindings.role=roles/secretmanager.secretAccessor AND bindings.members=$serviceAccount" `
--format="value(bindings.members)" 2>$null
return ($binding -match [regex]::Escape($serviceAccount))
}
function Test-DeployDone {
param([hashtable]$Cfg)
try {
gcloud run services describe $Cfg['SERVICE_NAME'] --region=$Cfg['GCP_REGION'] --project=$Cfg['GCP_PROJECT_ID'] 2>$null | Out-Null
return $true
} catch {
return $false
}
}
function Write-Done {
param([string]$Text)
Write-Host "[v] $Text"
}
Write-Host "================================================================"
Write-Host " Htmx deployment flow runner (Windows)"
Write-Host "================================================================"
if (-not (Test-Path $EnvFile)) {
Write-Host "[x] Step 0: GCR/.env is missing"
Write-Host " Copy GCR/.env.example to GCR/.env and fill required values."
exit 1
}
Write-Done "Step 0: .env exists"
$cfg = Get-EnvConfig
if (Test-GcloudInstalled) {
Write-Done "Step 1: gcloud installed"
} else {
Confirm-Run "Step 1: gcloud install" (Join-Path $RootDir "scripts\00-install-gcloud.ps1")
}
if (Test-Login $cfg) {
Write-Done "Step 2: login + docker auth configured"
} else {
Confirm-Run "Step 2: login" (Join-Path $RootDir "scripts\01-login.ps1")
}
if (Test-ProjectSetup $cfg) {
Write-Done "Step 3: project setup complete"
} else {
Confirm-Run "Step 3: project setup" (Join-Path $RootDir "scripts\02-setup-project.ps1")
}
if (Test-SecretsSetup $cfg) {
Write-Done "Step 4: secrets created and access granted"
} else {
Confirm-Run "Step 4: secrets setup" (Join-Path $RootDir "scripts\03-create-secrets.ps1")
}
if (Test-DeployDone $cfg) {
Write-Done "Step 5: service is already deployed"
} else {
Confirm-Run "Step 5: deploy" (Join-Path $RootDir "scripts\04-deploy.ps1")
}
Write-Host ""
Write-Host "================================================================"
Write-Host " Final verification"
Write-Host "================================================================"
if (Test-GcloudInstalled) { Write-Done "Step 1" } else { Write-Host "[x] Step 1" }
if (Test-Login $cfg) { Write-Done "Step 2" } else { Write-Host "[x] Step 2" }
if (Test-ProjectSetup $cfg) { Write-Done "Step 3" } else { Write-Host "[x] Step 3" }
if (Test-SecretsSetup $cfg) { Write-Done "Step 4" } else { Write-Host "[x] Step 4" }
if (Test-DeployDone $cfg) { Write-Done "Step 5" } else { Write-Host "[x] Step 5" }
Write-Host ""
Write-Host "Tip: run .\GCR\run-all.ps1 -Yes to auto-run missing steps without prompts."
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$(uname -s)" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/run-all.ps1"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
AUTO_YES="${1:-}"
confirm_run() {
local label="$1"
local script_path="$2"
if [[ "$AUTO_YES" == "--yes" ]]; then
echo "[x] $label not done yet. Running $script_path (--yes enabled)..."
bash "$script_path"
return
fi
local answer
read -rp "[x] $label not done yet. Run now? [y/N]: " answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
bash "$script_path"
fi
}
load_env() {
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${GCP_PROJECT_ID:?GCP_PROJECT_ID is not set in .env}"
: "${GCP_REGION:?GCP_REGION is not set in .env}"
: "${GCP_REPOSITORY:?GCP_REPOSITORY is not set in .env}"
: "${SERVICE_NAME:?SERVICE_NAME is not set in .env}"
}
check_env() {
[[ -f "$ENV_FILE" ]]
}
check_gcloud_installed() {
command -v gcloud >/dev/null 2>&1
}
check_login() {
local active_account
local current_project
local current_region
local docker_cfg
active_account="$(gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null | head -n1 || true)"
current_project="$(gcloud config get-value project 2>/dev/null || true)"
current_region="$(gcloud config get-value run/region 2>/dev/null || true)"
docker_cfg="${DOCKER_CONFIG:-$HOME/.docker}/config.json"
[[ -n "$active_account" ]] &&
[[ "$current_project" == "$GCP_PROJECT_ID" ]] &&
[[ "$current_region" == "$GCP_REGION" ]] &&
[[ -f "$docker_cfg" ]] &&
grep -q "\"${GCP_REGION}-docker.pkg.dev\"" "$docker_cfg"
}
check_project_setup() {
local billing_enabled
billing_enabled="$(gcloud billing projects describe "$GCP_PROJECT_ID" --format="value(billingEnabled)" 2>/dev/null || true)"
[[ "$billing_enabled" == "True" ]] || return 1
gcloud artifacts repositories describe "$GCP_REPOSITORY" \
--location="$GCP_REGION" \
--project="$GCP_PROJECT_ID" >/dev/null 2>&1 || return 1
local api
for api in run.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com cloudresourcemanager.googleapis.com; do
gcloud services list --enabled --project="$GCP_PROJECT_ID" --format="value(config.name)" 2>/dev/null \
| grep -Fxq "$api" || return 1
done
}
check_secrets_setup() {
local service_account
service_account="serviceAccount:${GCP_PROJECT_ID}@appspot.gserviceaccount.com"
gcloud secrets describe mongodb-connection-string --project="$GCP_PROJECT_ID" >/dev/null 2>&1 || return 1
gcloud secrets get-iam-policy mongodb-connection-string \
--project="$GCP_PROJECT_ID" \
--flatten="bindings[].members" \
--filter="bindings.role=roles/secretmanager.secretAccessor AND bindings.members=${service_account}" \
--format="value(bindings.members)" 2>/dev/null \
| grep -Fxq "$service_account"
}
check_deploy_done() {
gcloud run services describe "$SERVICE_NAME" \
--region="$GCP_REGION" \
--project="$GCP_PROJECT_ID" >/dev/null 2>&1
}
print_done() {
echo "[v] $1"
}
echo "================================================================"
echo " Htmx deployment flow runner (Linux)"
echo "================================================================"
if check_env; then
print_done "Step 0: .env exists"
else
echo "[x] Step 0: GCR/.env is missing"
echo " Copy GCR/.env.example to GCR/.env and fill required values."
exit 1
fi
load_env
if check_gcloud_installed; then
print_done "Step 1: gcloud installed"
else
confirm_run "Step 1: gcloud install" "$SCRIPT_DIR/scripts/00-install-gcloud.sh"
fi
if check_login; then
print_done "Step 2: login + docker auth configured"
else
confirm_run "Step 2: login" "$SCRIPT_DIR/scripts/01-login.sh"
fi
if check_project_setup; then
print_done "Step 3: project setup complete"
else
confirm_run "Step 3: project setup" "$SCRIPT_DIR/scripts/02-setup-project.sh"
fi
if check_secrets_setup; then
print_done "Step 4: secrets created and access granted"
else
confirm_run "Step 4: secrets setup" "$SCRIPT_DIR/scripts/03-create-secrets.sh"
fi
if check_deploy_done; then
print_done "Step 5: service is already deployed"
else
confirm_run "Step 5: deploy" "$SCRIPT_DIR/scripts/04-deploy.sh"
fi
echo ""
echo "================================================================"
echo " Final verification"
echo "================================================================"
if check_gcloud_installed; then print_done "Step 1"; else echo "[x] Step 1"; fi
if check_login; then print_done "Step 2"; else echo "[x] Step 2"; fi
if check_project_setup; then print_done "Step 3"; else echo "[x] Step 3"; fi
if check_secrets_setup; then print_done "Step 4"; else echo "[x] Step 4"; fi
if check_deploy_done; then print_done "Step 5"; else echo "[x] Step 5"; fi
echo ""
echo "Tip: run 'bash GCR/run-all.sh --yes' to auto-run missing steps without prompts."
+54
View File
@@ -0,0 +1,54 @@
# =============================================================================
# 00-install-gcloud.ps1 (Windows)
# Installs the Google Cloud CLI (gcloud) on Windows.
# Run this once in an elevated PowerShell prompt before doing anything else.
#
# Linux users: run GCR/scripts/00-install-gcloud.sh instead.
# =============================================================================
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Write-Host ">>> Installing Google Cloud CLI on Windows..."
Write-Host ""
# ── Try winget first (available on Windows 10 1709+ / Windows 11) ────────────
if (Get-Command winget -ErrorAction SilentlyContinue) {
Write-Host ">>> winget found — installing Google Cloud SDK via winget..."
winget install --id Google.CloudSDK --accept-source-agreements --accept-package-agreements
Write-Host ""
Write-Host ">>> gcloud installed via winget."
Write-Host " Close and reopen PowerShell so PATH changes take effect."
}
# ── Fallback: download the official Windows installer ────────────────────────
else {
Write-Host ">>> winget not available — downloading the official installer..."
Write-Host ""
$InstallerUrl = "https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe"
$InstallerPath = "$env:TEMP\GoogleCloudSDKInstaller.exe"
Write-Host " Downloading from: $InstallerUrl"
Invoke-WebRequest -Uri $InstallerUrl -OutFile $InstallerPath -UseBasicParsing
Write-Host " Launching installer..."
Write-Host " Follow the on-screen prompts. Make sure 'Add gcloud to PATH' is checked."
Write-Host ""
Start-Process -FilePath $InstallerPath -Wait
Remove-Item $InstallerPath -Force
}
Write-Host ""
Write-Host ">>> Verifying installation..."
try {
$version = & gcloud version 2>&1 | Select-Object -First 1
Write-Host " $version"
Write-Host ""
Write-Host ">>> gcloud is installed."
} catch {
Write-Host " gcloud not found on PATH yet."
Write-Host " Close and reopen PowerShell, then run: gcloud version"
}
Write-Host ""
Write-Host ">>> Next step: run GCR\scripts\01-login.ps1"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# =============================================================================
# 00-install-gcloud.sh (Linux)
# Installs the Google Cloud CLI (gcloud) on Debian/Ubuntu.
# Run this once on a new machine before doing anything else.
#
# Windows users: run GCR/scripts/00-install-gcloud.ps1 in PowerShell instead.
# =============================================================================
set -euo pipefail
OS="$(uname -s)"
if [[ "$OS" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/scripts/00-install-gcloud.ps1"
exit 1
fi
echo ">>> Installing Google Cloud CLI on Linux (Debian/Ubuntu)..."
apt_update_with_retry() {
local attempts=5
local i
for ((i=1; i<=attempts; i++)); do
if sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/* && sudo apt-get update; then
return 0
fi
echo " apt-get update failed (attempt ${i}/${attempts})."
if (( i < attempts )); then
echo " Retrying apt metadata refresh..."
fi
done
echo "ERROR: apt-get update failed after ${attempts} attempts."
echo "This is often a temporary mirror sync issue. Please try again in a few minutes."
exit 1
}
apt_install_with_retry() {
local attempts=3
local i
for ((i=1; i<=attempts; i++)); do
if sudo apt-get install -y --no-install-recommends "$@"; then
return 0
fi
echo " apt-get install failed (attempt ${i}/${attempts}) for: $*"
if (( i < attempts )); then
echo " Retrying package install..."
fi
done
echo "ERROR: apt-get install failed after ${attempts} attempts for: $*"
exit 1
}
# ── Install dependencies ───────────────────────────────────────────────────
apt_update_with_retry
apt_install_with_retry \
apt-transport-https \
ca-certificates \
curl \
gnupg
# ── Import the Google Cloud signing key ───────────────────────────────────
# Key is downloaded to a file rather than piped straight into gpg so it can
# be inspected or cached by CI systems if needed.
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
-o /tmp/cloud.google.gpg
sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg /tmp/cloud.google.gpg
rm /tmp/cloud.google.gpg
# ── Add the apt repository ────────────────────────────────────────────────
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] \
https://packages.cloud.google.com/apt cloud-sdk main" \
| sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
# ── Install gcloud ────────────────────────────────────────────────────────
apt_update_with_retry
apt_install_with_retry google-cloud-cli
echo ""
echo ">>> gcloud installed successfully."
gcloud version
echo ""
echo ">>> Next step: run GCR/scripts/01-login.sh"
+62
View File
@@ -0,0 +1,62 @@
# =============================================================================
# 01-login.ps1 (Windows)
# Authenticates your local machine to Google Cloud and configures Docker
# to push images to Artifact Registry.
#
# Run this once per machine (or whenever your credentials expire).
# Linux users: run GCR/scripts/01-login.sh instead.
# =============================================================================
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$EnvFile = Join-Path $ScriptDir "..\\.env"
# ── Load .env ─────────────────────────────────────────────────────────────────
if (-not (Test-Path $EnvFile)) {
Write-Error "ERROR: $EnvFile not found.`nCopy GCR\.env.example to GCR\.env and fill in your values first."
exit 1
}
$config = @{}
foreach ($line in Get-Content $EnvFile) {
# Skip blank lines and comments
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
if ($line -match '^([^=]+)=(.*)$') {
$config[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
$GCP_PROJECT_ID = $config['GCP_PROJECT_ID'] ?? ''
$GCP_REGION = $config['GCP_REGION'] ?? ''
if (-not $GCP_PROJECT_ID) { Write-Error "GCP_PROJECT_ID is not set in .env"; exit 1 }
if (-not $GCP_REGION) { Write-Error "GCP_REGION is not set in .env"; exit 1 }
# ── Step 1: Authenticate user account ────────────────────────────────────────
Write-Host ">>> Logging in to Google Cloud..."
Write-Host " A browser window will open. Sign in with the Google account"
Write-Host " that has access to project: $GCP_PROJECT_ID"
Write-Host ""
gcloud auth login
# ── Step 2: Set default project ──────────────────────────────────────────────
Write-Host ""
Write-Host ">>> Setting default project to: $GCP_PROJECT_ID"
gcloud config set project $GCP_PROJECT_ID
# ── Step 3: Set default region ───────────────────────────────────────────────
Write-Host ">>> Setting default region to: $GCP_REGION"
gcloud config set run/region $GCP_REGION
# ── Step 4: Configure Docker to authenticate against Artifact Registry ────────
Write-Host ""
Write-Host ">>> Configuring Docker to authenticate with Artifact Registry..."
gcloud auth configure-docker "$GCP_REGION-docker.pkg.dev" --quiet
Write-Host ""
Write-Host ">>> Login complete. You are now authenticated as:"
gcloud auth list --filter=status:ACTIVE --format="value(account)"
Write-Host ""
Write-Host ">>> Next step: run GCR\scripts\02-setup-project.ps1"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# =============================================================================
# 01-login.sh (Linux)
# Authenticates your local machine to Google Cloud and configures Docker
# to push images to Artifact Registry.
#
# Run this once per machine (or whenever your credentials expire).
# Windows users: run GCR/scripts/01-login.ps1 in PowerShell instead.
# =============================================================================
set -euo pipefail
if [[ "$(uname -s)" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/scripts/01-login.ps1"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Load .env ─────────────────────────────────────────────────────────────────
ENV_FILE="$SCRIPT_DIR/../.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE not found."
echo "Copy GCR/.env.example to GCR/.env and fill in your values first."
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${GCP_PROJECT_ID:?GCP_PROJECT_ID is not set in .env}"
: "${GCP_REGION:?GCP_REGION is not set in .env}"
# ── Step 1: Authenticate user account ────────────────────────────────────────
echo ">>> Logging in to Google Cloud..."
echo " A browser window will open. Sign in with the Google account that has"
echo " access to project: $GCP_PROJECT_ID"
echo ""
gcloud auth login
# ── Step 2: Set default project ──────────────────────────────────────────────
echo ""
echo ">>> Setting default project to: $GCP_PROJECT_ID"
gcloud config set project "$GCP_PROJECT_ID"
# ── Step 3: Set default region ───────────────────────────────────────────────
echo ">>> Setting default region to: $GCP_REGION"
gcloud config set run/region "$GCP_REGION"
# ── Step 4: Configure Docker to authenticate against Artifact Registry ────────
echo ""
echo ">>> Configuring Docker to authenticate with Artifact Registry..."
gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet
echo ""
echo ">>> Login complete. You are now authenticated as:"
gcloud auth list --filter=status:ACTIVE --format="value(account)"
echo ""
echo ">>> Next step: run GCR/scripts/02-setup-project.sh"
+126
View File
@@ -0,0 +1,126 @@
# =============================================================================
# 02-setup-project.ps1 (Windows)
# One-time GCP project setup:
# - Links a billing account to the project
# - Enables required APIs (Cloud Run, Artifact Registry, Secret Manager)
# - Creates an Artifact Registry Docker repository
# - Grants the current user the minimum required IAM roles
#
# Safe to re-run — most operations are idempotent.
# Linux users: run GCR/scripts/02-setup-project.sh instead.
# =============================================================================
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$EnvFile = Join-Path $ScriptDir "..\\.env"
# ── Load .env ─────────────────────────────────────────────────────────────────
if (-not (Test-Path $EnvFile)) {
Write-Error "ERROR: $EnvFile not found.`nCopy GCR\.env.example to GCR\.env and fill in your values first."
exit 1
}
$config = @{}
foreach ($line in Get-Content $EnvFile) {
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
if ($line -match '^([^=]+)=(.*)$') {
$config[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
$GCP_PROJECT_ID = $config['GCP_PROJECT_ID'] ?? ''
$GCP_REGION = $config['GCP_REGION'] ?? ''
$GCP_REPOSITORY = $config['GCP_REPOSITORY'] ?? ''
if (-not $GCP_PROJECT_ID) { Write-Error "GCP_PROJECT_ID is not set in .env"; exit 1 }
if (-not $GCP_REGION) { Write-Error "GCP_REGION is not set in .env"; exit 1 }
if (-not $GCP_REPOSITORY) { Write-Error "GCP_REPOSITORY is not set in .env"; exit 1 }
Write-Host ">>> Active project: $GCP_PROJECT_ID"
Write-Host ">>> Region: $GCP_REGION"
Write-Host ">>> AR repository: $GCP_REPOSITORY"
Write-Host ""
# ── Step 1: Link billing account ──────────────────────────────────────────────
Write-Host ">>> Checking billing status..."
$billingOutput = gcloud billing projects describe $GCP_PROJECT_ID --format="value(billingEnabled)" 2>$null
$billingEnabled = ($billingOutput -eq "True")
if ($billingEnabled) {
Write-Host " Billing is already enabled — skipping."
} else {
Write-Host ""
Write-Host " Billing is NOT enabled on this project."
Write-Host " Listing available billing accounts..."
Write-Host ""
gcloud billing accounts list --format="table(name,displayName,open)"
Write-Host ""
$BILLING_ACCOUNT_ID = Read-Host " Enter the BILLING_ACCOUNT_ID from the list above (format: XXXXXX-XXXXXX-XXXXXX)"
gcloud billing projects link $GCP_PROJECT_ID --billing-account=$BILLING_ACCOUNT_ID
Write-Host " Billing linked."
}
# ── Step 2: Enable required APIs ─────────────────────────────────────────────
Write-Host ""
Write-Host ">>> Enabling required Google Cloud APIs (this may take a minute)..."
gcloud services enable `
run.googleapis.com `
artifactregistry.googleapis.com `
secretmanager.googleapis.com `
cloudresourcemanager.googleapis.com `
--project=$GCP_PROJECT_ID
Write-Host " APIs enabled."
# ── Step 3: Create Artifact Registry Docker repository ───────────────────────
Write-Host ""
Write-Host ">>> Creating Artifact Registry repository: $GCP_REPOSITORY ..."
$repoExists = $false
try {
gcloud artifacts repositories describe $GCP_REPOSITORY `
--location=$GCP_REGION `
--project=$GCP_PROJECT_ID 2>$null | Out-Null
$repoExists = $true
} catch { }
if ($repoExists) {
Write-Host " Repository already exists — skipping."
} else {
gcloud artifacts repositories create $GCP_REPOSITORY `
--repository-format=docker `
--location=$GCP_REGION `
--description="Container images for Htmx app" `
--project=$GCP_PROJECT_ID
Write-Host " Repository created."
}
# ── Step 4: Grant current user the minimum required IAM roles ─────────────────
$CURRENT_USER = (gcloud config get-value account).Trim()
Write-Host ""
Write-Host ">>> Granting IAM roles to $CURRENT_USER ..."
foreach ($role in @(
"roles/run.developer",
"roles/artifactregistry.writer",
"roles/iam.serviceAccountUser",
"roles/secretmanager.admin",
"roles/secretmanager.secretAccessor",
"roles/secretmanager.secretVersionAdder"
)) {
Write-Host " Adding role: $role"
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID `
--member="user:$CURRENT_USER" `
--role=$role `
--quiet
}
Write-Host ""
Write-Host ">>> Project setup complete."
Write-Host ""
Write-Host ">>> Summary:"
Write-Host " Project ID: $GCP_PROJECT_ID"
Write-Host " Region: $GCP_REGION"
Write-Host " Artifact Registry: $GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/$GCP_REPOSITORY"
Write-Host ""
Write-Host ">>> Next step: run GCR\scripts\03-create-secrets.ps1"
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# =============================================================================
# 02-setup-project.sh (Linux)
# One-time GCP project setup:
# - Links a billing account to the project
# - Enables required APIs (Cloud Run, Artifact Registry, Secret Manager)
# - Creates an Artifact Registry Docker repository
# - Grants the current user the minimum required IAM roles
#
# Safe to re-run — most operations are idempotent.
# Windows users: run GCR/scripts/02-setup-project.ps1 in PowerShell instead.
# =============================================================================
set -euo pipefail
if [[ "$(uname -s)" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/scripts/02-setup-project.ps1"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Load .env ─────────────────────────────────────────────────────────────────
ENV_FILE="$SCRIPT_DIR/../.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE not found."
echo "Copy GCR/.env.example to GCR/.env and fill in your values first."
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${GCP_PROJECT_ID:?GCP_PROJECT_ID is not set in .env}"
: "${GCP_REGION:?GCP_REGION is not set in .env}"
: "${GCP_REPOSITORY:?GCP_REPOSITORY is not set in .env}"
# ── Confirm active project ────────────────────────────────────────────────────
echo ">>> Active project: $GCP_PROJECT_ID"
echo ">>> Region: $GCP_REGION"
echo ">>> AR repository: $GCP_REPOSITORY"
echo ""
# ── Step 1: Link billing account ──────────────────────────────────────────────
echo ">>> Checking billing status..."
BILLING_ENABLED=$(gcloud billing projects describe "$GCP_PROJECT_ID" \
--format="value(billingEnabled)" 2>/dev/null || echo "false")
if [[ "$BILLING_ENABLED" == "True" ]]; then
echo " Billing is already enabled — skipping."
else
echo ""
echo " Billing is NOT enabled on this project."
echo " Listing available billing accounts..."
echo ""
gcloud billing accounts list --format="table(name,displayName,open)"
echo ""
read -rp " Enter the BILLING_ACCOUNT_ID from the list above (format: XXXXXX-XXXXXX-XXXXXX): " BILLING_ACCOUNT_ID
gcloud billing projects link "$GCP_PROJECT_ID" \
--billing-account="$BILLING_ACCOUNT_ID"
echo " Billing linked."
fi
# ── Step 2: Enable required APIs ─────────────────────────────────────────────
echo ""
echo ">>> Enabling required Google Cloud APIs (this may take a minute)..."
gcloud services enable \
run.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
cloudresourcemanager.googleapis.com \
--project="$GCP_PROJECT_ID"
echo " APIs enabled."
# ── Step 3: Create Artifact Registry Docker repository ───────────────────────
echo ""
echo ">>> Creating Artifact Registry repository: $GCP_REPOSITORY ..."
if gcloud artifacts repositories describe "$GCP_REPOSITORY" \
--location="$GCP_REGION" \
--project="$GCP_PROJECT_ID" &>/dev/null; then
echo " Repository already exists — skipping."
else
gcloud artifacts repositories create "$GCP_REPOSITORY" \
--repository-format=docker \
--location="$GCP_REGION" \
--description="Container images for Htmx app" \
--project="$GCP_PROJECT_ID"
echo " Repository created."
fi
# ── Step 4: Grant current user the minimum required IAM roles ─────────────────
CURRENT_USER="$(gcloud config get-value account)"
echo ""
echo ">>> Granting IAM roles to $CURRENT_USER ..."
for ROLE in \
"roles/run.developer" \
"roles/artifactregistry.writer" \
"roles/iam.serviceAccountUser" \
"roles/secretmanager.admin" \
"roles/secretmanager.secretAccessor" \
"roles/secretmanager.secretVersionAdder"; do
echo " Adding role: $ROLE"
gcloud projects add-iam-policy-binding "$GCP_PROJECT_ID" \
--member="user:$CURRENT_USER" \
--role="$ROLE" \
--quiet
done
echo ""
echo ">>> Project setup complete."
echo ""
echo ">>> Summary:"
echo " Project ID: $GCP_PROJECT_ID"
echo " Region: $GCP_REGION"
echo " Artifact Registry: ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${GCP_REPOSITORY}"
echo ""
echo ">>> Next step: run GCR/scripts/03-create-secrets.sh"
+122
View File
@@ -0,0 +1,122 @@
# =============================================================================
# 03-create-secrets.ps1 (Windows)
# Creates and configures secrets in Google Cloud Secret Manager.
#
# Run this after 02-setup-project.ps1 to set up sensitive configuration
# values (e.g., MongoDB connection string).
#
# Linux users: run GCR/scripts/03-create-secrets.sh instead.
# =============================================================================
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$EnvFile = Join-Path $ScriptDir "..\\.env"
# ── Load .env ─────────────────────────────────────────────────────────────────
if (-not (Test-Path $EnvFile)) {
Write-Error "ERROR: $EnvFile not found.`nCopy GCR\.env.example to GCR\.env and fill in your values first."
exit 1
}
$config = @{}
foreach ($line in Get-Content $EnvFile) {
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
if ($line -match '^([^=]+)=(.*)$') {
$config[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
$GCP_PROJECT_ID = $config['GCP_PROJECT_ID'] ?? ''
if (-not $GCP_PROJECT_ID) { Write-Error "GCP_PROJECT_ID is not set in .env"; exit 1 }
Write-Host "================================================================"
Write-Host " Google Cloud Secret Manager setup"
Write-Host "================================================================"
Write-Host " Project: $GCP_PROJECT_ID"
Write-Host ""
# ── Helper function to create or update a secret ──────────────────────────────
function New-OrUpdateSecret {
param(
[string]$SecretName,
[string]$SecretPrompt
)
Write-Host ">>> Setting up secret: $SecretName"
Write-Host " $SecretPrompt"
# Read secret without echo
$SecretValue = Read-Host " Enter value (will not be echoed)" -AsSecureString
$PlainValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUni($SecretValue)
)
# Write to temp file without trailing newline to avoid contaminating the secret
$TempFile = [System.IO.Path]::GetTempFileName()
try {
[System.IO.File]::WriteAllText($TempFile, $PlainValue, [System.Text.Encoding]::UTF8)
$secretExists = $false
try {
gcloud secrets describe $SecretName --project=$GCP_PROJECT_ID 2>$null | Out-Null
$secretExists = $true
} catch { }
if ($secretExists) {
Write-Host " Secret already exists — creating new version..."
gcloud secrets versions add $SecretName `
--data-file=$TempFile `
--project=$GCP_PROJECT_ID
} else {
Write-Host " Creating new secret..."
gcloud secrets create $SecretName `
--data-file=$TempFile `
--replication-policy="automatic" `
--project=$GCP_PROJECT_ID
}
} finally {
Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
}
Write-Host " ✓ Secret '$SecretName' ready."
Write-Host ""
}
# ── Step 1: Create MongoDB connection string secret ──────────────────────────
New-OrUpdateSecret `
"mongodb-connection-string" `
"MongoDB Atlas or self-hosted connection URI (e.g., mongodb+srv://user:pass@cluster.mongodb.net)"
# ── Step 2: Grant Cloud Run service account access to secrets ─────────────────
Write-Host ">>> Granting Cloud Run service account access to secrets..."
Write-Host ""
# Get the default Cloud Run service account for this project
$SERVICE_ACCOUNT = "$GCP_PROJECT_ID@appspot.gserviceaccount.com"
foreach ($SECRET_NAME in @("mongodb-connection-string")) {
Write-Host " Granting Secret Accessor role for '$SECRET_NAME' to $SERVICE_ACCOUNT"
gcloud secrets add-iam-policy-binding $SECRET_NAME `
--member="serviceAccount:$SERVICE_ACCOUNT" `
--role="roles/secretmanager.secretAccessor" `
--project=$GCP_PROJECT_ID `
--quiet
}
Write-Host ""
Write-Host "================================================================"
Write-Host " Secret Manager setup complete!"
Write-Host "================================================================"
Write-Host ""
Write-Host ">>> Summary:"
Write-Host " Secrets created:"
Write-Host " • mongodb-connection-string"
Write-Host ""
Write-Host " Service account granted access:"
Write-Host "$SERVICE_ACCOUNT"
Write-Host ""
Write-Host ">>> Next step: run GCR\scripts\04-deploy.ps1"
Write-Host " (The deploy script will automatically inject secrets into"
Write-Host " the running container.)"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# =============================================================================
# 03-create-secrets.sh (Linux)
# Creates and configures secrets in Google Cloud Secret Manager.
#
# Run this after 02-setup-project.sh to set up sensitive configuration
# values (e.g., MongoDB connection string).
#
# Windows users: run GCR/scripts/03-create-secrets.ps1 in PowerShell instead.
# =============================================================================
set -euo pipefail
if [[ "$(uname -s)" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/scripts/03-create-secrets.ps1"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Load .env ─────────────────────────────────────────────────────────────────
ENV_FILE="$SCRIPT_DIR/../.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE not found."
echo "Copy GCR/.env.example to GCR/.env and fill in your values first."
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${GCP_PROJECT_ID:?GCP_PROJECT_ID is not set in .env}"
echo "================================================================"
echo " Google Cloud Secret Manager setup"
echo "================================================================"
echo " Project: $GCP_PROJECT_ID"
echo ""
# ── Helper function to create or update a secret ──────────────────────────────
create_or_update_secret() {
local SECRET_NAME="$1"
local SECRET_PROMPT="$2"
echo ">>> Setting up secret: $SECRET_NAME"
echo " $SECRET_PROMPT"
read -rsp " Enter value (will not be echoed): " SECRET_VALUE
echo ""
if gcloud secrets describe "$SECRET_NAME" --project="$GCP_PROJECT_ID" &>/dev/null; then
echo " Secret already exists — creating new version..."
printf '%s' "$SECRET_VALUE" | gcloud secrets versions add "$SECRET_NAME" \
--data-file=- \
--project="$GCP_PROJECT_ID"
else
echo " Creating new secret..."
printf '%s' "$SECRET_VALUE" | gcloud secrets create "$SECRET_NAME" \
--data-file=- \
--replication-policy="automatic" \
--project="$GCP_PROJECT_ID"
fi
echo " ✓ Secret '$SECRET_NAME' ready."
echo ""
}
# ── Step 1: Create MongoDB connection string secret ──────────────────────────
create_or_update_secret \
"mongodb-connection-string" \
"MongoDB Atlas or self-hosted connection URI (e.g., mongodb+srv://user:pass@cluster.mongodb.net)"
# ── Step 2: Grant Cloud Run service account access to secrets ─────────────────
echo ">>> Granting Cloud Run service account access to secrets..."
echo ""
# Get the default Cloud Run service account for this project
SERVICE_ACCOUNT="$GCP_PROJECT_ID@appspot.gserviceaccount.com"
for SECRET_NAME in mongodb-connection-string; do
echo " Granting Secret Accessor role for '$SECRET_NAME' to $SERVICE_ACCOUNT"
gcloud secrets add-iam-policy-binding "$SECRET_NAME" \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor" \
--project="$GCP_PROJECT_ID" \
--quiet
done
echo ""
echo "================================================================"
echo " Secret Manager setup complete!"
echo "================================================================"
echo ""
echo ">>> Summary:"
echo " Secrets created:"
echo " • mongodb-connection-string"
echo ""
echo " Service account granted access:"
echo "$SERVICE_ACCOUNT"
echo ""
echo ">>> Next step: run GCR/scripts/04-deploy.sh"
echo " (The deploy script will automatically inject secrets into"
echo " the running container.)"
+194
View File
@@ -0,0 +1,194 @@
# =============================================================================
# 04-deploy.ps1 (Windows)
# Builds the Docker image, pushes it to Artifact Registry, and deploys it
# to Cloud Run — all in one command.
#
# Usage:
# .\GCR\scripts\04-deploy.ps1 # deploy with tag = git short SHA
# .\GCR\scripts\04-deploy.ps1 -Tag my-tag # deploy with a custom tag
#
# Prerequisites:
# 1. GCR\.env exists and is filled in (copy from GCR\.env.example)
# 2. 01-login.ps1 has been run (gcloud auth + Docker configured)
# 3. 02-setup-project.ps1 has been run (APIs enabled, repo created)
# 4. 03-create-secrets.ps1 has been run (MongoDB secret created)
# 5. Docker Desktop is running
#
# Linux users: run GCR/scripts/04-deploy.sh instead.
# =============================================================================
#Requires -Version 5.1
[CmdletBinding()]
param(
[string]$Tag = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path
$EnvFile = Join-Path $ScriptDir "..\\.env"
# ── Load .env ─────────────────────────────────────────────────────────────────
if (-not (Test-Path $EnvFile)) {
Write-Error "ERROR: $EnvFile not found.`nCopy GCR\.env.example to GCR\.env and fill in your values first."
exit 1
}
$config = @{}
foreach ($line in Get-Content $EnvFile) {
if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
if ($line -match '^([^=]+)=(.*)$') {
$config[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
$GCP_PROJECT_ID = $config['GCP_PROJECT_ID'] ?? ''
$GCP_REGION = $config['GCP_REGION'] ?? ''
$GCP_REPOSITORY = $config['GCP_REPOSITORY'] ?? ''
$SERVICE_NAME = $config['SERVICE_NAME'] ?? ''
$MONGODB_DATABASE_NAME = $config['MONGODB_DATABASE_NAME'] ?? 'HtmxAppDb'
# Note: MONGODB_CONNECTION_STRING is stored in Secret Manager (mongodb-connection-string)
if (-not $GCP_PROJECT_ID) { Write-Error "GCP_PROJECT_ID is not set in .env"; exit 1 }
if (-not $GCP_REGION) { Write-Error "GCP_REGION is not set in .env"; exit 1 }
if (-not $GCP_REPOSITORY) { Write-Error "GCP_REPOSITORY is not set in .env"; exit 1 }
if (-not $SERVICE_NAME) { Write-Error "SERVICE_NAME is not set in .env"; exit 1 }
# Note: MONGODB_CONNECTION_STRING is stored in Secret Manager
function Test-SecretsReady {
$serviceAccount = "serviceAccount:$GCP_PROJECT_ID@appspot.gserviceaccount.com"
try {
gcloud secrets describe mongodb-connection-string --project=$GCP_PROJECT_ID 2>$null | Out-Null
} catch {
return $false
}
$binding = gcloud secrets get-iam-policy mongodb-connection-string `
--project=$GCP_PROJECT_ID `
--flatten="bindings[].members" `
--filter="bindings.role=roles/secretmanager.secretAccessor AND bindings.members=$serviceAccount" `
--format="value(bindings.members)" 2>$null
return ($binding -match [regex]::Escape($serviceAccount))
}
if (-not (Test-SecretsReady)) {
Write-Host ""
Write-Host ">>> Required secrets are not fully configured yet."
$runSecretSetup = Read-Host " Run GCR\scripts\03-create-secrets.ps1 now? [y/N]"
if ($runSecretSetup -match '^[Yy]$') {
& (Join-Path $ScriptDir "03-create-secrets.ps1")
} else {
Write-Host ""
Write-Error "Deployment requires secret setup first. Run: .\GCR\scripts\03-create-secrets.ps1"
exit 1
}
if (-not (Test-SecretsReady)) {
Write-Host ""
Write-Error "Secret setup check still failing after running 03-create-secrets.ps1."
exit 1
}
}
# ── Determine image tag ────────────────────────────────────────────────────────
if (-not $Tag) {
# Default to git short SHA if inside a git repo; otherwise use timestamp
try {
$Tag = (git -C $RepoRoot rev-parse --short HEAD 2>$null).Trim()
} catch { }
if (-not $Tag) {
$Tag = (Get-Date -Format "yyyyMMddHHmmss")
}
}
$REGISTRY = "$GCP_REGION-docker.pkg.dev"
$IMAGE_URI = "$REGISTRY/$GCP_PROJECT_ID/$GCP_REPOSITORY/${SERVICE_NAME}:$Tag"
Write-Host "================================================================"
Write-Host " Htmx -> Cloud Run deployment"
Write-Host "================================================================"
Write-Host " Project: $GCP_PROJECT_ID"
Write-Host " Region: $GCP_REGION"
Write-Host " Service: $SERVICE_NAME"
Write-Host " Image: $IMAGE_URI"
Write-Host "================================================================"
Write-Host ""
# ── Step 1: Ensure package-lock.json exists (required for `npm ci`) ───────────
$LockFile = Join-Path $RepoRoot "Htmx.ApiDemo\package-lock.json"
if (-not (Test-Path $LockFile)) {
Write-Host ">>> package-lock.json not found. Generating it now..."
Write-Host " (This requires node + npm to be installed locally)"
Push-Location (Join-Path $RepoRoot "Htmx.ApiDemo")
npm install --package-lock-only
Pop-Location
Write-Host " package-lock.json generated. Commit it to the repository."
Write-Host ""
}
# ── Step 2: Build the Docker image ────────────────────────────────────────────
Write-Host ">>> Building Docker image..."
Write-Host " Context: $RepoRoot"
Write-Host " Dockerfile: GCR\Dockerfile"
Write-Host ""
# Build from repo root so COPY instructions can reach both project directories.
# Docker on Windows accepts forward slashes in --file.
$DockerFile = Join-Path $RepoRoot "GCR\Dockerfile"
docker build --file $DockerFile --tag $IMAGE_URI $RepoRoot
Write-Host ""
Write-Host ">>> Image built: $IMAGE_URI"
# ── Step 3: Push image to Artifact Registry ───────────────────────────────────
Write-Host ""
Write-Host ">>> Pushing image to Artifact Registry..."
docker push $IMAGE_URI
Write-Host ">>> Push complete."
# ── Step 4: Deploy to Cloud Run via docker-compose.yml ───────────────────────
Write-Host ""
Write-Host ">>> Deploying to Cloud Run..."
# Set env vars consumed by docker-compose.yml variable substitution
$env:IMAGE_URI = $IMAGE_URI
$env:MONGODB_DATABASE_NAME = $MONGODB_DATABASE_NAME
$ComposeFile = Join-Path $RepoRoot "GCR\docker-compose.yml"
gcloud run services replace $ComposeFile `
--region=$GCP_REGION `
--project=$GCP_PROJECT_ID
# ── Step 4b: Inject MongoDB connection string from Secret Manager ────────────
Write-Host ""
Write-Host ">>> Injecting MongoDB connection string from Secret Manager..."
gcloud run services update $SERVICE_NAME `
--region=$GCP_REGION `
--project=$GCP_PROJECT_ID `
--set-secrets="ConnectionStrings__DefaultConnection=mongodb-connection-string:latest"
# ── Step 5: Make the service publicly accessible ──────────────────────────────
# Remove this block if you want the service to require authentication.
Write-Host ""
Write-Host ">>> Allowing public (unauthenticated) access to the service..."
gcloud run services add-iam-policy-binding $SERVICE_NAME `
--region=$GCP_REGION `
--project=$GCP_PROJECT_ID `
--member="allUsers" `
--role="roles/run.invoker"
# ── Print service URL ─────────────────────────────────────────────────────────
Write-Host ""
$SERVICE_URL = (gcloud run services describe $SERVICE_NAME `
--region=$GCP_REGION `
--project=$GCP_PROJECT_ID `
--format="value(status.url)").Trim()
Write-Host "================================================================"
Write-Host " Deployment complete!"
Write-Host " Service URL: $SERVICE_URL"
Write-Host "================================================================"
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# =============================================================================
# 04-deploy.sh (Linux)
# Builds the Docker image, pushes it to Artifact Registry, and deploys it
# to Cloud Run — all in one command.
#
# Usage:
# ./GCR/scripts/04-deploy.sh # deploy with tag = git short SHA
# ./GCR/scripts/04-deploy.sh my-tag # deploy with a custom tag
#
# Prerequisites:
# 1. GCR/.env exists and is filled in (copy from GCR/.env.example)
# 2. 01-login.sh has been run (gcloud auth + Docker configured)
# 3. 02-setup-project.sh has been run (APIs enabled, repo created)
# 4. 03-create-secrets.sh has been run (MongoDB secret created)
# 5. Docker daemon is running locally
#
# Windows users: run GCR/scripts/04-deploy.ps1 in PowerShell instead.
# =============================================================================
set -euo pipefail
if [[ "$(uname -s)" != "Linux" ]]; then
echo "ERROR: This script is for Linux only."
echo "Windows users: run GCR/scripts/04-deploy.ps1"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# ── Load .env ─────────────────────────────────────────────────────────────────
ENV_FILE="$SCRIPT_DIR/../.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE not found."
echo "Copy GCR/.env.example to GCR/.env and fill in your values first."
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${GCP_PROJECT_ID:?GCP_PROJECT_ID is not set in .env}"
: "${GCP_REGION:?GCP_REGION is not set in .env}"
: "${GCP_REPOSITORY:?GCP_REPOSITORY is not set in .env}"
: "${SERVICE_NAME:?SERVICE_NAME is not set in .env}"
: "${MONGODB_DATABASE_NAME:?MONGODB_DATABASE_NAME is not set in .env}"
# Note: MONGODB_CONNECTION_STRING is stored in Secret Manager (mongodb-connection-string)
# See GCR/README.md for Secret Manager setup
secret_setup_ready() {
local service_account
service_account="serviceAccount:${GCP_PROJECT_ID}@appspot.gserviceaccount.com"
gcloud secrets describe "mongodb-connection-string" --project="$GCP_PROJECT_ID" >/dev/null 2>&1 || return 1
gcloud secrets get-iam-policy "mongodb-connection-string" \
--project="$GCP_PROJECT_ID" \
--flatten="bindings[].members" \
--filter="bindings.role=roles/secretmanager.secretAccessor AND bindings.members=${service_account}" \
--format="value(bindings.members)" 2>/dev/null \
| grep -Fxq "$service_account"
}
if ! secret_setup_ready; then
echo ""
echo ">>> Required secrets are not fully configured yet."
read -rp " Run GCR/scripts/03-create-secrets.sh now? [y/N]: " RUN_SECRET_SETUP
if [[ "$RUN_SECRET_SETUP" =~ ^[Yy]$ ]]; then
bash "$SCRIPT_DIR/03-create-secrets.sh"
else
echo ""
echo "ERROR: Deployment requires secret setup first."
echo "Run: bash GCR/scripts/03-create-secrets.sh"
exit 1
fi
if ! secret_setup_ready; then
echo ""
echo "ERROR: Secret setup check still failing after running 03-create-secrets.sh."
exit 1
fi
fi
# ── Determine image tag ────────────────────────────────────────────────────────
TAG="${1:-}"
if [[ -z "$TAG" ]]; then
# Default to git short SHA if inside a git repo; otherwise use timestamp
if git -C "$REPO_ROOT" rev-parse --short HEAD &>/dev/null; then
TAG="$(git -C "$REPO_ROOT" rev-parse --short HEAD)"
else
TAG="$(date +%Y%m%d%H%M%S)"
fi
fi
REGISTRY="${GCP_REGION}-docker.pkg.dev"
IMAGE_URI="${REGISTRY}/${GCP_PROJECT_ID}/${GCP_REPOSITORY}/${SERVICE_NAME}:${TAG}"
echo "================================================================"
echo " Htmx → Cloud Run deployment"
echo "================================================================"
echo " Project: $GCP_PROJECT_ID"
echo " Region: $GCP_REGION"
echo " Service: $SERVICE_NAME"
echo " Image: $IMAGE_URI"
echo "================================================================"
echo ""
# ── Step 1: Ensure package-lock.json exists (required for `npm ci`) ───────────
LOCKFILE="$REPO_ROOT/Htmx.ApiDemo/package-lock.json"
if [[ ! -f "$LOCKFILE" ]]; then
echo ">>> package-lock.json not found. Generating it now..."
echo " (This requires node + npm to be installed locally)"
(cd "$REPO_ROOT/Htmx.ApiDemo" && npm install --package-lock-only)
echo " package-lock.json generated. Commit it to the repository."
echo ""
fi
# ── Step 2: Build the Docker image ────────────────────────────────────────────
echo ">>> Building Docker image..."
echo " Context: $REPO_ROOT"
echo " Dockerfile: GCR/Dockerfile"
echo ""
# Build from repo root so the COPY instructions can reach both
# Htmx.ApiDemo/ and Htmx.SourceGenerator/ directories.
docker build \
--file "$REPO_ROOT/GCR/Dockerfile" \
--tag "$IMAGE_URI" \
"$REPO_ROOT"
echo ""
echo ">>> Image built: $IMAGE_URI"
# ── Step 3: Push image to Artifact Registry ───────────────────────────────────
echo ""
echo ">>> Pushing image to Artifact Registry..."
docker push "$IMAGE_URI"
echo ">>> Push complete."
# ── Step 4: Deploy to Cloud Run via docker-compose.yml ───────────────────────
echo ""
echo ">>> Deploying to Cloud Run..."
# Export variables consumed by docker-compose.yml substitution
export IMAGE_URI
export MONGODB_DATABASE_NAME
gcloud run services replace "$REPO_ROOT/GCR/docker-compose.yml" \
--region="$GCP_REGION" \
--project="$GCP_PROJECT_ID"
# ── Step 4b: Inject MongoDB connection string from Secret Manager ────────────
echo ""
echo ">>> Injecting MongoDB connection string from Secret Manager..."
gcloud run services update "$SERVICE_NAME" \
--region="$GCP_REGION" \
--project="$GCP_PROJECT_ID" \
--set-secrets="ConnectionStrings__DefaultConnection=mongodb-connection-string:latest"
# ── Step 5: Make the service publicly accessible ──────────────────────────────
# Remove this block if you want the service to require authentication.
echo ""
echo ">>> Allowing public (unauthenticated) access to the service..."
gcloud run services add-iam-policy-binding "$SERVICE_NAME" \
--region="$GCP_REGION" \
--project="$GCP_PROJECT_ID" \
--member="allUsers" \
--role="roles/run.invoker"
# ── Print service URL ─────────────────────────────────────────────────────────
echo ""
SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" \
--region="$GCP_REGION" \
--project="$GCP_PROJECT_ID" \
--format="value(status.url)")
echo "================================================================"
echo " Deployment complete!"
echo " Service URL: $SERVICE_URL"
echo "================================================================"
+1 -1
View File
@@ -911,7 +911,7 @@
--tw-tracking: var(--tracking-tight); --tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight); letter-spacing: var(--tracking-tight);
} }
.break-words { .wrap-break-word {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.whitespace-nowrap { .whitespace-nowrap {