feat(notifications): per-repo Slack webhook routing to match Discord/PagerDuty#8471
Conversation
…PagerDuty Slack senders read a single global SLACK_WEBHOOK_URL, so a self-host operator running LoopOver across more than one repo gets every repo's terminal-action and review-recap messages mixed into one channel — unlike Discord and PagerDuty, which already resolve a per-repo channel/routing key first. Add resolveSlackWebhook(env, repoFullName) in notify-discord.ts, mirroring resolveDiscordWebhook's precedence: a SLACK_REPO_WEBHOOKS per-repo JSON map entry wins, else the global SLACK_WEBHOOK_URL, else disabled — every URL still gated by isValidSlackWebhook, two-source only (no legacy secret tier). Rewire the two per-repo senders (notifyActionToSlack, review-recap.ts's deliverRecapToSlack) to route through it and thread the resolution source into their audit events, exactly like their Discord siblings. The multi-repo maintainer digest deliverRecapToSlack stays global-only by design. Document SLACK_REPO_WEBHOOKS in .env.example. Closes JSONbored#8371
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8471 +/- ##
==========================================
+ Coverage 79.69% 89.67% +9.97%
==========================================
Files 791 99 -692
Lines 79319 22900 -56419
Branches 23954 3920 -20034
==========================================
- Hits 63216 20535 -42681
+ Misses 13298 2187 -11111
+ Partials 2805 178 -2627
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-24 15:41:41 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Summary
Slack senders read a single global
SLACK_WEBHOOK_URL, so a self-host operator running LoopOver across more than one repo gets every repo's terminal-action and review-recap messages mixed into one channel — unlike Discord (resolveDiscordWebhook) and PagerDuty (resolvePagerDutyRoutingKey), which already resolve a per-repo channel/routing key first.resolveSlackWebhook(env, repoFullName)insrc/services/notify-discord.ts, mirroringresolveDiscordWebhook's exact shape and precedence: aSLACK_REPO_WEBHOOKSper-repo JSON map entry (keys lower-cased) wins, else the globalSLACK_WEBHOOK_URL, else disabled. Returns the same discriminated union (configured/disabledwith the samereasonliterals). Every URL still passesisValidSlackWebhook(https://hooks.slack.com/services/...). Two-source only — no legacyWEBHOOK_SECRET_BY_REPO-style tier (Slack has no historical migration to preserve). A repo WITH a per-repo entry never silently falls back to the global channel.notifyActionToSlack(notify-discord.ts) now routes viaresolveSlackWebhookand audits the deniedreason+ threadssourceinto completed/error audits, exactly likenotifyActionToDiscord.deliverRecapToSlack(review-recap.ts, the per-repoReviewRecapsender) now routes viaresolveSlackWebhook(env, recap.repoFullName), mirroring its Discord siblingsendReviewRecapToDiscord.deliverRecapToSlackin notify-discord.ts (correctly global-only by design),notify-pagerduty.ts, andresolveDiscordWebhook.repoJsonMap(env, envName)in notify-discord.ts (same shape as notify-pagerduty.ts's) for the Slack map; the DiscordrepoWebhookMap/resolveDiscordWebhookpaths are byte-identical.SLACK_REPO_WEBHOOKSin.env.examplebesideSLACK_WEBHOOK_URL/DISCORD_REPO_WEBHOOKS.Tests
Extended
test/unit/notify-discord.test.tsandtest/unit/review-recap.test.ts(69 pass):resolveSlackWebhookrepo-map hit (valid/invalid/non-string), global fallback (valid/invalid/unset), malformedSLACK_REPO_WEBHOOKSJSON →{};notifyActionToSlackposts to the per-repo URL not the global, falls back when absent, denies an invalid per-repo entry (no silent global fallback);deliverRecapToSlackroutes two differentrecap.repoFullNamevalues independently (the core regression). Existing global-only Slack assertions updated (not deleted), global-fallback case kept covered.Validation
tsc --noEmitclean (changed files); both test files green (69).selfhost:env-reference:checkclean.main; no base conflict.isValidSlackWebhooknot weakened; no secret/wallet/hotkey/trust/reward terms.Closes #8371