mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-20 06:00:06 +08:00
290 lines
8.8 KiB
Python
Executable File
290 lines
8.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from setuptools_scm import get_version as scm_get_version
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Versions:
|
|
python: str
|
|
cargo: str
|
|
docker: str
|
|
git_tag: str
|
|
git_sha: str
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
return Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _run(cmd: list[str], *, env: dict[str, str] | None = None) -> None:
|
|
subprocess.run(cmd, check=True, env=env)
|
|
|
|
|
|
def _git(cmd: list[str]) -> str:
|
|
return (
|
|
subprocess.check_output(["git", *cmd], cwd=_repo_root()).decode("utf-8").strip()
|
|
)
|
|
|
|
|
|
def _compute_versions() -> Versions:
|
|
root = _repo_root()
|
|
python_version = scm_get_version(
|
|
# NOTE: Cargo doesn't support .post versions, so when we are releasing,
|
|
# but not on tag, we use a next version (bumps the patch).
|
|
# EXAMPLE: if the last tag was 0.1.0, then the next version will be 0.1.1-dev.0
|
|
version_scheme="guess-next-dev",
|
|
root=str(root),
|
|
fallback_version="0.0.0",
|
|
)
|
|
|
|
# Convert PEP 440 to a SemVer-ish string for Cargo:
|
|
# 0.1.0.dev3+gabcdef -> 0.1.0-dev.3+gabcdef
|
|
cargo_version = re.sub(r"\.dev(\d+)", r"-dev.\1", python_version)
|
|
|
|
# Docker tags can't contain '+'.
|
|
docker_version = cargo_version.replace("+", "-")
|
|
|
|
git_tag = _git(["describe", "--tags", "--abbrev=0"])
|
|
git_sha = _git(["rev-parse", "--short", "HEAD"])
|
|
|
|
return Versions(
|
|
python=python_version,
|
|
cargo=cargo_version,
|
|
docker=docker_version,
|
|
git_tag=git_tag,
|
|
git_sha=git_sha,
|
|
)
|
|
|
|
|
|
def _docker_image_name(
|
|
image: str,
|
|
registry: str | None,
|
|
namespace: str | None,
|
|
) -> str:
|
|
parts = [p for p in [registry, namespace, image] if p]
|
|
return "/".join(parts)
|
|
|
|
|
|
def _cargo_files_clean() -> bool:
|
|
"""Check if Cargo.toml and Cargo.lock have no uncommitted changes."""
|
|
status = _git(["status", "--porcelain", "Cargo.toml", "Cargo.lock"])
|
|
return status == ""
|
|
|
|
|
|
def set_version(version: str | None = None) -> None:
|
|
"""Set Cargo workspace version. Fails if Cargo files have uncommitted changes."""
|
|
if not _cargo_files_clean():
|
|
raise SystemExit(
|
|
"Cargo.toml or Cargo.lock have uncommitted changes. "
|
|
"Commit or stash them before setting version."
|
|
)
|
|
if version is None:
|
|
version = _compute_versions().cargo
|
|
_run(["cargo", "set-version", "--workspace", version])
|
|
print(f"Set version to {version}")
|
|
|
|
|
|
def reset_version() -> None:
|
|
"""Reset Cargo.toml and Cargo.lock to their git state."""
|
|
_run(["git", "checkout", "--", "Cargo.toml", "Cargo.lock"])
|
|
|
|
|
|
def docker_tag(images: list[str], version: str | None = None) -> None:
|
|
if version is None:
|
|
version = _compute_versions().docker
|
|
namespace = os.getenv("NAV_DOCKER_NAMESPACE")
|
|
registry = os.getenv("NAV_DOCKER_REGISTRY")
|
|
|
|
for image in images:
|
|
full_tag = _docker_image_name(image, registry, namespace)
|
|
_run(["docker", "tag", image, f"{full_tag}:{version}"])
|
|
print(f"{full_tag}:{version}")
|
|
|
|
|
|
def docker_push(
|
|
images: list[str], push_latest: bool, version: str | None = None
|
|
) -> None:
|
|
if version is None:
|
|
version = _compute_versions().docker
|
|
registry = os.getenv("NAV_DOCKER_REGISTRY")
|
|
namespace = os.getenv("NAV_DOCKER_NAMESPACE")
|
|
|
|
user = os.getenv("NAV_DOCKER_USER")
|
|
token = os.getenv("NAV_DOCKER_TOKEN")
|
|
|
|
if user and token and registry:
|
|
_run(["docker", "login", registry, "-u", user, "-p", token])
|
|
else:
|
|
raise SystemExit(
|
|
"Auth is not set up for publishing to Docker registry, see CONTRIBUTING.md for details."
|
|
)
|
|
|
|
for image in images:
|
|
full_tag = _docker_image_name(image, registry, namespace)
|
|
_run(["docker", "push", f"{full_tag}:{version}"])
|
|
if push_latest:
|
|
_run(["docker", "tag", f"{full_tag}:{version}", f"{full_tag}:latest"])
|
|
_run(["docker", "push", f"{full_tag}:latest"])
|
|
|
|
|
|
def python_publish(version: str | None = None) -> None:
|
|
if version is None:
|
|
version = _compute_versions().python
|
|
|
|
repo_url = os.getenv("NAV_PYPI_REPOSITORY_URL")
|
|
username = os.getenv("NAV_PYPI_USERNAME")
|
|
password = os.getenv("NAV_PYPI_PASSWORD")
|
|
|
|
if not repo_url or not username or not password:
|
|
raise SystemExit(
|
|
"Auth is not set up for publishing to PyPI registry, see CONTRIBUTING.md for details."
|
|
)
|
|
|
|
env = dict(os.environ)
|
|
env["UV_PUBLISH_USERNAME"] = username
|
|
env["UV_PUBLISH_PASSWORD"] = password
|
|
|
|
wheels_dir = _repo_root() / "target" / "wheels"
|
|
wheels_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
wheel_paths = sorted(
|
|
p for p in wheels_dir.glob("navigator-*.whl") if f"-{version}-" in p.name
|
|
)
|
|
|
|
if not wheel_paths:
|
|
available = "\n".join(sorted(p.name for p in wheels_dir.glob("*.whl")))
|
|
raise SystemExit(
|
|
f"No wheels found for version {version} in {wheels_dir}.\n"
|
|
f"Available wheels:\n{available or '(none)'}"
|
|
)
|
|
|
|
_run(
|
|
[
|
|
"uv",
|
|
"publish",
|
|
"--publish-url",
|
|
repo_url,
|
|
*[str(path) for path in wheel_paths],
|
|
],
|
|
env=env,
|
|
)
|
|
|
|
|
|
def get_version(format: str) -> None:
|
|
versions = _compute_versions()
|
|
if format == "python":
|
|
print(versions.python)
|
|
elif format == "cargo":
|
|
print(versions.cargo)
|
|
elif format == "docker":
|
|
print(versions.docker)
|
|
else:
|
|
print(f"VERSION_PY={versions.python}")
|
|
print(f"VERSION_CARGO={versions.cargo}")
|
|
print(f"VERSION_DOCKER={versions.docker}")
|
|
print(f"GIT_TAG={versions.git_tag}")
|
|
print(f"GIT_SHA={versions.git_sha}")
|
|
|
|
|
|
def release(images: list[str]) -> None:
|
|
docker_tag(images)
|
|
docker_push(images, push_latest=True)
|
|
python_publish()
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Navigator release tooling.")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
get_version_parser = sub.add_parser("get-version", help="Print computed version.")
|
|
get_version_parser.add_argument(
|
|
"--python", action="store_true", help="Print Python version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--cargo", action="store_true", help="Print Cargo version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--docker", action="store_true", help="Print Docker version only."
|
|
)
|
|
|
|
set_version_parser = sub.add_parser(
|
|
"set-version", help="Set Cargo version (fails if uncommitted changes)."
|
|
)
|
|
set_version_parser.add_argument(
|
|
"--version", help="Version to set (defaults to computed version from git)."
|
|
)
|
|
|
|
sub.add_parser(
|
|
"reset-version", help="Reset Cargo.toml and Cargo.lock to git state."
|
|
)
|
|
|
|
docker_tag_parser = sub.add_parser(
|
|
"docker-tag", help="Tag docker images for release."
|
|
)
|
|
docker_tag_parser.add_argument("images", nargs="+", help="Image names to tag.")
|
|
docker_tag_parser.add_argument(
|
|
"--version", help="Version to tag (defaults to computed version)."
|
|
)
|
|
|
|
docker_push_parser = sub.add_parser("docker-push", help="Push docker images.")
|
|
docker_push_parser.add_argument("images", nargs="+", help="Image names to push.")
|
|
docker_push_parser.add_argument(
|
|
"--version", help="Version to push (defaults to computed version)."
|
|
)
|
|
docker_push_parser.add_argument(
|
|
"--push-latest",
|
|
action="store_true",
|
|
help="Also push :latest tag.",
|
|
)
|
|
|
|
python_publish_parser = sub.add_parser(
|
|
"python-publish", help="Publish python wheel."
|
|
)
|
|
python_publish_parser.add_argument(
|
|
"--version", help="Version to publish (defaults to computed version)."
|
|
)
|
|
|
|
release_parser = sub.add_parser("release", help="Build and publish all artifacts.")
|
|
release_parser.add_argument("images", nargs="+", help="Image names to release.")
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "get-version":
|
|
if args.python:
|
|
get_version("python")
|
|
elif args.cargo:
|
|
get_version("cargo")
|
|
elif args.docker:
|
|
get_version("docker")
|
|
else:
|
|
get_version("all")
|
|
elif args.command == "set-version":
|
|
set_version(version=args.version)
|
|
elif args.command == "reset-version":
|
|
reset_version()
|
|
elif args.command == "docker-tag":
|
|
docker_tag(args.images, version=args.version)
|
|
elif args.command == "docker-push":
|
|
docker_push(args.images, push_latest=args.push_latest, version=args.version)
|
|
elif args.command == "python-publish":
|
|
python_publish(version=args.version)
|
|
elif args.command == "release":
|
|
release(args.images)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|