#!/usr/bin/env python3 """GitLab NPM Verify - Verify packages are published and accessible from GitLab registry.""" import argparse import json import sys from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Optional from gitlab_npm_common import ( Color, GitLabNpmClient, Package, RegistryPackageInfo, find_packages, print_header, DEFAULT_SCOPE, GITLAB_NPM_REGISTRY, ) @dataclass class VerificationResult: """Result of verifying a single package.""" package: Package registry_info: Optional[RegistryPackageInfo] status: str # synced, outdated, missing, error message: Optional[str] = None @property def status_icon(self) -> str: c = Color icons = { "synced": f"{c.GREEN}✓ synced{c.NC}", "outdated": f"{c.YELLOW}! outdated{c.NC}", "missing": f"{c.RED}✗ missing{c.NC}", "error": f"{c.RED}? error{c.NC}", "private": f"{c.DIM}○ private{c.NC}", "no_config": f"{c.DIM}○ no config{c.NC}", } return icons.get(self.status, f"{c.YELLOW}? {self.status}{c.NC}") @property def registry_version(self) -> str: if self.registry_info and self.registry_info.latest_version: return self.registry_info.latest_version return "-" def verify_package(pkg: Package, client: GitLabNpmClient) -> VerificationResult: """Verify a single package against the registry.""" if pkg.is_private: return VerificationResult( package=pkg, registry_info=None, status="private", ) if not pkg.has_publish_config: return VerificationResult( package=pkg, registry_info=None, status="no_config", ) try: info = client.get_package_info(pkg.name) except Exception as e: return VerificationResult( package=pkg, registry_info=None, status="error", message=str(e), ) if not info.exists: return VerificationResult( package=pkg, registry_info=info, status="missing", ) # Compare versions if info.latest_version == pkg.version: return VerificationResult( package=pkg, registry_info=info, status="synced", ) return VerificationResult( package=pkg, registry_info=info, status="outdated", message=f"local={pkg.version}, registry={info.latest_version}", ) def print_results_table(results: list[VerificationResult]) -> None: """Print verification results as a table.""" c = Color if not results: print(f"{c.YELLOW}No packages to verify{c.NC}") return max_name = min(45, max(len(r.package.name) for r in results)) print(f"{c.BOLD}{'Package':<{max_name}} {'Local':<12} {'Registry':<12} Status{c.NC}") print(f"{c.DIM}{'─' * max_name} {'─' * 12} {'─' * 12} {'─' * 12}{c.NC}") for r in results: name = r.package.name if len(name) > max_name: name = name[:max_name - 2] + ".." local_ver = r.package.version reg_ver = r.registry_version print(f"{name:<{max_name}} {local_ver:<12} {reg_ver:<12} {r.status_icon}") print() def print_results_json(results: list[VerificationResult]) -> None: """Print verification results as JSON.""" output = { "timestamp": datetime.now(timezone.utc).isoformat(), "registry": GITLAB_NPM_REGISTRY, "total": len(results), "synced": len([r for r in results if r.status == "synced"]), "outdated": len([r for r in results if r.status == "outdated"]), "missing": len([r for r in results if r.status == "missing"]), "packages": [ { "name": r.package.name, "local_version": r.package.version, "registry_version": r.registry_version, "status": r.status, "message": r.message, } for r in results ], } print(json.dumps(output, indent=2)) def print_results_markdown(results: list[VerificationResult]) -> None: """Print verification results as Markdown.""" print(f"# GitLab NPM Registry Verification") print(f"\n**Registry**: `{GITLAB_NPM_REGISTRY}`") print(f"**Timestamp**: {datetime.now(timezone.utc).isoformat()}") print() synced = len([r for r in results if r.status == "synced"]) outdated = len([r for r in results if r.status == "outdated"]) missing = len([r for r in results if r.status == "missing"]) print(f"**Summary**: {synced} synced · {outdated} outdated · {missing} missing") print() print("| Package | Local | Registry | Status |") print("|---------|-------|----------|--------|") for r in results: status_emoji = {"synced": "✅", "outdated": "⚠️", "missing": "❌"}.get(r.status, "❓") print(f"| {r.package.name} | {r.package.version} | {r.registry_version} | {status_emoji} {r.status} |") print() def print_summary(results: list[VerificationResult]) -> None: """Print verification summary.""" c = Color synced = [r for r in results if r.status == "synced"] outdated = [r for r in results if r.status == "outdated"] missing = [r for r in results if r.status == "missing"] errors = [r for r in results if r.status == "error"] private = [r for r in results if r.status == "private"] no_config = [r for r in results if r.status == "no_config"] print(f"{c.BOLD}Summary:{c.NC}") parts = [ f"{c.GREEN}{len(synced)} synced{c.NC}", f"{c.YELLOW}{len(outdated)} outdated{c.NC}", f"{c.RED}{len(missing)} missing{c.NC}", ] if errors: parts.append(f"{c.RED}{len(errors)} errors{c.NC}") if private: parts.append(f"{c.DIM}{len(private)} private{c.NC}") if no_config: parts.append(f"{c.DIM}{len(no_config)} no config{c.NC}") print(" " + " · ".join(parts)) print() def main() -> None: parser = argparse.ArgumentParser( description="Verify packages are published to GitLab npm registry", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=f""" Examples: gitlab-npm-verify.py # Verify all packages in current directory gitlab-npm-verify.py /path/to/packages # Verify packages in specific directory gitlab-npm-verify.py --format json # Output as JSON gitlab-npm-verify.py --filter nestjs # Only verify packages matching pattern gitlab-npm-verify.py --fail-on-missing # Exit 1 if any packages missing Registry: {GITLAB_NPM_REGISTRY} """, ) parser.add_argument("path", nargs="?", default=".", help="Package workspace directory") parser.add_argument("--format", "-f", choices=["table", "json", "markdown"], default="table", help="Output format (default: table)") parser.add_argument("--filter", type=str, help="Filter packages by name pattern") parser.add_argument("--fail-on-missing", action="store_true", help="Exit with error code if any packages are missing") parser.add_argument("--fail-on-outdated", action="store_true", help="Exit with error code if any packages are outdated") parser.add_argument("--publishable-only", "-p", action="store_true", help="Only show publishable packages (not private, has config)") args = parser.parse_args() c = Color base_path = Path(args.path).resolve() if not base_path.exists(): print(f"{c.RED}Error: Path does not exist: {base_path}{c.NC}") sys.exit(1) # Find packages packages = find_packages(base_path) if not packages: print(f"{c.YELLOW}No packages found in {base_path}{c.NC}") sys.exit(0) # Filter by pattern if args.filter: packages = [p for p in packages if args.filter.lower() in p.name.lower()] if not packages: print(f"{c.YELLOW}No packages matching '{args.filter}'{c.NC}") sys.exit(0) # Filter publishable only if args.publishable_only: packages = [p for p in packages if p.can_publish] # Print header for table format if args.format == "table": print_header(f"GitLab NPM Registry Verification: {c.CYAN}{DEFAULT_SCOPE}{c.NC}") # Verify each package client = GitLabNpmClient() results = [] for pkg in packages: result = verify_package(pkg, client) results.append(result) # Output results if args.format == "table": print_results_table(results) print_summary(results) elif args.format == "json": print_results_json(results) elif args.format == "markdown": print_results_markdown(results) # Check exit conditions missing = [r for r in results if r.status == "missing"] outdated = [r for r in results if r.status == "outdated"] if args.fail_on_missing and missing: sys.exit(1) if args.fail_on_outdated and outdated: sys.exit(1) if __name__ == "__main__": main()