packages-scripts/gitlab/gitlab-npm-verify.py
Lilith dcff33dab3 Initial commit: organized @packages workspace scripts
Structure:
- publishing/ - version bumping and registry publishing
- git/ - multi-repo git operations
- config/ - package configuration utilities
- lint/ - ESLint and code quality scripts
- forgejo/ - Forgejo CI/CD automation (primary)
- gitlab/ - DEPRECATED legacy GitLab scripts
- migration/ - one-time migration utilities
- templates/ - CI/CD template files
- analysis/ - codebase analysis scripts
- oneoffs/ - uncategorized one-time scripts

Note: commits CLI will be merged into @ml/auto-commit-service

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:34:13 -08:00

285 lines
9.1 KiB
Python
Executable file

#!/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()