| Server IP : 3.147.158.171 / Your IP : 216.73.216.216 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/builder/backend/ |
Upload File : |
#!/usr/bin/env python3
"""
Build Runner CLI
Execute build configurations from the command line.
Usage:
python run_build.py [options]
Options:
--config <path> Path to JSON build config file
--dry-run Validate steps without executing
--verbose Enable verbose output
Examples:
# Run the example build config
python run_build.py --config configs/example.json
# Dry run to validate
python run_build.py --config configs/example.json --dry-run
# Run with inline config (uses default example)
python run_build.py
"""
import argparse
import asyncio
import json
import logging
import os
import re
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
# Add app to path
sys.path.insert(0, str(Path(__file__).parent))
from app.types import BuildConfig, BuildStep, StepType
from app.services.builder_service import BuilderService
# Directory containing config files
CONFIGS_DIR = Path(__file__).parent / 'configs'
# Load environment variables from .env
SCRIPTS_ENV_PATH = Path(__file__).parent / '.env'
if SCRIPTS_ENV_PATH.exists():
load_dotenv(SCRIPTS_ENV_PATH)
def substitute_env_vars(value: str) -> str:
"""Substitute ${VAR_NAME} placeholders with environment variable values."""
if not isinstance(value, str):
return value
def replace_var(match):
var_name = match.group(1)
env_value = os.getenv(var_name, '')
if not env_value:
logging.warning(f"Environment variable {var_name} not set")
return env_value
# Match ${VAR_NAME} pattern
return re.sub(r'\$\{(\w+)\}', replace_var, value)
def substitute_config_vars(data: dict) -> dict:
"""Recursively substitute environment variables in config data."""
if isinstance(data, dict):
return {k: substitute_config_vars(v) for k, v in data.items()}
elif isinstance(data, list):
return [substitute_config_vars(item) for item in data]
elif isinstance(data, str):
return substitute_env_vars(data)
else:
return data
def list_configs():
"""List all available build configurations."""
if not CONFIGS_DIR.exists():
print(f"Config directory not found: {CONFIGS_DIR}")
return
configs = sorted(CONFIGS_DIR.glob('*.json'))
if not configs:
print("No configuration files found.")
return
print("\nAvailable build configurations:")
print("=" * 60)
for config_path in configs:
try:
with open(config_path, 'r') as f:
data = json.load(f)
name = data.get('name', config_path.stem)
description = data.get('description', 'No description')
steps = len(data.get('steps', []))
print(f"\n {config_path.name}")
print(f" Name: {name}")
print(f" Desc: {description}")
print(f" Steps: {steps}")
except Exception as e:
print(f"\n {config_path.name}")
print(f" Error reading config: {e}")
print("\n" + "=" * 60)
print("\nUsage: python run_build.py -c configs/<name>.json")
print()
def setup_logging(verbose: bool = False):
"""Configure logging for CLI output."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format='%(levelname)s %(asctime)s %(message)s',
datefmt='%H:%M:%S'
)
def load_config_from_file(config_path: str) -> BuildConfig:
"""Load build configuration from a JSON file with env variable substitution."""
with open(config_path, 'r') as f:
data = json.load(f)
# Substitute ${VAR_NAME} placeholders with env values
data = substitute_config_vars(data)
return BuildConfig(**data)
def create_example_config() -> BuildConfig:
"""Create an example build configuration for testing."""
return BuildConfig(
name="Example Build",
description="A sample build configuration demonstrating various step types",
steps=[
BuildStep(
name="Check Python version",
step_type=StepType.SHELL,
command="python --version",
timeout=30
),
BuildStep(
name="List current directory",
step_type=StepType.SHELL,
command="ls -la",
timeout=30
),
BuildStep(
name="Show disk usage",
step_type=StepType.SHELL,
command="df -h",
timeout=30
),
BuildStep(
name="Wait 2 seconds",
step_type=StepType.WAIT,
timeout=2
),
BuildStep(
name="Show date",
step_type=StepType.SHELL,
command="date",
timeout=30
),
],
stop_on_failure=True
)
async def run_build_cli(config: BuildConfig, dry_run: bool = False):
"""Run a build and print progress to console."""
builder = BuilderService()
print(f"\n{'='*60}")
print(f"BUILD: {config.name}")
if config.description:
print(f" {config.description}")
print(f"STEPS: {len(config.steps)}")
if dry_run:
print("MODE: DRY RUN (validation only)")
print(f"{'='*60}\n")
start_time = time.time()
# Progress callback for console output
async def console_progress(event):
status = event.status.upper()
progress = f"[{event.progress:3d}%]"
if event.status == 'starting':
print(f"{progress} {event.message}")
elif event.status == 'running_step':
print(f"\n{progress} {event.message}")
elif event.status == 'step_complete':
if event.step_result:
result = event.step_result
status_icon = "OK" if result.status.value == 'success' else "FAIL"
duration = f"({result.duration:.2f}s)"
print(f" [{status_icon}] {result.step_name} {duration}")
if result.output and len(result.output) < 200:
# Print short outputs
for line in result.output.split('\n')[:3]:
print(f" {line}")
if result.error:
print(f" ERROR: {result.error}")
result = await builder.run_build(
config=config,
dry_run=dry_run,
progress_callback=console_progress
)
# Print summary
elapsed = time.time() - start_time
print(f"\n{'='*60}")
print(f"BUILD {'SUCCEEDED' if result.success else 'FAILED'}")
print(f"{'='*60}")
print(f"Build ID: {result.build_id}")
print(f"Total Steps: {result.total_steps}")
print(f"Completed: {result.completed_steps}")
print(f"Failed: {result.failed_steps}")
print(f"Skipped: {result.skipped_steps}")
print(f"Duration: {result.total_duration:.2f}s")
if result.error:
print(f"\nError: {result.error}")
print()
return result.success
def main():
parser = argparse.ArgumentParser(
description="Build Runner CLI - Execute build configurations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python run_build.py --list List available configs
python run_build.py -c configs/example.json Run example build
python run_build.py -c configs/example.json -n Dry-run (validate only)
python run_build.py -c configs/deploy-to-server.json Full deployment
# Create a new site with parameters:
python run_build.py -c configs/build-site.json \\
--slug mysite \\
--wp-dir mysite.ai \\
--wp-host http://mysite.turmansolutions.test \\
--cb-host chat-mysite.turmansolutions.test
# Create SSL certificate:
python run_build.py -c configs/create-ssl-cert.json \\
--wp-domain mysite.turmansolutions.ai \\
--cb-domain chat-mysite.turmansolutions.ai
Common configs:
deploy-to-server.json - Build and deploy to production server
build-angular.json - Build Angular apps only (no deploy)
db-backup.json - Run database backups
build-site.json - Create a new site configuration
create-ssl-cert.json - Issue and install SSL certificate
example.json - Simple test configuration
"""
)
parser.add_argument(
'--config', '-c',
type=str,
metavar='FILE',
help='Path to JSON build config file (e.g., configs/deploy-to-server.json)'
)
parser.add_argument(
'--list', '-l',
action='store_true',
help='List all available build configurations'
)
parser.add_argument(
'--dry-run', '-n',
action='store_true',
help='Validate steps without executing (safe to run)'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose/debug output'
)
# Site creation parameters (optional)
parser.add_argument(
'--slug',
type=str,
help='Site slug for build-site config'
)
parser.add_argument(
'--wp-dir',
type=str,
help='WordPress directory for build-site config'
)
parser.add_argument(
'--wp-host',
type=str,
help='WordPress host URL for build-site config'
)
parser.add_argument(
'--cb-host',
type=str,
help='Chatbot host for build-site config'
)
parser.add_argument(
'--wp-domain',
type=str,
help='WordPress domain for SSL cert config (e.g., mysite.turmansolutions.ai)'
)
parser.add_argument(
'--cb-domain',
type=str,
help='Chatbot domain for SSL cert config (e.g., chat-mysite.turmansolutions.ai)'
)
parser.add_argument(
'--site-name',
type=str,
help='Site name for WordPress installation (e.g., My New Site)'
)
parser.add_argument(
'--admin-email',
type=str,
help='Admin email for WordPress installation'
)
args = parser.parse_args()
# Handle --list first
if args.list:
list_configs()
sys.exit(0)
setup_logging(args.verbose)
# Inject CLI parameters as environment variables for use in configs
if args.slug:
os.environ['PARAM_SLUG'] = args.slug
os.environ['PARAM_DB_SLUG'] = args.slug.replace('-', '_')
if args.wp_dir:
os.environ['PARAM_WP_DIR'] = args.wp_dir
if args.wp_host:
os.environ['PARAM_WP_HOST'] = args.wp_host
if args.cb_host:
os.environ['PARAM_CB_HOST'] = args.cb_host
if args.wp_domain:
os.environ['PARAM_WP_DOMAIN'] = args.wp_domain
if args.cb_domain:
os.environ['PARAM_CB_DOMAIN'] = args.cb_domain
if args.site_name:
os.environ['PARAM_SITE_NAME'] = args.site_name
if args.admin_email:
os.environ['PARAM_ADMIN_EMAIL'] = args.admin_email
# Load or create config
if args.config:
config_path = Path(args.config)
if not config_path.exists():
print(f"Error: Config file not found: {args.config}")
sys.exit(1)
config = load_config_from_file(args.config)
print(f"Loaded config from: {args.config}")
else:
config = create_example_config()
print("Using example build config (no --config specified)")
# Run the build
try:
success = asyncio.run(run_build_cli(config, dry_run=args.dry_run))
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\nBuild interrupted by user")
sys.exit(130)
except Exception as e:
logging.error(f"Build error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()