Skip to content

refactor(ci): switch Discord automation from webhooks to single bot#42

Open
EtienneLescot wants to merge 3 commits into
mainfrom
refactor/discord-bot-only
Open

refactor(ci): switch Discord automation from webhooks to single bot#42
EtienneLescot wants to merge 3 commits into
mainfrom
refactor/discord-bot-only

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Consolidates all Discord PR-review automations (intro post, thread updates, status mutations, weekly spotlight) under one bot identity (\DISCORD_BOT_TOKEN) instead of three separate webhook URLs. Removes the per-channel webhook secret-rotation surface and unifies rate limits, audit logs, and permissions under one app.

What changes

  • Intro post + forum thread creation: webhook POST (\ hread_name) → bot \POST /channels/{forumChannelId}/threads.
  • Per-event message in thread: webhook POST (\ hread_id) → bot \POST /channels/{threadId}/messages.
  • Thread status (tags, archive, lock): unchanged, already used bot — moved into shared \discord-bot-api.mjs\ helper.
  • Weekly leaderboard: webhook POST → bot POST to spotlight channel.
  • Failure alerts: optional, alert webhook → \DISCORD_ALERT_CHANNEL_ID\ (variable); unset to silence.

Bot display name + avatar are now the bot's configured Discord profile (set in the developer portal), no longer overridable per-message via \DISCORD_WEBHOOK_USERNAME\ / \DISCORD_WEBHOOK_AVATAR_URL.

Migration steps for the maintainer

  1. Add \DISCORD_PR_FORUM_CHANNEL_ID\ and \DISCORD_SPOTLIGHT_CHANNEL_ID\ as repo variables (channel IDs, not secrets).
  2. Optional: add \DISCORD_ALERT_CHANNEL_ID\ for failure alerts.
  3. In Discord, grant the bot on each channel:
    • PR forum: View, Send Messages, Embed Links, Create Public Threads, Manage Threads.
    • Spotlight channel: View, Send Messages, Embed Links.
  4. Delete the dead webhook secrets once the new wiring is verified: \DISCORD_WEBHOOK_URL, \DISCORD_PR_FORUM_WEBHOOK, \DISCORD_SPOTLIGHT_WEBHOOK_URL, \DISCORD_ALERT_WEBHOOK_URL, \DISCORD_WEBHOOK_USERNAME, \DISCORD_WEBHOOK_AVATAR_URL.

Files

  • .github/scripts/discord-bot-api.mjs\ (new): \createForumThread, \postChannelMessage, \patchChannel\ — single source of truth for bot calls, rate-limited responses flagged.
  • .github/scripts/discord-bot-api.test.mjs\ (new): 5 tests covering payload shape, 429, error path, and each helper.
  • .github/scripts/discord-pr-sync.mjs: webhook code → \createForumThread\ + \postChannelMessage\ + \patchChannel.
  • .github/scripts/discord-weekly-leaderboard.mjs: webhook POST → \postChannelMessage.
  • .github/workflows/discord-pr-notify.yml: dropped 4 webhook env vars, added \DISCORD_ALERT_CHANNEL_ID.
  • .github/workflows/discord-weekly-leaderboard.yml: dropped 3 webhook env vars, added \DISCORD_SPOTLIGHT_CHANNEL_ID.
  • \docs/github-actions-workflows.md: updated bot-only permission docs.

Testing


  • px vitest --run .github/scripts/\ → 12/12 pass (5 new + 7 existing).

  • px biome check .\ → clean.
  • Existing workflow behaviour preserved: same triggers, same Discord-side outcomes, same concurrency group.

Summary by CodeRabbit

  • New Features
    • Discord PR notifications now use a bot-driven forum thread, with tag/review/comment updates sent as channel messages (plus optional alerts).
    • The weekly contributor leaderboard now posts to the spotlight channel via bot REST messaging.
  • Bug Fixes
    • Added robust handling for Discord API failures, including explicit rate-limit (HTTP 429) detection and clearer error reporting.
  • Documentation
    • Updated workflow docs and required Discord permission guidance for the bot-based integration.
  • Tests
    • Added coverage for the Discord REST helper functions, including happy paths and failure scenarios.

All four PR-review automations (intro post, thread updates, status
mutations, weekly spotlight) now flow through one Discord bot identity
(DISCORD_BOT_TOKEN) instead of three separate webhook URLs. Removes
the per-channel webhook secret rotation surface and consolidates rate
limits, audit logs, and permissions under one app.

