File indexing completed on 2025-12-14 09:20:24
0001
0002
0003
0004
0005
0006
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
0042 PATTERNS = {
0043
0044 "docker_tag": re.compile(
0045 r"((?:registry\.cern\.ch/)?ghcr\.io/acts-project/[a-z0-9_]+):(\d+)"
0046 ),
0047
0048 "spack_container": re.compile(
0049 r"(ghcr\.io/acts-project/spack-container):(\d+\.\d+\.\d+)(_[a-z0-9._-]+)"
0050 ),
0051 }
0052
0053
0054 FILE_PATTERNS = [
0055 ".gitlab-ci.yml",
0056 ".github/workflows/*.yml",
0057 ".github/workflows/*.yaml",
0058 ".devcontainer/Dockerfile",
0059 "CI/**/*.sh",
0060 "docs/**/*.md",
0061 ]
0062
0063 def __init__(self, repo_root: Path):
0064 self.repo_root = repo_root
0065
0066 def find_files(self) -> list[Path]:
0067 """Find all files that might contain version references."""
0068 files = set()
0069 for pattern in self.FILE_PATTERNS:
0070 files.update(self.repo_root.glob(pattern))
0071
0072
0073 return [
0074 f
0075 for f in sorted(files)
0076 if f.is_file() and "build" not in f.parts and "_deps" not in f.parts
0077 ]
0078
0079 def find_versions(self, content: str, pattern_name: str) -> set[str]:
0080 """Find all versions matching the given pattern."""
0081 pattern = self.PATTERNS[pattern_name]
0082 matches = pattern.findall(content)
0083
0084 if pattern_name == "docker_tag":
0085
0086 return {match[1] for match in matches}
0087 elif pattern_name == "spack_container":
0088
0089 return {match[1] for match in matches}
0090
0091 return set()
0092
0093 def scan_versions(self) -> dict:
0094 """Scan the repository for all current versions."""
0095 versions = {
0096 "docker_tags": set(),
0097 "spack_container_versions": set(),
0098 "files_with_docker_tags": [],
0099 "files_with_spack_container": [],
0100 }
0101
0102 files = self.find_files()
0103 console.print(f"[dim]Scanning {len(files)} files...[/dim]")
0104
0105 for file_path in files:
0106 try:
0107 content = file_path.read_text()
0108
0109
0110 docker_tags = self.find_versions(content, "docker_tag")
0111 if docker_tags:
0112 versions["docker_tags"].update(docker_tags)
0113 versions["files_with_docker_tags"].append(file_path)
0114
0115
0116 spack_versions = self.find_versions(content, "spack_container")
0117 if spack_versions:
0118 versions["spack_container_versions"].update(spack_versions)
0119 versions["files_with_spack_container"].append(file_path)
0120
0121 except Exception as e:
0122 console.print(
0123 f"[yellow]Warning: Error reading {file_path}: {e}[/yellow]"
0124 )
0125
0126 return versions
0127
0128 def bump_docker_tag(
0129 self,
0130 file_path: Path,
0131 old_tags: set[str],
0132 new_tag: str,
0133 dry_run: bool = False,
0134 show_diff: bool = False,
0135 ) -> tuple[int, str, str]:
0136 """Bump Docker image tags in a file. Returns (replacements, old_content, new_content)."""
0137 content = file_path.read_text()
0138 pattern = self.PATTERNS["docker_tag"]
0139
0140 replacements = 0
0141
0142 def replace_tag(match):
0143 nonlocal replacements
0144 old_tag = match.group(2)
0145 if old_tag in old_tags and old_tag != new_tag:
0146 replacements += 1
0147 return f"{match.group(1)}:{new_tag}"
0148 return match.group(0)
0149
0150 new_content = pattern.sub(replace_tag, content)
0151
0152 if replacements > 0:
0153 if not dry_run:
0154 file_path.write_text(new_content)
0155
0156 prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0157 rel_path = file_path.relative_to(self.repo_root)
0158 console.print(
0159 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0160 )
0161
0162 if show_diff:
0163 self._show_diff(file_path, content, new_content)
0164
0165 return replacements, content, new_content
0166
0167 def bump_spack_container(
0168 self,
0169 file_path: Path,
0170 old_versions: set[str],
0171 new_version: str,
0172 dry_run: bool = False,
0173 show_diff: bool = False,
0174 ) -> tuple[int, str, str]:
0175 """Bump spack-container version in a file. Returns (replacements, old_content, new_content)."""
0176 content = file_path.read_text()
0177 pattern = self.PATTERNS["spack_container"]
0178
0179 replacements = 0
0180
0181 def replace_version(match):
0182 nonlocal replacements
0183 old_version = match.group(2)
0184 if old_version in old_versions and old_version != new_version:
0185 replacements += 1
0186 return f"{match.group(1)}:{new_version}{match.group(3)}"
0187 return match.group(0)
0188
0189 new_content = pattern.sub(replace_version, content)
0190
0191 if replacements > 0:
0192 if not dry_run:
0193 file_path.write_text(new_content)
0194
0195 prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0196 rel_path = file_path.relative_to(self.repo_root)
0197 console.print(
0198 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0199 )
0200
0201 if show_diff:
0202 self._show_diff(file_path, content, new_content)
0203
0204 return replacements, content, new_content
0205
0206 def _show_diff(self, file_path: Path, old_content: str, new_content: str):
0207 """Display a unified diff of the changes."""
0208 rel_path = file_path.relative_to(self.repo_root)
0209 diff = difflib.unified_diff(
0210 old_content.splitlines(),
0211 new_content.splitlines(),
0212 fromfile=str(rel_path),
0213 tofile=str(rel_path),
0214 lineterm="",
0215 )
0216
0217 diff_lines = list(diff)
0218 if diff_lines:
0219 console.print()
0220 diff_text = "\n".join(diff_lines)
0221 syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
0222 console.print(syntax)
0223
0224 def bump_all_docker_tags(
0225 self,
0226 old_tags: set[str],
0227 new_tag: str,
0228 dry_run: bool = False,
0229 show_diff: bool = False,
0230 ) -> int:
0231 """Bump all Docker tags across the repository."""
0232 files = self.find_files()
0233 total_replacements = 0
0234
0235 for file_path in files:
0236 try:
0237 replacements, _, _ = self.bump_docker_tag(
0238 file_path, old_tags, new_tag, dry_run, show_diff
0239 )
0240 total_replacements += replacements
0241 except Exception as e:
0242 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0243
0244 return total_replacements
0245
0246 def bump_all_spack_containers(
0247 self,
0248 old_versions: set[str],
0249 new_version: str,
0250 dry_run: bool = False,
0251 show_diff: bool = False,
0252 ) -> int:
0253 """Bump all spack-container versions across the repository."""
0254 files = self.find_files()
0255 total_replacements = 0
0256
0257 for file_path in files:
0258 try:
0259 replacements, _, _ = self.bump_spack_container(
0260 file_path, old_versions, new_version, dry_run, show_diff
0261 )
0262 total_replacements += replacements
0263 except Exception as e:
0264 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0265
0266 return total_replacements
0267
0268
0269 @app.command()
0270 def scan(
0271 repo_root: Annotated[
0272 Path, typer.Option(help="Root directory of the repository")
0273 ] = Path.cwd(),
0274 ):
0275 """
0276 Scan repository for current versions.
0277
0278 This command scans the codebase and displays all Docker image tags
0279 and spack-container versions currently in use.
0280 """
0281 bumper = VersionBumper(repo_root)
0282 versions = bumper.scan_versions()
0283
0284 console.print()
0285
0286
0287 if versions["docker_tags"]:
0288 console.print("[bold]Docker image tags:[/bold]")
0289 for tag in sorted(versions["docker_tags"]):
0290 console.print(f" [cyan]{tag}[/cyan]")
0291 console.print(
0292 f"[dim] Found in {len(versions['files_with_docker_tags'])} file(s)[/dim]"
0293 )
0294 else:
0295 console.print("[bold]Docker image tags:[/bold] [yellow]none found[/yellow]")
0296
0297 console.print()
0298
0299
0300 if versions["spack_container_versions"]:
0301 console.print("[bold]Spack-container versions:[/bold]")
0302 for version in sorted(versions["spack_container_versions"]):
0303 console.print(f" [cyan]{version}[/cyan]")
0304 console.print(
0305 f"[dim] Found in {len(versions['files_with_spack_container'])} file(s)[/dim]"
0306 )
0307 else:
0308 console.print(
0309 "[bold]Spack-container versions:[/bold] [yellow]none found[/yellow]"
0310 )
0311
0312
0313 @app.command()
0314 def bump_docker_tag(
0315 new_tag: Annotated[str, typer.Argument(help="New Docker tag to use (e.g., 84)")],
0316 repo_root: Annotated[
0317 Path, typer.Option(help="Root directory of the repository")
0318 ] = Path.cwd(),
0319 dry_run: Annotated[
0320 bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0321 ] = False,
0322 show_diff: Annotated[
0323 bool, typer.Option("--diff", help="Show diff of changes")
0324 ] = True,
0325 ):
0326 """
0327 Bump Docker image tags across the repository.
0328
0329 This command finds all Docker images like 'ubuntu2404:83' and updates
0330 all tag numbers to the new value.
0331
0332 Example:
0333 bump_versions.py bump-docker-tag 84
0334 bump_versions.py bump-docker-tag 84 --dry-run --diff
0335 """
0336 bumper = VersionBumper(repo_root)
0337
0338
0339 versions = bumper.scan_versions()
0340 if not versions["docker_tags"]:
0341 console.print("[red]Error: No Docker tags found in repository[/red]")
0342 raise typer.Exit(1)
0343
0344
0345 old_tags = versions["docker_tags"]
0346 console.print(f"[dim]Found Docker tags: {', '.join(sorted(old_tags))}[/dim]")
0347
0348
0349 if len(old_tags) == 1 and new_tag in old_tags:
0350 console.print(f"[yellow]Tag is already {new_tag}, no changes needed[/yellow]")
0351 raise typer.Exit(0)
0352
0353 console.print(f"[dim]Will replace ALL tags with: {new_tag}[/dim]")
0354
0355 console.print()
0356 if dry_run:
0357 console.print(
0358 Panel(
0359 f"[bold]DRY RUN:[/bold] Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0360 border_style="yellow",
0361 )
0362 )
0363 else:
0364 console.print(
0365 Panel(
0366 f"Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0367 border_style="green",
0368 )
0369 )
0370
0371 console.print()
0372 total = bumper.bump_all_docker_tags(old_tags, new_tag, dry_run, show_diff)
0373
0374 console.print()
0375 if total > 0:
0376 if dry_run:
0377 console.print(
0378 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s)"
0379 )
0380 console.print("[dim]Run without --dry-run to apply changes[/dim]")
0381 else:
0382 console.print(
0383 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s)"
0384 )
0385 else:
0386 console.print(f"[yellow]No occurrences found[/yellow]")
0387
0388
0389 @app.command()
0390 def bump_spack(
0391 new_version: Annotated[
0392 str, typer.Argument(help="New spack-container version (e.g., 19.0.0)")
0393 ],
0394 repo_root: Annotated[
0395 Path, typer.Option(help="Root directory of the repository")
0396 ] = Path.cwd(),
0397 dry_run: Annotated[
0398 bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0399 ] = False,
0400 show_diff: Annotated[
0401 bool, typer.Option("--diff", help="Show diff of changes")
0402 ] = True,
0403 ):
0404 """
0405 Bump spack-container version across the repository.
0406
0407 This command updates the spack-container image version used in
0408 .devcontainer/Dockerfile and other configuration files. It replaces
0409 all found versions with the new version.
0410
0411 Example:
0412 bump_versions.py bump-spack 19.0.0
0413 bump_versions.py bump-spack 19.0.0 --dry-run --diff
0414 """
0415 bumper = VersionBumper(repo_root)
0416
0417
0418 versions = bumper.scan_versions()
0419 if not versions["spack_container_versions"]:
0420 console.print(
0421 "[red]Error: No spack-container versions found in repository[/red]"
0422 )
0423 raise typer.Exit(1)
0424
0425
0426 old_versions = versions["spack_container_versions"]
0427 console.print(
0428 f"[dim]Found spack-container versions: {', '.join(sorted(old_versions))}[/dim]"
0429 )
0430
0431
0432 if len(old_versions) == 1 and new_version in old_versions:
0433 console.print(
0434 f"[yellow]Version is already {new_version}, no changes needed[/yellow]"
0435 )
0436 raise typer.Exit(0)
0437
0438 console.print(f"[dim]Will replace ALL versions with: {new_version}[/dim]")
0439
0440 console.print()
0441 if dry_run:
0442 console.print(
0443 Panel(
0444 f"[bold]DRY RUN:[/bold] Bumping spack-container [cyan]{', '.join(sorted(old_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0445 border_style="yellow",
0446 )
0447 )
0448 else:
0449 console.print(
0450 Panel(
0451 f"Bumping spack-container [cyan]{', '.join(sorted(old_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0452 border_style="green",
0453 )
0454 )
0455
0456 console.print()
0457 total = bumper.bump_all_spack_containers(
0458 old_versions, new_version, dry_run, show_diff
0459 )
0460
0461 console.print()
0462 if total > 0:
0463 if dry_run:
0464 console.print(
0465 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s)"
0466 )
0467 console.print("[dim]Run without --dry-run to apply changes[/dim]")
0468 else:
0469 console.print(
0470 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s)"
0471 )
0472 else:
0473 console.print(f"[yellow]No occurrences found[/yellow]")
0474
0475
0476 if __name__ == "__main__":
0477 app()