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>
293 lines
9.7 KiB
Python
Executable file
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()
|