253 lines
6.3 KiB
Python
Executable File
253 lines
6.3 KiB
Python
Executable File
#!/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()
|