Files
blackroad-operating-system/scripts/validate_deployment_config.py
Claude 9a728f655a Prevent BlackRoad-Operating-System monorepo from being added to Railway services
CRITICAL CHANGES:
- Add comprehensive deployment architecture documentation
- Prevent misconfiguration where monorepo is deployed instead of satellites
- Clarify monorepo-to-satellite sync model across all docs

CHANGES:
1. railway.toml
   - Add critical warning banner at top of file
   - Mark config as local development/testing only
   - Explain correct deployment model (satellites, not monorepo)

2. DEPLOYMENT_ARCHITECTURE.md (NEW)
   - Complete 500+ line deployment guide
   - Monorepo vs satellite model explained in detail
   - Critical rules: NEVER add monorepo to Railway
   - Service-to-repository mapping
   - Environment configuration guide
   - Cloudflare DNS configuration
   - Common mistakes and troubleshooting

3. README.md
   - Add prominent deployment warning box
   - Clarify monorepo is source of truth, not deployable
   - List satellite repos that should be deployed
   - Reference DEPLOYMENT_ARCHITECTURE.md

4. CLAUDE.md
   - Add critical deployment model section
   - Clarify Railway deployment is satellite-only
   - Update deployment workflow explanation
   - Add key rules for deployment

5. backend/.env.example
   - Fix ALLOWED_ORIGINS to reference satellites
   - Remove monorepo Railway URL reference
   - Add correct satellite service URLs

6. ops/domains.yaml
   - Fix os.blackroad.systems DNS target
   - Point to blackroad-os-core-production (satellite)
   - Remove incorrect monorepo Railway URL

7. scripts/validate_deployment_config.py (NEW)
   - Automated validation script
   - Checks for monorepo references in configs
   - Validates railway.toml, env files, DNS configs
   - Ensures DEPLOYMENT_ARCHITECTURE.md exists
   - Exit code 0 = pass, 1 = fail

WHY THIS MATTERS:
- Adding monorepo to Railway creates circular deploy loops
- Environment variables break (wrong service URLs)
- Cloudflare routing fails
- Service dependencies misconfigured
- Prevents production outages from misconfiguration

CORRECT MODEL:
- Monorepo = source of truth (orchestration only)
- Satellites = deployable services (Railway deployment)
- Code flows: monorepo → sync → satellite → Railway

See: DEPLOYMENT_ARCHITECTURE.md for complete details
2025-11-19 22:31:22 +00:00

