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}`); + }