Description
Two scoped fixes for the recurring "Jira triggers fire inconsistently" reports (Linear SUP-415 / INT-1994). This intentionally does not include the comment-event support or the single-URL-per-user webhook-recreate rework from #23022 — it ships only the low-risk, high-ROI pieces.
1. Honor project_key (project scoping was silently ignored)
JIRA_NEW_ISSUE_TRIGGER and JIRA_UPDATED_ISSUE_TRIGGER accept a project_key config, but matching keyed only on event type — so one inbound webhook fanned out to every trigger instance for that event type regardless of project. The only project scoping was a best-effort Jira-side JQL filter set at registration, which is unreliable because Jira Cloud allows a single webhook URL per user, shared across that user's triggers.
Both triggers now override resolve_event_identity() to surface the inbound issue's project key:
resolved_config={"project_key": project_key} if project_key else None
Verified end-to-end against production data — the value set at setup() is the same one computed at fanout:
| Source | Value |
|---|
Stored at setup() | Apollo trigger_instance.config (jsonb) | {"project_key": "PEEKU"} |
| Computed at fanout | decrypted inbound webhook → body.issue.fields.project.key | "PEEKU" |
- Both ends are Jira's canonical project key → exact match under the dispatcher's
equals predicate (config->'project_key' = '"PEEKU"').
resolve_event_identity reads project.key ("PEEKU"), not project.id ("10000") — .id would not match the stored config.
- The three real customer events checked are currently
has_matched_trigger = NULL (unresolved); this fix routes them correctly.
Routing path (all confirmed in code, not assumed):
- Mercury
process() (mercury/tools/api/tool.py:668) forwards resolved_config in each TransformResult.
- Apollo
broadcast.ts:250 → buildConfigWhereClause() → DB_TRIGGER_INSTANCE_FIND.byWebhookId({ webhookId: hook_id, configFilter }).
apps/apollo/src/lib/triggers/config_filter.ts builds the Prisma JSON-path WHERE; absent/null config value = wildcard. Same mechanism Slack channel_id (scalar) and Asana fields (array) already use.
project_key is a required config field, so there's no accidental wildcard. No hermes/Apollo change needed — the filter is generic over config keys. When the payload has no project key, resolved_config is None and routing falls back to hook_id only, preserving prior behavior.
2. Clarify the Updated Issue trigger scope (comments are out of scope)
In Jira, comment activity is delivered as a separate webhook event class that this trigger does not subscribe to, so a change that only adds/edits a comment never fires it. The docstrings (surfaced as the customer-facing trigger description) now state this in plain language — without leaking internal Jira event identifiers — and the New Issue description spells out its create + project scope.
Backward compatibility
- No payload-shape change, no change to webhook registration.
resolve_event_identity() runs per inbound event, so existing trigger instances get correct project scoping immediately on deploy — no re-registration required.
- Edge case: matching is exact string equality, so a config whose key differs only by case from Jira's canonical key (e.g.
peeku vs PEEKU) would not match. All current prod configs use the canonical key; can add case-normalization later if desired.
How did I test this PR
uv run ruff check ✅ and ruff format --check ✅ on both changed files.
mypy clean on both changed source files (remaining errors are pre-existing stub warnings in unrelated mercury/* infra).
- Existing jira tests still green (
tests/test_apps/test_jira/, 17 passed).
- End-to-end correctness verified against production data (stored config vs decrypted inbound payload project key — table above), and the routing path traced through Mercury
process() → Apollo broadcast.ts → config_filter.ts.
Refs SUP-415, INT-1994