From 0cb1189130b3d014767ed2e0f47bbf499ee4f943 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Mar 2026 15:44:47 +0100 Subject: [PATCH] workflows: add Discord notification for ready PRs (#33655) * workflows: add Discord notification for ready PRs Adds a new workflow that posts to Discord whenever a pull request is opened as non-draft or marked as ready for review. Uses jq to safely construct the JSON payload and sends it via curl to a webhook URL stored in repository secrets. Signed-off-by: Patrik Oldsberg Made-with: Cursor Signed-off-by: Patrik Oldsberg Made-with: Cursor Signed-off-by: Patrik Oldsberg Made-with: Cursor Signed-off-by: Patrik Oldsberg Made-with: Cursor * workflows: harden Discord webhook notification Add early guard for missing webhook secret, disable Discord mention parsing to prevent abuse via PR titles/bodies, and check the HTTP response to surface webhook failures. Signed-off-by: Patrik Oldsberg Made-with: Cursor * workflows: upgrade github-script to v8 and truncate embed title Align actions/github-script pin with the rest of the repo (v8.0.0) and truncate the Discord embed title to the 256-character limit to prevent webhook failures on long PR titles. Signed-off-by: Patrik Oldsberg Made-with: Cursor --------- Signed-off-by: Patrik Oldsberg --- .github/workflows/notify_discord-pr.yml | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/notify_discord-pr.yml diff --git a/.github/workflows/notify_discord-pr.yml b/.github/workflows/notify_discord-pr.yml new file mode 100644 index 0000000000..f7be8d3d3b --- /dev/null +++ b/.github/workflows/notify_discord-pr.yml @@ -0,0 +1,85 @@ +name: Discord PR notification + +on: + pull_request_target: + types: [opened, ready_for_review] + +permissions: + actions: none + checks: none + contents: none + deployments: none + issues: none + packages: none + pages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none + +jobs: + notify: + # Only run for non-draft PRs in the main repo + if: github.repository == 'backstage/backstage' && github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Send Discord notification + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + DISCORD_PULL_REQUESTS_WEBHOOK: ${{ secrets.DISCORD_PULL_REQUESTS_WEBHOOK }} + with: + script: | + if (!process.env.DISCORD_PULL_REQUESTS_WEBHOOK) { + throw new Error('DISCORD_PULL_REQUESTS_WEBHOOK secret is not set'); + } + + const { pull_request: pr, action } = context.payload; + + let description = (pr.body || '').slice(0, 300); + if ((pr.body || '').length > 300) { + description += '...'; + } + + // Discord embed titles are limited to 256 characters + const titlePrefix = `#${pr.number}: `; + const maxTitleLength = 256 - titlePrefix.length; + let prTitle = pr.title || ''; + if (prTitle.length > maxTitleLength) { + prTitle = prTitle.slice(0, maxTitleLength - 3) + '...'; + } + + const isOpened = action === 'opened'; + const color = isOpened ? 5763719 : 3447003; // green : blue + const footer = isOpened ? 'New pull request' : 'Ready for review'; + + const payload = { + embeds: [{ + title: `${titlePrefix}${prTitle}`, + url: pr.html_url, + description, + color, + author: { + name: pr.user.login, + url: pr.user.html_url, + icon_url: pr.user.avatar_url, + }, + footer: { text: footer }, + }], + allowed_mentions: { parse: [] }, + }; + + const response = await fetch(process.env.DISCORD_PULL_REQUESTS_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new Error(`Discord webhook failed: ${response.status} ${response.statusText} - ${body}`); + }