save progress
This commit is contained in:
350
.gitea/scripts/release/bump-service-version.py
Normal file
350
.gitea/scripts/release/bump-service-version.py
Normal file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
bump-service-version.py - Bump service version in centralized version storage
|
||||
|
||||
Sprint: CI/CD Enhancement - Per-Service Auto-Versioning
|
||||
This script manages service versions stored in src/Directory.Versions.props
|
||||
and devops/releases/service-versions.json.
|
||||
|
||||
Usage:
|
||||
python bump-service-version.py <service> <bump-type> [options]
|
||||
python bump-service-version.py authority patch
|
||||
python bump-service-version.py scanner minor --dry-run
|
||||
python bump-service-version.py cli major --commit
|
||||
|
||||
Arguments:
|
||||
service Service name (authority, attestor, concelier, scanner, etc.)
|
||||
bump-type Version bump type: major, minor, patch, or explicit version (e.g., 2.0.0)
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be changed without modifying files
|
||||
--commit Commit changes to git after updating
|
||||
--no-manifest Skip updating service-versions.json manifest
|
||||
--git-sha SHA Git SHA to record in manifest (defaults to HEAD)
|
||||
--docker-tag TAG Docker tag to record in manifest
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Repository paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent.parent
|
||||
VERSIONS_FILE = REPO_ROOT / "src" / "Directory.Versions.props"
|
||||
MANIFEST_FILE = REPO_ROOT / "devops" / "releases" / "service-versions.json"
|
||||
|
||||
# Service name mapping (lowercase key -> property suffix)
|
||||
SERVICE_MAP = {
|
||||
"authority": "Authority",
|
||||
"attestor": "Attestor",
|
||||
"concelier": "Concelier",
|
||||
"scanner": "Scanner",
|
||||
"policy": "Policy",
|
||||
"signer": "Signer",
|
||||
"excititor": "Excititor",
|
||||
"gateway": "Gateway",
|
||||
"scheduler": "Scheduler",
|
||||
"cli": "Cli",
|
||||
"orchestrator": "Orchestrator",
|
||||
"notify": "Notify",
|
||||
"sbomservice": "SbomService",
|
||||
"vexhub": "VexHub",
|
||||
"evidencelocker": "EvidenceLocker",
|
||||
}
|
||||
|
||||
|
||||
def parse_version(version_str: str) -> Tuple[int, int, int]:
|
||||
"""Parse semantic version string into tuple."""
|
||||
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version_str)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid version format: {version_str}")
|
||||
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||
|
||||
|
||||
def format_version(major: int, minor: int, patch: int) -> str:
|
||||
"""Format version tuple as string."""
|
||||
return f"{major}.{minor}.{patch}"
|
||||
|
||||
|
||||
def bump_version(current: str, bump_type: str) -> str:
|
||||
"""Bump version according to bump type."""
|
||||
# Check if bump_type is an explicit version
|
||||
if re.match(r"^\d+\.\d+\.\d+$", bump_type):
|
||||
return bump_type
|
||||
|
||||
major, minor, patch = parse_version(current)
|
||||
|
||||
if bump_type == "major":
|
||||
return format_version(major + 1, 0, 0)
|
||||
elif bump_type == "minor":
|
||||
return format_version(major, minor + 1, 0)
|
||||
elif bump_type == "patch":
|
||||
return format_version(major, minor, patch + 1)
|
||||
else:
|
||||
raise ValueError(f"Invalid bump type: {bump_type}")
|
||||
|
||||
|
||||
def read_version_from_props(service_key: str) -> Optional[str]:
|
||||
"""Read current version from Directory.Versions.props."""
|
||||
if not VERSIONS_FILE.exists():
|
||||
return None
|
||||
|
||||
property_name = f"StellaOps{SERVICE_MAP[service_key]}Version"
|
||||
pattern = rf"<{property_name}>(\d+\.\d+\.\d+)</{property_name}>"
|
||||
|
||||
content = VERSIONS_FILE.read_text(encoding="utf-8")
|
||||
match = re.search(pattern, content)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def update_version_in_props(service_key: str, new_version: str, dry_run: bool = False) -> bool:
|
||||
"""Update version in Directory.Versions.props."""
|
||||
if not VERSIONS_FILE.exists():
|
||||
print(f"Error: {VERSIONS_FILE} not found", file=sys.stderr)
|
||||
return False
|
||||
|
||||
property_name = f"StellaOps{SERVICE_MAP[service_key]}Version"
|
||||
pattern = rf"(<{property_name}>)\d+\.\d+\.\d+(</{property_name}>)"
|
||||
replacement = rf"\g<1>{new_version}\g<2>"
|
||||
|
||||
content = VERSIONS_FILE.read_text(encoding="utf-8")
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
|
||||
if count == 0:
|
||||
print(f"Error: Property {property_name} not found in {VERSIONS_FILE}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if dry_run:
|
||||
print(f"[DRY-RUN] Would update {VERSIONS_FILE}")
|
||||
print(f"[DRY-RUN] {property_name}: {new_version}")
|
||||
else:
|
||||
VERSIONS_FILE.write_text(new_content, encoding="utf-8")
|
||||
print(f"Updated {VERSIONS_FILE}")
|
||||
print(f" {property_name}: {new_version}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_manifest(
|
||||
service_key: str,
|
||||
new_version: str,
|
||||
git_sha: Optional[str] = None,
|
||||
docker_tag: Optional[str] = None,
|
||||
dry_run: bool = False,
|
||||
) -> bool:
|
||||
"""Update service-versions.json manifest."""
|
||||
if not MANIFEST_FILE.exists():
|
||||
print(f"Warning: {MANIFEST_FILE} not found, skipping manifest update", file=sys.stderr)
|
||||
return True
|
||||
|
||||
try:
|
||||
manifest = json.loads(MANIFEST_FILE.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing {MANIFEST_FILE}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if service_key not in manifest.get("services", {}):
|
||||
print(f"Warning: Service '{service_key}' not found in manifest", file=sys.stderr)
|
||||
return True
|
||||
|
||||
# Update service entry
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
service = manifest["services"][service_key]
|
||||
service["version"] = new_version
|
||||
service["releasedAt"] = now
|
||||
|
||||
if git_sha:
|
||||
service["gitSha"] = git_sha
|
||||
if docker_tag:
|
||||
service["dockerTag"] = docker_tag
|
||||
|
||||
# Update manifest timestamp
|
||||
manifest["lastUpdated"] = now
|
||||
|
||||
if dry_run:
|
||||
print(f"[DRY-RUN] Would update {MANIFEST_FILE}")
|
||||
print(f"[DRY-RUN] {service_key}.version: {new_version}")
|
||||
if docker_tag:
|
||||
print(f"[DRY-RUN] {service_key}.dockerTag: {docker_tag}")
|
||||
else:
|
||||
MANIFEST_FILE.write_text(
|
||||
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Updated {MANIFEST_FILE}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_git_sha() -> Optional[str]:
|
||||
"""Get current git HEAD SHA."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=REPO_ROOT,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()[:12] # Short SHA
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
|
||||
def commit_changes(service_key: str, old_version: str, new_version: str) -> bool:
|
||||
"""Commit version changes to git."""
|
||||
try:
|
||||
# Stage the files
|
||||
subprocess.run(
|
||||
["git", "add", str(VERSIONS_FILE), str(MANIFEST_FILE)],
|
||||
cwd=REPO_ROOT,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create commit
|
||||
commit_msg = f"""chore({service_key}): bump version {old_version} -> {new_version}
|
||||
|
||||
Automated version bump via bump-service-version.py
|
||||
|
||||
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"""
|
||||
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", commit_msg],
|
||||
cwd=REPO_ROOT,
|
||||
check=True,
|
||||
)
|
||||
print(f"Committed version bump: {old_version} -> {new_version}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error committing changes: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def generate_docker_tag(version: str) -> str:
|
||||
"""Generate Docker tag with datetime suffix: {version}+{YYYYMMDDHHmmss}."""
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
||||
return f"{version}+{timestamp}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bump service version in centralized version storage",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s authority patch # Bump authority from 1.0.0 to 1.0.1
|
||||
%(prog)s scanner minor --dry-run # Preview bumping scanner minor version
|
||||
%(prog)s cli 2.0.0 --commit # Set CLI to 2.0.0 and commit
|
||||
%(prog)s gateway patch --docker-tag # Bump and generate docker tag
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"service",
|
||||
choices=list(SERVICE_MAP.keys()),
|
||||
help="Service name to bump",
|
||||
)
|
||||
parser.add_argument(
|
||||
"bump_type",
|
||||
help="Bump type: major, minor, patch, or explicit version (e.g., 2.0.0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would be changed without modifying files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--commit",
|
||||
action="store_true",
|
||||
help="Commit changes to git after updating",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-manifest",
|
||||
action="store_true",
|
||||
help="Skip updating service-versions.json manifest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--git-sha",
|
||||
help="Git SHA to record in manifest (defaults to HEAD)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--docker-tag",
|
||||
nargs="?",
|
||||
const="auto",
|
||||
help="Docker tag to record in manifest (use 'auto' to generate)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-version",
|
||||
action="store_true",
|
||||
help="Output only the new version (for CI scripts)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-docker-tag",
|
||||
action="store_true",
|
||||
help="Output only the docker tag (for CI scripts)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read current version
|
||||
current_version = read_version_from_props(args.service)
|
||||
if not current_version:
|
||||
print(f"Error: Could not read current version for {args.service}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Calculate new version
|
||||
try:
|
||||
new_version = bump_version(current_version, args.bump_type)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Generate docker tag if requested
|
||||
docker_tag = None
|
||||
if args.docker_tag:
|
||||
docker_tag = generate_docker_tag(new_version) if args.docker_tag == "auto" else args.docker_tag
|
||||
|
||||
# Output mode for CI scripts
|
||||
if args.output_version:
|
||||
print(new_version)
|
||||
sys.exit(0)
|
||||
if args.output_docker_tag:
|
||||
print(docker_tag or generate_docker_tag(new_version))
|
||||
sys.exit(0)
|
||||
|
||||
# Print summary
|
||||
print(f"Service: {args.service}")
|
||||
print(f"Current version: {current_version}")
|
||||
print(f"New version: {new_version}")
|
||||
if docker_tag:
|
||||
print(f"Docker tag: {docker_tag}")
|
||||
print()
|
||||
|
||||
# Update version in props file
|
||||
if not update_version_in_props(args.service, new_version, args.dry_run):
|
||||
sys.exit(1)
|
||||
|
||||
# Update manifest if not skipped
|
||||
if not args.no_manifest:
|
||||
git_sha = args.git_sha or get_git_sha()
|
||||
if not update_manifest(args.service, new_version, git_sha, docker_tag, args.dry_run):
|
||||
sys.exit(1)
|
||||
|
||||
# Commit if requested
|
||||
if args.commit and not args.dry_run:
|
||||
if not commit_changes(args.service, current_version, new_version):
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print(f"Successfully bumped {args.service}: {current_version} -> {new_version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user