Initial commit
This commit is contained in:
269
skills/devops/scripts/cloudflare_deploy.py
Normal file
269
skills/devops/scripts/cloudflare_deploy.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user