Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-07-16 08:07:24

0001 #!/usr/bin/env python3
0002 # /// script
0003 # requires-python = ">=3.11"
0004 # dependencies = [
0005 #     "typer",
0006 #     "rich",
0007 # ]
0008 # ///
0009 """
0010 Utility to bump and make consistent all references to base Docker images and dependency versions.
0011 
0012 This script can:
0013 1. Bump Docker image tags (e.g., ubuntu2404:83 -> ubuntu2404:84)
0014 2. Update spack-container versions in .devcontainer/Dockerfile
0015 3. Make all references consistent across the codebase
0016 """
0017 
0018 import re
0019 import sys
0020 import difflib
0021 from pathlib import Path
0022 from typing import Annotated
0023 
0024 import typer
0025 from rich.console import Console
0026 from rich.table import Table
0027 from rich.panel import Panel
0028 from rich.syntax import Syntax
0029 from rich import box
0030 
0031 app = typer.Typer(
0032     help="Bump and make consistent Docker image tags and dependency versions",
0033     add_completion=False,
0034 )
0035 console = Console()
0036 
0037 
0038 class VersionBumper:
0039     """Handles version bumping for Docker images and dependencies."""
0040 
0041     # Patterns for different version formats
0042     PATTERNS = {
0043         # Matches: ubuntu2404:83, ubuntu2404_gnn:83, etc.
0044         "docker_tag": re.compile(
0045             r"((?:registry\.cern\.ch/)?ghcr\.io/acts-project/[a-z0-9_]+):(\d+)"
0046         ),
0047         # Matches: spack-container:18.0.0_linux-ubuntu24.04_gcc-13.3.0
0048         "spack_container": re.compile(
0049             r"(ghcr\.io/acts-project/spack-container):(\d+\.\d+\.\d+)(_[a-z0-9._-]+)"
0050         ),
0051         # Matches DEPENDENCY_TAG in two formats:
0052         # 1. GitLab CI: DEPENDENCY_TAG: v18.0.0
0053         # 2. GitHub Actions: DEPENDENCY_TAG: ... default: 'v18.0.0'
0054         "dependency_tag": re.compile(
0055             r"(DEPENDENCY_TAG:(?:\s*['\"]?|(?:[^\n]|\n(?!\s{0,2}\w))*?default:\s*['\"]))(v\d+\.\d+\.\d+)(['\"]?)",
0056             re.MULTILINE,
0057         ),
0058     }
0059 
0060     # File patterns to search
0061     FILE_PATTERNS = [
0062         ".gitlab-ci.yml",
0063         ".github/workflows/*.yml",
0064         ".github/workflows/*.yaml",
0065         ".github/actions/**/action.yml",
0066         ".devcontainer/Dockerfile",
0067         "CI/**/*.sh",
0068         "docs/**/*.md",
0069     ]
0070 
0071     def __init__(self, repo_root: Path):
0072         self.repo_root = repo_root
0073 
0074     def find_files(self) -> list[Path]:
0075         """Find all files that might contain version references."""
0076         files = set()
0077         for pattern in self.FILE_PATTERNS:
0078             files.update(self.repo_root.glob(pattern))
0079 
0080         # Filter out build directories and dependencies
0081         return [
0082             f
0083             for f in sorted(files)
0084             if f.is_file() and "build" not in f.parts and "_deps" not in f.parts
0085         ]
0086 
0087     def find_versions(self, content: str, pattern_name: str) -> set[str]:
0088         """Find all versions matching the given pattern."""
0089         pattern = self.PATTERNS[pattern_name]
0090         matches = pattern.findall(content)
0091 
0092         if pattern_name == "docker_tag":
0093             # Return unique tags (just the number part)
0094             return {match[1] for match in matches}
0095         elif pattern_name == "spack_container":
0096             # Return full version strings
0097             return {match[1] for match in matches}
0098         elif pattern_name == "dependency_tag":
0099             # Return full version strings (e.g., v18.0.0)
0100             return {match[1] for match in matches}
0101 
0102         return set()
0103 
0104     def scan_versions(self) -> dict:
0105         """Scan the repository for all current versions."""
0106         versions = {
0107             "docker_tags": set(),
0108             "spack_container_versions": set(),
0109             "dependency_tags": set(),
0110             "files_with_docker_tags": [],
0111             "files_with_spack_container": [],
0112             "files_with_dependency_tags": [],
0113         }
0114 
0115         files = self.find_files()
0116         console.print(f"[dim]Scanning {len(files)} files...[/dim]")
0117 
0118         for file_path in files:
0119             try:
0120                 content = file_path.read_text()
0121 
0122                 # Check for Docker tags
0123                 docker_tags = self.find_versions(content, "docker_tag")
0124                 if docker_tags:
0125                     versions["docker_tags"].update(docker_tags)
0126                     versions["files_with_docker_tags"].append(file_path)
0127 
0128                 # Check for spack-container versions
0129                 spack_versions = self.find_versions(content, "spack_container")
0130                 if spack_versions:
0131                     versions["spack_container_versions"].update(spack_versions)
0132                     versions["files_with_spack_container"].append(file_path)
0133 
0134                 # Check for dependency tags
0135                 dependency_tags = self.find_versions(content, "dependency_tag")
0136                 if dependency_tags:
0137                     versions["dependency_tags"].update(dependency_tags)
0138                     versions["files_with_dependency_tags"].append(file_path)
0139 
0140             except Exception as e:
0141                 console.print(
0142                     f"[yellow]Warning: Error reading {file_path}: {e}[/yellow]"
0143                 )
0144 
0145         return versions
0146 
0147     def bump_docker_tag(
0148         self,
0149         file_path: Path,
0150         old_tags: set[str],
0151         new_tag: str,
0152         dry_run: bool = False,
0153         show_diff: bool = False,
0154     ) -> tuple[int, str, str]:
0155         """Bump Docker image tags in a file. Returns (replacements, old_content, new_content)."""
0156         content = file_path.read_text()
0157         pattern = self.PATTERNS["docker_tag"]
0158 
0159         replacements = 0
0160 
0161         def replace_tag(match):
0162             nonlocal replacements
0163             old_tag = match.group(2)
0164             if old_tag in old_tags and old_tag != new_tag:
0165                 replacements += 1
0166                 return f"{match.group(1)}:{new_tag}"
0167             return match.group(0)
0168 
0169         new_content = pattern.sub(replace_tag, content)
0170 
0171         if replacements > 0:
0172             if not dry_run:
0173                 file_path.write_text(new_content)
0174 
0175             prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0176             rel_path = file_path.relative_to(self.repo_root)
0177             console.print(
0178                 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0179             )
0180 
0181             if show_diff:
0182                 self._show_diff(file_path, content, new_content)
0183 
0184         return replacements, content, new_content
0185 
0186     def bump_spack_container(
0187         self,
0188         file_path: Path,
0189         old_versions: set[str],
0190         new_version: str,
0191         dry_run: bool = False,
0192         show_diff: bool = False,
0193     ) -> tuple[int, str, str]:
0194         """Bump spack-container version in a file. Returns (replacements, old_content, new_content)."""
0195         content = file_path.read_text()
0196         pattern = self.PATTERNS["spack_container"]
0197 
0198         replacements = 0
0199 
0200         def replace_version(match):
0201             nonlocal replacements
0202             old_version = match.group(2)
0203             if old_version in old_versions and old_version != new_version:
0204                 replacements += 1
0205                 return f"{match.group(1)}:{new_version}{match.group(3)}"
0206             return match.group(0)
0207 
0208         new_content = pattern.sub(replace_version, content)
0209 
0210         if replacements > 0:
0211             if not dry_run:
0212                 file_path.write_text(new_content)
0213 
0214             prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0215             rel_path = file_path.relative_to(self.repo_root)
0216             console.print(
0217                 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0218             )
0219 
0220             if show_diff:
0221                 self._show_diff(file_path, content, new_content)
0222 
0223         return replacements, content, new_content
0224 
0225     def bump_dependency_tag(
0226         self,
0227         file_path: Path,
0228         old_versions: set[str],
0229         new_version: str,
0230         dry_run: bool = False,
0231         show_diff: bool = False,
0232     ) -> tuple[int, str, str]:
0233         """Bump dependency tag in a file. Returns (replacements, old_content, new_content)."""
0234         content = file_path.read_text()
0235         pattern = self.PATTERNS["dependency_tag"]
0236 
0237         replacements = 0
0238 
0239         def replace_version(match):
0240             nonlocal replacements
0241             old_version = match.group(2)
0242             if old_version in old_versions and old_version != new_version:
0243                 replacements += 1
0244                 return f"{match.group(1)}{new_version}{match.group(3)}"
0245             return match.group(0)
0246 
0247         new_content = pattern.sub(replace_version, content)
0248 
0249         if replacements > 0:
0250             if not dry_run:
0251                 file_path.write_text(new_content)
0252 
0253             prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0254             rel_path = file_path.relative_to(self.repo_root)
0255             console.print(
0256                 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0257             )
0258 
0259             if show_diff:
0260                 self._show_diff(file_path, content, new_content)
0261 
0262         return replacements, content, new_content
0263 
0264     def _show_diff(self, file_path: Path, old_content: str, new_content: str):
0265         """Display a unified diff of the changes."""
0266         rel_path = file_path.relative_to(self.repo_root)
0267         diff = difflib.unified_diff(
0268             old_content.splitlines(),
0269             new_content.splitlines(),
0270             fromfile=str(rel_path),
0271             tofile=str(rel_path),
0272             lineterm="",
0273         )
0274 
0275         diff_lines = list(diff)
0276         if diff_lines:
0277             console.print()
0278             diff_text = "\n".join(diff_lines)
0279             syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
0280             console.print(syntax)
0281 
0282     def bump_all_docker_tags(
0283         self,
0284         old_tags: set[str],
0285         new_tag: str,
0286         dry_run: bool = False,
0287         show_diff: bool = False,
0288     ) -> int:
0289         """Bump all Docker tags across the repository."""
0290         files = self.find_files()
0291         total_replacements = 0
0292 
0293         for file_path in files:
0294             try:
0295                 replacements, _, _ = self.bump_docker_tag(
0296                     file_path, old_tags, new_tag, dry_run, show_diff
0297                 )
0298                 total_replacements += replacements
0299             except Exception as e:
0300                 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0301 
0302         return total_replacements
0303 
0304     def bump_all_spack_containers(
0305         self,
0306         old_versions: set[str],
0307         new_version: str,
0308         dry_run: bool = False,
0309         show_diff: bool = False,
0310     ) -> int:
0311         """Bump all spack-container versions across the repository."""
0312         files = self.find_files()
0313         total_replacements = 0
0314 
0315         for file_path in files:
0316             try:
0317                 replacements, _, _ = self.bump_spack_container(
0318                     file_path, old_versions, new_version, dry_run, show_diff
0319                 )
0320                 total_replacements += replacements
0321             except Exception as e:
0322                 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0323 
0324         return total_replacements
0325 
0326     def bump_all_dependency_tags(
0327         self,
0328         old_versions: set[str],
0329         new_version: str,
0330         dry_run: bool = False,
0331         show_diff: bool = False,
0332     ) -> int:
0333         """Bump all dependency tags across the repository."""
0334         files = self.find_files()
0335         total_replacements = 0
0336 
0337         for file_path in files:
0338             try:
0339                 replacements, _, _ = self.bump_dependency_tag(
0340                     file_path, old_versions, new_version, dry_run, show_diff
0341                 )
0342                 total_replacements += replacements
0343             except Exception as e:
0344                 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0345 
0346         return total_replacements
0347 
0348 
0349 @app.command()
0350 def scan(
0351     repo_root: Annotated[
0352         Path, typer.Option(help="Root directory of the repository")
0353     ] = Path.cwd(),
0354 ):
0355     """
0356     Scan repository for current versions.
0357 
0358     This command scans the codebase and displays all Docker image tags
0359     and spack-container versions currently in use.
0360     """
0361     bumper = VersionBumper(repo_root)
0362     versions = bumper.scan_versions()
0363 
0364     console.print()
0365 
0366     # Docker tags
0367     if versions["docker_tags"]:
0368         console.print("[bold]Docker image tags:[/bold]")
0369         for tag in sorted(versions["docker_tags"]):
0370             console.print(f"  [cyan]{tag}[/cyan]")
0371         console.print(
0372             f"[dim]  Found in {len(versions['files_with_docker_tags'])} file(s)[/dim]"
0373         )
0374     else:
0375         console.print("[bold]Docker image tags:[/bold] [yellow]none found[/yellow]")
0376 
0377     console.print()
0378 
0379     # Spack-container versions
0380     if versions["spack_container_versions"]:
0381         console.print("[bold]Spack-container versions:[/bold]")
0382         for version in sorted(versions["spack_container_versions"]):
0383             console.print(f"  [cyan]{version}[/cyan]")
0384         console.print(
0385             f"[dim]  Found in {len(versions['files_with_spack_container'])} file(s)[/dim]"
0386         )
0387     else:
0388         console.print(
0389             "[bold]Spack-container versions:[/bold] [yellow]none found[/yellow]"
0390         )
0391 
0392     console.print()
0393 
0394     # Dependency tags
0395     if versions["dependency_tags"]:
0396         console.print("[bold]Dependency tags:[/bold]")
0397         for version in sorted(versions["dependency_tags"]):
0398             console.print(f"  [cyan]{version}[/cyan]")
0399         console.print(
0400             f"[dim]  Found in {len(versions['files_with_dependency_tags'])} file(s)[/dim]"
0401         )
0402     else:
0403         console.print("[bold]Dependency tags:[/bold] [yellow]none found[/yellow]")
0404 
0405 
0406 @app.command()
0407 def bump_docker_tag(
0408     new_tag: Annotated[str, typer.Argument(help="New Docker tag to use (e.g., 84)")],
0409     repo_root: Annotated[
0410         Path, typer.Option(help="Root directory of the repository")
0411     ] = Path.cwd(),
0412     dry_run: Annotated[
0413         bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0414     ] = False,
0415     show_diff: Annotated[
0416         bool, typer.Option("--diff", help="Show diff of changes")
0417     ] = True,
0418 ):
0419     """
0420     Bump Docker image tags across the repository.
0421 
0422     This command finds all Docker images like 'ubuntu2404:83' and updates
0423     all tag numbers to the new value.
0424 
0425     Example:
0426         bump_versions.py bump-docker-tag 84
0427         bump_versions.py bump-docker-tag 84 --dry-run --diff
0428     """
0429     bumper = VersionBumper(repo_root)
0430 
0431     # Scan for existing tags
0432     versions = bumper.scan_versions()
0433     if not versions["docker_tags"]:
0434         console.print("[red]Error: No Docker tags found in repository[/red]")
0435         raise typer.Exit(1)
0436 
0437     # Replace ALL found tags
0438     old_tags = versions["docker_tags"]
0439     console.print(f"[dim]Found Docker tags: {', '.join(sorted(old_tags))}[/dim]")
0440 
0441     # Check if new tag is same as all old tags
0442     if len(old_tags) == 1 and new_tag in old_tags:
0443         console.print(f"[yellow]Tag is already {new_tag}, no changes needed[/yellow]")
0444         raise typer.Exit(0)
0445 
0446     console.print(f"[dim]Will replace ALL tags with: {new_tag}[/dim]")
0447 
0448     console.print()
0449     if dry_run:
0450         console.print(
0451             Panel(
0452                 f"[bold]DRY RUN:[/bold] Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0453                 border_style="yellow",
0454             )
0455         )
0456     else:
0457         console.print(
0458             Panel(
0459                 f"Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0460                 border_style="green",
0461             )
0462         )
0463 
0464     console.print()
0465     total = bumper.bump_all_docker_tags(old_tags, new_tag, dry_run, show_diff)
0466 
0467     console.print()
0468     if total > 0:
0469         if dry_run:
0470             console.print(
0471                 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s)"
0472             )
0473             console.print("[dim]Run without --dry-run to apply changes[/dim]")
0474         else:
0475             console.print(
0476                 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s)"
0477             )
0478     else:
0479         console.print(f"[yellow]No occurrences found[/yellow]")
0480 
0481 
0482 @app.command()
0483 def bump_spack(
0484     new_version: Annotated[
0485         str, typer.Argument(help="New spack-container version (e.g., 19.0.0)")
0486     ],
0487     repo_root: Annotated[
0488         Path, typer.Option(help="Root directory of the repository")
0489     ] = Path.cwd(),
0490     dry_run: Annotated[
0491         bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0492     ] = False,
0493     show_diff: Annotated[
0494         bool, typer.Option("--diff/--no-diff", help="Show diff of changes")
0495     ] = True,
0496 ):
0497     """
0498     Bump spack-container version and DEPENDENCY_TAG across the repository.
0499 
0500     This command updates the spack-container image version used in
0501     .devcontainer/Dockerfile and other configuration files, as well as
0502     the DEPENDENCY_TAG in GitHub Actions and GitLab CI. It replaces
0503     all found versions with the new version.
0504 
0505     Example:
0506         bump_versions.py bump-spack 19.0.0
0507         bump_versions.py bump-spack 19.0.0 --dry-run --diff
0508     """
0509     bumper = VersionBumper(repo_root)
0510 
0511     # Scan for existing versions
0512     versions = bumper.scan_versions()
0513     if not versions["spack_container_versions"]:
0514         console.print(
0515             "[red]Error: No spack-container versions found in repository[/red]"
0516         )
0517         raise typer.Exit(1)
0518 
0519     # Replace ALL found spack-container versions
0520     old_spack_versions = versions["spack_container_versions"]
0521     console.print(
0522         f"[dim]Found spack-container versions: {', '.join(sorted(old_spack_versions))}[/dim]"
0523     )
0524 
0525     # Also check for dependency tags
0526     old_dependency_versions = versions.get("dependency_tags", set())
0527     if old_dependency_versions:
0528         console.print(
0529             f"[dim]Found dependency tags: {', '.join(sorted(old_dependency_versions))}[/dim]"
0530         )
0531 
0532     # Check if new version is same as all old versions (both spack and dependency)
0533     new_dependency_version = f"v{new_version}"
0534     spack_is_current = (
0535         len(old_spack_versions) == 1 and new_version in old_spack_versions
0536     )
0537     dependency_is_current = len(old_dependency_versions) <= 1 and (
0538         not old_dependency_versions or new_dependency_version in old_dependency_versions
0539     )
0540 
0541     if spack_is_current and dependency_is_current:
0542         console.print(
0543             f"[yellow]All versions are already {new_version} (DEPENDENCY_TAG: {new_dependency_version}), no changes needed[/yellow]"
0544         )
0545         raise typer.Exit(0)
0546 
0547     console.print(f"[dim]Will replace ALL versions with: {new_version}[/dim]")
0548 
0549     console.print()
0550     if dry_run:
0551         console.print(
0552             Panel(
0553                 f"[bold]DRY RUN:[/bold] Bumping spack-container and DEPENDENCY_TAG [cyan]{', '.join(sorted(old_spack_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0554                 border_style="yellow",
0555             )
0556         )
0557     else:
0558         console.print(
0559             Panel(
0560                 f"Bumping spack-container and DEPENDENCY_TAG [cyan]{', '.join(sorted(old_spack_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0561                 border_style="green",
0562             )
0563         )
0564 
0565     console.print()
0566 
0567     # Bump spack-container versions
0568     console.print("[bold]Updating spack-container versions:[/bold]")
0569     total_spack = bumper.bump_all_spack_containers(
0570         old_spack_versions, new_version, dry_run, show_diff
0571     )
0572 
0573     # Bump dependency tags with "v" prefix
0574     if old_dependency_versions:
0575         console.print()
0576         console.print("[bold]Updating DEPENDENCY_TAG:[/bold]")
0577         new_dependency_version = f"v{new_version}"
0578         total_dependency = bumper.bump_all_dependency_tags(
0579             old_dependency_versions, new_dependency_version, dry_run, show_diff
0580         )
0581     else:
0582         total_dependency = 0
0583 
0584     console.print()
0585     total = total_spack + total_dependency
0586     if total > 0:
0587         if dry_run:
0588             console.print(
0589                 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s) ([bold]{total_spack}[/bold] spack-container, [bold]{total_dependency}[/bold] DEPENDENCY_TAG)"
0590             )
0591             console.print("[dim]Run without --dry-run to apply changes[/dim]")
0592         else:
0593             console.print(
0594                 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s) ([bold]{total_spack}[/bold] spack-container, [bold]{total_dependency}[/bold] DEPENDENCY_TAG)"
0595             )
0596     else:
0597         console.print(f"[yellow]No occurrences found[/yellow]")
0598 
0599 
0600 if __name__ == "__main__":
0601     app()