diff --git a/scripts/deploy_verify.py b/scripts/deploy_verify.py new file mode 100644 index 00000000..51a6c267 --- /dev/null +++ b/scripts/deploy_verify.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Deployment Visual Verification +============================== + +Post-deployment step that uses vision to verify UI is rendered correctly. +Takes screenshots of deployed endpoints and checks for: +- Page rendering errors +- Missing assets +- Layout breaks +- Error messages visible +- Expected content present + +Usage: + python scripts/deploy_verify.py check https://my-app.com + python scripts/deploy_verify.py check https://my-app.com --expect "Welcome" + python scripts/deploy_verify.py batch urls.txt +""" + +import json +import sys +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Optional + + +@dataclass +class DeployCheck: + """A single deployment verification check.""" + url: str + status: str # passed, failed, warning + issues: list = field(default_factory=list) + screenshot_path: Optional[str] = None + expected_content: str = "" + timestamp: str = "" + + def summary(self) -> str: + emoji = {"passed": "✅", "failed": "❌", "warning": "⚠️"}.get(self.status, "❓") + lines = [ + f"{emoji} {self.url}", + f" Checked: {self.timestamp or 'pending'}", + ] + if self.expected_content: + lines.append(f" Expected: '{self.expected_content}'") + if self.issues: + lines.append(" Issues:") + for i in self.issues: + lines.append(f" - {i}") + else: + lines.append(" No issues detected") + return "\n".join(lines) + + +class DeployVerifier: + """Verifies deployed UI renders correctly using screenshots.""" + + def build_check_prompt(self, url: str, expected: str = "") -> dict: + """Build verification prompt for a deployed URL.""" + expect_clause = "" + if expected: + expect_clause = f"\n- Verify the text \"{expected}\" is visible on the page" + + prompt = f"""Take a screenshot of {url} and verify the deployment is healthy. + +Check for: +- Page loads without errors (no 404, 500, connection refused) +- No visible error messages or stack traces +- Layout is not broken (elements properly aligned, no overlapping) +- Images and assets load correctly (no broken image icons) +- Navigation elements are present and clickable{expect_clause} +- No "under construction" or placeholder content +- Responsive design elements render properly + +Return as JSON: +```json +{{ + "status": "passed|failed|warning", + "issues": ["list of issues found"], + "confidence": 0.9, + "page_title": "detected page title", + "visible_text_sample": "first 100 chars of visible text" +}} +``` +""" + return { + "url": url, + "prompt": prompt, + "screenshot_needed": True, + "instruction": f"browser_navigate to {url}, take screenshot with browser_vision, analyze with prompt" + } + + def verify_deployment(self, url: str, expected: str = "", screenshot_path: str = "") -> DeployCheck: + """Create a deployment verification check.""" + check = DeployCheck( + url=url, + status="pending", + expected_content=expected, + timestamp=datetime.now().isoformat(), + screenshot_path=screenshot_path or f"/tmp/deploy_verify_{url.replace('://', '_').replace('/', '_')}.png" + ) + return check + + +def main(): + if len(sys.argv) < 2: + print("Usage: deploy_verify.py [args...]") + return 1 + + verifier = DeployVerifier() + cmd = sys.argv[1] + + if cmd == "check": + if len(sys.argv) < 3: + print("Usage: deploy_verify.py check [--expect 'text']") + return 1 + url = sys.argv[2] + expected = "" + if "--expect" in sys.argv: + idx = sys.argv.index("--expect") + if idx + 1 < len(sys.argv): + expected = sys.argv[idx + 1] + + result = verifier.build_check_prompt(url, expected) + print(json.dumps(result, indent=2)) + + elif cmd == "batch": + if len(sys.argv) < 3: + print("Usage: deploy_verify.py batch ") + return 1 + urls_file = Path(sys.argv[2]) + if not urls_file.exists(): + print(f"File not found: {urls_file}") + return 1 + + urls = [line.strip() for line in urls_file.read_text().splitlines() if line.strip() and not line.startswith("#")] + for url in urls: + print(f"\n--- {url} ---") + result = verifier.build_check_prompt(url) + print(json.dumps(result, indent=2)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())