#!/usr/bin/env python3 """ Deploy Grey Haven applications to Cloudflare Workers with Doppler secrets. This script automates the deployment workflow: 1. Runs tests with Doppler environment 2. Builds the application 3. Runs database migrations 4. Deploys to Cloudflare Workers with Wrangler 5. Injects secrets from Doppler 6. Runs smoke tests 7. Automatically rolls back on failure Usage: # Development deployment python scripts/deploy.py --env dev # Staging deployment python scripts/deploy.py --env staging # Production deployment (with confirmation) python scripts/deploy.py --env production # Skip tests (not recommended) python scripts/deploy.py --env staging --skip-tests # Dry run (show what would happen) python scripts/deploy.py --env production --dry-run Always run with --help first to see all options. """ import argparse import subprocess import sys import json import os from typing import Optional def run_command(cmd: str, description: str, dry_run: bool = False) -> bool: """Run a shell command and return success status.""" print(f"\n{'[DRY RUN] ' if dry_run else ''}→ {description}") print(f" Command: {cmd}") if dry_run: return True result = subprocess.run(cmd, shell=True, capture_output=False) return result.returncode == 0 def confirm_production_deploy() -> bool: """Ask for confirmation before production deployment.""" print("\nWARNING: WARNING: You are about to deploy to PRODUCTION") print("This will affect live users.") response = input("Type 'deploy to production' to confirm: ") return response.strip().lower() == "deploy to production" def get_wrangler_config(env: str) -> str: """Get the appropriate wrangler config file for environment.""" if env == "production": return "wrangler.production.toml" elif env == "staging": return "wrangler.staging.toml" else: return "wrangler.toml" def main(): parser = argparse.ArgumentParser( description="Deploy Grey Haven application to Cloudflare Workers", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python scripts/deploy.py --env dev python scripts/deploy.py --env staging --skip-migrations python scripts/deploy.py --env production --dry-run Environments: dev - Development (wrangler.toml) staging - Staging (wrangler.staging.toml) production - Production (wrangler.production.toml) Doppler Configuration: Requires DOPPLER_TOKEN environment variable or doppler CLI configured. Secrets are injected from Doppler config matching the environment. """ ) parser.add_argument( "--env", required=True, choices=["dev", "staging", "production"], help="Deployment environment" ) parser.add_argument( "--skip-tests", action="store_true", help="Skip running tests (not recommended)" ) parser.add_argument( "--skip-migrations", action="store_true", help="Skip database migrations" ) parser.add_argument( "--skip-smoke-tests", action="store_true", help="Skip smoke tests after deployment" ) parser.add_argument( "--dry-run", action="store_true", help="Show what would happen without executing" ) args = parser.parse_args() # Production confirmation if args.env == "production" and not args.dry_run: if not confirm_production_deploy(): print("\nERROR: Production deployment cancelled") sys.exit(1) env = args.env wrangler_config = get_wrangler_config(env) print(f"\n{'=' * 70}") print(f" Grey Haven Deployment to {env.upper()}") print(f"{'=' * 70}") # Step 1: Run tests if not args.skip_tests: success = run_command( f"doppler run --config test -- npm run test", "Running tests", args.dry_run ) if not success: print("\nERROR: Tests failed. Deployment aborted.") sys.exit(1) else: print("\nWARNING: Skipping tests (--skip-tests)") # Step 2: Build application success = run_command( f"doppler run --config {env} -- npm run build", f"Building application for {env}", args.dry_run ) if not success: print("\nERROR: Build failed. Deployment aborted.") sys.exit(1) # Step 3: Run database migrations if not args.skip_migrations: success = run_command( f"doppler run --config {env} -- npm run db:migrate", "Running database migrations", args.dry_run ) if not success: print("\nERROR: Migrations failed. Deployment aborted.") sys.exit(1) else: print("\nWARNING: Skipping migrations (--skip-migrations)") # Step 4: Deploy to Cloudflare Workers success = run_command( f"npx wrangler deploy --config {wrangler_config}", f"Deploying to Cloudflare Workers ({wrangler_config})", args.dry_run ) if not success: print("\nERROR: Deployment failed.") sys.exit(1) # Step 5: Inject Doppler secrets to Cloudflare Workers if not args.dry_run: print("\n→ Injecting Doppler secrets to Cloudflare Workers") print(" This may take a minute...") # Download secrets from Doppler result = subprocess.run( f"doppler secrets download --config {env} --format json", shell=True, capture_output=True, text=True ) if result.returncode != 0: print("\nERROR: Failed to download Doppler secrets") sys.exit(1) secrets = json.loads(result.stdout) # Inject each secret to Cloudflare Workers for key, value in secrets.items(): # Skip non-secret env vars (like NODE_ENV, ENVIRONMENT) if key in ["NODE_ENV", "ENVIRONMENT", "CI"]: continue cmd = f'echo "{value}" | npx wrangler secret put {key} --config {wrangler_config}' subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(f" ✓ Injected {len(secrets)} secrets") else: print("\n[DRY RUN] → Would inject Doppler secrets to Cloudflare Workers") # Step 6: Run smoke tests if not args.skip_smoke_tests: success = run_command( f"doppler run --config {env} -- npm run test:e2e:smoke", "Running smoke tests", args.dry_run ) if not success: print("\nWARNING: Smoke tests failed. Rolling back deployment...") if not args.dry_run: subprocess.run( f"npx wrangler rollback --config {wrangler_config}", shell=True ) print("\nERROR: Deployment rolled back due to smoke test failure") sys.exit(1) else: print("\nWARNING: Skipping smoke tests (--skip-smoke-tests)") # Success! print(f"\n{'=' * 70}") print(f" SUCCESS: Deployment to {env.upper()} successful!") print(f"{'=' * 70}") if env == "production": print(f"\n Production URL: https://app.greyhaven.studio") elif env == "staging": print(f"\n Staging URL: https://staging.greyhaven.studio") else: print(f"\n Dev URL: https://dev.greyhaven.studio") print("\nNext steps:") print(" • Monitor logs: npx wrangler tail") print(" • Check Sentry for errors") print(" • Verify Axiom logs") if __name__ == "__main__": main()