From 8988d3efd0cfbca29be5de247f52cf7262eaa7a5 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:00:09 -0700 Subject: [PATCH] fix(update): the receipt's build stage fails only for a build follow-up (C3) Both completion paths marked the build stage failed whenever ANY tail follow-up was owed, so a run whose only debt was config_migration (or maintenance / dependencies) reported a failed build. record_build_stage marks it failed only for launchers/build (what the build stage publishes) or completion (the tail raised before proving them); other owed steps stay their own follow-ups. --- hermes_cli/update_completion.py | 5 +++-- hermes_cli/update_finish.py | 4 ++-- hermes_cli/update_receipt.py | 11 +++++++++++ tests/hermes_cli/test_update_completion_process.py | 1 + tests/hermes_cli/test_update_finish.py | 2 ++ tests/hermes_cli/test_update_post_commit_safety.py | 3 +++ 6 files changed, 22 insertions(+), 4 deletions(-) diff --git a/hermes_cli/update_completion.py b/hermes_cli/update_completion.py index e0112f8fcb..d609adf5ab 100644 --- a/hermes_cli/update_completion.py +++ b/hermes_cli/update_completion.py @@ -263,7 +263,8 @@ def _complete_selected(request: dict) -> bool: from hermes_cli import main, update_cmd, update_cmd_config from hermes_cli.source_completion import complete_source_checkout from hermes_cli.update_inventory import RuntimeRecord, UpdatePlan - from hermes_cli.update_receipt import TAIL_FOLLOWUPS, record_followup, record_skip, record_stage + from hermes_cli.update_receipt import ( + TAIL_FOLLOWUPS, record_build_stage, record_followup, record_skip, record_stage) root = Path(request["source"]) main.PROJECT_ROOT = root @@ -294,7 +295,7 @@ def _complete_selected(request: dict) -> bool: record_followup("completion", reason) followups.append(("completion", reason)) tail_owed = any(step in TAIL_FOLLOWUPS for step, _ in followups) - record_stage("build", "failed" if tail_owed else "success") + record_build_stage(followups) if not tail_owed: from hermes_cli.venv_sync import clear_completion clear_completion(root) diff --git a/hermes_cli/update_finish.py b/hermes_cli/update_finish.py index ac2801e3c8..96527643b7 100644 --- a/hermes_cli/update_finish.py +++ b/hermes_cli/update_finish.py @@ -22,7 +22,7 @@ def finish_update(*, root, assume_yes, gateway_mode, pre_update_snapshot_id, _restart_gateway_fleet_after_update, _verify_fleet_after_update, _write_gateway_update_exit_code, _resume_windows_gateways_and_merge_outcome, ) - from hermes_cli.update_receipt import TAIL_FOLLOWUPS, record_followup, record_stage + from hermes_cli.update_receipt import TAIL_FOLLOWUPS, record_build_stage, record_followup owed = followups if followups is not None else [] runtime_safe = False @@ -38,7 +38,7 @@ def finish_update(*, root, assume_yes, gateway_mode, pre_update_snapshot_id, record_followup("maintenance", reason) owed.append(("maintenance", reason)) tail_owed = any(step in TAIL_FOLLOWUPS for step, _ in owed) - record_stage("build", "failed" if tail_owed else "success") + record_build_stage(owed) if not tail_owed: from hermes_cli.source_stamp import write_source_stamp from hermes_cli.venv_sync import clear_completion diff --git a/hermes_cli/update_receipt.py b/hermes_cli/update_receipt.py index d7a423f4ed..9668a2f2aa 100644 --- a/hermes_cli/update_receipt.py +++ b/hermes_cli/update_receipt.py @@ -317,6 +317,17 @@ def persist_running_receipt() -> None: #: Follow-up steps whose failure leaves the source-update tail owed (``source-completion-pending``). TAIL_FOLLOWUPS = frozenset({"dependencies", "launchers", "build", "maintenance", "config_migration", "completion"}) +#: Follow-ups that mean the BUILD stage did not succeed (C3: the receipt names what actually failed): +#: the products or the launchers the build publishes failed, or the tail raised before proving them. +#: Owed dependencies, config migration or maintenance keep the tail armed but say nothing about the +#: build; they are reported as their own follow-ups. +BUILD_FOLLOWUPS = frozenset({"launchers", "build", "completion"}) + + +def record_build_stage(followups) -> None: + """Mark the build stage from the owed ``(step, reason)`` follow-ups: failed only for a build one.""" + record_stage("build", "failed" if any(step in BUILD_FOLLOWUPS for step, _ in followups) else "success") + def record_followup(step: str, reason: str, *, retry: str = "the next launch or `hermes update` retries it") -> None: """A post-commit step failed: print ⚠, keep the run a success, and say what is still owed. diff --git a/tests/hermes_cli/test_update_completion_process.py b/tests/hermes_cli/test_update_completion_process.py index c10009a538..013f13e961 100644 --- a/tests/hermes_cli/test_update_completion_process.py +++ b/tests/hermes_cli/test_update_completion_process.py @@ -155,6 +155,7 @@ def transition(tmp_path): "_current = contextvars.ContextVar('receipt', default=None)\n" "class UpdateReceipt: pass\n" "def record_stage(*args, **kwargs): pass\n" + "def record_build_stage(*args, **kwargs): pass\n" "def record_skip(*args, **kwargs): pass\n" "TAIL_FOLLOWUPS = frozenset({'dependencies', 'launchers', 'build', 'maintenance', 'config_migration', 'completion'})\n" "def record_followup(step, reason, **kwargs):\n" diff --git a/tests/hermes_cli/test_update_finish.py b/tests/hermes_cli/test_update_finish.py index 2a8b401a52..781a71139c 100644 --- a/tests/hermes_cli/test_update_finish.py +++ b/tests/hermes_cli/test_update_finish.py @@ -428,6 +428,8 @@ def test_real_compiler_failure_is_owed_build_and_completion_continues(completion receipt = json.loads((home / "logs/update_receipts/latest.json").read_text()) assert receipt["outcome"] == "success" assert [row["step"] for row in receipt["followups"]] == ["build"] + # Only a build follow-up marks the build stage failed (an owed config/dependency step does not). + assert {mark["name"]: mark["outcome"] for mark in receipt["stages"]}.get("build") == "failed" assert receipt["update_id"] == request["update_id"] assert receipt["steps"][0] == request["receipt"]["steps"][0] assert receipt["pm_venv_rebuild"] == request["pm_receipt"]["venv_rebuild"] diff --git a/tests/hermes_cli/test_update_post_commit_safety.py b/tests/hermes_cli/test_update_post_commit_safety.py index a52a64b64b..555f535eac 100644 --- a/tests/hermes_cli/test_update_post_commit_safety.py +++ b/tests/hermes_cli/test_update_post_commit_safety.py @@ -87,6 +87,7 @@ _CHILD = textwrap.dedent(''' (cell / "summary.json").write_text(json.dumps({ "code": code, "outcome": latest.get("outcome"), "followups": [row["step"] for row in latest.get("followups", [])], + "stages": {mark["name"]: mark["outcome"] for mark in latest.get("stages", [])}, "config_version": yaml.safe_load((home / "config.yaml").read_text())["_config_version"], "latest_config_version": DEFAULT_CONFIG["_config_version"], "pending": completion_pending_path(source).is_file(), @@ -153,6 +154,8 @@ def test_config_format_write_failure_is_owed_not_reported_complete(tmp_path): assert summary["followups"] == ["config_migration"] assert summary["pending"] is True assert summary["stamp"] is False + # C3: the receipt names what actually failed. The build succeeded; only the config is owed. + assert summary["stages"].get("build") == "success", summary["stages"] _RECEIPT_CHILD = textwrap.dedent('''