374 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Deployment Configuration Validator
This script validates that the BlackRoad-Operating-System monorepo
is NOT being incorrectly added to Railway configurations or service
environment variables.
Usage:
python scripts/validate_deployment_config.py
Exit codes:
0 - All validations passed
1 - Validation failures detected
Author: BlackRoad OS Team
Last Updated: 2025-11-19
"""
import os
import sys
import re
import json
import yaml
from pathlib import Path
from typing import List, Tuple, Dict
# ANSI color codes
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
BOLD = "\033[1m"
# Repository root
REPO_ROOT = Path(__file__).parent.parent
# Patterns that indicate monorepo is being incorrectly referenced
FORBIDDEN_PATTERNS = [
r"BlackRoad-Operating-System",
r"blackroad-operating-system",
r"BLACKROAD_OPERATING_SYSTEM",
r"monorepo\.up\.railway\.app",
r"blackroad-os-monorepo",
]
# Allowed contexts where monorepo reference is OK
ALLOWED_FILES = [
"README.md",
"DEPLOYMENT_ARCHITECTURE.md",
"CLAUDE.md",
"docs/",
".git/",
".github/",
"infra/github/",
"scripts/",
".md", # All markdown files (usually docs)
"IMPLEMENTATION", # Implementation plan docs
"PHASE", # Phase summary docs
"ORG_STRUCTURE.md",
"CODEBASE_STATUS.md",
]
# Files to check for forbidden patterns
CHECK_PATTERNS = [
"**/.env",
"**/.env.example",
"**/.env.production",
"**/.env.staging",
"**/.env.development",
"**/railway.json",
"**/vercel.json",
"**/netlify.toml",
]
class ValidationResult:
"""Stores validation results"""
def __init__(self):
self.errors: List[Tuple[str, str]] = []
self.warnings: List[Tuple[str, str]] = []
self.passed: List[str] = []
def add_error(self, check: str, message: str):
"""Add a validation error"""
self.errors.append((check, message))
def add_warning(self, check: str, message: str):
"""Add a validation warning"""
self.warnings.append((check, message))
def add_pass(self, check: str):
"""Add a passing check"""
self.passed.append(check)
def has_failures(self) -> bool:
"""Check if there are any failures"""
return len(self.errors) > 0
def print_summary(self):
"""Print validation summary"""
print(f"\n{BOLD}{'=' * 70}{RESET}")
print(f"{BOLD}Deployment Configuration Validation Results{RESET}")
print(f"{BOLD}{'=' * 70}{RESET}\n")
# Print errors
if self.errors:
print(f"{RED}{BOLD}❌ ERRORS ({len(self.errors)}):{RESET}")
for check, message in self.errors:
print(f"{RED}{check}:{RESET} {message}")
print()
# Print warnings
if self.warnings:
print(f"{YELLOW}{BOLD}⚠️ WARNINGS ({len(self.warnings)}):{RESET}")
for check, message in self.warnings:
print(f"{YELLOW}{check}:{RESET} {message}")
print()
# Print passed checks
if self.passed:
print(f"{GREEN}{BOLD}✅ PASSED ({len(self.passed)}):{RESET}")
for check in self.passed:
print(f"{GREEN}{check}{RESET}")
print()
# Overall status
print(f"{BOLD}{'=' * 70}{RESET}")
if self.has_failures():
print(f"{RED}{BOLD}❌ VALIDATION FAILED{RESET}")
print(f"\nThe monorepo is being incorrectly referenced in deployment configs.")
print(f"See {BLUE}DEPLOYMENT_ARCHITECTURE.md{RESET} for correct deployment model.\n")
return 1
elif self.warnings:
print(f"{YELLOW}{BOLD}⚠️ VALIDATION PASSED WITH WARNINGS{RESET}\n")
return 0
else:
print(f"{GREEN}{BOLD}✅ ALL VALIDATIONS PASSED{RESET}\n")
return 0
def is_allowed_file(file_path: Path) -> bool:
"""Check if file is in allowed list for monorepo references"""
file_str = str(file_path)
for allowed in ALLOWED_FILES:
if allowed in file_str:
return True
return False
def check_railway_toml(result: ValidationResult):
"""Validate railway.toml is marked for local dev only"""
railway_toml = REPO_ROOT / "railway.toml"
if not railway_toml.exists():
result.add_warning("railway.toml", "File not found (OK if not using Railway)")
return
content = railway_toml.read_text()
# Check for warning banner
if "CRITICAL WARNING" not in content:
result.add_error(
"railway.toml",
"Missing CRITICAL WARNING banner at top of file"
)
# Check for "LOCAL DEV" or similar marker
if "LOCAL DEV" not in content and "DEVELOPMENT" not in content:
result.add_error(
"railway.toml",
"Not clearly marked as local development only"
)
if not result.errors:
result.add_pass("railway.toml has proper warnings")
def check_env_files(result: ValidationResult):
"""Check environment files for monorepo references"""
env_files = []
for pattern in CHECK_PATTERNS:
env_files.extend(REPO_ROOT.glob(pattern))
found_issues = False
checked_count = 0
for env_file in env_files:
if is_allowed_file(env_file):
continue
checked_count += 1
content = env_file.read_text()
for pattern in FORBIDDEN_PATTERNS:
matches = re.finditer(pattern, content, re.IGNORECASE)
for match in matches:
# Get line number
line_num = content[:match.start()].count('\n') + 1
result.add_error(
f"{env_file.name}:{line_num}",
f"Contains forbidden reference: '{match.group()}'"
)
found_issues = True
if checked_count == 0:
result.add_warning("env files", "No environment files found to check")
elif not found_issues:
result.add_pass(f"Environment files clean ({checked_count} checked)")
def check_satellite_configs(result: ValidationResult):
"""Check if satellites are properly configured"""
sync_config = REPO_ROOT / "infra/github/sync-config.yml"
if not sync_config.exists():
result.add_warning(
"sync-config.yml",
"Satellite sync config not found"
)
return
with open(sync_config) as f:
config = yaml.safe_load(f)
# Expected satellites
expected_services = ["core-api", "public-api", "operator"]
expected_apps = ["prism-console", "web"]
services = config.get("services", {})
apps = config.get("apps", {})
# Check all expected services are configured
missing_services = [s for s in expected_services if s not in services]
missing_apps = [a for a in expected_apps if a not in apps]
if missing_services:
result.add_warning(
"sync-config.yml",
f"Missing service configs: {', '.join(missing_services)}"
)
if missing_apps:
result.add_warning(
"sync-config.yml",
f"Missing app configs: {', '.join(missing_apps)}"
)
if not missing_services and not missing_apps:
result.add_pass("Satellite sync configuration complete")
def check_cloudflare_docs(result: ValidationResult):
"""Check Cloudflare documentation for correct DNS setup"""
cloudflare_doc = REPO_ROOT / "CLOUDFLARE_DNS_BLUEPRINT.md"
if not cloudflare_doc.exists():
result.add_warning(
"Cloudflare docs",
"CLOUDFLARE_DNS_BLUEPRINT.md not found"
)
return
content = cloudflare_doc.read_text()
# Check for incorrect monorepo references in DNS
forbidden_dns = [
"blackroad-operating-system.up.railway.app",
"monorepo.up.railway.app",
]
found_issues = False
for forbidden in forbidden_dns:
if forbidden in content.lower():
result.add_error(
"CLOUDFLARE_DNS_BLUEPRINT.md",
f"Contains forbidden DNS target: {forbidden}"
)
found_issues = True
if not found_issues:
result.add_pass("Cloudflare DNS documentation is correct")
def check_deployment_architecture_exists(result: ValidationResult):
"""Verify DEPLOYMENT_ARCHITECTURE.md exists"""
doc_path = REPO_ROOT / "DEPLOYMENT_ARCHITECTURE.md"
if not doc_path.exists():
result.add_error(
"DEPLOYMENT_ARCHITECTURE.md",
"Critical deployment documentation is missing"
)
return
content = doc_path.read_text()
# Check for key sections
required_sections = [
"Monorepo vs Satellite Model",
"Critical Rules",
"NEVER DO THIS",
"ALWAYS DO THIS",
]
missing_sections = []
for section in required_sections:
if section not in content:
missing_sections.append(section)
if missing_sections:
result.add_error(
"DEPLOYMENT_ARCHITECTURE.md",
f"Missing sections: {', '.join(missing_sections)}"
)
else:
result.add_pass("DEPLOYMENT_ARCHITECTURE.md is complete")
def check_readme_warnings(result: ValidationResult):
"""Verify README.md has deployment warnings"""
readme = REPO_ROOT / "README.md"
if not readme.exists():
result.add_error("README.md", "README.md not found")
return
content = readme.read_text()
if "DEPLOYMENT WARNING" not in content:
result.add_error(
"README.md",
"Missing deployment warning section"
)
if "DO NOT" not in content or "satellite" not in content.lower():
result.add_error(
"README.md",
"Deployment warnings are not clear or comprehensive"
)
if not result.errors:
result.add_pass("README.md has proper deployment warnings")
def main():
"""Run all validation checks"""
print(f"\n{BOLD}{BLUE}BlackRoad OS Deployment Configuration Validator{RESET}")
print(f"{BLUE}{'=' * 70}{RESET}\n")
result = ValidationResult()
# Run all checks
print("Running validation checks...\n")
check_railway_toml(result)
check_env_files(result)
check_satellite_configs(result)
check_cloudflare_docs(result)
check_deployment_architecture_exists(result)
check_readme_warnings(result)
# Print results
exit_code = result.print_summary()
sys.exit(exit_code)
if __name__ == "__main__":
main()