Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d0df500
BUY-5602: Add deploy-cloud-run-staging.yml workflow
BuyWhere Apr 29, 2026
6683065
BUY-5602: Add deploy-cloud-run-production.yml workflow
BuyWhere Apr 29, 2026
6f5e4a4
BUY-5602: Add deploy-frontend-vercel.yml workflow
BuyWhere Apr 29, 2026
88c1351
BUY-5602: Add deploy-logging-production.yml workflow
BuyWhere Apr 29, 2026
f9b9b1d
BUY-5602: Add deploy-mcp-cloud-run-production.yml workflow
BuyWhere Apr 29, 2026
af76f22
BUY-5602: Add deploy-static-site.yml workflow
BuyWhere Apr 29, 2026
7d1fc15
BUY-5602: Add deploy-us.yml workflow
BuyWhere Apr 29, 2026
ca47966
BUY-5602: Add docs.yml workflow
BuyWhere Apr 29, 2026
ab522cb
BUY-5602: Add enable-pg-stat-statements-staging.yml workflow
BuyWhere Apr 29, 2026
6a82a07
BUY-5602: Add inject-posthog-vm.yml workflow
BuyWhere Apr 29, 2026
2243240
BUY-5602: Add nginx-deploy.yml workflow
BuyWhere Apr 29, 2026
c931c85
BUY-5602: Add publish-python-sdk.yml workflow
BuyWhere Apr 29, 2026
9991cc4
BUY-5602: Add sentry-alerts.yml workflow
BuyWhere Apr 29, 2026
34c8baa
BUY-5602: Add ssl-renewal.yml workflow
BuyWhere Apr 29, 2026
32a187b
BUY-5602: Add update-cloud-run-sentry.yml workflow
BuyWhere Apr 29, 2026
7b00218
BUY-5602: Add deploy-cloud-run-production.yml workflow
BuyWhere Apr 29, 2026
30f08e4
BUY-5602: Add deploy-cloud-run-staging.yml workflow
BuyWhere Apr 29, 2026
e63ca85
BUY-5602: Add deploy-frontend-vercel.yml workflow
BuyWhere Apr 29, 2026
72cab3c
BUY-5602: Add deploy-logging-production.yml workflow
BuyWhere Apr 29, 2026
89ded23
BUY-5602: Add deploy-mcp-cloud-run-production.yml workflow
BuyWhere Apr 29, 2026
30b4fe3
BUY-5602: Add deploy-static-site.yml workflow
BuyWhere Apr 29, 2026
54822fd
BUY-5602: Add deploy-us.yml workflow
BuyWhere Apr 29, 2026
1202b65
BUY-5602: Add docs.yml workflow
BuyWhere Apr 29, 2026
2c4695a
BUY-5602: Add enable-pg-stat-statements-staging.yml workflow
BuyWhere Apr 29, 2026
334bff6
BUY-5602: Add inject-posthog-vm.yml workflow
BuyWhere Apr 29, 2026
1af1f5e
BUY-5602: Add nginx-deploy.yml workflow
BuyWhere Apr 29, 2026
82c83e3
BUY-5602: Add publish-python-sdk.yml workflow
BuyWhere Apr 29, 2026
449560b
BUY-5602: Add sentry-alerts.yml workflow
BuyWhere Apr 29, 2026
a073bfc
BUY-5602: Add ssl-renewal.yml workflow
BuyWhere Apr 29, 2026
0d75c3a
BUY-5602: Add update-cloud-run-sentry.yml workflow
BuyWhere Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 341 additions & 0 deletions .github/workflows/deploy-cloud-run-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
name: Deploy to Cloud Run (Production)

on:
push:
branches:
- release
workflow_dispatch:
inputs:
skip_tests:
description: 'Skip smoke tests and load checks'
required: false
default: 'false'
type: boolean

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
GCP_PROJECT_ID: buywhere-production
GCP_REGION: asia-southeast1
CLOUD_RUN_SERVICE: buywhere-api-production
CLOUD_SQL_INSTANCE: buywhere-production:asia-southeast1:buywhere-db

jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
outputs:
image_tag: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
deployment_id: ${{ steps.deploy_info.outputs.deployment_id }}
version: ${{ steps.deploy_info.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push API image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Generate deployment info
id: deploy_info
run: |
DEPLOYMENT_ID="cr-prod-$(date +%Y%m%d-%H%M%S)-${GITHUB_SHA::8}"
VERSION="${GITHUB_REF_NAME}-${GITHUB_SHA::8}"
echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Image tag: ${{ steps.meta.outputs.tags }}"
echo "Deployment ID: $DEPLOYMENT_ID"
echo "Version: $VERSION"

deploy-cloud-run:
name: Deploy to Cloud Run
needs: build
runs-on: ubuntu-latest
environment: production
outputs:
deployment_id: ${{ needs.build.outputs.deployment_id }}
version: ${{ needs.build.outputs.version }}
service_url: ${{ steps.deploy.outputs.url }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD || secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker auth for GHCR
run: gcloud auth configure-docker ${{ env.REGISTRY }} --quiet

- name: Create GCP Secret Manager secrets and deploy
id: deploy
run: |
echo "Creating/updating GCP Secret Manager secrets..."
echo -n "${{ secrets.ADMIN_API_KEY_PRODUCTION }}" | gcloud secrets versions add buywhere-admin-api-key --data-file=- --project ${{ env.GCP_PROJECT_ID }} 2>/dev/null || \
gcloud secrets create buywhere-admin-api-key --data-file=- --project ${{ env.GCP_PROJECT_ID }} <<< "${{ secrets.ADMIN_API_KEY_PRODUCTION }}"
echo -n "${{ secrets.POSTHOG_API_KEY_PRODUCTION }}" | gcloud secrets versions add buywhere-posthog-api-key --data-file=- --project ${{ env.GCP_PROJECT_ID }} 2>/dev/null || \
gcloud secrets create buywhere-posthog-api-key --data-file=- --project ${{ env.GCP_PROJECT_ID }} <<< "${{ secrets.POSTHOG_API_KEY_PRODUCTION }}"

echo "Granting secretAccessor role to Cloud Run service account..."
GCP_SA=$(gcloud run services describe ${{ env.CLOUD_RUN_SERVICE }} --region ${{ env.GCP_REGION }} --project ${{ env.GCP_PROJECT_ID }} --format "value(spec.template.spec.serviceAccountName)" 2>/dev/null || echo "")
[ -z "$GCP_SA" ] && GCP_SA="${{ env.GCP_PROJECT_ID }}-sa@${{ env.GCP_PROJECT_ID }}.iam.gserviceaccount.com"
gcloud secrets add-iam-policy-binding buywhere-admin-api-key --member "serviceAccount:$GCP_SA" --role "roles/secretmanager.secretAccessor" --project ${{ env.GCP_PROJECT_ID }} 2>/dev/null || true
gcloud secrets add-iam-policy-binding buywhere-posthog-api-key --member "serviceAccount:$GCP_SA" --role "roles/secretmanager.secretAccessor" --project ${{ env.GCP_PROJECT_ID }} 2>/dev/null || true

echo "Deploying to Cloud Run..."
SERVICE_URL=$(gcloud run deploy ${{ env.CLOUD_RUN_SERVICE }} \
--project ${{ env.GCP_PROJECT_ID }} \
--region ${{ env.GCP_REGION }} \
--image ${{ needs.build.outputs.image_tag }} \
--platform managed \
--allow-unauthenticated \
--add-cloudsql-instances ${{ env.CLOUD_SQL_INSTANCE }} \
--set-env-vars "ENVIRONMENT=production,DATABASE_URL=${{ secrets.CLOUD_SQL_PRODUCTION_CONNECTION_STRING }}" \
--set-env-vars "REDIS_URL=${{ secrets.REDIS_PRODUCTION_URL }}" \
--set-env-vars "API_KEY_SECRET=${{ secrets.API_KEY_SECRET_PRODUCTION }}" \
--set-env-vars "SENTRY_DSN=${{ secrets.SENTRY_DSN_PRODUCTION }}" \
--set-secrets "ADMIN_API_KEY=buywhere-admin-api-key:latest,POSTHOG_PROJECT_KEY=buywhere-posthog-api-key:latest" \
--memory 1Gi \
--cpu 2 \
--min-instances 2 \
--max-instances 20 \
--concurrency 80 \
--timeout 60s \
--label "deployment-id=${{ needs.build.outputs.deployment_id }}" \
--label "version=${{ needs.build.outputs.version }}" \
--label "commit-sha=${{ github.sha }}" \
--output json | jq -r '.status.url')

echo "Service URL: $SERVICE_URL"
echo "url=$SERVICE_URL" >> $GITHUB_OUTPUT

- name: Wait for service to stabilize
run: |
echo "Waiting 30 seconds for Cloud Run service to initialize..."
sleep 30

- name: Verify Cloud Run deployment
run: |
gcloud run services describe ${{ env.CLOUD_RUN_SERVICE }} \
--project ${{ env.GCP_PROJECT_ID }} \
--region ${{ env.GCP_REGION }} \
--format "table(status.conditions.name,status.conditions.status,status.conditions.reason)"

api-latency-smoke-test:
name: API Latency Smoke Test
needs: deploy-cloud-run
runs-on: ubuntu-latest
if: ${{ github.event.inputs.skip_tests != 'true' }}
outputs:
test_passed: ${{ steps.smoke_test.outputs.passed }}
avg_latency_ms: ${{ steps.smoke_test.outputs.avg_latency }}
p95_latency_ms: ${{ steps.smoke_test.outputs.p95_latency }}
max_latency_ms: ${{ steps.smoke_test.outputs.max_latency }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run API latency smoke test
id: smoke_test
env:
API_BASE_URL: ${{ needs.deploy-cloud-run.outputs.service_url }}
DEPLOYMENT_ID: ${{ needs.deploy-cloud-run.outputs.deployment_id }}
run: |
chmod +x scripts/cloud-run-latency-smoke-test.sh

set -o pipefail
RESULT=$(./scripts/cloud-run-latency-smoke-test.sh "$API_BASE_URL" 2>&1)
EXIT_CODE=$?

echo "$RESULT"
echo "passed=$((EXIT_CODE == 0))" >> $GITHUB_OUTPUT

AVG_LATENCY=$(echo "$RESULT" | grep "Average latency:" | awk '{print $3}' | tr -d 'ms')
P95_LATENCY=$(echo "$RESULT" | grep "P95 latency:" | awk '{print $3}' | tr -d 'ms')
MAX_LATENCY=$(echo "$RESULT" | grep "Max latency:" | awk '{print $3}' | tr -d 'ms')

echo "avg_latency=${AVG_LATENCY:-0}" >> $GITHUB_OUTPUT
echo "p95_latency=${P95_LATENCY:-0}" >> $GITHUB_OUTPUT
echo "max_latency=${MAX_LATENCY:-0}" >> $GITHUB_OUTPUT

exit $EXIT_CODE

cloud-sql-load-check:
name: Cloud SQL Load Check
needs: deploy-cloud-run
runs-on: ubuntu-latest
if: ${{ github.event.inputs.skip_tests != 'true' }}
outputs:
load_check_passed: ${{ steps.load_check.outputs.passed }}
avg_query_time_ms: ${{ steps.load_check.outputs.avg_query_time }}
slow_query_count: ${{ steps.load_check.outputs.slow_query_count }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD || secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Run Cloud SQL load check
id: load_check
env:
API_BASE_URL: ${{ needs.deploy-cloud-run.outputs.service_url }}
DEPLOYMENT_ID: ${{ needs.deploy-cloud-run.outputs.deployment_id }}
CLOUD_SQL_CONNECTION: ${{ secrets.CLOUD_SQL_PRODUCTION_CONNECTION_STRING }}
run: |
chmod +x scripts/cloud-sql-load-check.sh

set -o pipefail
RESULT=$(./scripts/cloud-sql-load-check.sh "$API_BASE_URL" "$CLOUD_SQL_CONNECTION" 2>&1)
EXIT_CODE=$?

echo "$RESULT"
echo "passed=$((EXIT_CODE == 0))" >> $GITHUB_OUTPUT

AVG_QUERY=$(echo "$RESULT" | grep "Average query time:" | awk '{print $4}' | tr -d 'ms')
SLOW_COUNT=$(echo "$RESULT" | grep "Slow queries:" | awk '{print $3}')

echo "avg_query_time=${AVG_QUERY:-0}" >> $GITHUB_OUTPUT
echo "slow_query_count=${SLOW_COUNT:-0}" >> $GITHUB_OUTPUT

exit $EXIT_CODE

configure-sentry-alerts:
name: Configure Sentry Alerts
needs: deploy-cloud-run
runs-on: ubuntu-latest
if: ${{ github.event.inputs.skip_tests != 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: pip install requests

- name: Configure Sentry Alerts
env:
SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'buywhere' }}
run: |
python scripts/configure_sentry_alerts.py

rollback:
name: Rollback on Failure
needs: [deploy-cloud-run, api-latency-smoke-test, cloud-sql-load-check, configure-sentry-alerts]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD || secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Rollback to previous revision
run: |
echo "Initiating Cloud Run rollback..."
LATEST_REVISION=$(gcloud run revisions list \
--service ${{ env.CLOUD_RUN_SERVICE }} \
--project ${{ env.GCP_PROJECT_ID }} \
--region ${{ env.GCP_REGION }} \
--sort-by=~creationTime \
--format "value(name)" | tail -n +2 | head -1)

if [ -n "$LATEST_REVISION" ]; then
echo "Rolling back to previous revision: $LATEST_REVISION"
gcloud run services update-traffic ${{ env.CLOUD_RUN_SERVICE }} \
--project ${{ env.GCP_PROJECT_ID }} \
--region ${{ env.GCP_REGION }} \
--to-revisions $LATEST_REVISION=100
else
echo "No previous revision found to rollback to"
exit 1
fi

- name: Verify rollback
run: |
gcloud run services describe ${{ env.CLOUD_RUN_SERVICE }} \
--project ${{ env.GCP_PROJECT_ID }} \
--region ${{ env.GCP_REGION }} \
--format "table(status.conditions.name,status.conditions.status)"

summary:
name: Deployment Summary
needs: [deploy-cloud-run, api-latency-smoke-test, cloud-sql-load-check, configure-sentry-alerts]
if: always()
runs-on: ubuntu-latest
steps:
- name: Generate deployment summary
run: |
echo "## Cloud Run Production Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Cloud Run Deployment | ${{ needs.deploy-cloud-run.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| API Latency Smoke Test | ${{ needs.api-latency-smoke-test.outputs.test_passed == 'true' && 'PASSED' || 'FAILED' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Cloud SQL Load Check | ${{ needs.cloud-sql-load-check.outputs.load_check_passed == 'true' && 'PASSED' || 'FAILED' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Sentry Alerts Configuration | ${{ needs.configure-sentry-alerts.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Latency Metrics" >> $GITHUB_STEP_SUMMARY
echo "- Average: ${{ needs.api-latency-smoke-test.outputs.avg_latency_ms }}ms" >> $GITHUB_STEP_SUMMARY
echo "- P95: ${{ needs.api-latency-smoke-test.outputs.p95_latency_ms }}ms" >> $GITHUB_STEP_SUMMARY
echo "- Max: ${{ needs.api-latency-smoke-test.outputs.max_latency_ms }}ms" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Cloud SQL Metrics" >> $GITHUB_STEP_SUMMARY
echo "- Average Query Time: ${{ needs.cloud-sql-load-check.outputs.avg_query_time_ms }}ms" >> $GITHUB_STEP_SUMMARY
echo "- Slow Queries (>100ms): ${{ needs.cloud-sql-load-check.outputs.slow_query_count }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Service URL: ${{ needs.deploy-cloud-run.outputs.service_url }}" >> $GITHUB_STEP_SUMMARY
Loading