fix(release): reject unpackageable tagged artifacts
This commit is contained in:
@@ -10,12 +10,9 @@ govoplan-admin @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git@v0.
|
|||||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git@v0.1.8
|
govoplan-policy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git@v0.1.8
|
||||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git@v0.1.8
|
govoplan-audit @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git@v0.1.8
|
||||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git@v0.1.8
|
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git@v0.1.8
|
||||||
govoplan-addresses @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-addresses.git@v0.1.8
|
|
||||||
govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.8
|
govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.8
|
||||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.8
|
govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.8
|
||||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.8
|
govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.8
|
||||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.8
|
govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.8
|
||||||
govoplan-scheduling @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-scheduling.git@v0.1.8
|
|
||||||
govoplan-notifications @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-notifications.git@v0.1.8
|
|
||||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.8
|
govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.8
|
||||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.8
|
govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.8
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
@@ -179,6 +180,46 @@ class VersionAlignmentTests(unittest.TestCase):
|
|||||||
self.assertEqual("1.2.3", issues[0].expected)
|
self.assertEqual("1.2.3", issues[0].expected)
|
||||||
self.assertEqual("1.2.4", issues[0].actual)
|
self.assertEqual("1.2.4", issues[0].actual)
|
||||||
|
|
||||||
|
def test_release_refs_must_point_at_installable_versioned_artifacts(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
workspace = Path(tmp)
|
||||||
|
meta = workspace / "govoplan"
|
||||||
|
core = workspace / "govoplan-core"
|
||||||
|
module = workspace / "govoplan-example"
|
||||||
|
meta.mkdir()
|
||||||
|
(core / "webui").mkdir(parents=True)
|
||||||
|
module.mkdir()
|
||||||
|
(meta / "requirements-release.txt").write_text(
|
||||||
|
"govoplan-example @ git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3\n"
|
||||||
|
)
|
||||||
|
(core / "webui" / "package.release.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@govoplan/example-webui": (
|
||||||
|
"git+ssh://git@example.test/acme/govoplan-example.git#v1.2.3"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
subprocess.run(["git", "init", "-q", str(module)], check=True)
|
||||||
|
subprocess.run(["git", "-C", str(module), "config", "user.email", "test@example.test"], check=True)
|
||||||
|
subprocess.run(["git", "-C", str(module), "config", "user.name", "Test"], check=True)
|
||||||
|
(module / "README.md").write_text("uninstallable release\n")
|
||||||
|
subprocess.run(["git", "-C", str(module), "add", "README.md"], check=True)
|
||||||
|
subprocess.run(["git", "-C", str(module), "commit", "-qm", "release"], check=True)
|
||||||
|
subprocess.run(["git", "-C", str(module), "tag", "v1.2.3"], check=True)
|
||||||
|
|
||||||
|
issues = release_composition_issues(meta, core_root=core)
|
||||||
|
|
||||||
|
self.assertEqual(2, len(issues))
|
||||||
|
self.assertEqual(
|
||||||
|
{"v1.2.3:pyproject.toml", "v1.2.3:webui/package.json"},
|
||||||
|
{issue.source for issue in issues},
|
||||||
|
)
|
||||||
|
self.assertTrue(all("must contain" in issue.message for issue in issues))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tomllib
|
||||||
|
|
||||||
from .git_state import collect_versions
|
from .git_state import collect_versions
|
||||||
from .workspace import load_repository_specs, resolve_repo_path
|
from .workspace import load_repository_specs, resolve_repo_path
|
||||||
@@ -164,7 +165,7 @@ def selected_repository_version_issues(
|
|||||||
|
|
||||||
|
|
||||||
def release_composition_issues(meta_root: Path, *, core_root: Path) -> tuple[VersionAlignmentIssue, ...]:
|
def release_composition_issues(meta_root: Path, *, core_root: Path) -> tuple[VersionAlignmentIssue, ...]:
|
||||||
"""Require backend and WebUI release references for a module to agree."""
|
"""Require release references to agree and point at installable artifacts."""
|
||||||
|
|
||||||
python_refs = _python_release_refs(meta_root / "requirements-release.txt")
|
python_refs = _python_release_refs(meta_root / "requirements-release.txt")
|
||||||
webui_refs = _webui_release_refs(core_root / "webui" / "package.release.json")
|
webui_refs = _webui_release_refs(core_root / "webui" / "package.release.json")
|
||||||
@@ -184,9 +185,74 @@ def release_composition_issues(meta_root: Path, *, core_root: Path) -> tuple[Ver
|
|||||||
message="frontend release ref must match the backend release ref",
|
message="frontend release ref must match the backend release ref",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
workspace = meta_root.parent
|
||||||
|
for repo, version in sorted(python_refs.items()):
|
||||||
|
repo_path = workspace / repo
|
||||||
|
if repo_path.exists():
|
||||||
|
issues.extend(
|
||||||
|
_release_artifact_issues(
|
||||||
|
repo=repo,
|
||||||
|
repo_path=repo_path,
|
||||||
|
version=version,
|
||||||
|
artifact="pyproject.toml",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for repo, version in sorted(webui_refs.items()):
|
||||||
|
repo_path = workspace / repo
|
||||||
|
if repo_path.exists():
|
||||||
|
issues.extend(
|
||||||
|
_release_artifact_issues(
|
||||||
|
repo=repo,
|
||||||
|
repo_path=repo_path,
|
||||||
|
version=version,
|
||||||
|
artifact="webui/package.json",
|
||||||
|
)
|
||||||
|
)
|
||||||
return tuple(issues)
|
return tuple(issues)
|
||||||
|
|
||||||
|
|
||||||
|
def _release_artifact_issues(
|
||||||
|
*,
|
||||||
|
repo: str,
|
||||||
|
repo_path: Path,
|
||||||
|
version: str,
|
||||||
|
artifact: str,
|
||||||
|
) -> list[VersionAlignmentIssue]:
|
||||||
|
tag = f"v{version.removeprefix('v')}"
|
||||||
|
payload = _git_tag_file(repo_path, tag, artifact)
|
||||||
|
if payload is None:
|
||||||
|
return [
|
||||||
|
VersionAlignmentIssue(
|
||||||
|
repo=repo,
|
||||||
|
source=f"{tag}:{artifact}",
|
||||||
|
expected="installable package metadata",
|
||||||
|
actual="missing or unreadable",
|
||||||
|
message="release tag must contain its declared package artifact",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
if artifact == "pyproject.toml":
|
||||||
|
parsed = tomllib.loads(payload)
|
||||||
|
project = parsed.get("project")
|
||||||
|
artifact_version = project.get("version") if isinstance(project, dict) else None
|
||||||
|
else:
|
||||||
|
parsed = json.loads(payload)
|
||||||
|
artifact_version = parsed.get("version") if isinstance(parsed, dict) else None
|
||||||
|
except (json.JSONDecodeError, tomllib.TOMLDecodeError):
|
||||||
|
artifact_version = None
|
||||||
|
if artifact_version == version.removeprefix("v"):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
VersionAlignmentIssue(
|
||||||
|
repo=repo,
|
||||||
|
source=f"{tag}:{artifact}",
|
||||||
|
expected=version.removeprefix("v"),
|
||||||
|
actual=str(artifact_version or ""),
|
||||||
|
message="release artifact version must match its immutable tag",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def candidate_catalog_version_issues(payload: object) -> tuple[VersionAlignmentIssue, ...]:
|
def candidate_catalog_version_issues(payload: object) -> tuple[VersionAlignmentIssue, ...]:
|
||||||
"""Validate versions, refs, and selected-unit provenance inside a catalog."""
|
"""Validate versions, refs, and selected-unit provenance inside a catalog."""
|
||||||
|
|
||||||
@@ -514,6 +580,20 @@ def _git_tag_commit(repo_path: Path, tag: str) -> str | None:
|
|||||||
return value if result.returncode == 0 and value else None
|
return value if result.returncode == 0 and value else None
|
||||||
|
|
||||||
|
|
||||||
|
def _git_tag_file(repo_path: Path, tag: str, relative_path: str) -> str | None:
|
||||||
|
if not (repo_path / ".git").exists():
|
||||||
|
return None
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-C", str(repo_path), "show", f"{tag}:{relative_path}"],
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
return result.stdout if result.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
def _json_version(path: Path) -> str | None:
|
def _json_version(path: Path) -> str | None:
|
||||||
value = _payload_version(_json_object(path))
|
value = _payload_version(_json_object(path))
|
||||||
return value
|
return value
|
||||||
|
|||||||
Reference in New Issue
Block a user