fix: post AC4 recurrence comments on linked incident issues (#607)

Remediates review 479/481 findings F1 and F2 on PR #767.

F1: issue #607 AC4 requires that an existing linked Gitea issue receives a
recurrence comment when Sentry events continue. That path did not exist.
This adds:

- incident_bridge.incident_recurred() — true only when event_count or
  last_seen genuinely advanced, compared against the pre-upsert link row, so
  an unchanged rescan stays silent and the exactly-once property holds.
- incident_bridge.build_recurrence_comment_body() — reuses the same
  redact_text path as build_gitea_issue_body, so redaction is not
  re-implemented.
- A comment_issue_fn hook on reconcile_incident, invoked only on
  OUTCOME_UPDATED with genuinely new events. The durable incident_links row
  is written first, so a comment failure degrades to "link updated, comment
  withheld" without corrupting the mapping or creating a duplicate issue.
- _incident_recurrence_comment_fn() in gitea_mcp_server.py, routing through
  the sanctioned gitea_create_issue_comment path named in issue #607. It
  returns None on dry runs and when the profile lacks gitea.issue.comment, so
  the AC8 dry-run default and disabled-mode safety hold.
- comment_issue_fn threading through sentry_incident_bridge.watchdog(), with
  the per-issue recurrence_comment outcome recorded in the scan result.

F2: observation_from_issue never populates a fingerprint, so the "deduped by
Sentry issue id and fingerprint" claim was unsupported. The
gitea_sentry_reconcile_issue and gitea_sentry_watchdog docstrings now state
the real dedupe basis: provider identity (provider, base URL, org, project,
Sentry issue id).

Tests: five new AC4 cases in tests/test_sentry_incident_bridge.py covering a
comment on the second scan, dry-run silence, no-new-events silence, link
durability when the comment fails, and comment redaction. Focused suite
43 passed (was 38).

Refs #607
This commit is contained in:
2026-07-20 04:08:52 -05:00
parent 716fc21a0d
commit bc968dd2e0
4 changed files with 330 additions and 2 deletions
+6
View File
@@ -590,6 +590,7 @@ def watchdog(
max_pages: int = DEFAULT_MAX_PAGES,
http_fn: HttpFn | None = None,
create_issue_fn: Any = None,
comment_issue_fn: Any = None,
reconcile_fn: Callable[..., dict[str, Any]] | None = None,
fetch_events: bool = True,
) -> dict[str, Any]:
@@ -597,6 +598,9 @@ def watchdog(
Dry-run by default. ``apply=True`` additionally requires the bridge to be
explicitly enabled via ``MCP_SENTRY_ISSUE_BRIDGE_ENABLED``.
``comment_issue_fn`` carries the sanctioned issue-comment route used for
AC4 recurrence comments on already-linked issues; dry runs never comment.
"""
result: dict[str, Any] = {
"success": False,
@@ -682,6 +686,7 @@ def watchdog(
mappings=list(mappings or []),
apply=bool(apply),
create_issue_fn=create_issue_fn,
comment_issue_fn=comment_issue_fn,
)
except Exception as exc: # noqa: BLE001 - one bad issue must not abort the scan
result["failed"] += 1
@@ -702,6 +707,7 @@ def watchdog(
"outcome": reconciled.get("outcome"),
"gitea_issue": reconciled.get("gitea_issue"),
"existing_link": reconciled.get("existing_link"),
"recurrence_comment": reconciled.get("recurrence_comment"),
"reasons": reconciled.get("reasons"),
}
)