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/selection.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

113 lines
6.4 KiB
Python

"""Shared evidence gathering for choosing tested bytes: used by the workflow's reuse step and by the trusted publisher.
Both callers ask the same question, so they run the same code: given the commit being released and the planned targets, which earlier
successful run built exactly this input? The expected build inputs are recomputed here from the forge's own facts (the commit's
tree, the declared version at that commit, and the planned target), never taken from an artifact, so an artifact cannot vouch for itself.
"""
import urllib.error
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from platform_ci.build_inputs import BuildInputsRecord, compute_build_inputs
from platform_ci.delivery.errors import DeliveryError
from platform_ci.delivery.forge import ForgeGitHistory, ForgeReader, RepositoryScope, fetch_bundles, inspect_run, prepare_job_started, read_file
from platform_ci.delivery.manifest import BuildInputs
from platform_ci.delivery.reuse import Rejection, ReleaseArtifact, ReuseQuery, SourceRun, select_reusable_artifact
from platform_ci.effective_version import ReleaseActivityError, release_activity_since
from platform_ci.plan import Target
from platform_ci.versions import UNDECLARED, UNVERSIONED, parse_declared_version, version_file_path
from platform_ci.workspace import PipelineError
HISTORY_PAGES = 20
COMPLETED = "completed"
def declared_version_at(forge: ForgeReader, scope: RepositoryScope, target: Target, ref: str) -> str:
"""Read the component's declared version at a commit through the forge; an absent version file is `undeclared`."""
source = target.get("version_source")
file_name = version_file_path(target["build_tool"], source)
if not file_name:
return UNVERSIONED
try:
content = read_file(forge, scope.repository, (PurePosixPath(target["directory"]) / file_name).as_posix(), ref)
except urllib.error.HTTPError:
return UNDECLARED
return UNDECLARED if content is None else parse_declared_version(target["build_tool"], content, target["commands"], source)
def release_activity_for_run(forge: ForgeReader, scope: RepositoryScope, release_sha: str, run_id: int) -> bool:
"""Return whether the release commit's history holds a captured release merge or bump inside the 3 days before ONE run's build start (the develop SNAPSHOT question).
The build start is the server's `started_at` of the run's prepare job and the history is the forge's commit listing of `release_sha`, so `prepare` (while it runs), the publisher, the tag planner
and the write-back all compute the same answer from the same forge facts, never from the current time. Any failure raises `ReleaseActivityError`: the version is then unknown, not guessed.
"""
try:
since = release_activity_since(prepare_job_started(forge, scope, run_id))
return ForgeGitHistory(forge, scope.repository, HISTORY_PAGES).has_release_activity(release_sha, since)
except (DeliveryError, urllib.error.URLError, OSError, ValueError) as error: # GitHistoryError is a DeliveryError
raise ReleaseActivityError(f"the release activity of run {run_id} at {release_sha[:12]} could not be established: {error}") from error
def expected_inputs(forge: ForgeReader, scope: RepositoryScope, history: ForgeGitHistory, target: Target, release_sha: str) -> BuildInputs:
"""Recompute, from the forge's own facts, the build inputs a rebuild of the release commit would record."""
return as_manifest_inputs(compute_build_inputs(target, history.tree_of(release_sha), declared_version_at(forge, scope, target, release_sha)))
def as_manifest_inputs(record: BuildInputsRecord) -> BuildInputs:
"""Convert the build-input record to the delivery package's typed form."""
return BuildInputs(
source_tree=record["source_tree"],
input_digest=record["input_digest"],
toolchain=tuple(sorted(record["toolchain"].items())),
platform=record["platform"],
config_digest=record["config_digest"],
version=record["version"],
)
def query_for(forge: ForgeReader, scope: RepositoryScope, history: ForgeGitHistory, target: Target, release_sha: str) -> ReuseQuery:
"""Build the reuse query for one planned target of the release commit."""
return ReuseQuery(component=target["component"], target=target["name"], inputs=expected_inputs(forge, scope, history, target, release_sha), release_sha=release_sha)
@dataclass
class SourceRuns:
"""Authoritative records of candidate runs, built lazily and once: a run is downloaded only when something asks for it."""
forge: ForgeReader
scope: RepositoryScope
scratch: Path
_cache: dict[int, SourceRun | Rejection] | None = None
def get(self, run_id: int) -> SourceRun | Rejection:
"""Return the run's verified record, or a rejection when anything about it cannot be proven (never a crash)."""
if self._cache is None:
self._cache = {}
if run_id not in self._cache:
self._cache[run_id] = self._build(run_id)
return self._cache[run_id]
def _build(self, run_id: int) -> SourceRun | Rejection:
try:
facts = inspect_run(self.forge, self.scope, run_id)
bundles = fetch_bundles(self.forge, self.scope, facts, self.scratch) if facts.status == COMPLETED and facts.conclusion == "success" else ()
except (PipelineError, DeliveryError, urllib.error.URLError, OSError, ValueError) as error:
return Rejection(str(run_id), f"{type(error).__name__}: {error}" if isinstance(error, PipelineError) else type(error).__name__)
return SourceRun(run_id=facts.run_id, attempt=facts.attempt, head_sha=facts.head_sha, status=facts.status, conclusion=facts.conclusion, created_at=facts.created_at, expected_targets=frozenset(bundle.manifest.target for bundle in bundles), bundles=bundles, event=facts.event)
def select_for_query(query: ReuseQuery, run_ids: list[int], runs: SourceRuns, history: ForgeGitHistory) -> tuple[ReleaseArtifact | None, list[Rejection]]:
"""Walk candidate runs in the given order and return the first artifact that provably equals the query, with every rejection met."""
rejections: list[Rejection] = []
for run_id in run_ids:
source = runs.get(run_id)
if isinstance(source, Rejection):
rejections.append(source)
continue
decision = select_reusable_artifact(query, (source,), history)
rejections.extend(decision.rejections)
if decision.selected is not None:
return decision.selected, rejections
return None, rejections