Behavioural changes:
- Intro post + forum thread creation: was POST to webhook with
  thread_name, now POST /channels/{forumChannelId}/threads via bot.
- Per-event message in thread: was POST to webhook with thread_id, now
  POST /channels/{threadId}/messages via bot.
- Thread status (tags, archive, lock): unchanged, already used bot.
- Weekly leaderboard: was POST to spotlight webhook, now POST to
  spotlight channel via bot.
- Failure alerts: optional, was alert webhook, now alert channel via
  bot (DISCORD_ALERT_CHANNEL_ID); unset to silence.

Display name and avatar for bot messages are now the bot's configured
profile in the Discord developer portal rather than per-webhook env
vars. Remove the old secrets after migration:
DISCORD_WEBHOOK_URL, DISCORD_PR_FORUM_WEBHOOK, DISCORD_SPOTLIGHT_WEBHOOK_URL,
DISCORD_ALERT_WEBHOOK_URL, DISCORD_WEBHOOK_USERNAME, DISCORD_WEBHOOK_AVATAR_URL.

Add as variables (channel ids, not secrets):
DISCORD_PR_FORUM_CHANNEL_ID, DISCORD_SPOTLIGHT_CHANNEL_ID,
DISCORD_ALERT_CHANNEL_ID (optional).

Bot permissions required on each channel:
- PR forum: View, Send Messages, Embed Links, Create Public Threads,
  Manage Threads.
- Spotlight channel: View, Send Messages, Embed Links.
- Alert channel (optional): View, Send Messages.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Discord bot REST helper module, switches PR sync and weekly leaderboard posting to bot token plus channel ID configuration, and updates the related workflows and docs to match the new Discord delivery path.

Changes

Discord Bot API Migration

Layer / File(s) Summary
Bot API helper module and tests
.github/scripts/discord-bot-api.mjs, .github/scripts/discord-bot-api.test.mjs
callDiscord sends authenticated JSON requests to the Discord v10 API, returns null for 204, throws on 429 and other non-OK responses, and backs createForumThread, postChannelMessage, and patchChannel. Tests cover request shape, rate-limit handling, and response parsing.
discord-pr-sync migration to bot API
.github/scripts/discord-pr-sync.mjs
The sync script now uses bot token, forum channel, and alert channel settings; creates forum threads through createForumThread; updates thread state through patchChannel; posts thread messages through postChannelMessage; and keeps non-blocking failure handling.
discord-weekly-leaderboard migration to bot API
.github/scripts/discord-weekly-leaderboard.mjs
The leaderboard script now posts through postChannelMessage, reads bot token and spotlight channel id from the environment, and removes webhook-specific payload fields and delivery logic.
Workflow config and docs
.github/workflows/discord-pr-notify.yml, .github/workflows/discord-weekly-leaderboard.yml, docs/github-actions-workflows.md
Both workflows switch from webhook secrets to bot token and channel id variables, and the docs describe the bot-based Discord flows and channel permissions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • getopenscreen/openscreen#40: Directly related to the Discord PR sync flow that this PR rewires from webhook calls to bot REST helpers.

Poem

🐇 I hop through threads with bot-token cheer,
No webhook puddles linger here.
A forum thread, a message, a patch,
The Discord path now fits its match.
With tidy calls and alerts that sing,
This little carrot keeps its spring 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the switch from webhooks to a single Discord bot.
Description check ✅ Passed The PR description is detailed and covers the summary, migration steps, file impacts, and testing, matching the template overall.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/discord-bot-only

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/discord-bot-api.mjs:
- Around line 5-13: The callDiscord helper can hang indefinitely because it
makes a fetch without any request timeout, which can block PR sync and
leaderboard workflows. Update callDiscord in discord-bot-api.mjs to use an
AbortController-based timeout, matching the pattern used by the other Discord
helper, and ensure the controller is wired into fetch and cleared/handled after
the request completes or fails.

In `@docs/github-actions-workflows.md`:
- Around line 133-134: The Discord permissions documentation for the forum
workflow is incorrect: the bot needs thread-scoped posting permission rather
than listing Create Public Threads. Update the permissions text in the Discord
workflow docs to mention Send Messages in Threads for posting into existing
forum threads, and keep the other required permissions aligned with the forum
channel behavior described by the thread creation and message-posting flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8305e2ba-ed4d-4418-8092-a2fa146e981d

📥 Commits

Reviewing files that changed from the base of the PR and between f6b81e7 and ec667bb.

