Complete guide for deploying the GitHub Code Example Copier to Google Cloud Run with Secret Manager.
- Prerequisites
- Architecture Overview
- Secret Manager Setup
- Configuration
- Deployment
- Post-Deployment
- Monitoring
- Troubleshooting
- Deployment Checklist
- Go 1.26+ - For local development and testing
- Google Cloud SDK - For deployment
- GitHub App - With appropriate permissions
- MongoDB Atlas (optional) - For audit logging
- Google Cloud project with billing enabled
- GitHub organization admin access (to create/configure GitHub App)
- MongoDB Atlas account (if using audit logging)
# macOS
brew install --cask google-cloud-sdk
# Verify installation
gcloud --version# Login to Google Cloud
gcloud auth login
# Set application default credentials
gcloud auth application-default login
# Set your project
gcloud config set project YOUR_PROJECT_ID
# Verify
gcloud config get-value project┌─────────────────────────────────────────────────────────────┐
│ GitHub Repository │
│ (docs-code-examples) │
└────────────────────┬────────────────────────────────────────┘
│ Webhook (PR merged)
↓
┌─────────────────────────────────────────────────────────────┐
│ Google Cloud Run │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ github-copier Service (Container) │ │
│ │ - Receives webhook │ │
│ │ - Validates signature │ │
│ │ - Loads config from source repo │ │
│ │ - Matches files against patterns │ │
│ │ - Copies to target repos │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────┬────────────────────────────┬───────────────────┘
│ │
↓ ↓
┌────────────────────────┐ ┌───────────────────────────────┐
│ Secret Manager │ │ Cloud Logging │
│ - GitHub private key │ │ - Application logs │
│ - Webhook secret │ │ - Webhook events │
│ - MongoDB URI │ │ - Error tracking │
└────────────────────────┘ └───────────────────────────────┘
│
↓
┌────────────────────────┐
│ MongoDB Atlas │
│ - Audit events │
│ - Metrics │
└────────────────────────┘
This application uses Google Cloud Run (serverless containers):
Key benefits:
- Serverless - Scales to zero when not in use, scales up automatically
- Container-based - Uses Dockerfile for consistent builds
- Cost-effective - Pay only for actual usage (webhook processing time)
- Fast deployments - Typically deploys in 1-2 minutes
- Built-in Secret Manager integration - Secure secret access
- Automatic HTTPS - Managed SSL certificates
Deployment:
gcloud run deploy github-copier \
--source . \
--region us-central1 \
--env-vars-file=env-cloudrun.yaml- Security: Secrets encrypted at rest and in transit
- Audit Trail: All access logged
- Rotation: Update secrets without redeployment
- Access Control: Fine-grained IAM permissions
- No Hardcoding: Secrets never in config files or version control
gcloud services enable secretmanager.googleapis.com# Store your GitHub App private key
gcloud secrets create CODE_COPIER_PEM \
--data-file=/path/to/your/private-key.pem \
--replication-policy="automatic"# Generate a secure webhook secret
WEBHOOK_SECRET=$(openssl rand -hex 32)
echo "Generated: $WEBHOOK_SECRET"
# Store in Secret Manager
echo -n "$WEBHOOK_SECRET" | gcloud secrets create webhook-secret \
--data-file=- \
--replication-policy="automatic"
# Save this value - you'll need it for GitHub webhook configuration# Store MongoDB connection string
echo -n "mongodb+srv://user:pass@cluster.mongodb.net/dbname" | \
gcloud secrets create mongo-uri \
--data-file=- \
--replication-policy="automatic"# Get your project number
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
# Cloud Run service account (default compute service account)
SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# Grant access to each secret
gcloud secrets add-iam-policy-binding CODE_COPIER_PEM \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding webhook-secret \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding mongo-uri \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"Note: Cloud Run uses the default compute service account by default. You can also create a dedicated service account for better security isolation.
Or use the provided script:
cd github-copier
./scripts/grant-secret-access.sh# List all secrets
gcloud secrets list
# View secret metadata
gcloud secrets describe CODE_COPIER_PEM
# Verify IAM permissions
gcloud secrets get-iam-policy CODE_COPIER_PEMThe env-cloudrun.yaml file contains environment variables for Cloud Run deployment.
cd github-copier
# Copy from example or create new
cp env.yaml env-cloudrun.yaml
# Edit with your values
nano env-cloudrun.yaml # or vim, code, etc.
# Add to .gitignore (if not already there)
echo "env-cloudrun.yaml" >> .gitignoreImportant Notes:
- Do NOT set
PORTinenv-cloudrun.yaml- Cloud Run automatically sets this - The application defaults to port 8080 for local development
- Secret Manager references must include
/versions/latestor a specific version number - Format: Simple
KEY: valuepairs (not nested underenv_variables:)
# =============================================================================
# GitHub Configuration (Non-sensitive)
# =============================================================================
GITHUB_APP_ID: "YOUR_APP_ID"
INSTALLATION_ID: "YOUR_INSTALLATION_ID"
REPO_OWNER: "your-org"
REPO_NAME: "your-repo"
SRC_BRANCH: "main"
# =============================================================================
# Secret Manager References (Sensitive - SECURE!)
# =============================================================================
GITHUB_APP_PRIVATE_KEY_SECRET_NAME: "projects/PROJECT_NUMBER/secrets/CODE_COPIER_PEM/versions/latest"
WEBHOOK_SECRET_NAME: "projects/PROJECT_NUMBER/secrets/webhook-secret/versions/latest"
MONGO_URI_SECRET_NAME: "projects/PROJECT_NUMBER/secrets/mongo-uri/versions/latest"
# =============================================================================
# Application Settings
# =============================================================================
# PORT: "8080" # DO NOT SET - Cloud Run sets this automatically
WEBSERVER_PATH: "/events"
MAIN_CONFIG_FILE: ".copier/workflows/main.yaml"
USE_MAIN_CONFIG: "true"
DEPRECATION_FILE: "deprecated_examples.json"
# =============================================================================
# Committer Information
# =============================================================================
COMMITTER_NAME: "GitHub Copier App"
COMMITTER_EMAIL: "bot@example.com"
# =============================================================================
# Google Cloud Configuration
# =============================================================================
GOOGLE_CLOUD_PROJECT_ID: "your-project-id"
COPIER_LOG_NAME: "code-copier-log"
# =============================================================================
# Feature Flags
# =============================================================================
AUDIT_ENABLED: "true"
METRICS_ENABLED: "true"
# DRY_RUN: "false"About env-cloudrun.yaml:
- This file contains environment variables only (application configuration)
- It does NOT contain infrastructure settings (CPU, memory, timeout, etc.)
- Infrastructure settings must be specified via command-line flags or a
service.yamlfile - Use the deployment script (
./scripts/deploy-cloudrun.sh) to avoid typing all the flags
✅ DO:
- Use Secret Manager references (
*_SECRET_NAMEvariables) - Keep
env-cloudrun.yamlin.gitignore - Use simple
KEY: valueformat (noenv_variables:wrapper)
❌ DON'T:
- Put actual secrets in
env-cloudrun.yaml(use*_SECRET_NAMEinstead) - Commit
env-cloudrun.yamlto version control - Share
env-cloudrun.yamlvia email/chat
Application Startup:
1. Cloud Run loads env-cloudrun.yaml → environment variables
2. Read WEBHOOK_SECRET_NAME from env
3. Call Secret Manager API to get actual secret
4. Store in config.WebhookSecret
5. Use for webhook signature validation
Code flow:
// app.go
config, _ := configs.LoadEnvironment(envFile)
services.LoadWebhookSecret(config) // Loads from Secret Manager
services.LoadMongoURI(config) // Loads from Secret Manager- Secrets created in Secret Manager
- IAM permissions granted to Cloud Run service account
-
env-cloudrun.yamlcreated and configured -
env-cloudrun.yamlin.gitignore -
Dockerfileexists in project root
The simplest way to deploy is using the provided script:
cd github-copier
# Deploy to default region (us-central1)
./scripts/deploy-cloudrun.sh
# Or specify a different region
./scripts/deploy-cloudrun.sh us-east1The script will:
- ✅ Check that
env-cloudrun.yamlexists - ✅ Verify your Google Cloud project is set
- ✅ Show configuration before deploying
- ✅ Deploy with all recommended settings
- ✅ Display the service URL and next steps
If you prefer to run the command directly:
cd github-copier
gcloud run deploy github-copier \
--source . \
--region us-central1 \
--env-vars-file=env-cloudrun.yaml \
--allow-unauthenticated \
--max-instances=10 \
--cpu=1 \
--memory=512Mi \
--timeout=300s \
--concurrency=80 \
--port=8080Deployment options explained:
--source .- Build from Dockerfile in current directory--region us-central1- Deploy to US Central region--env-vars-file- Load environment variables from file--allow-unauthenticated- Allow public webhook access (required for GitHub webhooks)--max-instances=10- Limit concurrent instances (cost control)--cpu=1- 1 vCPU per instance--memory=512Mi- 512MB RAM per instance--timeout=300s- 5 minute timeout for webhook processing--concurrency=80- Handle up to 80 concurrent requests per instance--port=8080- Container port (matches Dockerfile EXPOSE)
# Check deployment status
gcloud run services list --region=us-central1
# Get service URL
SERVICE_URL=$(gcloud run services describe github-copier \
--region=us-central1 \
--format="value(status.url)")
echo "Service URL: ${SERVICE_URL}"
# View logs
gcloud run services logs read github-copier --region=us-central1 --limit=50
# Or tail logs in real-time
gcloud run services logs tail github-copier --region=us-central1# Test health
curl ${SERVICE_URL}/health
# Expected response:
# {
# "status": "healthy",
# "started": true,
# "github": {
# "status": "healthy",
# "authenticated": true
# },
# "queues": {
# "upload_count": 0,
# "deprecation_count": 0
# },
# "uptime": "5m30s"
# }-
Navigate to repository settings
- Go to:
https://github.com/YOUR_ORG/YOUR_REPO/settings/hooks
- Go to:
-
Add or edit webhook
- Payload URL:
https://YOUR_SERVICE_URL/events(use your Cloud Run URL) - Content type:
application/json - Secret: (the webhook secret from Secret Manager)
- Events: Select "Pull requests"
- Active: ✓ Checked
- Payload URL:
-
Get webhook secret from Secret Manager
gcloud secrets versions access latest --secret=webhook-secret
-
Save webhook
Option A: Merge a test PR
# Create and merge a test PR
# Watch logs for webhook receipt
gcloud run services logs read github-copier --limit=50Option B: Redeliver from GitHub
- Go to webhook settings
- Click "Recent Deliveries"
- Click on a delivery
- Click "Redeliver"
- Watch logs
# Check logs for successful processing
gcloud run services logs read github-copier --limit=50
# Look for:
# ✅ "Starting web server on port :8080"
# ✅ "webhook received"
# ✅ "Config file loaded successfully"
# ✅ "file matched pattern"
# ✅ "Copied file to target repo"
# Should NOT see:
# ❌ "failed to load webhook secret"
# ❌ "failed to load MongoDB URI"
# ❌ "webhook signature verification failed"# Recent logs
gcloud run services logs read github-copier --limit=100
# Real-time log streaming
gcloud beta run services logs tail github-copier# Metrics endpoint
curl https://YOUR_SERVICE_URL/metrics
# Response includes:
# - webhooks_received
# - webhooks_processed
# - files_matched
# - files_uploaded
# - processing_time (p50, p95, p99)Query MongoDB for audit events:
// Connect to MongoDB
mongosh "mongodb+srv://..."
// Recent events
db.audit_events.find().sort({timestamp: -1}).limit(10)
// Failed operations
db.audit_events.find({success: false})
// Statistics by rule
db.audit_events.aggregate([
{$match: {event_type: "copy"}},
{$group: {
_id: "$rule_name",
count: {$sum: 1}
}}
])Quick reference checklist for deploying the GitHub Code Example Copier to Google Cloud Run.
# Verify Go
go version # Should be 1.23+
# Verify gcloud
gcloud --version
# Verify authentication
gcloud auth list# Set project
gcloud config set project YOUR_PROJECT_ID
# Verify
gcloud config get-value project
# Enable required APIs
gcloud services enable secretmanager.googleapis.com
gcloud services enable appengine.googleapis.com# List secrets
gcloud secrets list
# Expected secrets:
# ✅ CODE_COPIER_PEM - GitHub App private key
# ✅ webhook-secret - Webhook signature validation
# ✅ mongo-uri - MongoDB connection (optional)If secrets don't exist, create them:
# GitHub private key
gcloud secrets create CODE_COPIER_PEM \
--data-file=/path/to/private-key.pem \
--replication-policy="automatic"
# Webhook secret
WEBHOOK_SECRET=$(openssl rand -hex 32)
echo -n "$WEBHOOK_SECRET" | gcloud secrets create webhook-secret \
--data-file=- \
--replication-policy="automatic"
echo "Save this: $WEBHOOK_SECRET"
# MongoDB URI (optional)
echo -n "mongodb+srv://..." | gcloud secrets create mongo-uri \
--data-file=- \
--replication-policy="automatic"# Run the grant script
cd github-copier
./scripts/grant-secret-access.shOr manually:
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
# Use the Cloud Run service account (default compute, or a custom one)
SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
gcloud secrets add-iam-policy-binding CODE_COPIER_PEM \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding webhook-secret \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding mongo-uri \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"Verify:
gcloud secrets get-iam-policy CODE_COPIER_PEM | grep compute
gcloud secrets get-iam-policy webhook-secret | grep compute
gcloud secrets get-iam-policy mongo-uri | grep computecd github-copier
# Copy from template
cp configs/env.yaml.production env.yaml
# Or convert from .env
./scripts/convert-env-to-yaml.sh configs/.env env.yaml
# Edit with your values if needed
nano env.yamlRequired changes in env.yaml:
GITHUB_APP_ID- Your GitHub App IDINSTALLATION_ID- Your installation IDREPO_OWNER- Source repository ownerREPO_NAME- Source repository nameGITHUB_APP_PRIVATE_KEY_SECRET_NAME- Update project numberWEBHOOK_SECRET_NAME- Update project numberMONGO_URI_SECRET_NAME- Update project number (if using audit logging)GOOGLE_PROJECT_ID- Your Google Cloud project ID
# Check
grep "env.yaml" .gitignore
# If not found, add it
echo "env.yaml" >> .gitignoreEnsure the Dockerfile exists and is correctly configured. The app uses a multi-stage build with a non-root user and HEALTHCHECK.
cd github-copier
# Deploy using the deployment script
./scripts/deploy-cloudrun.sh# Get service URL
gcloud run services describe github-copier --format="value(status.url)"# View recent logs
gcloud run services logs read github-copier --limit=50Look for:
- ✅ "Starting web server on port :8080"
- ✅ No errors about secrets
- ✅ No "failed to load webhook secret"
- ✅ No "failed to load MongoDB URI"
Should NOT see:
- ❌ "failed to load webhook secret"
- ❌ "failed to load MongoDB URI"
- ❌ "SKIP_SECRET_MANAGER=true"
# Get service URL
SERVICE_URL=$(gcloud run services describe github-copier --format="value(status.url)")
# Test health (liveness)
curl ${SERVICE_URL}/health
# Test readiness
curl ${SERVICE_URL}/readyExpected response:
{
"status": "healthy",
"started": true,
"github": {
"status": "healthy",
"authenticated": true
},
"queues": {
"upload_count": 0,
"deprecation_count": 0
},
"uptime": "1m23s"
}# Get the webhook secret value
gcloud secrets versions access latest --secret=webhook-secretSave this value - you'll need it for GitHub webhook configuration.
-
Go to repository settings
- URL:
https://github.com/YOUR_ORG/YOUR_REPO/settings/hooks
- URL:
-
Add or edit webhook
- Payload URL:
https://YOUR_SERVICE_URL/events - Content type:
application/json - Secret: (paste the value from step 12)
- SSL verification: Enable SSL verification
- Events: Select "Pull requests"
- Active: ✓ Checked
- Payload URL:
-
Save webhook
Option A: Redeliver existing webhook
- Go to webhook settings
- Click "Recent Deliveries"
- Click on a delivery
- Click "Redeliver"
Option B: Create test PR
- Create a test PR in your source repository
- Merge it
- Watch logs for webhook receipt
# Watch logs
gcloud run services logs read github-copier --limit=50# Check logs for secret loading
gcloud run services logs read github-copier --limit=100Should NOT see:
- ❌ "failed to load webhook secret"
- ❌ "failed to load MongoDB URI"
# Watch logs during webhook delivery
gcloud run services logs read github-copier --limit=50Look for:
- "webhook received"
- "signature verified"
- "processing webhook"
Should NOT see:
- "webhook signature verification failed"
- "invalid signature"
# Watch logs during PR merge
gcloud run services logs read github-copier --limit=50Look for:
- ✅ "Config file loaded successfully"
- ✅ "file matched pattern"
- ✅ "Copied file to target repo"
# Connect to MongoDB
mongosh "YOUR_MONGO_URI"
# Check for recent events
db.audit_events.find().sort({timestamp: -1}).limit(5)# Check metrics endpoint
curl https://YOUR_SERVICE_URL/metricsExpected response:
{
"webhooks": {
"received": 1,
"processed": 1,
"failed": 0
},
"files": {
"matched": 5,
"uploaded": 5,
"failed": 0
}
}# Verify env.yaml doesn't contain actual secrets
cat env.yaml | grep -E "BEGIN|mongodb\+srv|ghp_"
# Should return NOTHING (only Secret Manager paths)
# Verify env.yaml is not committed
git status | grep env.yaml
# Should show: nothing to commit (or untracked)
# Verify IAM permissions
gcloud secrets get-iam-policy CODE_COPIER_PEM | grep compute
gcloud secrets get-iam-policy webhook-secret | grep compute
# Should see the Cloud Run service accountCause: Secret Manager access denied
Fix:
./scripts/grant-secret-access.shCause: Secret in Secret Manager doesn't match GitHub webhook secret
Fix:
# Get secret from Secret Manager
gcloud secrets versions access latest --secret=webhook-secret
# Update GitHub webhook with this value
# OR update Secret Manager with GitHub's valueCause: Audit logging enabled but MongoDB URI not loaded
Fix:
# Option 1: Disable audit logging
# In env-cloudrun.yaml: AUDIT_ENABLED: "false"
# Option 2: Ensure MONGO_URI_SECRET_NAME is set
# In env-cloudrun.yaml: MONGO_URI_SECRET_NAME: "projects/.../secrets/mongo-uri/versions/latest"
# Redeploy
./scripts/deploy-cloudrun.shCause: Main config file missing from config repository
Fix:
# Add main config file to your config repository
# Default location: .copier/workflows/main.yaml
# See MAIN-CONFIG-README.md for formatAll items should be ✅:
- ✅ Deployment completes without errors
- ✅ Cloud Run service is running
- ✅ Health endpoint returns 200 OK
- ✅ Logs show no secret loading errors
- ✅ Webhook receives PR events
- ✅ Webhook signature validation works
- ✅ Files are copied to target repos
- ✅ Audit events logged (if enabled)
- ✅ Metrics available (if enabled)
- ✅ No secrets in config files
- ✅ env.yaml not in version control
Your application is deployed with:
- ✅ All secrets in Secret Manager (secure!)
- ✅ No hardcoded secrets in config files
- ✅ Easy secret rotation (just update in Secret Manager)
- ✅ Audit trail of secret access
- ✅ Fine-grained IAM permissions
Next steps:
- Monitor logs for first few PRs
- Verify files are copied correctly
- Set up alerts (optional)
- Document any custom configuration
# Deploy
./scripts/deploy-cloudrun.sh
# View logs
gcloud run services logs read github-copier --limit=100
# Check health
curl https://YOUR_SERVICE_URL/health
# Check readiness
curl https://YOUR_SERVICE_URL/ready
# Check metrics
curl https://YOUR_SERVICE_URL/metrics
# List secrets
gcloud secrets list
# Get secret value
gcloud secrets versions access latest --secret=SECRET_NAME
# Grant access
./scripts/grant-secret-access.sh| Error | Cause | Solution |
|---|---|---|
| "failed to load webhook secret" | Secret Manager access denied | Run ./grant-secret-access.sh |
| "webhook signature verification failed" | Secret mismatch | Verify secret matches GitHub webhook |
| "MONGO_URI is required" | Audit enabled but no URI | Set MONGO_URI_SECRET_NAME or disable audit |
| "Config file not found" | Missing main config | Add main config to config repo |
# Grant secret access
./scripts/grant-secret-access.sh
# View secret value
gcloud secrets versions access latest --secret=webhook-secret
# Disable audit logging
# In env-cloudrun.yaml: AUDIT_ENABLED: "false"
# Redeploy
./scripts/deploy-cloudrun.sh- Monitor first few PRs - Watch logs to ensure files are copied correctly
- Set up alerts (optional) - Configure Cloud Monitoring alerts
- Document custom config - Add notes about your specific setup
- Plan secret rotation - Schedule regular secret updates
See also:
- FAQ.md - Frequently asked questions
- TROUBLESHOOTING.md - Troubleshooting guide