intermediate commit
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 16s
Security Audit / security-audit (push) Failing after 11s

This commit is contained in:
2026-07-14 13:32:09 +02:00
parent dd796b4d3c
commit 05ae81d641
58 changed files with 7526 additions and 66 deletions

View File

@@ -0,0 +1,176 @@
"""Read-only release plan suggestions.
This is deliberately not an executor yet. The first console slice should make
state and next actions explicit before any commit, tag, signing, or publish
operation becomes clickable.
"""
from __future__ import annotations
from datetime import UTC, datetime
import shlex
from .model import PlanAction, ReleaseDashboard, ReleasePlan
def build_release_plan(dashboard: ReleaseDashboard) -> ReleasePlan:
actions: list[PlanAction] = []
target_version = dashboard.target_version
target_tag = dashboard.target_tag
for repo in dashboard.repositories:
if not repo.exists:
actions.append(
PlanAction(
id=f"{repo.spec.name}:missing",
title=f"{repo.spec.name} is missing locally",
detail="Clone or bootstrap the repository before planning a release.",
command=f"tools/repo/bootstrap-repositories.py --repo {shlex.quote(repo.spec.name)}",
cwd=dashboard.meta_root,
mutating=True,
severity="blocker",
repo=repo.spec.name,
)
)
continue
if not repo.is_git:
actions.append(
PlanAction(
id=f"{repo.spec.name}:not-git",
title=f"{repo.spec.name} is not a Git repository",
detail=repo.absolute_path,
severity="blocker",
repo=repo.spec.name,
)
)
continue
if repo.safe_directory_required:
actions.append(
PlanAction(
id=f"{repo.spec.name}:safe-directory",
title=f"{repo.spec.name} requires Git safe.directory approval",
detail="Git refused to inspect this checkout until it is trusted for the current user.",
command=repo.safe_directory_command,
cwd=repo.absolute_path,
mutating=True,
severity="blocker",
repo=repo.spec.name,
)
)
continue
if not repo.has_head:
actions.append(
PlanAction(
id=f"{repo.spec.name}:initial-commit",
title=f"{repo.spec.name} needs an initial commit",
detail="The repository exists but has no HEAD yet, so it cannot be tagged or released.",
command="git add -A && git commit -m \"Initialize repository\" && git push -u origin HEAD",
cwd=repo.absolute_path,
mutating=True,
severity="blocker",
repo=repo.spec.name,
)
)
if repo.dirty:
actions.append(
PlanAction(
id=f"{repo.spec.name}:dirty",
title=f"{repo.spec.name} has uncommitted changes",
detail=f"{len(repo.dirty_entries)} changed path(s) need review before release.",
command="git status --short && git diff --stat",
cwd=repo.absolute_path,
severity="blocker",
repo=repo.spec.name,
)
)
if repo.behind:
actions.append(
PlanAction(
id=f"{repo.spec.name}:behind",
title=f"{repo.spec.name} is behind {repo.upstream}",
detail=f"Behind by {repo.behind} commit(s). Release from an up-to-date branch.",
command="git fetch --all --tags --prune",
cwd=repo.absolute_path,
mutating=True,
severity="blocker",
repo=repo.spec.name,
)
)
if repo.ahead:
actions.append(
PlanAction(
id=f"{repo.spec.name}:ahead",
title=f"{repo.spec.name} has unpushed commits",
detail=f"Ahead by {repo.ahead} commit(s). Tags and catalogs should point at pushed commits.",
command="git push",
cwd=repo.absolute_path,
mutating=True,
severity="warning",
repo=repo.spec.name,
)
)
if target_tag and repo.versions.primary == target_version and repo.local_target_tag_exists is False:
actions.append(
PlanAction(
id=f"{repo.spec.name}:tag-missing",
title=f"{repo.spec.name} has no local {target_tag} tag",
detail="The version file matches the target release, but the release tag is missing locally.",
command=f"git tag -a {shlex.quote(target_tag)} -m \"Release {shlex.quote(target_tag)}\"",
cwd=repo.absolute_path,
mutating=True,
severity="info",
repo=repo.spec.name,
)
)
catalog = dashboard.catalog
if target_version and catalog.core_release_version != target_version:
actions.append(
PlanAction(
id="catalog:core-version",
title="Stable catalog does not point at the target core release",
detail=f"catalog core={catalog.core_release_version or '-'}, target={target_version}",
command=(
"KEY_DIR=\"$HOME/.config/govoplan/release-keys\" "
f"tools/release/publish-release-catalog.sh --version {shlex.quote(target_version)} "
"--catalog-signing-key \"release-key-1=$KEY_DIR/release-key-1.pem\" --build-web --dry-run"
),
cwd=dashboard.meta_root,
severity="warning",
)
)
if catalog.catalog_exists and catalog.signature_count == 0:
actions.append(
PlanAction(
id="catalog:unsigned",
title="Stable catalog is unsigned",
detail="Release catalogs must be signed before operators can trust them.",
cwd=dashboard.meta_root,
severity="blocker",
)
)
if not catalog.keyring_exists or catalog.key_count == 0:
actions.append(
PlanAction(
id="catalog:keyring",
title="Catalog keyring is missing or empty",
detail="Generate or publish a public keyring before enforcing signed catalogs.",
command=(
"tools/release/generate-catalog-keypair.py --key-id release-key-1 "
"--private-key \"$HOME/.config/govoplan/release-keys/release-key-1.pem\" "
"--keyring \"$HOME/.config/govoplan/release-keys/keyring.json\""
),
cwd=dashboard.meta_root,
mutating=True,
severity="warning",
)
)
status = "blocked" if any(action.severity == "blocker" for action in actions) else "attention" if actions else "ready"
return ReleasePlan(
generated_at=datetime.now(tz=UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
target_version=target_version,
target_tag=target_tag,
status=status,
actions=tuple(actions),
)