feat(release): publish verified source tags
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 12s

This commit is contained in:
2026-07-22 01:17:07 +02:00
parent eca75cbc93
commit 30f9ed152a
12 changed files with 1735 additions and 40 deletions

View File

@@ -18,6 +18,7 @@ from govoplan_release import (
prepare_repositories,
push_repositories,
sync_repositories,
tag_repositories,
)
from govoplan_release.model import to_jsonable
from govoplan_release.workspace import DEFAULT_WORKSPACE_ROOT, META_ROOT
@@ -35,6 +36,7 @@ class CatalogCandidateRequest(BaseModel):
sequence: int | None = None
public_base_url: str = "https://govoplan.add-ideas.de"
repository_base: str = "git+ssh://git@git.add-ideas.de/add-ideas"
source_remote: str = "origin"
check_public: bool = True
@@ -48,6 +50,7 @@ class PublishCandidateRequest(BaseModel):
tag: bool = False
push: bool = False
remote: str = "origin"
source_remote: str = "origin"
branch: str | None = None
tag_name: str | None = None
npm: str = "npm"
@@ -77,6 +80,16 @@ class RepositoryPrepareRequest(BaseModel):
confirm: str = ""
class RepositoryTagRequest(BaseModel):
repos: list[str] = Field(default_factory=list)
repo_versions: dict[str, str] = Field(default_factory=dict)
remote: str = "origin"
message: str | None = None
apply: bool = False # noqa: A003 - API field mirrors CLI.
push: bool = False
confirm: str = ""
def create_app(*, workspace_root: Path = DEFAULT_WORKSPACE_ROOT, token: str | None = None) -> FastAPI:
app = FastAPI(title="GovOPlaN Release Console", version="0.1.0")
app.state.workspace_root = workspace_root
@@ -203,19 +216,23 @@ def create_app(*, workspace_root: Path = DEFAULT_WORKSPACE_ROOT, token: str | No
signing_keys = tuple(request.signing_keys or default_signing_keys())
if not signing_keys:
raise HTTPException(status_code=400, detail="No signing key supplied and default release key was not found")
candidate = build_selective_catalog_candidate(
repo_versions=repo_versions,
channel=request.channel,
workspace_root=app.state.workspace_root,
output_dir=request.output_dir,
base_catalog=request.base_catalog,
signing_keys=signing_keys,
public_base_url=request.public_base_url,
repository_base=request.repository_base,
expires_days=request.expires_days,
sequence=request.sequence,
check_public=request.check_public,
)
try:
candidate = build_selective_catalog_candidate(
repo_versions=repo_versions,
channel=request.channel,
workspace_root=app.state.workspace_root,
output_dir=request.output_dir,
base_catalog=request.base_catalog,
signing_keys=signing_keys,
public_base_url=request.public_base_url,
repository_base=request.repository_base,
source_remote=request.source_remote,
expires_days=request.expires_days,
sequence=request.sequence,
check_public=request.check_public,
)
except ValueError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return to_jsonable(candidate)
@app.post("/api/catalog-candidates/publish")
@@ -235,6 +252,7 @@ def create_app(*, workspace_root: Path = DEFAULT_WORKSPACE_ROOT, token: str | No
tag=request.tag,
push=request.push,
remote=request.remote,
source_remote=request.source_remote,
branch=request.branch,
tag_name=request.tag_name,
npm=request.npm,
@@ -278,6 +296,26 @@ def create_app(*, workspace_root: Path = DEFAULT_WORKSPACE_ROOT, token: str | No
)
return to_jsonable(result)
@app.post("/api/repositories/tag")
def repository_tag(request: RepositoryTagRequest) -> dict[str, object]:
repos = tuple(dict.fromkeys(repo.strip() for repo in request.repos if repo.strip()))
if not repos:
raise HTTPException(status_code=400, detail="At least one repository must be selected")
if request.apply and request.push and request.confirm != "PUBLISH":
raise HTTPException(status_code=400, detail="Release tag publication requires confirm=PUBLISH")
if request.apply and not request.push and request.confirm != "TAG":
raise HTTPException(status_code=400, detail="Release tag creation requires confirm=TAG")
result = tag_repositories(
repos=repos,
repo_versions=request.repo_versions,
message=request.message,
workspace_root=app.state.workspace_root,
remote=request.remote,
apply=request.apply,
push=request.push,
)
return to_jsonable(result)
return app