This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
platform-ci/platform_ci/delivery/source_lifecycle.py
forge-recoveryandClaude Sonnet 5.5 a36f17b857
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 6s
feat(ci): advance shared pipeline capabilities
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 10:30:25 +00:00

456 lines
25 KiB
Python

"""The source-lifecycle write phase after a verified, published release: persist the build-effective version, from the captured `qrun-source-version.mjs`.
UNTESTED: written while test execution was suspended; nothing here has run, signed, pushed or read a credential. Reached only by the `finalize-version` command (`delivery/finalize_version.py`),
which no workflow job calls yet (see "Activation prerequisites" in `IMPLEMENTATION.md`).
What it ports, and from where (`references/release-lifecycle/b06800a6…/scripts/qrun-source-version.mjs`, receipt `references/source-version-helper-receipt.json`; the `revisionOnlyChange` rule is
from the captured `qrun-maven-prepare.mjs`): the version-plan checks, "no source write until the whole reactor is read back byte-identical from the private registry and the Central completion
is terminal", and `persistVersion`: a staging branch at the exact source commit, one server-signed `PUT` of the one file, strict verification of that commit (one file, one parent,
server-verified, exact message, exact bytes), a normal NON-forced fast-forward push through bare Git, an acknowledged head, and deletion of only the exact heads this run created. A push
failure is a warning unless the channel requires the push.
What changed, and only what had to: the reactor, the publication and the Central completion are read from the VERIFIED BUNDLE and the terminal records the candidate's publisher already writes
(`maven_reactor`, `completion`, `completion_receipt`), not from a legacy Actions artifact and a poll; the plan is derived from repository configuration (`version_write_back`) and those facts, not
from a prepare-script artifact. Source-writing code is this pinned platform module only: no application script ever runs with the write credential. The credential is the job's own ephemeral
token, given to Git only through the child environment (`SubprocessTagGit`, the existing tag boundary's class) and to the forge only by `SourceWriter`, which confines every request to the
enrolled repository, the one planned file and the one staging branch.
"""
import base64
import hashlib
import json
import re
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, Final, cast
from platform_ci.configuration import WriteBackPolicy
from platform_ci.delivery import completion_receipt, legacy
from platform_ci.delivery.completion import reactor_for, verified_modules
from platform_ci.delivery.errors import DeliveryError
from platform_ci.delivery.manifest import ManifestFile
from platform_ci.delivery.release_tag import TRUSTED_FORGE
from platform_ci.delivery.reuse import ReleaseArtifact
from platform_ci.delivery.signed_tag import SignedTagError, SubprocessTagGit
_HTTP_OK: Final = 200
_HTTP_MULTIPLE_CHOICES: Final = 300
_HTTP_NOT_FOUND: Final = 404
MAX_POM_BYTES: Final = 512 * 1024
SUPPORTED_FILE: Final = "pom.xml"
"""The captured helper rewrites only the root POM's `<properties><revision>`; no other file or property is supported."""
_REVISION_VALUE: Final = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]*$")
_SHA40: Final = re.compile(r"^[0-9a-f]{40}$")
_TOKENS: Final = re.compile(r"<(/?)([A-Za-z][\w.-]*)[^>]*?(/?)>|([^<]+)")
class SourceLifecycleError(DeliveryError):
"""A source-lifecycle refusal."""
class Fatal(SourceLifecycleError): # noqa: N818 - the established name of the captured postpublish contract, imported by name
"""A condition that makes the publication or the plan untrustworthy: never downgraded to a warning."""
class PushFailure(SourceLifecycleError): # noqa: N818 - the established name of the captured postpublish contract, imported by name
"""The version could not be pushed (race, protection, forge): a warning, except where the channel requires the push."""
class NotYetPublished(SourceLifecycleError): # noqa: N818 - the established name of the captured postpublish contract, imported by name
"""The registry files or the Central completion are not (all) there yet: nothing is written and the next invocation tries again."""
# --------------------------------------------------------------------------------------------------- the revision rule
def _revision_paths(text: str) -> list[str]:
"""Return the element path of every `<revision>` from a comment, PI and CDATA free token scan (DOCTYPE and ENTITY refused)."""
if re.search(r"<!\s*(DOCTYPE|ENTITY)", text, re.IGNORECASE):
raise SourceLifecycleError("DTD/entity declarations are not allowed")
body = re.sub(r"<!\[CDATA\[[\s\S]*?\]\]>", "", re.sub(r"<\?[\s\S]*?\?>", "", re.sub(r"<!--[\s\S]*?-->", "", text)))
stack: list[str] = []
found: list[str] = []
for match in _TOKENS.finditer(body):
closing, name, self_closing, text_node = match.groups()
if text_node is not None:
continue
if closing:
if stack:
stack.pop()
else:
if not self_closing:
stack.append(name)
if name == "revision":
found.append("/".join(stack))
return found
def revision_only_change(old: str, new: str) -> tuple[str, str]:
"""Return (declared, effective) when the two root POMs differ ONLY in the one root `properties/revision` value, exactly as the captured rule; anything else raises."""
def locate(text: str) -> re.Match[str]:
matches = list(re.finditer(r"<revision>([^<]*)</revision>", text))
paths = _revision_paths(text)
if len(matches) != 1 or len(re.findall(r"<revision[\s>/]", text)) != 1 or paths != ["project/properties/revision"]:
raise SourceLifecycleError("Expected exactly one root properties revision")
return matches[0]
before, after = locate(old), locate(new)
if not _REVISION_VALUE.match(after.group(1)):
raise SourceLifecycleError("Unsupported computed revision value")
if new != old[: before.start()] + after.group(0) + old[before.end() :]:
raise SourceLifecycleError("The version rewrite changed more than the root revision value")
return before.group(1), after.group(1)
def rewrite_revision(old: str, effective: str) -> str:
"""Return `old` with ONLY the one root `properties/revision` value replaced by `effective`; the result is re-checked by `revision_only_change`, so nothing else can differ."""
if not _REVISION_VALUE.match(effective):
raise SourceLifecycleError("Unsupported computed revision value")
matches = list(re.finditer(r"<revision>([^<]*)</revision>", old))
if len(matches) != 1:
raise SourceLifecycleError("Expected exactly one root properties revision")
start, end = matches[0].span(1)
new = old[:start] + effective + old[end:]
revision_only_change(old, new)
return new
def git_blob_sha(data: bytes) -> str:
"""Return the Git blob id of the bytes (what the forge's `contents` API reports as `sha`)."""
return hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest() # noqa: S324 - a Git object id, not a security digest
# ---------------------------------------------------------------------------------------------------- the plan
@dataclass(frozen=True)
class WriteContext:
"""The authoritative facts of the write run: the repository, the verified source commit, the branch the bump goes to, an optional run tag, and the run identity."""
repository: str
sha: str
branch: str
run: int
attempt: int
tag: str = ""
@dataclass(frozen=True)
class VersionPlan:
"""One planned bump: the exact old and new bytes of the one file, bound to the source commit and blob, changing only the revision to the verified version."""
path: str
source_commit: str
source_blob: str
old: bytes
new: bytes
declared: str
effective: str
require_push: bool
def build_plan(ctx: WriteContext, policy: WriteBackPolicy, *, old: bytes, new: bytes, verified_version: str) -> VersionPlan | None:
"""Derive the plan from the source POM and the rewritten POM; None when the version is unchanged (nothing to persist, which is not an error).
The effective version must be exactly the version the verified bundle recorded; a different one is a relabelling and is refused.
"""
if len(old) > MAX_POM_BYTES or len(new) > MAX_POM_BYTES:
raise Fatal("the POM exceeds the plan bound")
declared, effective = revision_only_change(old.decode("utf-8"), new.decode("utf-8"))
if effective != verified_version:
raise Fatal("the planned revision is not the version the verified bundle recorded")
if old == new:
return None
if ctx.tag and not policy.target_branch:
raise Fatal("a tag run persists a version only to a branch the channel's configuration names")
if not _SHA40.fullmatch(ctx.sha):
raise Fatal("the source commit must be a full commit id")
return VersionPlan(SUPPORTED_FILE, ctx.sha, git_blob_sha(old), old, new, declared, effective, policy.require_push)
# ---------------------------------------------------------------------------------------------------- the confined write client
Send = Callable[[str, str, Mapping[str, str], bytes | None], tuple[int, bytes]]
def _urllib_send(method: str, url: str, headers: Mapping[str, str], data: bytes | None) -> tuple[int, bytes]:
"""One HTTPS request that never follows a redirect; an HTTP error status is returned, not raised, as the captured helper's `call` did."""
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args: object, **kwargs: object) -> None: # type: ignore[override]
raise SourceLifecycleError("unexpected redirect")
request = urllib.request.Request(url, method=method, data=data, headers=dict(headers)) # noqa: S310 - the URL is built from the trusted origin by SourceWriter only
try:
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
return response.status, response.read(8 * 1024 * 1024)
except urllib.error.HTTPError as error:
status = error.code
error.close()
return status, b""
except (urllib.error.URLError, TimeoutError, OSError):
return 0, b""
class SourceWriter:
"""The one forge client of the write phase: `call(method, path, body=None) -> (status, json)`, confined to what `persist_version` needs.
Allowed: GET the repository, one branch, one commit, one Git commit, one run, one tag, and the planned file at a commit id; POST exactly the staging branch; PUT exactly the planned
file on exactly the staging branch without force; DELETE exactly the staging branch. Everything else is refused before any request, whatever the token could do.
"""
def __init__(self, token: str, ctx: WriteContext, plan_path: str = SUPPORTED_FILE, send: Send = _urllib_send) -> None:
"""Bind the token, the repository and the run's staging branch name."""
if not token or re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", ctx.repository) is None:
raise SourceLifecycleError("the job token and an owner/name repository are required")
self._token, self._send = token, send
self.repository, self.path = ctx.repository, plan_path
self.stage = f"ci/version-stage-{ctx.run}-{ctx.attempt}"
base = f"/repos/{ctx.repository}"
self._get = re.compile(re.escape(base) + r"(?:|/branches/[^/?#]+|/commits/[0-9a-f]{40}|/git/commits/[0-9a-f]{40}|/actions/runs/[1-9][0-9]*|/tags/[A-Za-z0-9][A-Za-z0-9._-]*|/contents/" + re.escape(plan_path) + r"\?ref=[0-9a-f]{40})")
self._exact = {("POST", f"{base}/branches"), ("PUT", f"{base}/contents/{plan_path}"), ("DELETE", f"{base}/branches/{urllib.parse.quote(self.stage, safe='')}")}
def __call__(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> tuple[int, Any]:
"""Send one confined request and return (status, decoded JSON or None)."""
if not ((method == "GET" and self._get.fullmatch(path) and body is None) or ((method, path) in self._exact and self._body_ok(method, body))):
raise SourceLifecycleError("refusing a forge request outside the planned version write")
headers = {"Authorization": f"token {self._token}", "Accept": "application/json", "Content-Type": "application/json"}
status, raw = self._send(method, TRUSTED_FORGE + "/api/v1" + path, headers, None if body is None else json.dumps(body).encode())
try:
return status, json.loads(raw) if raw else None
except ValueError:
return status, None
def _body_ok(self, method: str, body: Mapping[str, Any] | None) -> bool:
"""Return whether the body is one the staging write may carry: the staging branch at the source commit, or the planned file on the staging branch without force."""
if method == "DELETE":
return body is None
if method == "POST":
return isinstance(body, dict) and body.get("new_branch_name") == self.stage and set(body) == {"new_branch_name", "old_ref_name"}
return isinstance(body, dict) and body.get("branch") == self.stage and body.get("force_push") is False and set(body) == {"branch", "sha", "content", "message", "force_push"}
class VersionGit(SubprocessTagGit):
"""Bare Git with the token only in the child environment: fetch the verified staging commit, verify it again, push it as a normal fast-forward."""
def push_verified(self, stage: str, child: str, source: str, branch: str) -> None:
"""Fetch the staging branch, require it to be exactly `child` with `source` as its sole parent, and push `child` to `branch` (never forced, hooks off)."""
with tempfile.TemporaryDirectory() as work:
try:
self._git(work, "init", "--bare", "--template=", "-q")
self._git(work, "fetch", "--no-tags", self.url, f"refs/heads/{stage}:refs/cas/stage")
except SignedTagError as error:
raise PushFailure("git could not fetch the staging commit") from error
try:
fetched = self._git(work, "rev-parse", "--verify", "refs/cas/stage^{commit}").decode().strip()
lineage = self._git(work, "rev-list", "--parents", "-n", "1", child).decode().strip()
except SignedTagError as error:
raise Fatal("the fetched object cannot be verified") from error
if fetched != child:
raise Fatal("the fetched object is not the verified staged commit")
if lineage != f"{child} {source}":
raise Fatal("the fetched commit parent is not the source commit")
try:
self._git(work, "push", "--no-verify", self.url, f"{child}:refs/heads/{branch}")
except SignedTagError as error:
raise PushFailure("the version push was rejected") from error
# --------------------------------------------------------------------------------------------------------- the write itself
Writer = Callable[..., tuple[int, Any]]
def _good(result: tuple[int, Any], what: str) -> object:
"""Return the JSON of a 2xx answer; any other status is a push failure (a race, protection or a forge hiccup)."""
status, body = result
if not _HTTP_OK <= status < _HTTP_MULTIPLE_CHOICES:
raise PushFailure(f"{what} status {status}")
return cast("object", body)
def _fields(value: object) -> dict[str, Any]:
"""Return a JSON object's fields; any other value (absent, null, a list) reads as an empty object, as the captured `x || {}` did."""
return cast("dict[str, Any]", value) if isinstance(value, dict) else {}
def _items(value: object) -> list[dict[str, Any]]:
"""Return the JSON objects of a JSON array; anything else reads as no items."""
return [_fields(item) for item in cast("list[object]", value)] if isinstance(value, list) else []
def verify_bump_commit(ctx: WriteContext, plan: VersionPlan, child: str, writer: Writer) -> None:
"""Re-verify, from the forge alone, that `child` is exactly the planned bump: one file, the source commit as sole parent, server-verified, exact message and bytes."""
prefix = f"/repos/{ctx.repository}"
commit = _fields(_good(writer("GET", f"{prefix}/commits/{child}"), "commit"))
if [item.get("filename") for item in _items(commit.get("files"))] != [plan.path]:
raise Fatal(f"the staged commit does not change exactly {plan.path}")
if [item.get("sha") for item in _items(commit.get("parents"))] != [plan.source_commit]:
raise Fatal("the staged commit parent is not the source commit")
git_commit = _fields(_good(writer("GET", f"{prefix}/git/commits/{child}"), "git commit"))
embedded = _fields(git_commit.get("commit"))
verification = _fields(embedded.get("verification") or git_commit.get("verification"))
if verification.get("verified") is not True:
raise Fatal("the staged commit is not server-verified")
message = str(embedded.get("message") or git_commit.get("message") or "").strip()
if message != f"Bump version to {plan.effective} [skip ci]":
raise Fatal("the staged commit message differs")
staged = _fields(_good(writer("GET", f"{prefix}/contents/{plan.path}?ref={child}"), "contents"))
try:
staged_bytes = base64.b64decode(staged.get("content", ""), validate=True)
except ValueError as error:
raise Fatal("the staged file is not valid base64") from error
if staged_bytes != plan.new:
raise Fatal("the staged bytes differ from the plan")
def persist_version(ctx: WriteContext, plan: VersionPlan, writer: Writer, git: VersionGit) -> str:
"""Persist the bump as the captured helper did, normal and non-forced, and return the pushed commit id (a `PushFailure` leaves the branch untouched)."""
prefix = f"/repos/{ctx.repository}"
stage = f"ci/version-stage-{ctx.run}-{ctx.attempt}"
owned: set[str] = set()
def head(branch: str) -> str | None:
body = _fields(_good(writer("GET", f"{prefix}/branches/{urllib.parse.quote(branch, safe='')}"), f"branch {branch}"))
return cast("str | None", _fields(body.get("commit")).get("id"))
try:
if ctx.tag:
status, found = writer("GET", f"{prefix}/tags/{ctx.tag}")
if status != _HTTP_OK or _fields(_fields(found).get("commit")).get("sha") != ctx.sha:
raise Fatal("the run tag no longer points at the source commit")
if head(ctx.branch) != ctx.sha:
raise PushFailure("the canonical branch moved since the source commit")
_good(writer("POST", f"{prefix}/branches", {"new_branch_name": stage, "old_ref_name": ctx.sha}), "create staging branch")
owned.add(ctx.sha)
if head(stage) != ctx.sha:
raise Fatal("the staging branch is not at the source commit")
written = _good(
writer("PUT", f"{prefix}/contents/{plan.path}", {"branch": stage, "sha": plan.source_blob, "content": base64.b64encode(plan.new).decode(), "message": f"Bump version to {plan.effective} [skip ci]", "force_push": False}),
"write staged file",
)
child = _fields(_fields(written).get("commit")).get("sha")
if not isinstance(child, str) or not _SHA40.fullmatch(child) or child == ctx.sha:
raise Fatal("bad staged commit sha")
owned.add(child)
verify_bump_commit(ctx, plan, child, writer)
git.push_verified(stage, child, ctx.sha, ctx.branch)
if head(ctx.branch) != child:
raise PushFailure("the version push was not acknowledged")
return child
finally:
try:
status, current = writer("GET", f"{prefix}/branches/{urllib.parse.quote(stage, safe='')}")
if status == _HTTP_OK and _fields(_fields(current).get("commit")).get("id") in owned:
writer("DELETE", f"{prefix}/branches/{urllib.parse.quote(stage, safe='')}")
except SourceLifecycleError:
pass # cleanup is best effort and exact: only a staging head this run created is ever deleted
# ------------------------------------------------------------------------------------------ evidence read from the verified bundle
def verify_publication(artifact: ReleaseArtifact, files: tuple[ManifestFile, ...], version: str, owner: str, reader: Callable[..., tuple[int, bytes]]) -> None:
"""Read every published Maven file back from the private registry and require the retained bytes (the captured `waitForPublication`, without the poll).
A file that is not there yet is `NotYetPublished`; a file with other bytes is `Fatal`. The paths are the frozen publisher's own planner's, so they are exactly what it wrote.
"""
modules = verified_modules(artifact, files, version)
evidence = {"version": version, "modules": [{key: value for key, value in module.items() if key != "version"} for module in modules]}
snapshot = legacy.snapshot_stamp(artifact.run.created_at, int(artifact.run.run_id)) if version.endswith("-SNAPSHOT") else None
with legacy.translate_refusals():
plan = legacy.qrun_maven_publication.plan_files(evidence, owner, snapshot)
for item in plan:
try:
status, body = reader("GET", item["path"])
except urllib.error.HTTPError as error:
error.close()
if error.code == _HTTP_NOT_FOUND:
raise NotYetPublished(f"{item['path']} is not in the registry yet") from error
raise
if status != _HTTP_OK or hashlib.sha256(body).digest() != hashlib.sha256(item["data"]).digest():
raise Fatal("a published Maven file differs from the verified bytes")
def verify_completion(artifact: ReleaseArtifact, files: tuple[ManifestFile, ...], version: str, *, repository: str, repository_id: int, owner: str, reader: Callable[..., tuple[int, bytes]]) -> dict[str, Any]: # noqa: PLR0913 - one verified reactor and its identity
"""Read the immutable Central completion receipt and require it to describe exactly this tested reactor and to be terminal (the captured `waitForCentralCompletion`, without the poll).
Absent is `NotYetPublished`; present but different is `Fatal`. The receipt module validates the terminal-state rules itself.
"""
reactor = reactor_for(artifact, files, version)
modules = verified_modules(artifact, files, version)
target = "central-snapshots" if version.endswith("-SNAPSHOT") else "central-publisher"
run_id = int(artifact.run.run_id)
expected: dict[str, Any] = {
"repository": repository,
"repository_id": repository_id,
"run_id": run_id,
"source_commit": artifact.build_sha,
"version": version,
"archive_sha256": reactor.archive_sha256,
"manifest_sha256": reactor.manifest_sha256,
"gav": sorted(f"{module['groupId']}:{module['artifactId']}:{version}" for module in modules),
"target": target,
}
try:
return completion_receipt.read(owner, reader, repository_id, run_id, artifact.build_sha, expected)
except urllib.error.HTTPError as error:
error.close()
if error.code == _HTTP_NOT_FOUND:
raise NotYetPublished("the Central completion receipt does not exist yet") from error
raise
except ValueError as error:
raise Fatal(f"the Central completion receipt does not match this tested reactor: {error}") from error
def postpublish( # noqa: PLR0913 - keyword-only evidence of one release; the call site names every field
ctx: WriteContext,
policy: WriteBackPolicy,
*,
artifact: ReleaseArtifact,
files: tuple[ManifestFile, ...],
version: str,
owner: str,
repository_id: int,
central_required: bool,
writer: Writer,
registry_reader: Callable[..., tuple[int, bytes]],
completion_reader: Callable[..., tuple[int, bytes]],
git: VersionGit,
extra_evidence: tuple[tuple[ReleaseArtifact, tuple[ManifestFile, ...]], ...] = (),
) -> dict[str, str]:
"""Run the captured `postpublish` order over verified evidence: source POM, whole-reactor readback, terminal Central completion, then (only then) the version write.
`version` is the effective version the verified bundle recorded. Nothing is written until the registry and (where required) Central evidence are complete; an absent one raises
`NotYetPublished` so the next invocation retries. An unchanged version ends honestly as `skipped`. A push failure is a warning unless the policy requires the push. `extra_evidence`
lists further (artifact, files) pairs of the same release whose registry and completion evidence must be complete too before the one POM is written.
"""
status, answer = writer("GET", f"/repos/{ctx.repository}/contents/{policy.file}?ref={ctx.sha}")
if status != _HTTP_OK or not isinstance(answer, dict):
raise Fatal("the source POM cannot be read at the verified commit")
document = cast("dict[str, Any]", answer) # a JSON object, checked just above
try:
old = base64.b64decode(document.get("content", ""), validate=True)
except ValueError as error:
raise Fatal("the source POM is not valid base64") from error
if document.get("sha") != git_blob_sha(old):
raise Fatal("the source POM blob id differs from its bytes")
plan = build_plan(ctx, policy, old=old, new=rewrite_revision(old.decode("utf-8"), version).encode("utf-8"), verified_version=version)
for evidenced, evidenced_files in ((artifact, files), *extra_evidence):
verify_publication(evidenced, evidenced_files, version, owner, registry_reader)
if central_required:
verify_completion(evidenced, evidenced_files, version, repository=ctx.repository, repository_id=repository_id, owner=owner, reader=completion_reader)
if plan is None:
return {"status": "skipped", "reason": "the version did not change", "pushed_sha": ""}
try:
return {"status": "pushed", "reason": "", "pushed_sha": persist_version(ctx, plan, writer, git)}
except PushFailure as error:
if plan.require_push:
raise Fatal(f"the required version push was not acknowledged ({error})") from error
return {"status": "push-failed-warning", "reason": str(error), "pushed_sha": ""}