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>
285 lines
9.1 KiB
Python
Executable file
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()
|