📒 Files selected for processing (7)
  • .github/scripts/discord-bot-api.mjs
  • .github/scripts/discord-bot-api.test.mjs
  • .github/scripts/discord-pr-sync.mjs
  • .github/scripts/discord-weekly-leaderboard.mjs
  • .github/workflows/discord-pr-notify.yml
  • .github/workflows/discord-weekly-leaderboard.yml
  • docs/github-actions-workflows.md

Comment on lines +5 to +13
async function callDiscord(botToken, method, path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files '.github/scripts/*' | sed -n '1,120p'

printf '\nOutline discord-bot-api.mjs:\n'
ast-grep outline .github/scripts/discord-bot-api.mjs --view expanded || true

printf '\nRelevant source:\n'
cat -n .github/scripts/discord-bot-api.mjs | sed -n '1,220p'

printf '\nSearch for AbortController/timeout usage in scripts:\n'
rg -n --hidden --glob '.github/scripts/*' 'AbortController|setTimeout|timeout|signal:' .github/scripts || true

printf '\nCallers:\n'
rg -n --hidden --glob '.github/scripts/*' 'callDiscord\(' .github/scripts || true

Repository: getopenscreen/openscreen

Length of output: 3478


Add a timeout to callDiscord()

Every Discord request can hang indefinitely here, which can stall the PR sync and leaderboard workflows until the job times out. Add an AbortController-based timeout like the other Discord helper uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/discord-bot-api.mjs around lines 5 - 13, The callDiscord
helper can hang indefinitely because it makes a fetch without any request
timeout, which can block PR sync and leaderboard workflows. Update callDiscord
in discord-bot-api.mjs to use an AbortController-based timeout, matching the
pattern used by the other Discord helper, and ensure the controller is wired
into fetch and cleared/handled after the request completes or fails.

Comment on lines +133 to +134
All Discord traffic goes through a single bot (`DISCORD_BOT_TOKEN`, secret). The script creates the forum thread via `POST /channels/{forumChannelId}/threads` and posts review/comment updates into the existing thread via `POST /channels/{threadId}/messages`. Required bot permissions on the PR forum channel: View Channel, Send Messages, Embed Links, Create Public Threads, Manage Threads (for tag/archive/lock). Optional failure alerts can be sent to a separate channel via `DISCORD_ALERT_CHANNEL_ID` (variable); unset to silence.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the forum permissions Discord checks.

Forum posts and thread replies need Send Messages in Threads; Create Public Threads is not the right permission to list here. Without that, the bot can still hit 403s when it posts PR updates.

✏️ Proposed doc fix
-All Discord traffic goes through a single bot (`DISCORD_BOT_TOKEN`, secret). The script creates the forum thread via `POST /channels/{forumChannelId}/threads` and posts review/comment updates into the existing thread via `POST /channels/{threadId}/messages`. Required bot permissions on the PR forum channel: View Channel, Send Messages, Embed Links, Create Public Threads, Manage Threads (for tag/archive/lock). Optional failure alerts can be sent to a separate channel via `DISCORD_ALERT_CHANNEL_ID` (variable); unset to silence.
+All Discord traffic goes through a single bot (`DISCORD_BOT_TOKEN`, secret). The script creates the forum thread via `POST /channels/{forumChannelId}/threads` and posts review/comment updates into the existing thread via `POST /channels/{threadId}/messages`. Required bot permissions on the PR forum channel: View Channel, Send Messages, Send Messages in Threads, Embed Links, and Manage Threads (for tag/archive/lock). Optional failure alerts can be sent to a separate channel via `DISCORD_ALERT_CHANNEL_ID` (variable); unset to silence.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
All Discord traffic goes through a single bot (`DISCORD_BOT_TOKEN`, secret). The script creates the forum thread via `POST /channels/{forumChannelId}/threads` and posts review/comment updates into the existing thread via `POST /channels/{threadId}/messages`. Required bot permissions on the PR forum channel: View Channel, Send Messages, Embed Links, Create Public Threads, Manage Threads (for tag/archive/lock). Optional failure alerts can be sent to a separate channel via `DISCORD_ALERT_CHANNEL_ID` (variable); unset to silence.
All Discord traffic goes through a single bot (`DISCORD_BOT_TOKEN`, secret). The script creates the forum thread via `POST /channels/{forumChannelId}/threads` and posts review/comment updates into the existing thread via `POST /channels/{threadId}/messages`. Required bot permissions on the PR forum channel: View Channel, Send Messages, Send Messages in Threads, Embed Links, and Manage Threads (for tag/archive/lock). Optional failure alerts can be sent to a separate channel via `DISCORD_ALERT_CHANNEL_ID` (variable); unset to silence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/github-actions-workflows.md` around lines 133 - 134, The Discord
permissions documentation for the forum workflow is incorrect: the bot needs
thread-scoped posting permission rather than listing Create Public Threads.
Update the permissions text in the Discord workflow docs to mention Send
Messages in Threads for posting into existing forum threads, and keep the other
required permissions aligned with the forum channel behavior described by the
thread creation and message-posting flow.

…ok migration

Three regressions from the bot-only migration, caught on self-review:

1. Webhook POSTs defaulted to allowed_mentions: { parse: [] }; bot POSTs
   default to allowing all mentions. Without restoring it, the
   <@&{reviewerRoleId}> embed in ready_for_review and changes_requested
   review posts would start pinging the reviewer role, spamming
   maintainers.

2. patchChannel now throws on non-ok (was: warn-and-return in the
   inline patchDiscordThread). That made the user-facing message after
   each tag/archive patch (PR merged, review state, etc.) skipped when
   the patch failed, because the throw bubbled to the outer catch.
   Added safePatchChannel that logs and continues so the message still
   posts even if tagging fails.

3. Move THREAD_MARKER_REGEX back to the top of the file (the rewrite
   dropped it below the function that uses it — TDZ-safe but ugly).

Also: drop the unused rateLimited flag from the bot-api error (no
consumer read it; the message is enough), and restore trailing
newlines on the two yml files.
5 near-duplicate tests (3 happy-path + 2 error, only on createForumThread)
became 9 parameterized tests (3 cases × 3 paths) covering all 3 helpers
uniformly, in 39 fewer lines.

Side benefit: postChannelMessage and patchChannel now also have explicit
rate-limit and error-path coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/scripts/discord-bot-api.test.mjs (1)

50-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the remaining callDiscord contract.

This table still leaves two important behaviors untested: it never asserts the "Content-Type": "application/json" header, and it never exercises the 204 branch that should resolve to null in .github/scripts/discord-bot-api.mjs. Adding both checks here would make the wrapper tests match the helper’s full request/response contract.

Suggested test additions
 describe.each(happyCases)("$name", ({ call, args, expectUrl, expectMethod, expectBody }) => {
 	it("calls Discord with the right URL, method, bot auth, and payload", async () => {
 		mockResponse();

 		await call({ ...args, botToken });

 		const [url, init] = vi.mocked(fetch).mock.calls[0];
 		expect(url).toBe(expectUrl);
 		expect(init.method).toBe(expectMethod);
 		expect(init.headers.Authorization).toBe(`Bot ${botToken}`);
+		expect(init.headers["Content-Type"]).toBe("application/json");
 		expect(JSON.parse(init.body)).toEqual(expectBody);
 	});
+
+	it("returns null on 204", async () => {
+		mockResponse({ status: 204, body: {} });
+		await expect(call({ ...args, botToken })).resolves.toBeNull();
+	});

 	it("throws on 429 with rate-limit message", async () => {
 		mockResponse({ status: 429, body: { retry_after: 1 } });
 		await expect(call({ ...args, botToken })).rejects.toThrow(/rate-limited \(429\)/);
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/discord-bot-api.test.mjs around lines 50 - 70, Update the
`describe.each(happyCases)` tests in `discord-bot-api.test.mjs` to cover the
full `callDiscord` contract: assert that the request sent by `call` includes the
`"Content-Type": "application/json"` header alongside the bot auth header, and
add a test case that exercises the `204` response branch so `callDiscord`
resolves to `null` as implemented in `.github/scripts/discord-bot-api.mjs`. Use
the existing `call`, `mockResponse`, and `happyCases` setup to keep the checks
consistent with the current wrapper tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/scripts/discord-bot-api.test.mjs:
- Around line 50-70: Update the `describe.each(happyCases)` tests in
`discord-bot-api.test.mjs` to cover the full `callDiscord` contract: assert that
the request sent by `call` includes the `"Content-Type": "application/json"`
header alongside the bot auth header, and add a test case that exercises the
`204` response branch so `callDiscord` resolves to `null` as implemented in
`.github/scripts/discord-bot-api.mjs`. Use the existing `call`, `mockResponse`,
and `happyCases` setup to keep the checks consistent with the current wrapper
tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e892549c-d592-4dc9-b17e-892607ef45b7

📥 Commits

Reviewing files that changed from the base of the PR and between eb42661 and 0c5c1b8.

📒 Files selected for processing (1)
  • .github/scripts/discord-bot-api.test.mjs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant