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:
+65
-2
@@ -18158,6 +18158,41 @@ def gitea_observability_list_projects(
|
||||
}
|
||||
|
||||
|
||||
def _incident_recurrence_comment_fn(
|
||||
*,
|
||||
apply: bool,
|
||||
remote: str,
|
||||
host: str | None,
|
||||
org: str | None,
|
||||
repo: str | None,
|
||||
worktree_path: str | None = None,
|
||||
):
|
||||
"""Sanctioned issue-comment route for AC4 recurrence comments (#607).
|
||||
|
||||
Returns ``None`` on dry runs (never comment) and when the active profile
|
||||
lacks ``gitea.issue.comment``. Withholding the callable degrades to "link
|
||||
updated, no comment" rather than failing the durable mapping, and the
|
||||
reconcile result records why the comment was withheld.
|
||||
"""
|
||||
if not apply:
|
||||
return None
|
||||
if _profile_operation_gate("gitea.issue.comment"):
|
||||
return None
|
||||
|
||||
def comment_fn(issue_number, body, g_org, g_repo):
|
||||
return gitea_create_issue_comment(
|
||||
issue_number=int(issue_number),
|
||||
body=body,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=g_org or org,
|
||||
repo=g_repo or repo,
|
||||
worktree_path=worktree_path,
|
||||
)
|
||||
|
||||
return comment_fn
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_observability_reconcile_incident(
|
||||
observation_json: str,
|
||||
@@ -18284,12 +18319,22 @@ def gitea_observability_reconcile_incident(
|
||||
worktree_path=worktree_path,
|
||||
)
|
||||
|
||||
comment_fn = _incident_recurrence_comment_fn(
|
||||
apply=bool(apply),
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
worktree_path=worktree_path,
|
||||
)
|
||||
|
||||
result = incident_bridge.reconcile_incident(
|
||||
db,
|
||||
observation=observation if isinstance(observation, dict) else {},
|
||||
mappings=mappings,
|
||||
apply=bool(apply),
|
||||
create_issue_fn=create_fn,
|
||||
comment_issue_fn=comment_fn,
|
||||
force_gitea_issue_number=force_gitea_issue_number,
|
||||
)
|
||||
result["remote"] = remote
|
||||
@@ -18471,6 +18516,10 @@ def gitea_sentry_reconcile_issue(
|
||||
observation, then delegates dedupe/link/create to the sanctioned
|
||||
``gitea_observability_reconcile_incident`` path. Dry-run by default.
|
||||
|
||||
Dedupe basis: provider identity — provider, base URL, org, project, and
|
||||
Sentry issue id. This bridge does not populate ``fingerprint``, so
|
||||
fingerprint plays no part in its dedupe decisions.
|
||||
|
||||
Gitea remains the source of truth; the raw Sentry incident is never an
|
||||
assignable control-plane work item.
|
||||
"""
|
||||
@@ -18626,8 +18675,14 @@ def gitea_sentry_watchdog(
|
||||
|
||||
Dry-run by default. ``apply=true`` additionally requires
|
||||
``MCP_SENTRY_ISSUE_BRIDGE_ENABLED`` and issue-create permission. Each
|
||||
incident is deduped through the #612 ``incident_links`` substrate, so a
|
||||
recurring failure updates one issue instead of creating duplicates.
|
||||
incident is deduped through the #612 ``incident_links`` substrate by
|
||||
provider identity (provider, base URL, org, project, Sentry issue id), so
|
||||
a recurring failure updates one issue instead of creating duplicates.
|
||||
|
||||
When a linked issue sees continued events, a sanitized recurrence comment
|
||||
is posted through ``gitea_create_issue_comment`` (AC4). That requires
|
||||
``gitea.issue.comment``; without it the link still updates and the result
|
||||
records that the comment was withheld.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
@@ -18724,6 +18779,14 @@ def gitea_sentry_watchdog(
|
||||
limit=limit,
|
||||
max_pages=max_pages,
|
||||
create_issue_fn=create_fn,
|
||||
comment_issue_fn=_incident_recurrence_comment_fn(
|
||||
apply=bool(apply),
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
worktree_path=worktree_path,
|
||||
),
|
||||
)
|
||||
result["remote"] = remote
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user