packages-scripts/gitlab/gitlab-npm-consumer.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

293 lines
9.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""GitLab NPM Consumer Setup - Configure projects to consume from GitLab npm registry."""
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Optional
from gitlab_npm_common import (
Color,
GitLabNpmClient,
LinkReference,
find_link_references,
get_npmrc_registry_lines,
print_header,
DEFAULT_SCOPE,
GITLAB_NPM_REGISTRY,
)
def parse_npmrc(npmrc_path: Path) -> tuple[list[str], bool, int]:
"""Parse .npmrc and check for existing registry config.
Returns: (lines, has_registry_config, registry_line_index)
"""
if not npmrc_path.exists():
return [], False, -1
with open(npmrc_path) as f:
lines = f.readlines()
has_registry = False
registry_idx = -1
for i, line in enumerate(lines):
if line.strip().startswith(f"{DEFAULT_SCOPE}:registry="):
has_registry = True
registry_idx = i
break
return lines, has_registry, registry_idx
def update_npmrc(
npmrc_path: Path,
dry_run: bool = False,
scope: str = DEFAULT_SCOPE,
token_env: str = "GITLAB_NPM_TOKEN",
) -> bool:
"""Add or update registry configuration in .npmrc."""
c = Color
lines, has_registry, registry_idx = parse_npmrc(npmrc_path)
registry_lines = get_npmrc_registry_lines(scope, token_env)
registry_line_list = registry_lines.strip().split("\n")
if has_registry:
# Check if auth line is present too
auth_pattern = "//gitlab.com/api/v4/packages/npm/:_authToken"
has_auth = any(auth_pattern in line for line in lines)
if has_auth:
print(f" {c.GREEN}✓ Registry already configured{c.NC}")
return True
# Add auth line after registry line
print(f" {c.YELLOW}Adding auth token line...{c.NC}")
if not dry_run:
lines.insert(registry_idx + 1, registry_line_list[1] + "\n")
with open(npmrc_path, "w") as f:
f.writelines(lines)
return True
# No registry config - add at top
print(f" {c.CYAN}Adding registry configuration...{c.NC}")
if dry_run:
print(f" {c.DIM}Would add to {npmrc_path}:{c.NC}")
for line in registry_line_list:
print(f" {c.DIM} {line}{c.NC}")
return True
# Prepend registry lines
new_content = registry_lines + "\n"
if lines:
new_content += "".join(lines)
with open(npmrc_path, "w") as f:
f.write(new_content)
print(f" {c.GREEN}✓ Registry configuration added{c.NC}")
return True
def convert_link_references(
refs: list[LinkReference],
client: GitLabNpmClient,
dry_run: bool = False,
) -> tuple[int, int]:
"""Convert link:/file:// references to registry versions.
Returns: (converted_count, failed_count)
"""
c = Color
converted = 0
failed = 0
# Group by package.json file
by_file: dict[Path, list[LinkReference]] = {}
for ref in refs:
if ref.package_json_path not in by_file:
by_file[ref.package_json_path] = []
by_file[ref.package_json_path].append(ref)
for pkg_json, file_refs in by_file.items():
print(f" {c.DIM}{pkg_json.relative_to(pkg_json.parent.parent)}{c.NC}")
try:
with open(pkg_json) as f:
data = json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f" {c.RED}Error reading: {e}{c.NC}")
failed += len(file_refs)
continue
modified = False
for ref in file_refs:
# Get latest version from registry
latest = client.get_latest_version(ref.dependency_name)
if latest:
new_value = f"^{latest}"
else:
# Package not in registry - use wildcard
new_value = "*"
print(f" {c.YELLOW}! {ref.dependency_name} not in registry, using '*'{c.NC}")
print(f" {ref.dependency_name}: {c.RED}{ref.current_value}{c.NC}{c.GREEN}{new_value}{c.NC}")
if not dry_run:
if ref.dep_type in data and ref.dependency_name in data[ref.dep_type]:
data[ref.dep_type][ref.dependency_name] = new_value
modified = True
converted += 1
else:
converted += 1
if modified and not dry_run:
with open(pkg_json, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
return converted, failed
def verify_setup(base_path: Path) -> bool:
"""Verify pnpm can resolve packages from registry."""
c = Color
print(f"\n{c.BOLD}Verifying setup...{c.NC}")
result = subprocess.run(
["pnpm", "install", "--dry-run"],
cwd=base_path,
capture_output=True,
text=True,
)
if result.returncode == 0:
print(f" {c.GREEN}✓ pnpm install --dry-run succeeded{c.NC}")
return True
# Check for specific errors
if "404" in result.stderr or "not found" in result.stderr.lower():
print(f" {c.YELLOW}! Some packages not found in registry{c.NC}")
print(f" {c.DIM}This may be expected if packages haven't been published yet{c.NC}")
else:
print(f" {c.RED}✗ pnpm install failed{c.NC}")
print(f" {c.DIM}{result.stderr[:500]}{c.NC}")
return False
def analyze_project(base_path: Path, scope: str = DEFAULT_SCOPE) -> dict:
"""Analyze project for registry configuration status."""
npmrc_path = base_path / ".npmrc"
lines, has_registry, _ = parse_npmrc(npmrc_path)
auth_pattern = "//gitlab.com/api/v4/packages/npm/:_authToken"
has_auth = any(auth_pattern in line for line in lines)
link_refs = find_link_references(base_path, scope)
return {
"npmrc_exists": npmrc_path.exists(),
"has_registry": has_registry,
"has_auth": has_auth,
"link_refs": link_refs,
"link_ref_count": len(link_refs),
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Configure projects to consume packages from GitLab npm registry",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Examples:
gitlab-npm-consumer.py # Analyze current directory
gitlab-npm-consumer.py /path/to/project # Analyze specific project
gitlab-npm-consumer.py --dry-run # Show what would change
gitlab-npm-consumer.py --convert-links # Convert link:/file:// refs to registry versions
gitlab-npm-consumer.py --verify # Verify setup works
Registry: {GITLAB_NPM_REGISTRY}
Scope: {DEFAULT_SCOPE}
""",
)
parser.add_argument("path", nargs="?", default=".", help="Project directory (default: current)")
parser.add_argument("--dry-run", "-d", action="store_true", help="Show changes without applying")
parser.add_argument("--convert-links", "-c", action="store_true", help="Convert link:/file:// refs to registry versions")
parser.add_argument("--verify", "-v", action="store_true", help="Verify configuration works")
parser.add_argument("--scope", default=DEFAULT_SCOPE, help=f"Package scope (default: {DEFAULT_SCOPE})")
parser.add_argument("--token-env", default="GITLAB_NPM_TOKEN", help="Environment variable for auth token")
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)
mode = "DRY RUN" if args.dry_run else "SETUP"
print_header(f"GitLab NPM Consumer {mode}: {c.CYAN}{base_path.name}{c.NC}")
# Analyze project
print(f"{c.BOLD}Analyzing project...{c.NC}")
analysis = analyze_project(base_path, args.scope)
npmrc_path = base_path / ".npmrc"
if analysis["npmrc_exists"]:
if analysis["has_registry"] and analysis["has_auth"]:
print(f" {c.GREEN}✓ .npmrc has registry configuration{c.NC}")
elif analysis["has_registry"]:
print(f" {c.YELLOW}! .npmrc has registry but missing auth token line{c.NC}")
else:
print(f" {c.YELLOW}! .npmrc exists but missing registry configuration{c.NC}")
else:
print(f" {c.CYAN}○ No .npmrc file found{c.NC}")
print(f" Found {c.CYAN}{analysis['link_ref_count']}{c.NC} link:/file:// references to {args.scope}")
print()
# Update .npmrc
print(f"{c.BOLD}Configuring .npmrc...{c.NC}")
update_npmrc(npmrc_path, dry_run=args.dry_run, scope=args.scope, token_env=args.token_env)
print()
# Convert link references if requested
if args.convert_links and analysis["link_refs"]:
print(f"{c.BOLD}Converting link references...{c.NC}")
client = GitLabNpmClient()
converted, failed = convert_link_references(
analysis["link_refs"],
client,
dry_run=args.dry_run,
)
print()
print(f" {c.GREEN}{converted} converted{c.NC}, {c.RED}{failed} failed{c.NC}")
print()
elif analysis["link_refs"] and not args.convert_links:
print(f"{c.DIM}Use --convert-links to convert link:/file:// references{c.NC}")
print()
# Verify if requested
if args.verify and not args.dry_run:
verify_setup(base_path)
print()
# Summary
print(f"{c.BOLD}Summary:{c.NC}")
if args.dry_run:
print(f" {c.YELLOW}This was a dry run - no changes were made{c.NC}")
print(f" {c.DIM}Run without --dry-run to apply changes{c.NC}")
else:
print(f" {c.GREEN}✓ Consumer configuration complete{c.NC}")
if analysis["link_refs"] and not args.convert_links:
print(f" {c.DIM}Note: {analysis['link_ref_count']} link: refs remain - use --convert-links to update{c.NC}")
if __name__ == "__main__":
main()