feat(release): gate tags on scoped workflows
This commit is contained in:
@@ -24,7 +24,9 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
||||
|
||||
def test_lockstep_release_runs_source_and_full_gates_before_remote_push(self) -> None:
|
||||
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
|
||||
workflow = script.rsplit("\nconfirm_release\n", 1)[1]
|
||||
manifest_gate = script.index("run_manifest_shape_gate\n\nconfirm_release")
|
||||
confirm = script.index("\nconfirm_release\n", manifest_gate)
|
||||
workflow = script[confirm:]
|
||||
|
||||
source_gate = workflow.index("run_version_alignment_gate source")
|
||||
first_commit = workflow.index('run git -C "$repo" commit')
|
||||
@@ -36,6 +38,7 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
||||
self.assertLess(first_commit, lock_generation)
|
||||
self.assertLess(lock_generation, full_gate)
|
||||
self.assertLess(full_gate, first_push)
|
||||
self.assertLess(manifest_gate, confirm)
|
||||
|
||||
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
|
||||
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
|
||||
|
||||
@@ -30,6 +30,13 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
name="govoplan-core",
|
||||
version="0.1.10",
|
||||
)
|
||||
self.manifest_repo, self.manifest_remote = create_release_repo(
|
||||
root=self.root,
|
||||
workspace=self.workspace,
|
||||
name="govoplan-access",
|
||||
version="0.1.10",
|
||||
)
|
||||
add_scoped_workflow_manifest(self.manifest_repo)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
@@ -95,13 +102,42 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
self.assertIn("worktree is not clean", dirty["repositories"][0]["detail"])
|
||||
self.assertFalse(ref_exists(self.repo, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_batch_preflight_blocks_every_mutation_when_a_later_repo_is_dirty(self) -> None:
|
||||
access, access_remote = create_release_repo(
|
||||
def test_user_workflow_scope_contract_blocks_source_tagging(self) -> None:
|
||||
replace_with_unscoped_workflow_manifest(self.manifest_repo)
|
||||
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-core",),
|
||||
repo_versions={"govoplan-core": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "blocked")
|
||||
self.assertIn("scope-conditioned alternative", result["repositories"][0]["detail"])
|
||||
self.assertFalse(ref_exists(self.repo, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_empty_manifest_workspace_blocks_source_tagging(self) -> None:
|
||||
workspace = self.root / "empty-manifest-workspace"
|
||||
workspace.mkdir()
|
||||
core, _ = create_release_repo(
|
||||
root=self.root,
|
||||
workspace=self.workspace,
|
||||
name="govoplan-access",
|
||||
workspace=workspace,
|
||||
name="govoplan-core",
|
||||
version="0.1.10",
|
||||
)
|
||||
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-core",),
|
||||
repo_versions={"govoplan-core": "0.1.10"},
|
||||
workspace_root=workspace,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "blocked")
|
||||
self.assertIn("No module manifests found", result["repositories"][0]["detail"])
|
||||
self.assertFalse(ref_exists(core, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_batch_preflight_blocks_every_mutation_when_a_later_repo_is_dirty(self) -> None:
|
||||
access = self.manifest_repo
|
||||
access_remote = self.manifest_remote
|
||||
(access / "uncommitted.txt").write_text("not reviewed\n", encoding="utf-8")
|
||||
|
||||
result = tag_repositories(
|
||||
@@ -183,6 +219,86 @@ def create_release_repo(*, root: Path, workspace: Path, name: str, version: str)
|
||||
return repo, remote
|
||||
|
||||
|
||||
def add_scoped_workflow_manifest(repo: Path) -> None:
|
||||
package = repo / "src" / "govoplan_access"
|
||||
backend = package / "backend"
|
||||
backend.mkdir(parents=True)
|
||||
(package / "__init__.py").write_text("", encoding="utf-8")
|
||||
(backend / "__init__.py").write_text("", encoding="utf-8")
|
||||
(backend / "manifest.py").write_text(
|
||||
"""from govoplan_core.core.modules import DocumentationCondition, DocumentationTopic, ModuleManifest, PermissionDefinition
|
||||
|
||||
|
||||
def get_manifest():
|
||||
return ModuleManifest(
|
||||
id="access",
|
||||
name="Access",
|
||||
version="0.1.10",
|
||||
permissions=(
|
||||
PermissionDefinition(
|
||||
scope="access:item:read",
|
||||
module_id="access",
|
||||
resource="item",
|
||||
action="read",
|
||||
label="Read items",
|
||||
description="Read example items.",
|
||||
category="Example",
|
||||
level="tenant",
|
||||
),
|
||||
),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="access.workflow.scoped",
|
||||
title="Scoped task",
|
||||
summary="This is a valid release fixture.",
|
||||
documentation_types=("user",),
|
||||
conditions=(DocumentationCondition(required_scopes=("access:item:read",)),),
|
||||
metadata={"kind": "workflow"},
|
||||
),
|
||||
),
|
||||
)
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (repo / "pyproject.toml").open("a", encoding="utf-8") as handle:
|
||||
handle.write(
|
||||
'\n[project.entry-points."govoplan.modules"]\n'
|
||||
'access = "govoplan_access.backend.manifest:get_manifest"\n'
|
||||
)
|
||||
git(repo, "add", "pyproject.toml", "src")
|
||||
git(repo, "commit", "-m", "Add scoped workflow manifest")
|
||||
git(repo, "push", "origin", "main")
|
||||
|
||||
|
||||
def replace_with_unscoped_workflow_manifest(repo: Path) -> None:
|
||||
manifest = repo / "src" / "govoplan_access" / "backend" / "manifest.py"
|
||||
manifest.write_text(
|
||||
"""from govoplan_core.core.modules import DocumentationTopic, ModuleManifest
|
||||
|
||||
|
||||
def get_manifest():
|
||||
return ModuleManifest(
|
||||
id="access",
|
||||
name="Access",
|
||||
version="0.1.10",
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="access.workflow.unscoped",
|
||||
title="Unscoped task",
|
||||
summary="This must block release.",
|
||||
documentation_types=("user",),
|
||||
metadata={"kind": "workflow"},
|
||||
),
|
||||
),
|
||||
)
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
git(repo, "add", str(manifest.relative_to(repo)))
|
||||
git(repo, "commit", "-m", "Replace scoped workflow with unscoped workflow")
|
||||
git(repo, "push", "origin", "main")
|
||||
|
||||
|
||||
def git_text(cwd: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
("git", *args),
|
||||
|
||||
Reference in New Issue
Block a user