Execute receipt-bound durable release runs
This commit is contained in:
@@ -55,7 +55,8 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
||||
self.assertIn("trusted_keys_from_keyring(\n target_keyring_payload", publisher)
|
||||
self.assertIn("trusted_keys=publication_trust", publisher)
|
||||
self.assertNotIn("trusted_keys=candidate_keys", publisher)
|
||||
self.assertIn("write_module_directory(", publisher)
|
||||
self.assertIn("module_directory_payloads(", publisher)
|
||||
self.assertIn("verify_committed_publication(", publisher)
|
||||
self.assertNotIn("shutil.copytree(candidate_modules", publisher)
|
||||
|
||||
def test_release_integration_honors_independent_module_versions(self) -> None:
|
||||
|
||||
@@ -230,10 +230,14 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
for repository in (self.repo, self.remote, campaign, campaign_remote):
|
||||
self.assertFalse(ref_exists(repository, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_api_requires_explicit_confirmation_and_ui_exposes_source_release(self) -> None:
|
||||
with TestClient(create_app(workspace_root=self.workspace)) as client:
|
||||
def test_api_keeps_legacy_source_release_preview_only(self) -> None:
|
||||
with TestClient(
|
||||
create_app(workspace_root=self.workspace, token="token")
|
||||
) as client:
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
rejected = client.post(
|
||||
"/api/repositories/tag",
|
||||
headers=headers,
|
||||
json={
|
||||
"repos": ["govoplan-core"],
|
||||
"repo_versions": {"govoplan-core": "0.1.10"},
|
||||
@@ -244,6 +248,7 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
)
|
||||
preview = client.post(
|
||||
"/api/repositories/tag",
|
||||
headers=headers,
|
||||
json={
|
||||
"repos": ["govoplan-core"],
|
||||
"repo_versions": {"govoplan-core": "0.1.10"},
|
||||
@@ -251,14 +256,47 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
"push": True,
|
||||
},
|
||||
)
|
||||
legacy_push = client.post(
|
||||
"/api/repositories/push",
|
||||
headers=headers,
|
||||
json={
|
||||
"repos": ["govoplan-core"],
|
||||
"apply": True,
|
||||
"confirm": "PUSH",
|
||||
},
|
||||
)
|
||||
legacy_sync = client.post(
|
||||
"/api/repositories/sync",
|
||||
headers=headers,
|
||||
json={
|
||||
"repos": ["govoplan-core"],
|
||||
"apply": True,
|
||||
"confirm": "SYNC",
|
||||
},
|
||||
)
|
||||
ui = client.get("/")
|
||||
|
||||
self.assertEqual(rejected.status_code, 400)
|
||||
self.assertEqual(rejected.status_code, 409)
|
||||
self.assertIn("durable release run", rejected.json()["detail"])
|
||||
self.assertEqual(preview.status_code, 200)
|
||||
self.assertEqual(409, legacy_push.status_code)
|
||||
self.assertEqual(409, legacy_sync.status_code)
|
||||
self.assertIn("durable", legacy_push.json()["detail"])
|
||||
self.assertIn("durable", legacy_sync.json()["detail"])
|
||||
self.assertEqual(preview.json()["repositories"][0]["repo"], "govoplan-core")
|
||||
self.assertFalse(ref_exists(self.repo, "refs/tags/v0.1.10"))
|
||||
self.assertIn('id="publishReleaseTags"', ui.text)
|
||||
self.assertIn(
|
||||
'id="publishReleaseTags" data-release-disabled="true" disabled',
|
||||
ui.text,
|
||||
)
|
||||
self.assertIn('postJson("/api/repositories/tag"', ui.text)
|
||||
self.assertIn(
|
||||
'id="syncRepos" disabled data-release-disabled="true"', ui.text
|
||||
)
|
||||
self.assertIn(
|
||||
'id="pushRepos" disabled data-release-disabled="true"', ui.text
|
||||
)
|
||||
self.assertIn("Signed Website Catalog", ui.text)
|
||||
self.assertIn("Apply + Website Tag", ui.text)
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@ from govoplan_release.release_run import ( # noqa: E402
|
||||
ReleaseRunStore,
|
||||
_record_digest,
|
||||
)
|
||||
from govoplan_release.candidate_artifact import ( # noqa: E402
|
||||
CandidateArtifactReceipt,
|
||||
candidate_output_path,
|
||||
harden_private_candidate_tree,
|
||||
issue_candidate_receipt,
|
||||
)
|
||||
from server.app import ( # noqa: E402
|
||||
create_app,
|
||||
default_release_run_root,
|
||||
@@ -51,8 +57,24 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
self.root,
|
||||
expected_workspace_fingerprint=WORKSPACE_FINGERPRINT,
|
||||
)
|
||||
self.runtime_trust = patch(
|
||||
"server.app.require_trusted_release_runtime"
|
||||
)
|
||||
self.runtime_binding = patch(
|
||||
"server.app.verify_release_runtime_binding"
|
||||
)
|
||||
self.source_bindings = patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
)
|
||||
self.runtime_trust.start()
|
||||
self.runtime_binding.start()
|
||||
self.source_bindings.start()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.source_bindings.stop()
|
||||
self.runtime_binding.stop()
|
||||
self.runtime_trust.stop()
|
||||
self.temporary.cleanup()
|
||||
|
||||
def test_create_persists_private_immutable_plan_and_ordered_steps(self) -> None:
|
||||
@@ -68,7 +90,7 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
record_path = self.root / f"{run['run_id']}.json"
|
||||
|
||||
self.assertEqual("govoplan.release-run", reopened["schema"])
|
||||
self.assertEqual(3, reopened["schema_version"])
|
||||
self.assertEqual(4, reopened["schema_version"])
|
||||
self.assertEqual(
|
||||
{"govoplan-core": "1.2.3", "govoplan-files": "1.2.4"},
|
||||
reopened["immutable"]["input"]["repo_versions"],
|
||||
@@ -82,6 +104,119 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
self.assertNotIn("processed_requests", reopened["state"])
|
||||
self.assertNotIn("attempt_fingerprint", reopened["state"]["steps"][0])
|
||||
|
||||
def test_catalog_receipt_is_bounded_persisted_and_idempotent(self) -> None:
|
||||
run_id = create_run(
|
||||
self.store,
|
||||
plan_snapshot=mutating_first_plan(),
|
||||
request_id="receipt-create-request-0001",
|
||||
)["run_id"]
|
||||
attempt_id = "receipt-attempt-request-0001"
|
||||
self.store.start_step(run_id, "core:tag", attempt_id=attempt_id)
|
||||
receipt = {
|
||||
"kind": "catalog_candidate",
|
||||
"candidate_id": "candidate-" + "a" * 32,
|
||||
"catalog_sha256": "b" * 64,
|
||||
}
|
||||
|
||||
finished = self.store.finish_step(
|
||||
run_id,
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="catalog_candidate_ready",
|
||||
result_receipt=receipt,
|
||||
)
|
||||
replayed = self.store.finish_step(
|
||||
run_id,
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="catalog_candidate_ready",
|
||||
result_receipt=receipt,
|
||||
)
|
||||
|
||||
self.assertEqual(receipt, finished["state"]["steps"][0]["result_receipt"])
|
||||
self.assertEqual(finished, replayed)
|
||||
with self.assertRaisesRegex(ReleaseRunConflict, "different recorded outcome"):
|
||||
self.store.finish_step(
|
||||
run_id,
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="catalog_candidate_ready",
|
||||
result_receipt=receipt | {"catalog_sha256": "c" * 64},
|
||||
)
|
||||
|
||||
def test_catalog_publication_receipt_binds_candidate_origin_commit_and_tag(
|
||||
self,
|
||||
) -> None:
|
||||
run_id = create_run(
|
||||
self.store,
|
||||
plan_snapshot=mutating_first_plan(),
|
||||
request_id="publication-receipt-create-0001",
|
||||
)["run_id"]
|
||||
attempt_id = "publication-receipt-attempt-0001"
|
||||
self.store.start_step(run_id, "core:tag", attempt_id=attempt_id)
|
||||
receipt = {
|
||||
"kind": "catalog_publication",
|
||||
"candidate_id": "candidate-" + "a" * 32,
|
||||
"catalog_sha256": "b" * 64,
|
||||
"keyring_sha256": "c" * 64,
|
||||
"publication_commit_sha": "d" * 40,
|
||||
"publication_tag_object_sha": "e" * 40,
|
||||
"publication_tag_commit_sha": "d" * 40,
|
||||
"branch": "main",
|
||||
"tag_name": "catalog-stable-1",
|
||||
"remote": "origin",
|
||||
"remote_sha256": "f" * 64,
|
||||
}
|
||||
|
||||
finished = self.store.finish_step(
|
||||
run_id,
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="catalog_published",
|
||||
result_receipt=receipt,
|
||||
)
|
||||
|
||||
self.assertEqual(receipt, finished["state"]["steps"][0]["result_receipt"])
|
||||
with self.assertRaisesRegex(
|
||||
ReleaseRunConflict, "publication receipt identity"
|
||||
):
|
||||
self.store.finish_step(
|
||||
run_id,
|
||||
"core:tag",
|
||||
attempt_id=attempt_id,
|
||||
succeeded=True,
|
||||
result_code="catalog_published",
|
||||
result_receipt=receipt
|
||||
| {"publication_tag_commit_sha": "0" * 40},
|
||||
)
|
||||
|
||||
def test_schema_three_record_is_verified_then_migrated_privately(self) -> None:
|
||||
run_id = create_run(
|
||||
self.store,
|
||||
request_id="legacy-schema-create-request-0001",
|
||||
)["run_id"]
|
||||
path = self.root / f"{run_id}.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
payload["schema_version"] = 3
|
||||
for step in payload["state"]["steps"]:
|
||||
step.pop("result_receipt")
|
||||
payload["record_digest"] = _record_digest(payload)
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
|
||||
migrated = self.store.get(run_id)
|
||||
persisted = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(4, migrated["schema_version"])
|
||||
self.assertEqual(4, persisted["schema_version"])
|
||||
self.assertTrue(
|
||||
all(step["result_receipt"] is None for step in persisted["state"]["steps"])
|
||||
)
|
||||
|
||||
def test_zero_step_plan_is_rejected(self) -> None:
|
||||
plan = release_plan()
|
||||
plan["dry_run_steps"] = []
|
||||
@@ -293,7 +428,7 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
self.assertEqual(0, untouched["state"]["steps"][0]["attempt_count"])
|
||||
|
||||
accepted_plan = mutating_first_plan()
|
||||
accepted_plan["notes"] = ["x" * (MAX_RECORD_BYTES - 3_000)]
|
||||
accepted_plan["notes"] = ["x" * (MAX_RECORD_BYTES - 4_300)]
|
||||
accepted_run_id = create_run(
|
||||
self.store,
|
||||
request_id="byte-capacity-accepted-create-0001",
|
||||
@@ -334,7 +469,7 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
) -> None:
|
||||
plan = mutating_first_plan()
|
||||
plan["dry_run_steps"][0]["mutating"] = False # type: ignore[index]
|
||||
plan["notes"] = ["x" * (MAX_RECORD_BYTES - 3_000)]
|
||||
plan["notes"] = ["x" * (MAX_RECORD_BYTES - 5_500)]
|
||||
run_id = create_run(
|
||||
self.store,
|
||||
request_id="byte-capacity-read-only-create-0001",
|
||||
@@ -747,7 +882,7 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
listed = self.store.list(limit=1)
|
||||
self.assertEqual([local_run["run_id"]], [item["run_id"] for item in listed])
|
||||
|
||||
def test_list_orders_verified_runs_by_updated_timestamp_before_unavailable(
|
||||
def test_list_orders_verified_runs_by_creation_timestamp_before_unavailable(
|
||||
self,
|
||||
) -> None:
|
||||
with patch(
|
||||
@@ -786,11 +921,11 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
|
||||
listed = self.store.list(limit=3)
|
||||
self.assertEqual(
|
||||
[older["run_id"], newer["run_id"], corrupt["run_id"]],
|
||||
[newer["run_id"], older["run_id"], corrupt["run_id"]],
|
||||
[item["run_id"] for item in listed],
|
||||
)
|
||||
self.assertEqual("unavailable", listed[-1]["status"])
|
||||
self.assertEqual(older["run_id"], self.store.list(limit=1)[0]["run_id"])
|
||||
self.assertEqual(newer["run_id"], self.store.list(limit=1)[0]["run_id"])
|
||||
|
||||
def test_cursor_pagination_is_stable_and_keeps_unavailable_records_visible(
|
||||
self,
|
||||
@@ -822,6 +957,43 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ReleaseRunConflict, "cursor is invalid"):
|
||||
self.store.list_page(limit=1, cursor=f"{first['next_cursor']}tampered")
|
||||
|
||||
def test_cursor_does_not_skip_or_repeat_when_an_older_run_is_updated(
|
||||
self,
|
||||
) -> None:
|
||||
created: list[dict[str, object]] = []
|
||||
for index in range(3):
|
||||
with patch(
|
||||
"govoplan_release.release_run._timestamp",
|
||||
return_value=f"2026-07-22T0{index}:00:00Z",
|
||||
):
|
||||
created.append(
|
||||
create_run(
|
||||
self.store,
|
||||
request_id=f"cursor-update-create-request-{index:04d}",
|
||||
)
|
||||
)
|
||||
first = self.store.list_page(limit=1)
|
||||
with patch(
|
||||
"govoplan_release.release_run._timestamp",
|
||||
return_value="2026-07-22T12:00:00Z",
|
||||
):
|
||||
self.store.start_step(
|
||||
created[0]["run_id"],
|
||||
"core:preflight",
|
||||
attempt_id="cursor-update-attempt-request-0001",
|
||||
)
|
||||
second = self.store.list_page(limit=1, cursor=first["next_cursor"])
|
||||
third = self.store.list_page(limit=1, cursor=second["next_cursor"])
|
||||
|
||||
self.assertEqual(
|
||||
[item["run_id"] for item in reversed(created)],
|
||||
[
|
||||
first["runs"][0]["run_id"],
|
||||
second["runs"][0]["run_id"],
|
||||
third["runs"][0]["run_id"],
|
||||
],
|
||||
)
|
||||
|
||||
def test_retention_prunes_only_oldest_completed_records_and_fails_closed(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -963,6 +1135,22 @@ class ReleaseRunStoreTests(unittest.TestCase):
|
||||
|
||||
|
||||
class ReleaseRunApiTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.runtime_trust = patch("server.app.require_trusted_release_runtime")
|
||||
self.runtime_binding = patch("server.app.verify_release_runtime_binding")
|
||||
self.source_bindings = patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
)
|
||||
self.runtime_trust.start()
|
||||
self.runtime_binding.start()
|
||||
self.source_bindings.start()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.source_bindings.stop()
|
||||
self.runtime_binding.stop()
|
||||
self.runtime_trust.stop()
|
||||
|
||||
def test_api_exposes_and_validates_cursor_pagination(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(
|
||||
@@ -1008,6 +1196,338 @@ class ReleaseRunApiTests(unittest.TestCase):
|
||||
self.assertIsNone(second["next_cursor"])
|
||||
self.assertEqual(409, invalid.status_code)
|
||||
|
||||
def test_executor_claims_before_effect_and_replays_lost_success_response(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(
|
||||
workspace_root=Path(temp_dir),
|
||||
run_state_root=Path(temp_dir) / "runs",
|
||||
candidate_root=Path(temp_dir) / "candidates",
|
||||
token="token",
|
||||
)
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
with (
|
||||
patch("server.app.build_dashboard", return_value=object()),
|
||||
patch(
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=release_plan(),
|
||||
),
|
||||
patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
),
|
||||
patch(
|
||||
"server.app.verify_repository_preflight_binding",
|
||||
return_value=repository_receipt(),
|
||||
),
|
||||
patch(
|
||||
"server.app.execute_repository_step",
|
||||
return_value=({"status": "inspected"}, repository_receipt()),
|
||||
) as executor,
|
||||
TestClient(app) as client,
|
||||
):
|
||||
created = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(request_id="api-executor-create-0001"),
|
||||
).json()
|
||||
run_id = created["run_id"]
|
||||
self.assertTrue(created["state"]["steps"][0]["executor"]["available"])
|
||||
self.assertEqual(
|
||||
"preflight", created["state"]["steps"][0]["executor"]["kind"]
|
||||
)
|
||||
payload = {"request_id": "api-executor-attempt-0001"}
|
||||
executed = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:preflight/execute",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
replayed = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:preflight/execute",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
self.assertEqual(200, executed.status_code)
|
||||
self.assertEqual("succeeded", executed.json()["state"]["steps"][0]["state"])
|
||||
self.assertEqual("replayed", replayed.json()["execution_result"]["status"])
|
||||
executor.assert_called_once()
|
||||
|
||||
def test_lost_finish_write_interrupts_without_reexecuting_effect(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(
|
||||
workspace_root=Path(temp_dir),
|
||||
run_state_root=Path(temp_dir) / "runs",
|
||||
token="token",
|
||||
)
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
with (
|
||||
patch("server.app.build_dashboard", return_value=object()),
|
||||
patch(
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=release_plan(),
|
||||
),
|
||||
patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
),
|
||||
patch(
|
||||
"server.app.verify_repository_preflight_binding",
|
||||
return_value=repository_receipt(),
|
||||
),
|
||||
patch(
|
||||
"server.app.execute_repository_step",
|
||||
return_value=({"status": "inspected"}, repository_receipt()),
|
||||
) as executor,
|
||||
TestClient(app) as client,
|
||||
):
|
||||
run_id = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(request_id="api-lost-finish-create-0001"),
|
||||
).json()["run_id"]
|
||||
with patch.object(
|
||||
app.state.release_runs,
|
||||
"finish_step",
|
||||
side_effect=ReleaseRunCorrupt("simulated durable write failure"),
|
||||
):
|
||||
first = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:preflight/execute",
|
||||
headers=headers,
|
||||
json={"request_id": "api-lost-finish-attempt-0001"},
|
||||
)
|
||||
replay = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:preflight/execute",
|
||||
headers=headers,
|
||||
json={"request_id": "api-lost-finish-attempt-0001"},
|
||||
)
|
||||
loaded = client.get(
|
||||
f"/api/release-runs/{run_id}", headers=headers
|
||||
).json()
|
||||
|
||||
self.assertEqual(409, first.status_code)
|
||||
self.assertEqual(409, replay.status_code)
|
||||
self.assertEqual("interrupted", loaded["state"]["steps"][0]["state"])
|
||||
executor.assert_called_once()
|
||||
|
||||
def test_executor_exception_is_interrupted_and_never_retried_implicitly(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(
|
||||
workspace_root=Path(temp_dir),
|
||||
run_state_root=Path(temp_dir) / "runs",
|
||||
token="token",
|
||||
)
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
with (
|
||||
patch("server.app.build_dashboard", return_value=object()),
|
||||
patch(
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=mutating_first_plan(),
|
||||
),
|
||||
patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
),
|
||||
patch("server.app.verify_repository_step_precondition"),
|
||||
patch(
|
||||
"server.app.execute_repository_step",
|
||||
side_effect=RuntimeError("connection lost after push"),
|
||||
) as executor,
|
||||
TestClient(app) as client,
|
||||
):
|
||||
run_id = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(request_id="api-ambiguous-create-0001"),
|
||||
).json()["run_id"]
|
||||
payload = {
|
||||
"request_id": "api-ambiguous-attempt-0001",
|
||||
"confirm": "TAG",
|
||||
}
|
||||
first = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:tag/execute",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
second = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/core:tag/execute",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
loaded = client.get(
|
||||
f"/api/release-runs/{run_id}", headers=headers
|
||||
).json()
|
||||
|
||||
self.assertEqual(409, first.status_code)
|
||||
self.assertEqual(409, second.status_code)
|
||||
self.assertEqual("interrupted", loaded["state"]["steps"][0]["state"])
|
||||
self.assertEqual(
|
||||
"executor_outcome_unknown",
|
||||
loaded["state"]["steps"][0]["result_code"],
|
||||
)
|
||||
executor.assert_called_once()
|
||||
|
||||
def test_catalog_execution_persists_and_consumes_only_issued_receipt(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
candidate_root = Path(temp_dir) / "candidates"
|
||||
app = create_app(
|
||||
workspace_root=Path(temp_dir),
|
||||
run_state_root=Path(temp_dir) / "runs",
|
||||
candidate_root=candidate_root,
|
||||
token="token",
|
||||
)
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
|
||||
def generated(**kwargs: object) -> tuple[dict[str, object], CandidateArtifactReceipt]:
|
||||
candidate_id = str(kwargs["candidate_id"])
|
||||
candidate = candidate_output_path(candidate_root, candidate_id)
|
||||
channels = candidate / "channels"
|
||||
channels.mkdir(parents=True)
|
||||
(channels / "stable.json").write_text(
|
||||
json.dumps({"channel": "stable", "signatures": [{}]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
harden_private_candidate_tree(candidate)
|
||||
return (
|
||||
{"status": "ready"},
|
||||
issue_candidate_receipt(
|
||||
root=candidate_root,
|
||||
candidate_id=candidate_id,
|
||||
channel="stable",
|
||||
),
|
||||
)
|
||||
|
||||
with (
|
||||
patch("server.app.build_dashboard", return_value=object()),
|
||||
patch(
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=catalog_release_plan(),
|
||||
),
|
||||
patch(
|
||||
"server.app.bind_plan_source_states",
|
||||
side_effect=bind_test_plan_source_states,
|
||||
),
|
||||
patch(
|
||||
"server.app.verify_catalog_publication_precondition",
|
||||
return_value=repository_receipt(
|
||||
repo="addideas-govoplan-website", target_tag=""
|
||||
),
|
||||
),
|
||||
patch("server.app.default_signing_keys", return_value=("key=/private",)),
|
||||
patch(
|
||||
"server.app.generate_catalog_candidate", side_effect=generated
|
||||
) as generator,
|
||||
patch(
|
||||
"server.app.publish_received_candidate",
|
||||
side_effect=published_candidate_result,
|
||||
) as publisher,
|
||||
TestClient(app) as client,
|
||||
):
|
||||
run_id = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(request_id="api-candidate-create-0001"),
|
||||
).json()["run_id"]
|
||||
generated_response = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/catalog:selective-generator/execute",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": "api-candidate-generate-0001",
|
||||
"confirm": "GENERATE",
|
||||
},
|
||||
)
|
||||
with patch("server.app.default_signing_keys", return_value=()):
|
||||
replayed_generation = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/catalog:selective-generator/execute",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": "api-candidate-generate-0001",
|
||||
"confirm": "GENERATE",
|
||||
},
|
||||
)
|
||||
published = client.post(
|
||||
f"/api/release-runs/{run_id}/steps/catalog:validate-sign-publish/execute",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": "api-candidate-publish-0001",
|
||||
"confirm": "PUSH",
|
||||
},
|
||||
)
|
||||
|
||||
receipt = generated_response.json()["state"]["steps"][0]["result_receipt"]
|
||||
self.assertRegex(receipt["candidate_id"], r"^candidate-[0-9a-f]{32}$")
|
||||
self.assertEqual(64, len(receipt["catalog_sha256"]))
|
||||
self.assertEqual(200, replayed_generation.status_code)
|
||||
self.assertEqual(
|
||||
"replayed",
|
||||
replayed_generation.json()["execution_result"]["status"],
|
||||
)
|
||||
generator.assert_called_once()
|
||||
self.assertEqual(200, published.status_code)
|
||||
self.assertEqual("completed", published.json()["state"]["status"])
|
||||
publisher.assert_called_once_with(
|
||||
candidate_path=candidate_root / receipt["candidate_id"],
|
||||
candidate_receipt=receipt,
|
||||
channel="stable",
|
||||
workspace_root=Path(temp_dir),
|
||||
remote="origin",
|
||||
expected_website_receipt=repository_receipt(
|
||||
repo="addideas-govoplan-website", target_tag=""
|
||||
),
|
||||
)
|
||||
|
||||
def test_legacy_catalog_endpoint_is_preview_only(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(workspace_root=Path(temp_dir), token="token")
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/catalog-candidates/publish",
|
||||
headers={"X-Release-Console-Token": "token"},
|
||||
json={
|
||||
"candidate_dir": "/tmp/untrusted",
|
||||
"apply": True,
|
||||
"push": True,
|
||||
"confirm": "PUSH",
|
||||
},
|
||||
)
|
||||
self.assertEqual(409, response.status_code)
|
||||
self.assertIn("durable release run", response.json()["detail"])
|
||||
|
||||
def test_release_channels_and_durable_remote_are_strictly_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
app = create_app(workspace_root=Path(temp_dir), token="token")
|
||||
headers = {"X-Release-Console-Token": "token"}
|
||||
with (
|
||||
patch("server.app.build_dashboard") as dashboard,
|
||||
TestClient(app) as client,
|
||||
):
|
||||
query = client.get(
|
||||
"/api/dashboard", headers=headers, params={"channel": "../stable"}
|
||||
)
|
||||
created = client.post(
|
||||
"/api/release-runs",
|
||||
headers=headers,
|
||||
json=api_run_input(
|
||||
request_id="api-invalid-channel-create-0001",
|
||||
channel="/absolute",
|
||||
),
|
||||
)
|
||||
execute = client.post(
|
||||
"/api/release-runs/not-a-run/steps/core:tag/execute",
|
||||
headers=headers,
|
||||
json={
|
||||
"request_id": "api-invalid-remote-attempt-0001",
|
||||
"confirm": "TAG",
|
||||
"remote": "upstream",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(422, query.status_code)
|
||||
self.assertEqual(422, created.status_code)
|
||||
self.assertEqual(422, execute.status_code)
|
||||
dashboard.assert_not_called()
|
||||
|
||||
def test_token_guarded_create_list_read_resume_and_retry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir) / "runs"
|
||||
@@ -1205,6 +1725,10 @@ class ReleaseRunApiTests(unittest.TestCase):
|
||||
"server.app.build_selective_release_plan",
|
||||
return_value=mutating_first_plan(),
|
||||
),
|
||||
patch(
|
||||
"server.app.reconciled_repository_receipt",
|
||||
return_value=repository_receipt(),
|
||||
),
|
||||
TestClient(app) as client,
|
||||
):
|
||||
created = client.post(
|
||||
@@ -1372,12 +1896,16 @@ class ReleaseRunApiTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertIn("Durable Run State", webui)
|
||||
self.assertIn("State tracking only.", webui)
|
||||
self.assertIn("Durable execution.", webui)
|
||||
self.assertIn("Create Run from Selection", webui)
|
||||
self.assertIn("Prepare Retry", webui)
|
||||
self.assertIn("step.retry_available", webui)
|
||||
self.assertIn("Record Reconciliation", webui)
|
||||
self.assertIn("effect_succeeded", webui)
|
||||
self.assertNotIn(
|
||||
'step.id === "catalog:validate-sign-publish" ? "disabled"',
|
||||
webui,
|
||||
)
|
||||
self.assertIn("renderReleaseRunStoreUnavailable", webui)
|
||||
self.assertIn('const runsRequest = api("/api/release-runs")', webui)
|
||||
self.assertIn("pendingReleaseRunPayload", webui)
|
||||
@@ -1385,13 +1913,30 @@ class ReleaseRunApiTests(unittest.TestCase):
|
||||
self.assertIn("recoverPendingRunCommands", webui)
|
||||
self.assertIn("inFlightRunCommandId", webui)
|
||||
self.assertIn("Run saved; list refresh unavailable", webui)
|
||||
self.assertIn("This foundation tracks state only.", runbook)
|
||||
self.assertIn("The run record is execution evidence only", runbook)
|
||||
self.assertIn("isolated checkout", runbook)
|
||||
self.assertIn("effect_absent", runbook)
|
||||
self.assertIn("$XDG_STATE_HOME", runbook)
|
||||
self.assertIn("same-directory", runbook)
|
||||
self.assertIn("caller-generated `request_id`", runbook)
|
||||
self.assertIn("reserves the two command-ledger", runbook)
|
||||
|
||||
def test_entire_release_console_script_is_valid_javascript(self) -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
self.skipTest("Node.js is required for the Release Console UI check")
|
||||
webui = (RELEASE_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
|
||||
match = re.search(r"<script>(?P<script>.*?)</script>", webui, re.DOTALL)
|
||||
self.assertIsNotNone(match, "release console script element is missing")
|
||||
result = subprocess.run( # noqa: S603
|
||||
[node, "--check", "-"],
|
||||
input=match.group("script") if match is not None else "",
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
|
||||
def test_ui_reuses_uncertain_ids_and_separates_saved_run_list_failure(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -1493,6 +2038,16 @@ async function api(_path) {
|
||||
const reconcileRequest = pendingRunCommandRequest("reconcile:rr-known:core:tag:effect_succeeded", "reconcile-request");
|
||||
assert(reconcileRequest.path.endsWith("/steps/core%3Atag/reconcile"), "reconciliation recovery path was not executable");
|
||||
assert(reconcileRequest.body.confirm === "RECONCILE" && reconcileRequest.body.outcome === "effect_succeeded", "reconciliation recovery body changed");
|
||||
const executeKey = "execute:rr-known:catalog:selective-generator:GENERATE";
|
||||
const executeId = inFlightRunCommandId(executeKey, "execute", { signing_keys: ["release-key=/private/key"] });
|
||||
assert(!values.get(pendingRunCommandsKey).includes("signing_keys"), "signing key path was persisted in session storage");
|
||||
const executeRequest = pendingRunCommandRequest(executeKey, executeId);
|
||||
assert(!Object.hasOwn(executeRequest.body, "signing_keys"), "execute replay reconstructed signing material");
|
||||
values.set(pendingRunCommandsKey, JSON.stringify({ [commandKey]: commandOne, [executeKey]: { request_id: executeId, body: { signing_keys: ["legacy-secret"] } } }));
|
||||
const migratedCommands = readInFlightRunCommands();
|
||||
assert(migratedCommands[executeKey] === executeId, "legacy pending execute request ID was lost");
|
||||
assert(!values.get(pendingRunCommandsKey).includes("legacy-secret"), "legacy persisted signing material was not purged");
|
||||
clearInFlightRunCommand(executeKey, executeId);
|
||||
|
||||
failCreate = true;
|
||||
const uncertainCommand = await recoverPendingRunCommands();
|
||||
@@ -1591,6 +2146,64 @@ def run_input(
|
||||
}
|
||||
|
||||
|
||||
def repository_receipt(
|
||||
*, repo: str = "govoplan-core", target_tag: str = "v1.2.3"
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"kind": "repository_state",
|
||||
"repo": repo,
|
||||
"head": "a" * 40,
|
||||
"branch": "main",
|
||||
"remote": "origin",
|
||||
"remote_sha256": "b" * 64,
|
||||
"worktree_clean": True,
|
||||
"target_tag": target_tag,
|
||||
"tag_object": None,
|
||||
}
|
||||
|
||||
|
||||
def published_candidate_result(
|
||||
**kwargs: object,
|
||||
) -> tuple[dict[str, object], dict[str, object]]:
|
||||
candidate = kwargs["candidate_receipt"]
|
||||
website = kwargs["expected_website_receipt"]
|
||||
if not isinstance(candidate, dict) or not isinstance(website, dict):
|
||||
raise AssertionError("publication test receipts are unavailable")
|
||||
receipt = {
|
||||
"kind": "catalog_publication",
|
||||
"candidate_id": candidate["candidate_id"],
|
||||
"catalog_sha256": candidate["catalog_sha256"],
|
||||
"keyring_sha256": "c" * 64,
|
||||
"publication_commit_sha": "d" * 40,
|
||||
"publication_tag_object_sha": "e" * 40,
|
||||
"publication_tag_commit_sha": "d" * 40,
|
||||
"branch": website["branch"],
|
||||
"tag_name": "catalog-stable-7",
|
||||
"remote": "origin",
|
||||
"remote_sha256": website["remote_sha256"],
|
||||
}
|
||||
return {"status": "published"}, receipt
|
||||
|
||||
|
||||
def bind_test_plan_source_states(**kwargs: object) -> dict[str, object]:
|
||||
plan = kwargs["plan"]
|
||||
if not isinstance(plan, dict):
|
||||
raise AssertionError("test plan is not an object")
|
||||
steps = plan.get("dry_run_steps")
|
||||
if not isinstance(steps, list):
|
||||
raise AssertionError("test plan has no steps")
|
||||
for step in steps:
|
||||
if not isinstance(step, dict):
|
||||
continue
|
||||
if isinstance(step.get("repo"), str):
|
||||
step["source_binding"] = repository_receipt()
|
||||
elif step.get("id") == "catalog:validate-sign-publish":
|
||||
step["source_binding"] = repository_receipt(
|
||||
repo="addideas-govoplan-website", target_tag=""
|
||||
)
|
||||
return plan
|
||||
|
||||
|
||||
def release_plan() -> dict[str, object]:
|
||||
return {
|
||||
"generated_at": "2026-07-22T00:00:00Z",
|
||||
@@ -1641,5 +2254,32 @@ def mutating_first_plan() -> dict[str, object]:
|
||||
return plan
|
||||
|
||||
|
||||
def catalog_release_plan() -> dict[str, object]:
|
||||
plan = release_plan()
|
||||
plan["dry_run_steps"] = [
|
||||
{
|
||||
"id": "catalog:selective-generator",
|
||||
"title": "Generate candidate",
|
||||
"detail": "Build artifacts and sign a candidate.",
|
||||
"command": "release-catalog selective",
|
||||
"cwd": "/workspace/govoplan",
|
||||
"mutating": True,
|
||||
"repo": None,
|
||||
"status": "planned",
|
||||
},
|
||||
{
|
||||
"id": "catalog:validate-sign-publish",
|
||||
"title": "Publish candidate",
|
||||
"detail": "Publish only the receipt-bound candidate.",
|
||||
"command": "release-catalog publish-candidate",
|
||||
"cwd": "/workspace/govoplan",
|
||||
"mutating": True,
|
||||
"repo": None,
|
||||
"status": "planned",
|
||||
},
|
||||
]
|
||||
return plan
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user