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 <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
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 <poldsberg@gmail.com>
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 <poldsberg@gmail.com>
Made-with: Cursor

---------

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-03-28 15:44:47 +01:00
committed by GitHub
parent 781815fd25
commit 0cb1189130
+85
View File
@@ -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}`);
}