# ============================================================================= # 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 "================================================================"