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
20 changes: 19 additions & 1 deletion heart/checks/cloud_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ def matching_rehearsal(
)


def _effective_created(run: dict):
"""When a producer run started, robust to a skewed ``created_at``.

A run cannot finish before it was created, so ``created_at`` later than
``updated_at`` is a bad start stamp, not a late run: on 2026-11-01 GitHub
reported PyAutoHands run 41290559836 as created 09:50:55Z yet updated
09:09:27Z (every job start stamp an hour late), and the valid rehearsal
was discarded. Take the earlier of the two; ``None`` when either is
missing, so an unstamped run is still skipped. Ordering against the
integration remains ``matching_rehearsal``'s ``produced <= start`` check.
"""
created = release_run._parse_ts(run.get("created_at"))
updated = release_run._parse_ts(run.get("updated_at"))
if not created or not updated:
return None
return min(created, updated)


def collect(config: dict, fetch=api, get_artifact=download) -> dict[str, Any]:
"""Observe the newest integration only; never fall back to an older pass.

Expand Down Expand Up @@ -136,7 +154,7 @@ def collect(config: dict, fetch=api, get_artifact=download) -> dict[str, Any]:
f"repos/{producer_repo}/actions/workflows/release.yml/runs?branch=main&per_page=20"
).get("workflow_runs", [])
for candidate in candidates[:20]:
created = release_run._parse_ts(candidate.get("created_at"))
created = _effective_created(candidate)
if (
not created
or created > observed
Expand Down
21 changes: 21 additions & 0 deletions tests/test_cloud_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ def test_failed_producers_override_pass(evidence, which, conclusion):
assert run(evidence)["validation_outcome"] == "fail"


def test_skewed_start_stamp_does_not_hide_a_finished_rehearsal(evidence):
# PyAutoHands run 41290559836 (2026-11-01): created_at an hour late, after
# its own updated_at, while the run really finished before the integrate.
evidence["producer"]["created_at"] = "2026-10-01T12:40:00Z"
evidence["producer"]["updated_at"] = "2026-10-01T11:59:51Z"
result = run(evidence)
assert result["rehearsal_matched"]
assert result["validation_outcome"] == "pass"
# The skewed stamp must not date the evidence after the integrate start.
assert report(evidence)["ts"] <= "2026-10-01T12:00:00"
assert "rehearse" in report(evidence)["stages"]


def test_skewed_start_stamp_cannot_admit_a_later_rehearsal(evidence):
evidence["producer"]["created_at"] = "2026-10-01T12:40:00Z"
evidence["producer"]["updated_at"] = "2026-10-01T12:20:00Z"
result = run(evidence)
assert not result["rehearsal_matched"]
assert result["validation_outcome"] == "incomplete"


def test_missing_rehearsal_is_incomplete(evidence):
evidence["artifact"] = None
assert run(evidence)["validation_outcome"] == "incomplete"
Expand Down
Loading