Complete guided release console workflow
Dependency Audit / dependency-audit (push) Successful in 2m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 15s

This commit is contained in:
2026-07-31 05:46:50 +02:00
parent b4248a849e
commit f1fd143ef5
13 changed files with 2332 additions and 117 deletions
@@ -25,6 +25,10 @@ from .version_alignment import (
selected_release_webui_bundle_issues,
selected_repository_version_issues,
)
from .version_metadata import (
VersionMetadataError,
version_metadata_mutations,
)
def build_selective_release_plan(
@@ -46,6 +50,7 @@ def build_selective_release_plan(
)
for repo in repositories
)
units = dependency_ordered_units(units)
units = apply_repository_version_gate(
units, workspace=Path(dashboard.workspace_root)
)
@@ -103,7 +108,35 @@ def apply_repository_version_gate(
workspace: Path,
) -> tuple[ReleasePlanUnit, ...]:
issues_by_repo: dict[str, list[ReleaseGateFinding]] = {}
version_update_supported_by_repo: dict[str, bool] = {}
deferred_core_lock_repos: set[str] = set()
for unit in units:
version_update_supported = unit.current_version == unit.target_version
if unit.current_version and unit.current_version != unit.target_version:
try:
version_update_supported = bool(
version_metadata_mutations(
workspace / unit.repo,
target_version=unit.target_version,
)
)
except VersionMetadataError as exc:
issues_by_repo.setdefault(unit.repo, []).append(
ReleaseGateFinding(
code="repository_version_mutation_unsupported",
severity="blocker",
message=str(exc),
remediation=(
"Align the unsupported version declarations manually, "
"commit the reviewed result, then rebuild this plan."
),
repo=unit.repo,
source="repository version metadata",
expected=unit.target_version,
actual=unit.current_version,
)
)
version_update_supported_by_repo[unit.repo] = version_update_supported
try:
issues = selected_repository_version_issues(
repo_versions={unit.repo: unit.target_version},
@@ -130,6 +163,18 @@ def apply_repository_version_gate(
)
continue
for issue in issues:
if (
version_update_supported
and issue.message
== "source version must match the requested release version"
):
continue
if (
unit.repo == "govoplan-core"
and issue.source.startswith("package-lock.release.json")
):
deferred_core_lock_repos.add(unit.repo)
continue
issues_by_repo.setdefault(issue.repo, []).append(
ReleaseGateFinding(
code="repository_version_alignment",
@@ -161,17 +206,68 @@ def apply_repository_version_gate(
source_preflight_ready=False,
)
if unit.repo in issues_by_repo
else unit
else replace(
unit,
warnings=planned_version_warnings(
unit,
defer_core_lock=unit.repo in deferred_core_lock_repos,
),
status=(
"attention"
if (
(
unit.current_version
and unit.current_version != unit.target_version
)
or unit.repo in deferred_core_lock_repos
)
and unit.status == "ready"
else unit.status
),
source_preflight_ready=(
unit.source_preflight_ready
or (
bool(unit.current_version)
and unit.current_version != unit.target_version
and version_update_supported_by_repo.get(unit.repo, False)
and not any(
finding.code == "worktree_dirty"
for finding in unit.gate_findings
)
)
),
)
for unit in units
)
def planned_version_warnings(
unit: ReleasePlanUnit,
*,
defer_core_lock: bool,
) -> tuple[str, ...]:
warnings = list(unit.warnings)
if unit.current_version and unit.current_version != unit.target_version:
warnings.append(
"version metadata will be updated by the bounded release executor"
)
if defer_core_lock:
warnings.append(
"Core release lock metadata will be regenerated by the bounded "
"bundle executor"
)
return tuple(warnings)
def apply_release_webui_bundle_gate(
units: tuple[ReleasePlanUnit, ...],
*,
workspace: Path,
) -> tuple[ReleasePlanUnit, ...]:
issues_by_repo: dict[str, list[ReleaseGateFinding]] = {}
selected = {unit.repo for unit in units}
core_selected = "govoplan-core" in selected
planned_bundle_repos: set[str] = set()
try:
issues = selected_release_webui_bundle_issues(
repo_versions={unit.repo: unit.target_version for unit in units},
@@ -200,6 +296,9 @@ def apply_release_webui_bundle_gate(
)
issues = ()
for issue in issues:
if core_selected and issue.repo in selected and issue.repo != "govoplan-core":
planned_bundle_repos.add(issue.repo)
continue
issues_by_repo.setdefault(issue.repo, []).append(
ReleaseGateFinding(
code="release_webui_composition_alignment",
@@ -230,7 +329,25 @@ def apply_release_webui_bundle_gate(
source_preflight_ready=False,
)
if unit.repo in issues_by_repo
else unit
else replace(
unit,
warnings=unit.warnings
+ (
(
"Core release WebUI references will be regenerated after "
"the selected module tags are created"
),
)
if unit.repo == "govoplan-core" and planned_bundle_repos
else unit.warnings,
status=(
"attention"
if unit.repo == "govoplan-core"
and planned_bundle_repos
and unit.status == "ready"
else unit.status
),
)
for unit in units
)
@@ -408,6 +525,7 @@ def build_unit(
status="blocked" if blockers else "attention" if warnings else "ready",
blockers=tuple(blockers),
warnings=tuple(warnings),
capabilities=repository_capabilities(repo, contracts=contracts),
provides_interfaces=provides,
requires_interfaces=requires,
gate_findings=tuple(findings),
@@ -416,11 +534,83 @@ def build_unit(
and not repo.dirty
and repo.has_head
and bool(repo.branch)
and current_version == resolved_target
and current_version is not None
),
)
def repository_capabilities(
repo: RepositorySnapshot,
*,
contracts: ModuleContractSnapshot | None,
) -> tuple[str, ...]:
"""Describe the release operations applicable to one repository shape."""
root = Path(repo.absolute_path)
capabilities = {"git-source"}
if (root / "pyproject.toml").is_file():
capabilities.add("python-package")
if (root / "package.json").is_file() or (root / "webui/package.json").is_file():
capabilities.add("webui-package")
if contracts is not None:
capabilities.add("module-manifest")
if (
(root / "alembic/versions").is_dir()
or (root / "src").is_dir()
and any((root / "src").glob("**/migrations/versions"))
):
capabilities.add("database-migrations")
if (root / "docs").is_dir() or repo.spec.subtype in {"docs", "documentation"}:
capabilities.add("documentation")
if repo.spec.name == "govoplan-core":
capabilities.add("core-release-bundle")
if repo.spec.name == "govoplan":
capabilities.add("release-control-plane")
return tuple(sorted(capabilities))
def dependency_ordered_units(
units: tuple[ReleasePlanUnit, ...],
) -> tuple[ReleasePlanUnit, ...]:
"""Order module providers before consumers while keeping Core last."""
by_repo = {unit.repo: unit for unit in units}
providers: dict[str, set[str]] = {}
for unit in units:
if unit.repo == "govoplan-core":
continue
for provided in unit.provides_interfaces:
providers.setdefault(provided.name, set()).add(unit.repo)
dependencies: dict[str, set[str]] = {unit.repo: set() for unit in units}
for unit in units:
for requirement in unit.requires_interfaces:
dependencies[unit.repo].update(
provider
for provider in providers.get(requirement.name, set())
if provider != unit.repo
)
if "govoplan-core" in dependencies:
dependencies["govoplan-core"].update(
repo for repo in by_repo if repo != "govoplan-core"
)
ordered: list[ReleasePlanUnit] = []
remaining = set(by_repo)
while remaining:
ready = sorted(
repo
for repo in remaining
if not (dependencies[repo] & remaining)
)
if not ready:
ready = [sorted(remaining)[0]]
for repo in ready:
ordered.append(by_repo[repo])
remaining.remove(repo)
return tuple(ordered)
def gate_finding(
*,
repo: str,
@@ -501,24 +691,38 @@ def dry_run_steps(
) -> tuple[ReleasePlanStep, ...]:
steps: list[ReleasePlanStep] = []
snapshots = {repo.spec.name: repo for repo in dashboard.repositories}
selected_names = {unit.repo for unit in units}
if "govoplan-core" in selected_names and len(selected_names) > 1:
steps.append(
ReleasePlanStep(
id="release:cross-repository-ordering",
title="Resolve module/Core release ordering",
detail=(
"A combined Core/module release needs a dependency-aware DAG: "
"local module tags, Core release-lock regeneration and commit, "
"Core tag, full alignment, then atomic pushes. The current linear "
"executor must not publish a module before that lock is committed."
),
command=None,
cwd=dashboard.meta_root,
mutating=True,
status="needs-executor",
core_unit = next((unit for unit in units if unit.repo == "govoplan-core"), None)
non_core_units = tuple(unit for unit in units if unit.repo != "govoplan-core")
bundle_repos: tuple[str, ...] = ()
core_lock_regeneration = False
if core_unit is not None:
try:
bundle_repos = tuple(
sorted(
{
issue.repo
for issue in selected_release_webui_bundle_issues(
repo_versions={
unit.repo: unit.target_version for unit in units
},
workspace=Path(dashboard.workspace_root),
)
if issue.repo != "govoplan-core"
}
)
)
)
core_lock_regeneration = bool(bundle_repos) or any(
issue.source.startswith("package-lock.release.json")
for issue in selected_repository_version_issues(
repo_versions={
"govoplan-core": core_unit.target_version,
},
workspace=Path(dashboard.workspace_root),
)
)
except Exception: # noqa: BLE001 - a structured gate already reports it.
bundle_repos = ()
for unit in units:
steps.append(
ReleasePlanStep(
@@ -531,32 +735,74 @@ def dry_run_steps(
repo=unit.repo,
)
)
for unit in units:
if unit.current_version != unit.target_version:
try:
paths = tuple(
mutation.path
for mutation in version_metadata_mutations(
Path(dashboard.workspace_root) / unit.repo,
target_version=unit.target_version,
)
)
except VersionMetadataError:
paths = ()
snapshot = snapshots.get(unit.repo)
version_supported = bool(
paths and snapshot is not None and not snapshot.dirty
)
steps.append(
ReleasePlanStep(
id=f"{unit.repo}:version",
title=f"Update {unit.repo} version metadata",
detail=f"Prepare version files and manifest metadata for {unit.target_version}.",
command=None,
detail=(
f"Update only the recognized version declarations for "
f"{unit.target_version}: {', '.join(paths) or 'unsupported metadata'}."
),
command=(
"bounded version metadata update "
+ " ".join(shlex.quote(path) for path in paths)
)
if paths
else None,
cwd=f"{dashboard.workspace_root}/{unit.repo}",
mutating=True,
repo=unit.repo,
status="needs-executor",
status="planned" if version_supported else "needs-executor",
)
)
for unit in non_core_units:
snapshot = snapshots.get(unit.repo)
if unit.current_version != unit.target_version or (
needs_commit = unit.current_version != unit.target_version or (
snapshot is not None and snapshot.dirty
):
)
if needs_commit:
commit_supported = bool(
unit.current_version != unit.target_version
and snapshot is not None
and not snapshot.dirty
)
steps.append(
ReleasePlanStep(
id=f"{unit.repo}:commit",
title=f"Commit {unit.repo} release changes",
detail="Commit reviewed changes for this release unit only.",
command=f"git add -A && git commit -m {shlex.quote(f'Release {unit.repo} {unit.target_tag}')}",
detail=(
"Commit only the receipt-bound files written by the version "
"executor."
if commit_supported
else "Pre-existing worktree changes require review outside the release executor."
),
command=(
f"git commit -m {shlex.quote(f'Release {unit.repo} {unit.target_tag}')}"
if commit_supported
else None
),
cwd=f"{dashboard.workspace_root}/{unit.repo}",
mutating=True,
repo=unit.repo,
status="planned" if commit_supported else "needs-executor",
)
)
steps.append(
@@ -570,6 +816,97 @@ def dry_run_steps(
repo=unit.repo,
)
)
if core_unit is not None:
if core_lock_regeneration:
steps.append(
ReleasePlanStep(
id="govoplan-core:bundle",
title="Regenerate the Core release WebUI bundle",
detail=(
"After local module tags exist, update and lock the exact "
"selected WebUI references"
+ (
" for: " + ", ".join(bundle_repos)
if bundle_repos
else ""
)
+ ", and reconcile package/lock metadata."
),
command=(
"tools/release/generate-release-lock.sh "
+ " ".join(
f"--local-git-repo {shlex.quote(str(Path(dashboard.workspace_root) / repo))}"
for repo in bundle_repos
)
),
cwd=dashboard.meta_root,
mutating=True,
repo="govoplan-core",
)
)
core_snapshot = snapshots.get(core_unit.repo)
core_needs_commit = (
core_unit.current_version != core_unit.target_version
or core_lock_regeneration
or (core_snapshot is not None and core_snapshot.dirty)
)
if core_needs_commit:
core_commit_supported = bool(
core_snapshot is not None
and not core_snapshot.dirty
and (
core_unit.current_version != core_unit.target_version
or core_lock_regeneration
)
)
steps.append(
ReleasePlanStep(
id="govoplan-core:commit",
title="Commit govoplan-core release changes",
detail=(
"Commit only the receipt-bound Core version and release-bundle files."
if core_commit_supported
else "Pre-existing Core worktree changes require review outside the release executor."
),
command=(
f"git commit -m {shlex.quote(f'Release govoplan-core {core_unit.target_tag}')}"
if core_commit_supported
else None
),
cwd=f"{dashboard.workspace_root}/govoplan-core",
mutating=True,
repo="govoplan-core",
status="planned" if core_commit_supported else "needs-executor",
)
)
steps.append(
ReleasePlanStep(
id="govoplan-core:tag",
title=f"Create govoplan-core tag {core_unit.target_tag}",
detail="Create the Core tag only after selected module tags and the release lock are verified.",
command=f"git tag -a {shlex.quote(core_unit.target_tag)} -m {shlex.quote(f'Release govoplan-core {core_unit.target_tag}')}",
cwd=f"{dashboard.workspace_root}/govoplan-core",
mutating=True,
repo="govoplan-core",
)
)
if units:
steps.append(
ReleasePlanStep(
id="release:alignment",
title="Verify frozen source and release composition",
detail=(
"Re-run version, contract, tag, and Core WebUI composition gates "
"after local mutations and before any remote publication."
),
command="receipt-bound release alignment",
cwd=dashboard.meta_root,
)
)
for unit in units:
steps.append(
ReleasePlanStep(
id=f"{unit.repo}:push",
@@ -640,6 +977,20 @@ def dry_run_steps(
status="planned",
)
)
steps.append(
ReleasePlanStep(
id="release:install-verify",
title="Verify candidate package installation",
detail=(
"Install every selected candidate wheel into a private "
"temporary target without network access or dependencies, "
"then verify installed package metadata against the frozen plan."
),
command="python -m pip install --no-index --no-deps <candidate wheels>",
cwd=dashboard.meta_root,
status="planned",
)
)
return tuple(steps)