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/gitops.py
forge-recoveryandClaude Sonnet 5.5 fd4689fd19
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 3s
feat(ci): reuse verified builds before merge verification
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 03:54:30 +00:00

210 lines
9.9 KiB
Python

"""Pin published container DIGESTS in one enrolled GitOps kustomization with one compare-and-swap commit.
This reuses the frozen GitOps reader and its safety rules (enrolled repository, protected branch, signature-verified
commits, exactly one touched file) but pins an immutable digest instead of a tag. Only the `digest` line of an existing
`images[]` entry whose `newName` already names the enrolled registry image is written. The target comes from an operator
binding, never from an artifact or run. A pin is a commit, not a rollout: the receipt always says `deployment_verified` is
false.
"""
import base64
import hashlib
import re
import urllib.error
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, cast
from platform_ci.delivery import legacy
from platform_ci.delivery.authorization import GitOpsTarget
from platform_ci.delivery.errors import PublicationError
_COMMIT_ID = re.compile(r"[0-9a-f]{40}")
_DEFAULT_ATTEMPTS = 3
@dataclass(frozen=True)
class ImagePin:
"""One image digest, already published and read back, to pin under its kustomize entry name."""
kustomize_name: str
digest: str
@dataclass(frozen=True)
class ReleaseIdentity:
"""The build that produced the bytes, the release being delivered, and the source run: three separate facts."""
build_sha: str
release_sha: str
run_id: str
@dataclass(frozen=True)
class GitOpsClients:
"""Injected, already-scoped clients: the one-file GitOps client and a read-only registry client per enrolled image."""
gitops: legacy.GitOpsCall
registry_reader: Mapping[str, legacy.RegistryCall]
@dataclass(frozen=True)
class GitOpsReceipt:
"""The outcome of one pin commit. `deployment_verified` is never true: observing a rollout is a separate step."""
repository: str
branch: str
path: str
commit: str
status: str
pins: tuple[tuple[str, str], ...]
build_sha: str
release_sha: str
deployment_verified: bool = False
def set_image_digests(text: str, updates: Mapping[str, tuple[str, str]]) -> str:
"""Set or add the `digest` of each enrolled entry and return the new text; all other bytes are kept.
`updates` maps a kustomize entry name to (the registry name its newName must already be, the new digest).
Anything the narrow parser does not recognise is refused.
"""
if "\r" in text:
raise PublicationError("unsupported line endings in the GitOps file")
lines = text.split("\n")
bounds = legacy.image_entry_bounds(lines)
operations = [_plan_digest_line(lines, bounds, name, new_name, digest) for name, (new_name, digest) in updates.items()]
for index, replace_existing, line in sorted(operations, reverse=True):
if replace_existing:
lines[index] = line
else:
lines.insert(index, line)
return "\n".join(lines)
def pin_image_digests(target: GitOpsTarget, pins: tuple[ImagePin, ...], release: ReleaseIdentity, clients: GitOpsClients, attempts: int = _DEFAULT_ATTEMPTS) -> GitOpsReceipt:
"""Verify each digest is served by content, then pin them all in one signed commit, or write nothing."""
enrolled = _verify_pins_are_served(target, pins, clients)
updates = {pin.kustomize_name: (legacy.REGISTRY_PREFIX + enrolled[pin.kustomize_name], pin.digest) for pin in pins}
policy: dict[str, Any] = {"repository": target.repository, "repository_id": target.repository_id, "path": target.path}
gitops = clients.gitops
base = "/repos/" + target.repository
previous: str | None = None
for _ in range(attempts):
file_sha, text, head_id = legacy.read_gitops_file(gitops, target.branch, policy)
if file_sha == previous:
raise PublicationError("GitOps update was refused without a competing change")
updated = set_image_digests(text, updates)
if updated == text:
return _receipt(target, pins, _current_verified_commit(target, gitops), "already-current", release)
if not _COMMIT_ID.fullmatch(head_id):
raise PublicationError("forge did not report the GitOps branch head")
_, head_commit = gitops("GET", f"{base}/git/commits/{head_id}")
legacy.require_verified_commit(head_commit, "GitOps branch head (server-side signing is not qualified)")
images = "\n".join(f"Image: {enrolled[pin.kustomize_name]} {pin.digest}" for pin in pins)
message = f"deploy: pin image digests\n\nBuild: {release.build_sha}\nRelease: {release.release_sha} run {release.run_id}\n{images}"
body = {"content": base64.b64encode(updated.encode("utf-8")).decode(), "sha": file_sha, "branch": target.branch, "message": message}
try:
_, written = gitops("PUT", f"{base}/contents/{target.path}", body)
except urllib.error.HTTPError as error:
error.close()
if error.code not in legacy.GITOPS_CONFLICT_STATUSES:
raise
previous = file_sha
continue
commit = str(cast(dict[str, Any], written)["commit"]["sha"])
_verify_written_commit(target, gitops, commit, updated)
return _receipt(target, pins, commit, "committed", release)
raise PublicationError("GitOps branch kept changing; no pin was written")
def _plan_digest_line(lines: list[str], bounds: list[tuple[int, int]], name: str, new_name: str, digest: str) -> tuple[int, bool, str]:
"""Return (line index, replace-existing, new text) for one enrolled entry after validating its shape."""
if not legacy.IMAGE_NAME_PATTERN.fullmatch(new_name) or not legacy.DIGEST_PATTERN.fullmatch(digest):
raise PublicationError("invalid image pin")
chosen = [(start, stop) for start, stop in bounds if _entry_match(lines[start]).group(2) == name]
if len(chosen) != 1:
raise PublicationError(f"enrolled image {name!r} is not exactly one images entry")
start, stop = chosen[0]
indent = " " * (len(_entry_match(lines[start]).group(1)) + 2)
seen: dict[str, int] = {}
for number in range(start + 1, stop):
line = lines[number]
if not line.strip() or line.lstrip().startswith("#"):
continue
match = legacy.IMAGE_KEY_PATTERN.match(line)
if not match or match.group(1) != indent or match.group(2) not in ("newName", "newTag", "digest") or match.group(2) in seen:
raise PublicationError(f"unsupported or duplicate keys in images entry {name!r}")
seen[match.group(2)] = number
name_line = legacy.IMAGE_KEY_PATTERN.match(lines[seen["newName"]]) if "newName" in seen else None
if name_line is None or name_line.group(3) != new_name:
raise PublicationError(f"images entry {name!r} must already point at the enrolled registry image")
text = indent + "digest: " + digest
if "digest" in seen:
return seen["digest"], True, text
return max(seen.values()) + 1, False, text
def _verify_pins_are_served(target: GitOpsTarget, pins: tuple[ImagePin, ...], clients: GitOpsClients) -> dict[str, str]:
"""Require unique enrolled pins whose digests the registry serves by content; return the enrolled image map."""
enrolled = dict(target.images)
if not pins or len({pin.kustomize_name for pin in pins}) != len(pins):
raise PublicationError("pins must be a nonempty list with unique entry names")
for pin in pins:
if pin.kustomize_name not in enrolled:
raise PublicationError(f"{pin.kustomize_name!r} is not an image enrolled for this GitOps destination")
image = enrolled[pin.kustomize_name]
_, _, manifest = clients.registry_reader[image]("GET", f"/v2/{image}/manifests/{pin.digest}")
if "sha256:" + hashlib.sha256(manifest).hexdigest() != pin.digest:
raise PublicationError(f"registry does not serve {pin.digest} for {image} by content")
return enrolled
def _current_verified_commit(target: GitOpsTarget, gitops: legacy.GitOpsCall) -> str:
"""For an already-current file, return the newest commit touching it after requiring it to be signature-verified."""
path = target.path.replace("/", "%2F")
_, found = gitops("GET", f"/repos/{target.repository}/commits?sha={target.branch}&path={path}&limit=1&stat=false")
commits = cast(list[dict[str, Any]], found)
if len(commits) != 1:
raise PublicationError("no commit found for the already-current GitOps file")
legacy.require_verified_commit(commits[0], "GitOps commit")
return str(commits[0]["sha"])
def _verify_written_commit(target: GitOpsTarget, gitops: legacy.GitOpsCall, commit: str, intended: str) -> None:
"""After the only write: the commit is signed, touches only the enrolled file, and holds exactly the intended bytes."""
base = "/repos/" + target.repository
if not _COMMIT_ID.fullmatch(commit):
raise PublicationError("forge did not return a commit id")
_, fetched = gitops("GET", f"{base}/git/commits/{commit}")
legacy.require_verified_commit(fetched, "GitOps commit")
changed_files = cast(list[dict[str, Any]], cast(dict[str, Any], fetched).get("files") or [])
touched = [item.get("filename") for item in changed_files]
if touched != [target.path]:
raise PublicationError("GitOps commit touched files other than the enrolled path")
_, after = gitops("GET", f"{base}/contents/{target.path}?ref={commit}")
if base64.b64decode("".join(str(cast(dict[str, Any], after)["content"]).split()), validate=True).decode("utf-8") != intended:
raise PublicationError("committed GitOps bytes differ from the intended update")
def _receipt(target: GitOpsTarget, pins: tuple[ImagePin, ...], commit: str, status: str, release: ReleaseIdentity) -> GitOpsReceipt:
"""Assemble the pin receipt."""
return GitOpsReceipt(
repository=target.repository,
branch=target.branch,
path=target.path,
commit=commit,
status=status,
pins=tuple(sorted((pin.kustomize_name, pin.digest) for pin in pins)),
build_sha=release.build_sha,
release_sha=release.release_sha,
)
def _entry_match(line: str) -> re.Match[str]:
"""Match an `- name:` images entry line."""
match = legacy.IMAGE_ENTRY_PATTERN.match(line)
if match is None:
raise PublicationError("unexpected images entry line")
return match