feat(hermes): file the issue an escalated incident never received
All checks were successful
Tests / Declarative: Post Actions passed: 1196
All checks were successful
Tests / Declarative: Post Actions passed: 1196
An incident triaged before issue filing existed, or while its repository was unmapped, is deduped on every later tick and so never reaches the service's tracker even though the job is still red. soteria and metis sat in exactly that state: broken, escalated, and permanently unable to file. The backfill reuses the classification the original diagnosis recorded, so the existing dedupe applies unchanged - one open issue per job and classification, refiled only if it is closed while the job still fails. Filing under a guessed label would miss the open issue and duplicate it, which is what the dedupe exists to prevent, so an incident with no recorded classification is skipped rather than guessed at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
281f06dccd
commit
c9c205484e
@ -145,6 +145,7 @@ def _handle_failure( # noqa: PLR0913 - the tick's issue budget travels with the
|
||||
incident_id = f"{job}/{number}"
|
||||
existing = incidents.get(incident_id)
|
||||
if existing is not None and existing.get("status") != "detected":
|
||||
_backfill_issue(storage, job, number, incident_id, existing, tick_state)
|
||||
return {"status": "deduped", "incident_id": incident_id}
|
||||
stale = _awaiting_rebuild_incident(incidents, job, incident_id)
|
||||
if stale is not None:
|
||||
@ -153,6 +154,33 @@ def _handle_failure( # noqa: PLR0913 - the tick's issue budget travels with the
|
||||
return _run_pipeline(storage, incident_id, job, last_build, tick_state)
|
||||
|
||||
|
||||
def _backfill_issue( # noqa: PLR0913 - the tick's issue budget travels with the incident
|
||||
storage: Any, job: str, number: int, incident_id: str, existing: dict, tick_state: dict[str, Any]
|
||||
) -> None:
|
||||
"""File the issue an escalated incident never received.
|
||||
|
||||
An incident triaged before issue filing existed, or while its repository
|
||||
was unmapped, is deduped on every later tick and would otherwise never
|
||||
reach the service's tracker even though the job is still red.
|
||||
|
||||
Reuses the classification the original diagnosis recorded so the existing
|
||||
dedupe applies: one open issue per job and classification, refiled only if
|
||||
it is closed while the job still fails. Skipped when no classification was
|
||||
recorded, because filing under a guessed label would duplicate rather than
|
||||
match. Additive and never raises.
|
||||
"""
|
||||
|
||||
if existing.get("status") != "human_required":
|
||||
return
|
||||
classification = hermes_events.recorded_classification(storage, incident_id)
|
||||
if not classification:
|
||||
return
|
||||
base = {"incident_id": incident_id, "job": job, "build_number": number}
|
||||
bundle = {"jenkins": {"job": job, "build_number": number, "console_failures": [], "console_tail": ""}}
|
||||
diagnosis = {**_diagnosis(bundle, None, "issue_backfill", None), "classification": classification}
|
||||
_file_incident_issue(storage, base, diagnosis, tick_state)
|
||||
|
||||
|
||||
def _awaiting_rebuild_incident(
|
||||
incidents: dict[str, dict[str, Any]], job: str, exclude_id: str
|
||||
) -> dict[str, Any] | None:
|
||||
|
||||
@ -208,3 +208,29 @@ def _int_value(value: Any) -> int:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def recorded_classification(storage: Any, incident_id: str) -> str | None:
|
||||
"""Return the classification a past diagnosis recorded for an incident.
|
||||
|
||||
Inputs: the event storage and an incident id. Outputs: the classification
|
||||
string, or None when no diagnosis event carries one.
|
||||
|
||||
Issue dedupe matches on job and classification, so a later filing must
|
||||
reuse the label the original diagnosis produced. Filing under a different
|
||||
one would not match the open issue and would duplicate it, which is
|
||||
precisely what the dedupe exists to prevent - hence None rather than a
|
||||
guess when nothing was recorded.
|
||||
"""
|
||||
|
||||
rows = storage.list_events(limit=_EVENT_SCAN_LIMIT, event_type=DIAGNOSIS_EVENT_TYPE)
|
||||
for row in rows:
|
||||
detail = event_detail(row) or {}
|
||||
if str(detail.get("incident_id") or "") != incident_id:
|
||||
continue
|
||||
outcome = detail.get("outcome")
|
||||
if isinstance(outcome, dict):
|
||||
classification = str(outcome.get("classification") or "").strip()
|
||||
if classification:
|
||||
return classification
|
||||
return None
|
||||
|
||||
68
tests/test_hermes_issue_backfill.py
Normal file
68
tests/test_hermes_issue_backfill.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Tests for filing the issue an escalated incident never received."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ariadne.services import hermes_autotriage as module
|
||||
from tests.hermes_autotriage_harness import (
|
||||
INCIDENT_ID,
|
||||
JOB,
|
||||
_build,
|
||||
_prepare_code,
|
||||
_seed_incident,
|
||||
)
|
||||
|
||||
|
||||
def _diagnosis_event(storage, incident_id: str, classification: str) -> None:
|
||||
storage.record_event(
|
||||
module.DIAGNOSIS_EVENT_TYPE,
|
||||
{"incident_id": incident_id, "job": JOB, "outcome": {"classification": classification}},
|
||||
)
|
||||
|
||||
|
||||
def test_a_deduped_escalation_files_the_issue_it_never_got(monkeypatch) -> None:
|
||||
"""The job is still red; the tracker should not stay empty forever."""
|
||||
|
||||
env = _prepare_code(monkeypatch, last_build=_build(12, "FAILURE"))
|
||||
_seed_incident(env.storage, "human_required")
|
||||
_diagnosis_event(env.storage, INCIDENT_ID, "coverage_quality_gate_failure")
|
||||
|
||||
summary = module.run_hermes_autotriage(env.storage)["jobs"][JOB]
|
||||
|
||||
assert summary == {"status": "deduped", "incident_id": INCIDENT_ID}
|
||||
filed = env.calls["issues"]
|
||||
assert len(filed) == 1
|
||||
assert filed[0]["classification"] == "coverage_quality_gate_failure"
|
||||
|
||||
|
||||
def test_backfill_reuses_the_recorded_classification(monkeypatch) -> None:
|
||||
"""A different label would miss the open issue and duplicate it."""
|
||||
|
||||
env = _prepare_code(monkeypatch, last_build=_build(12, "FAILURE"))
|
||||
_seed_incident(env.storage, "human_required")
|
||||
_diagnosis_event(env.storage, INCIDENT_ID, "pytest_test_failure")
|
||||
|
||||
module.run_hermes_autotriage(env.storage)
|
||||
|
||||
assert env.calls["issues"][0]["classification"] == "pytest_test_failure"
|
||||
|
||||
|
||||
def test_no_recorded_classification_means_no_backfill(monkeypatch) -> None:
|
||||
"""Guessing a label would duplicate rather than dedupe."""
|
||||
|
||||
env = _prepare_code(monkeypatch, last_build=_build(12, "FAILURE"))
|
||||
_seed_incident(env.storage, "human_required")
|
||||
|
||||
module.run_hermes_autotriage(env.storage)
|
||||
|
||||
assert env.calls["issues"] == []
|
||||
|
||||
|
||||
def test_only_human_required_incidents_are_backfilled(monkeypatch) -> None:
|
||||
"""A resolved or in-flight incident needs no issue."""
|
||||
|
||||
for status in ("resolved", "awaiting_rebuild", "repairing"):
|
||||
env = _prepare_code(monkeypatch, last_build=_build(12, "FAILURE"))
|
||||
_seed_incident(env.storage, status)
|
||||
_diagnosis_event(env.storage, INCIDENT_ID, "pytest_test_failure")
|
||||
module.run_hermes_autotriage(env.storage)
|
||||
assert env.calls["issues"] == [], status
|
||||
Loading…
x
Reference in New Issue
Block a user