270 lines
7.5 KiB
Python
270 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Cloudflare Worker Deployment Utility
|
|
|
|
Automates Cloudflare Worker deployments with wrangler.toml configuration handling,
|
|
multi-environment support, and comprehensive error handling.
|
|
|
|
Usage:
|
|
python cloudflare-deploy.py --env production --dry-run
|
|
python cloudflare-deploy.py --project ./my-worker --env staging
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
|
|
class CloudflareDeployError(Exception):
|
|
"""Custom exception for Cloudflare deployment errors."""
|
|
pass
|
|
|
|
|
|
class CloudflareDeploy:
|
|
"""Handle Cloudflare Worker deployments with wrangler CLI."""
|
|
|
|
def __init__(self, project_dir: Path, env: Optional[str] = None,
|
|
dry_run: bool = False, verbose: bool = False):
|
|
"""
|
|
Initialize CloudflareDeploy.
|
|
|
|
Args:
|
|
project_dir: Path to Worker project directory
|
|
env: Environment name (production, staging, dev)
|
|
dry_run: Preview deployment without actually deploying
|
|
verbose: Enable verbose output
|
|
"""
|
|
self.project_dir = Path(project_dir).resolve()
|
|
self.env = env
|
|
self.dry_run = dry_run
|
|
self.verbose = verbose
|
|
self.wrangler_toml = self.project_dir / "wrangler.toml"
|
|
|
|
def validate_project(self) -> bool:
|
|
"""
|
|
Validate project directory and wrangler.toml existence.
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
|
|
Raises:
|
|
CloudflareDeployError: If validation fails
|
|
"""
|
|
if not self.project_dir.exists():
|
|
raise CloudflareDeployError(
|
|
f"Project directory does not exist: {self.project_dir}"
|
|
)
|
|
|
|
if not self.wrangler_toml.exists():
|
|
raise CloudflareDeployError(
|
|
f"wrangler.toml not found in: {self.project_dir}"
|
|
)
|
|
|
|
return True
|
|
|
|
def check_wrangler_installed(self) -> bool:
|
|
"""
|
|
Check if wrangler CLI is installed.
|
|
|
|
Returns:
|
|
True if installed, False otherwise
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["wrangler", "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
if self.verbose:
|
|
print(f"Wrangler version: {result.stdout.strip()}")
|
|
return True
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return False
|
|
|
|
def run_command(self, cmd: List[str], check: bool = True) -> Tuple[int, str, str]:
|
|
"""
|
|
Run shell command and capture output.
|
|
|
|
Args:
|
|
cmd: Command and arguments as list
|
|
check: Raise exception on non-zero exit code
|
|
|
|
Returns:
|
|
Tuple of (exit_code, stdout, stderr)
|
|
|
|
Raises:
|
|
CloudflareDeployError: If command fails and check=True
|
|
"""
|
|
if self.verbose:
|
|
print(f"Running: {' '.join(cmd)}")
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=self.project_dir,
|
|
check=check
|
|
)
|
|
return result.returncode, result.stdout, result.stderr
|
|
except subprocess.CalledProcessError as e:
|
|
if check:
|
|
raise CloudflareDeployError(
|
|
f"Command failed: {' '.join(cmd)}\n{e.stderr}"
|
|
)
|
|
return e.returncode, e.stdout, e.stderr
|
|
|
|
def get_worker_name(self) -> str:
|
|
"""
|
|
Extract worker name from wrangler.toml.
|
|
|
|
Returns:
|
|
Worker name
|
|
|
|
Raises:
|
|
CloudflareDeployError: If name cannot be extracted
|
|
"""
|
|
try:
|
|
with open(self.wrangler_toml, 'r') as f:
|
|
for line in f:
|
|
if line.strip().startswith('name'):
|
|
# Parse: name = "worker-name"
|
|
return line.split('=')[1].strip().strip('"\'')
|
|
except Exception as e:
|
|
raise CloudflareDeployError(f"Failed to read worker name: {e}")
|
|
|
|
raise CloudflareDeployError("Worker name not found in wrangler.toml")
|
|
|
|
def build_deploy_command(self) -> List[str]:
|
|
"""
|
|
Build wrangler deploy command with appropriate flags.
|
|
|
|
Returns:
|
|
Command as list of strings
|
|
"""
|
|
cmd = ["wrangler", "deploy"]
|
|
|
|
if self.env:
|
|
cmd.extend(["--env", self.env])
|
|
|
|
if self.dry_run:
|
|
cmd.append("--dry-run")
|
|
|
|
return cmd
|
|
|
|
def deploy(self) -> bool:
|
|
"""
|
|
Execute deployment.
|
|
|
|
Returns:
|
|
True if successful
|
|
|
|
Raises:
|
|
CloudflareDeployError: If deployment fails
|
|
"""
|
|
# Validate
|
|
self.validate_project()
|
|
|
|
if not self.check_wrangler_installed():
|
|
raise CloudflareDeployError(
|
|
"wrangler CLI not installed. Install: npm install -g wrangler"
|
|
)
|
|
|
|
worker_name = self.get_worker_name()
|
|
env_suffix = f" ({self.env})" if self.env else ""
|
|
mode = "DRY RUN" if self.dry_run else "DEPLOY"
|
|
|
|
print(f"\n{mode}: {worker_name}{env_suffix}")
|
|
print(f"Project: {self.project_dir}\n")
|
|
|
|
# Build and run command
|
|
cmd = self.build_deploy_command()
|
|
exit_code, stdout, stderr = self.run_command(cmd)
|
|
|
|
# Output results
|
|
if stdout:
|
|
print(stdout)
|
|
if stderr:
|
|
print(stderr, file=sys.stderr)
|
|
|
|
if exit_code == 0:
|
|
status = "would be deployed" if self.dry_run else "deployed successfully"
|
|
print(f"\n✓ Worker {status}")
|
|
return True
|
|
else:
|
|
raise CloudflareDeployError("Deployment failed")
|
|
|
|
|
|
def main():
|
|
"""CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Deploy Cloudflare Worker with wrangler",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python cloudflare-deploy.py
|
|
python cloudflare-deploy.py --env production
|
|
python cloudflare-deploy.py --project ./my-worker --env staging
|
|
python cloudflare-deploy.py --dry-run
|
|
python cloudflare-deploy.py --env prod --verbose
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--project",
|
|
type=str,
|
|
default=".",
|
|
help="Path to Worker project directory (default: current directory)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--env",
|
|
type=str,
|
|
choices=["production", "staging", "dev"],
|
|
help="Environment to deploy to (production, staging, dev)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Preview deployment without actually deploying"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
action="store_true",
|
|
help="Enable verbose output"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
deployer = CloudflareDeploy(
|
|
project_dir=args.project,
|
|
env=args.env,
|
|
dry_run=args.dry_run,
|
|
verbose=args.verbose
|
|
)
|
|
|
|
success = deployer.deploy()
|
|
sys.exit(0 if success else 1)
|
|
|
|
except CloudflareDeployError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except KeyboardInterrupt:
|
|
print("\nDeployment cancelled by user", file=sys.stderr)
|
|
sys.exit(130)
|
|
except Exception as e:
|
|
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|