Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:12 +08:00
commit 8d8103de19
10 changed files with 1264 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
#!/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()

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
Run database migrations for Grey Haven applications with Doppler.
Supports both Drizzle (TypeScript) and Alembic (Python) migrations
across multiple environments.
Usage:
# Run migrations for development
python scripts/migrate.py --env dev
# Run migrations for staging
python scripts/migrate.py --env staging
# Run migrations for production (with confirmation)
python scripts/migrate.py --env production
# Rollback last migration
python scripts/migrate.py --env dev --rollback
# Rollback to specific migration
python scripts/migrate.py --env dev --rollback --to 20250115_add_users
# Dry run (show what would happen)
python scripts/migrate.py --env production --dry-run
# Use Alembic instead of Drizzle
python scripts/migrate.py --env dev --backend alembic
Always run with --help first to see all options.
"""
import argparse
import subprocess
import sys
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_migration() -> bool:
"""Ask for confirmation before production migration."""
print("\nWARNING: WARNING: You are about to run migrations on PRODUCTION database")
print("This operation is IRREVERSIBLE and will affect live data.")
response = input("Type 'run production migrations' to confirm: ")
return response.strip().lower() == "run production migrations"
def main():
parser = argparse.ArgumentParser(
description="Run database migrations with Doppler environment variables",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run Drizzle migrations for development
python scripts/migrate.py --env dev
# Rollback last Drizzle migration
python scripts/migrate.py --env dev --rollback
# Run Alembic migrations for staging
python scripts/migrate.py --env staging --backend alembic
# Rollback to specific Alembic migration
python scripts/migrate.py --env staging --backend alembic --rollback --to abc123
Environments:
dev - Local development database
test - CI/CD test database
staging - Staging database
production - Production database (requires confirmation)
Backends:
drizzle - Drizzle Kit (TypeScript/TanStack Start)
alembic - Alembic (Python/FastAPI)
Doppler Configuration:
Requires doppler CLI configured with appropriate access.
Uses DATABASE_URL_ADMIN from Doppler config.
"""
)
parser.add_argument(
"--env",
required=True,
choices=["dev", "test", "staging", "production"],
help="Environment to run migrations against"
)
parser.add_argument(
"--backend",
default="drizzle",
choices=["drizzle", "alembic"],
help="Migration backend to use (default: drizzle)"
)
parser.add_argument(
"--rollback",
action="store_true",
help="Rollback migrations instead of applying"
)
parser.add_argument(
"--to",
type=str,
help="Rollback to specific migration (use with --rollback)"
)
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 and not args.rollback:
if not confirm_production_migration():
print("\nERROR: Production migration cancelled")
sys.exit(1)
env = args.env
backend = args.backend
print(f"\n{'=' * 70}")
print(f" Database Migration - {env.upper()} ({backend.upper()})")
print(f"{'=' * 70}")
# Check Doppler configuration
if not args.dry_run:
result = subprocess.run(
f"doppler secrets get DATABASE_URL_ADMIN --config {env}",
shell=True,
capture_output=True
)
if result.returncode != 0:
print(f"\nERROR: Failed to get DATABASE_URL_ADMIN from Doppler config '{env}'")
print(" Make sure Doppler is configured: doppler setup")
sys.exit(1)
# Construct migration command
if backend == "drizzle":
if args.rollback:
if args.to:
cmd = f"doppler run --config {env} -- drizzle-kit migrate:rollback --to {args.to}"
else:
cmd = f"doppler run --config {env} -- drizzle-kit migrate:rollback"
description = "Rolling back Drizzle migration"
else:
cmd = f"doppler run --config {env} -- drizzle-kit push:pg"
description = "Applying Drizzle migrations"
elif backend == "alembic":
if args.rollback:
if args.to:
cmd = f"doppler run --config {env} -- alembic downgrade {args.to}"
else:
cmd = f"doppler run --config {env} -- alembic downgrade -1"
description = "Rolling back Alembic migration"
else:
cmd = f"doppler run --config {env} -- alembic upgrade head"
description = "Applying Alembic migrations"
# Run migration
success = run_command(cmd, description, args.dry_run)
if not success:
print(f"\nERROR: Migration failed for {env}")
sys.exit(1)
# Success!
print(f"\n{'=' * 70}")
if args.rollback:
print(f" SUCCESS: Rollback successful for {env.upper()}")
else:
print(f" SUCCESS: Migration successful for {env.upper()}")
print(f"{'=' * 70}")
if not args.rollback:
print("\nNext steps:")
print(" • Verify schema changes in database")
print(" • Run tests: doppler run --config test -- npm run test")
print(" • Deploy application if migrations succeeded")
else:
print("\nNext steps:")
print(" • Verify rollback was successful")
print(" • Re-deploy previous application version if needed")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Rollback Grey Haven Cloudflare Workers deployment to previous version.
This script handles emergency rollbacks when a deployment fails or causes
production issues. It can rollback both the Workers deployment and database
migrations.
Usage:
# Rollback to previous Workers deployment
python scripts/rollback.py --env production
# Rollback Workers and database migration
python scripts/rollback.py --env production --with-migration
# Rollback to specific deployment ID
python scripts/rollback.py --env production --deployment-id abc123
# Rollback database only
python scripts/rollback.py --env production --migration-only
# Dry run (show what would happen)
python scripts/rollback.py --env production --dry-run
Always run with --help first to see all options.
"""
import argparse
import subprocess
import sys
from typing import Optional
def run_command(cmd: str, description: str, dry_run: bool = False, capture: bool = False) -> tuple[bool, Optional[str]]:
"""Run a shell command and return success status and output."""
print(f"\n{'[DRY RUN] ' if dry_run else ''}{description}")
print(f" Command: {cmd}")
if dry_run:
return True, None
result = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
return result.returncode == 0, result.stdout if capture else None
def confirm_production_rollback() -> bool:
"""Ask for confirmation before production rollback."""
print("\nWARNING: WARNING: You are about to ROLLBACK PRODUCTION deployment")
print("This will affect live users immediately.")
response = input("Type 'rollback production' to confirm: ")
return response.strip().lower() == "rollback 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 list_recent_deployments(wrangler_config: str, dry_run: bool = False) -> None:
"""List recent deployments for reference."""
if dry_run:
print("\n[DRY RUN] → Would list recent deployments")
return
print("\n→ Fetching recent deployments...")
subprocess.run(
f"npx wrangler deployments list --config {wrangler_config}",
shell=True
)
def main():
parser = argparse.ArgumentParser(
description="Rollback Grey Haven Cloudflare Workers deployment",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Emergency rollback of production Workers deployment
python scripts/rollback.py --env production
# Rollback Workers and database migration
python scripts/rollback.py --env production --with-migration
# Rollback to specific deployment
python scripts/rollback.py --env production --deployment-id abc123
# Rollback database migration only
python scripts/rollback.py --env production --migration-only --backend drizzle
Environments:
dev - Development
staging - Staging
production - Production (requires confirmation)
Emergency Rollback Procedure:
1. Identify the issue (check Sentry, Axiom, Cloudflare logs)
2. Run rollback script with appropriate flags
3. Verify rollback with smoke tests
4. Notify team and update Linear issue
5. Create postmortem for root cause analysis
"""
)
parser.add_argument(
"--env",
required=True,
choices=["dev", "staging", "production"],
help="Environment to rollback"
)
parser.add_argument(
"--deployment-id",
type=str,
help="Specific deployment ID to rollback to (optional)"
)
parser.add_argument(
"--with-migration",
action="store_true",
help="Also rollback database migration"
)
parser.add_argument(
"--migration-only",
action="store_true",
help="Rollback database migration only (not Workers)"
)
parser.add_argument(
"--backend",
default="drizzle",
choices=["drizzle", "alembic"],
help="Migration backend (default: drizzle)"
)
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_rollback():
print("\nERROR: Production rollback cancelled")
sys.exit(1)
env = args.env
wrangler_config = get_wrangler_config(env)
print(f"\n{'=' * 70}")
print(f" Emergency Rollback - {env.upper()}")
print(f"{'=' * 70}")
# Rollback Workers deployment (unless migration-only)
if not args.migration_only:
# List recent deployments first
if not args.deployment_id:
list_recent_deployments(wrangler_config, args.dry_run)
# Construct rollback command
if args.deployment_id:
cmd = f"npx wrangler rollback --deployment-id {args.deployment_id} --config {wrangler_config}"
description = f"Rolling back to deployment {args.deployment_id}"
else:
cmd = f"npx wrangler rollback --config {wrangler_config}"
description = "Rolling back to previous deployment"
success, _ = run_command(cmd, description, args.dry_run)
if not success:
print(f"\nERROR: Workers rollback failed for {env}")
sys.exit(1)
print(f"\nSUCCESS: Workers deployment rolled back successfully")
# Rollback database migration (if requested)
if args.with_migration or args.migration_only:
backend = args.backend
print(f"\n→ Rolling back {backend} migration for {env}")
if backend == "drizzle":
cmd = f"doppler run --config {env} -- drizzle-kit migrate:rollback"
elif backend == "alembic":
cmd = f"doppler run --config {env} -- alembic downgrade -1"
success, _ = run_command(cmd, f"Rolling back {backend} migration", args.dry_run)
if not success:
print(f"\nERROR: Database migration rollback failed for {env}")
sys.exit(1)
print(f"\nSUCCESS: Database migration rolled back successfully")
# Success!
print(f"\n{'=' * 70}")
print(f" SUCCESS: Rollback complete for {env.upper()}")
print(f"{'=' * 70}")
# Run smoke tests
print("\n→ Running smoke tests to verify rollback...")
success, _ = run_command(
f"doppler run --config {env} -- npm run test:e2e:smoke",
"Verifying rollback with smoke tests",
args.dry_run
)
if not success:
print("\nWARNING: Warning: Smoke tests failed after rollback")
print(" Manual verification required!")
else:
print("\nSUCCESS: Smoke tests passed - rollback verified")
# Post-rollback checklist
print("\n📋 Post-Rollback Checklist:")
print(" ✓ Deployment rolled back")
if args.with_migration or args.migration_only:
print(" ✓ Database migration rolled back")
print("\n WARNING: Action Items:")
print(" • Check Sentry for errors")
print(" • Verify Axiom logs")
print(" • Monitor Cloudflare Workers analytics")
print(" • Update Linear issue with rollback status")
print(" • Create postmortem for root cause analysis")
print(" • Fix the issue before re-deploying")
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")
if __name__ == "__main__":
main()