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
Adds the read half of the inbound observability path: pull unresolved issues
and events from the self-hosted Sentry API, normalize them into #612
observations, and reconcile them into durable Gitea issues.
Design: the existing #612 incident_bridge already owns dedupe, linking,
redaction, and issue creation on the #613 incident_links substrate, so this
change adds only what was genuinely missing - a Sentry API read layer,
observation mapping, a policy gate, and a watchdog. No second linking store is
introduced, which is what makes the mapping survive restarts (AC6).
New module sentry_incident_bridge.py:
* list_issues() with Link-header cursor pagination and statsPeriod windowing
* get_issue_events() returning sanitized recent + latest event
* observation_from_issue() mapping onto the #612 observation contract
* should_bridge_issue() policy gate (unresolved + event-count threshold)
* watchdog() scanning and reconciling, dry-run by default
* HTTP access injected as http_fn so the surface is testable without Sentry
New MCP tools: gitea_sentry_list_issues, gitea_sentry_get_issue_events,
gitea_sentry_reconcile_issue, gitea_sentry_link_gitea_issue,
gitea_sentry_watchdog.
Safety:
* SENTRY_AUTH_TOKEN is read from the environment only and never returned,
logged, or stored; config projections cannot carry it by construction
* missing config fails closed as not_configured, and a missing token fails
closed as missing_token before any HTTP call is made
* apply=true requires both MCP_SENTRY_ISSUE_BRIDGE_ENABLED and issue-create
permission; Sentry outages create nothing
* secrets are redacted and absolute local paths reduced to a category token
before any value can reach a Gitea issue body
* raw Sentry incidents are never assignable control-plane work items
Tests: 38 new cases covering create, update/recurrence, dedupe, resolved-issue
non-reopen, redaction, pagination, missing token, unavailable server,
self-hosted base URL, restart persistence, policy gates, and the five tool
wrappers.
Full suite: 3733 passed, 6 skipped. The 2 remaining failures
(test_issue_702_review_findings_f1_f6, test_reconciler_supersession_close) were
verified to fail identically on unmodified master at fcf6981 and are unrelated
to this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_011w1WGVV3duWEf45SJRJ1DL