Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions control_plane/product_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3946,11 +3946,10 @@ def verified_testing_rebuild_artifact(
if profile.driver_id != "odoo" or lane.instance != "testing":
raise ValueError("Requested build selection requires an Odoo testing lane.")
identity = product_repository_identity(record_store, profile)
transport = _TestingRebuildBuildReads(resolve_build_provenance_transport(record_store, profile))
try:
return verify_requested_build_artifact(
transport=_TestingRebuildBuildReads(
resolve_build_provenance_transport(record_store, profile)
),
transport=transport,
repository=profile.repository,
repository_id=identity.repository_id,
commit=source_git_ref,
Expand All @@ -3961,13 +3960,18 @@ def verified_testing_rebuild_artifact(
except BuildProvenanceError as error:
if error.code != "build_purpose_mismatch":
raise
verified = latest_verified_testing_build(record_store, profile, lane)
if (verified.manifest.artifact_id, verified.manifest.source_commit) != (
artifact_id,
source_git_ref,
):
verified = verify_build_artifact(
transport=transport,
repository=profile.repository,
repository_id=identity.repository_id,
commit=source_git_ref,
purpose="release",
context=lane.context,
image_repository=profile.image.repository,
)
if verified.manifest.artifact_id != artifact_id:
raise BuildProvenanceError(
"The selected build is not the current testing release.", code="build_purpose_mismatch"
"The selected build run or attempt changed.", code="manifest_commit_mismatch"
)
return verified

Expand Down
5 changes: 4 additions & 1 deletion docs/disposable-lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2786,7 +2786,10 @@ major matching and production release eligibility are unchanged.
Automatic release reconciliation still holds behind a serving PR candidate until
source history proves forward movement. An explicit rebuild can select the newest
verified release independently; selection retains the recorded release baseline
for complete run/history checks and refuses an older requested release.
for complete run/history checks. A migrated pair can instead name the main build
it was produced with, even after main moves on; that build must still be a
verified release of the default branch's first-parent history, and the pair must
match its image. Reconciliation then moves testing forward on the restored data.

Once the normal worker has recorded its private candidate application, the
original authenticated testing caller uploads one part at a time to
Expand Down
35 changes: 22 additions & 13 deletions tests/test_product_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,20 +928,36 @@ def test_request_during_run_returns_it_to_pending_with_the_plan(self) -> None:


class ProductReconcileTestingTests(ProductReconcileTestCase):
def test_requested_migrated_release_cannot_select_an_ancestor_build(self) -> None:
def test_migrated_release_selects_its_pair_main_build_after_main_moves_on(self) -> None:
from control_plane.product_reconcile import verified_testing_rebuild_artifact

self._running_release(20, DEPLOYABLE)
self.github.add_run(10, OLDER)
profile = self.store.read_product_profile_record("site")
lane = next(item for item in profile.lanes if item.instance == "testing")
current = verified_testing_rebuild_artifact(
self.store, profile, lane, artifact_id="artifact-cm-run-20-1", source_git_ref=DEPLOYABLE
older = verified_testing_rebuild_artifact(
self.store, profile, lane, artifact_id="artifact-cm-run-10-1", source_git_ref=OLDER
)
self.assertEqual(current.manifest.source_commit, DEPLOYABLE)
assert older.manifest.source_build is not None
self.assertEqual(
(older.manifest.source_commit, older.manifest.source_build.purpose),
(OLDER, "release"),
)

def test_migrated_release_refuses_a_push_build_that_never_was_main_tip(self) -> None:
from control_plane.product_reconcile import verified_testing_rebuild_artifact

self._running_release(20, DEPLOYABLE)
self.github.add_run(50, PR_HEAD)
profile = self.store.read_product_profile_record("site")
lane = next(item for item in profile.lanes if item.instance == "testing")
with self.assertRaises(BuildProvenanceError):
verified_testing_rebuild_artifact(
self.store, profile, lane, artifact_id="artifact-cm-run-10-1", source_git_ref=OLDER
self.store,
profile,
lane,
artifact_id="artifact-cm-run-50-1",
source_git_ref=PR_HEAD,
)

def serving_candidate(
Expand Down Expand Up @@ -1166,10 +1182,7 @@ def divergent_main(path: str) -> object:
self.assertEqual(verified.manifest.source_commit, NEWEST)

def test_explicit_release_selection_keeps_release_baseline_while_candidate_serves(self) -> None:
from control_plane.product_reconcile import (
latest_verified_testing_release,
verified_testing_rebuild_artifact,
)
from control_plane.product_reconcile import latest_verified_testing_release

statuses: tuple[Literal["pass", "reconciliation_required", "fail"], ...] = (
"pass",
Expand Down Expand Up @@ -1198,10 +1211,6 @@ def test_explicit_release_selection_keeps_release_baseline_while_candidate_serve
)
self.assertEqual((held["current_pull_request_number"], held["held"]), (5, True))
self.github.add_run(10, OLDER)
with self.assertRaises(BuildProvenanceError):
verified_testing_rebuild_artifact(
self.store, profile, lane, artifact_id="artifact-cm-run-10-1", source_git_ref=OLDER
)
self.github.add_run(40, NEWEST)
self.assertEqual(
latest_verified_testing_release(self.store, profile, lane).manifest.source_commit,
Expand Down
Loading