feat: add Sentry-to-Gitea incident bridge for MCP workflow failures (Closes #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
This commit is contained in:
@@ -131,7 +131,59 @@ and `incident_links` rows.
|
||||
- The bridge remains the **only** sanctioned route from an alert back into
|
||||
Gitea workflow state.
|
||||
|
||||
## 7. Non-goals
|
||||
## 7. Reading Sentry back into Gitea (#607)
|
||||
|
||||
[`sentry_incident_bridge.py`](../../sentry_incident_bridge.py) supplies the
|
||||
**read** half of the inbound path: it pulls unresolved issues/events from the
|
||||
self-hosted Sentry API, normalizes them into #612 observations, and hands them
|
||||
to `incident_bridge.reconcile_incident`. It never adds a second linking store —
|
||||
`incident_links` on the #613 control-plane DB stays canonical, which is what
|
||||
makes the mapping survive restarts.
|
||||
|
||||
### Configuration
|
||||
|
||||
| Variable | Purpose | Default |
|
||||
| --- | --- | --- |
|
||||
| `SENTRY_BASE_URL` | Self-hosted Sentry root | `https://sentry.prgs.cc` |
|
||||
| `SENTRY_AUTH_TOKEN` | API token — **env only**, never logged or returned | _(unset)_ |
|
||||
| `SENTRY_ORG` | Sentry organization slug | _(unset)_ |
|
||||
| `SENTRY_PROJECT` | Sentry project slug | _(unset)_ |
|
||||
| `MCP_SENTRY_ISSUE_BRIDGE_ENABLED` | Required for `apply=true` | `false` |
|
||||
| `MCP_SENTRY_MIN_EVENTS_FOR_ISSUE` | Recurrence threshold before an issue is worth filing | `2` |
|
||||
| `MCP_SENTRY_LOOKBACK` | Scan window (`statsPeriod`, e.g. `24h`) | `24h` |
|
||||
|
||||
Missing org/project fails closed as `not_configured`; a missing token fails
|
||||
closed as `missing_token` **before** any HTTP call is made.
|
||||
|
||||
### Tools
|
||||
|
||||
| Tool | Mode | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `gitea_sentry_list_issues` | read-only | Unresolved issues, `Link`-header pagination |
|
||||
| `gitea_sentry_get_issue_events` | read-only | Sanitized recent + latest event for one issue |
|
||||
| `gitea_sentry_reconcile_issue` | dry-run default | One Sentry issue → durable Gitea issue |
|
||||
| `gitea_sentry_link_gitea_issue` | dry-run default | Link a Sentry issue to an existing Gitea issue |
|
||||
| `gitea_sentry_watchdog` | dry-run default | Scan + create/update issues for active incidents |
|
||||
|
||||
### Policy
|
||||
|
||||
- **Dedupe:** one Sentry issue maps to exactly one Gitea issue, keyed by
|
||||
provider + base URL + org + project + issue id. Recurrence updates the link
|
||||
(and its `event_count`) instead of filing a duplicate.
|
||||
- **No reopen:** a Sentry issue that is no longer `unresolved` is skipped; the
|
||||
bridge never reopens or re-files a closed Gitea issue.
|
||||
- **Threshold:** issues below `MCP_SENTRY_MIN_EVENTS_FOR_ISSUE` are skipped, so
|
||||
one-off noise does not become durable work.
|
||||
- **Apply is explicit:** `apply=true` requires both
|
||||
`MCP_SENTRY_ISSUE_BRIDGE_ENABLED` and issue-create permission on the profile.
|
||||
- **Outages fail closed:** an unreachable Sentry returns `sentry_unavailable`
|
||||
and creates nothing.
|
||||
- **Redaction:** secrets are scrubbed and absolute local paths are reduced to a
|
||||
category token (`[path:author]`, `[path:root]`, …) before any value reaches a
|
||||
Gitea issue body. Sensitive tag keys (`authorization`, `cookie`, …) are
|
||||
dropped, and permalinks carrying embedded credentials are discarded entirely.
|
||||
|
||||
## 8. Non-goals
|
||||
|
||||
- Sentry must **not** become the workflow source of truth.
|
||||
- Sentry must **not** approve, merge, close, or mutate Gitea workflow state.
|
||||
|
||||
Reference in New Issue
Block a user