from __future__ import annotations import os import stat import subprocess import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] SCRIPTS = REPO_ROOT / "skills" / "builder" / "scripts" VERSION_SH = SCRIPTS / "version.sh" PUBLISH_DOCKER = SCRIPTS / "publish_docker.sh" VERSION_MK = SCRIPTS / "version.mk" def git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str: merged = os.environ.copy() if env: merged.update(env) merged.setdefault("GIT_AUTHOR_NAME", "Test") merged.setdefault("GIT_AUTHOR_EMAIL", "test@example.com") merged.setdefault("GIT_COMMITTER_NAME", "Test") merged.setdefault("GIT_COMMITTER_EMAIL", "test@example.com") result = subprocess.run( ["git", *args], cwd=cwd, env=merged, text=True, capture_output=True, check=False, ) if result.returncode != 0: raise AssertionError(f"git {args} failed: {result.stderr}") return result.stdout.strip() def commit(cwd: Path, message: str) -> str: git(cwd, "add", "-A") git(cwd, "commit", "-m", message) return git(cwd, "rev-parse", "HEAD") def sha7(cwd: Path) -> str: return git(cwd, "rev-parse", "HEAD")[:7] def version_sh(*args: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]: merged = os.environ.copy() if env: merged.update(env) return subprocess.run( ["bash", str(VERSION_SH), *args], cwd=cwd, env=merged, text=True, capture_output=True, check=False, ) def init_repo(path: Path) -> None: git(path, "init", "-b", "main") git(path, "config", "user.email", "test@example.com") git(path, "config", "user.name", "Test") class BuilderVersionTests(unittest.TestCase): def test_version_sh_is_executable(self) -> None: mode = VERSION_SH.stat().st_mode self.assertTrue(mode & stat.S_IXUSR, "version.sh must be executable") def test_official_on_stable_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "-a", "v1.4.2", "-m", "1.4.2") result = version_sh(cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout, "1.4.2\n") docker = version_sh("--docker", cwd=repo) self.assertEqual(docker.returncode, 0, docker.stderr) self.assertEqual(docker.stdout, "1.4.2\n") def test_test_version_uses_ancestor_not_global_max(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) repo = root / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "base") git(repo, "tag", "v1.4.2") git(repo, "checkout", "-b", "feat/login-v2") (repo / "b").write_text("2\n", encoding="utf-8") commit(repo, "feature") (repo / "c").write_text("3\n", encoding="utf-8") commit(repo, "feature 2") feature_sha = sha7(repo) git(repo, "checkout", "main") (repo / "d").write_text("4\n", encoding="utf-8") commit(repo, "release") git(repo, "tag", "v1.4.3") git(repo, "checkout", "feat/login-v2") result = version_sh(cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( result.stdout.strip(), f"1.4.2~feat-login-v2.2+g{feature_sha}", ) docker = version_sh("--docker", cwd=repo) self.assertEqual( docker.stdout.strip(), f"1.4.2-feat-login-v2.2.g{feature_sha}", ) def test_ignores_prerelease_and_app_tags(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v1.0.0") (repo / "b").write_text("2\n", encoding="utf-8") commit(repo, "next") git(repo, "tag", "v1.1.0-rc.1") git(repo, "tag", "v1.1.0-app-1") sha = sha7(repo) result = version_sh(cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), f"1.0.0~main.1+g{sha}") def test_no_stable_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") sha = sha7(repo) result = version_sh(cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), f"0.0.0~main+g{sha}") def test_detached_uses_env_branch(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v0.1.0") (repo / "b").write_text("2\n", encoding="utf-8") commit(repo, "next") sha = sha7(repo) git(repo, "checkout", "--detach", "HEAD") result = version_sh(cwd=repo, env={"BUILD_BRANCH": "ci_job"}) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), f"0.1.0~ci-job.1+g{sha}") def test_override_official_rejected_off_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v1.4.2") (repo / "b").write_text("2\n", encoding="utf-8") commit(repo, "next") result = version_sh("--version", "1.4.3", cwd=repo) self.assertEqual(result.returncode, 2) self.assertIn("looks official", result.stderr) def test_override_official_ok_on_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v1.4.2") result = version_sh("--version", "v1.4.2", cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), "1.4.2") def test_override_test_shaped_accepted(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") result = version_sh("--version", "1.4.2~feat.1+gabc1234", "--docker", cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), "1.4.2-feat.1.gabc1234") def test_version_mk_sets_make_version(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "a").write_text("1\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v2.0.0") makefile = repo / "Makefile" makefile.write_text( "include {mk}\n" ".PHONY: version\n" "version:\n" "\t@echo \"$(VERSION)\"\n".format(mk=VERSION_MK), encoding="utf-8", ) result = subprocess.run( ["make", "--no-print-directory", "-C", str(repo), "version"], text=True, capture_output=True, check=False, ) self.assertEqual(result.returncode, 0, result.stderr + result.stdout) self.assertEqual(result.stdout.strip(), "2.0.0") def test_publish_docker_dry_run_uses_derived_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v3.1.4") result = subprocess.run( [ "bash", str(PUBLISH_DOCKER), "--registry", "registry.example.com", "--repository", "ns/app", "--dry-run", ], cwd=repo, text=True, capture_output=True, check=False, ) self.assertEqual(result.returncode, 0, result.stderr + result.stdout) self.assertIn("Image: registry.example.com/ns/app:3.1.4", result.stdout) def test_publish_docker_rejects_official_tag_off_head(self) -> None: with tempfile.TemporaryDirectory() as temp: repo = Path(temp) / "repo" repo.mkdir() init_repo(repo) (repo / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8") commit(repo, "init") git(repo, "tag", "v1.0.0") (repo / "b").write_text("2\n", encoding="utf-8") commit(repo, "next") result = subprocess.run( [ "bash", str(PUBLISH_DOCKER), "--registry", "registry.example.com", "--repository", "ns/app", "--tag", "1.0.1", "--dry-run", ], cwd=repo, text=True, capture_output=True, check=False, ) self.assertEqual(result.returncode, 2) self.assertIn("looks official", result.stderr) if __name__ == "__main__": unittest.main()