From 3d82761f9ac88c99645846e47066e96eb64affc4 Mon Sep 17 00:00:00 2001 From: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com> Date: Sun, 24 May 2026 14:33:49 -0400 Subject: [PATCH] Add Discord changelog workflow --- .github/workflows/discord-changelog.yml | 301 ++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 .github/workflows/discord-changelog.yml diff --git a/.github/workflows/discord-changelog.yml b/.github/workflows/discord-changelog.yml new file mode 100644 index 000000000..f4b572402 --- /dev/null +++ b/.github/workflows/discord-changelog.yml @@ -0,0 +1,301 @@ +name: Discord changelog + +on: + push: + branches: + - main + - master + pull_request: + types: + - opened + - reopened + - synchronize + - closed + release: + types: + - published + create: + delete: + workflow_dispatch: + inputs: + release_tag: + description: Optional release tag to test. Leave blank to use the latest release. + required: false + type: string + +permissions: + contents: read + pull-requests: read + +jobs: + discord-changelog: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Post changelog to Discord + uses: actions/github-script@v7 + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} + RELEASE_TAG: ${{ inputs.release_tag }} + with: + script: | + const webhookUrl = process.env.DISCORD_WEBHOOK_URL; + + if (!webhookUrl) { + core.setFailed("Missing DISCORD_CHANGELOG_WEBHOOK secret."); + return; + } + + let eventName = context.eventName; + let event = context.payload; + const repoFullName = `${context.repo.owner}/${context.repo.repo}`; + const repoUrl = `https://github.com/${repoFullName}`; + + if (eventName === "workflow_dispatch") { + const releaseTag = process.env.RELEASE_TAG; + const response = releaseTag + ? await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: releaseTag, + }) + : await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + eventName = "release"; + event = { + action: "published", + repository: { + full_name: repoFullName, + html_url: repoUrl, + }, + sender: { + login: context.actor, + }, + release: response.data, + }; + } + + const discordPayload = buildDiscordPayload(eventName, event); + const discordResponse = await fetch(webhookUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(discordPayload), + }); + + if (!discordResponse.ok) { + core.setFailed( + `Discord webhook failed with ${discordResponse.status}: ${await discordResponse.text()}`, + ); + } + + function buildDiscordPayload(eventName, event) { + return { + username: "Repo Changelog", + content: buildContent(eventName, event), + embeds: [ + { + ...buildEmbed(eventName, event), + footer: { + text: `GitHub ${eventName} event`, + }, + timestamp: new Date().toISOString(), + }, + ], + }; + } + + function buildContent(eventName, event) { + const repo = getRepoName(event); + + if (eventName === "release") { + const tag = event.release?.tag_name || event.release?.name || "release"; + const action = event.action === "published" ? "published" : event.action || "updated"; + + return `**${repo}** ${action} **${tag}**`; + } + + return `**${repo}** update from ${getActor(event)}`; + } + + function buildEmbed(eventName, event) { + if (eventName === "release") { + return buildReleaseEmbed(event); + } + + if (eventName === "push") { + return buildPushEmbed(event); + } + + if (eventName === "pull_request") { + return buildPullRequestEmbed(event); + } + + if (eventName === "create" || eventName === "delete") { + return buildRefEmbed(eventName, event); + } + + return { + title: `GitHub ${eventName} event`, + description: `Received a ${eventName} event for ${getRepoName(event)}.`, + url: event.repository?.html_url, + color: 0x99aab5, + }; + } + + function buildReleaseEmbed(event) { + const release = event.release || {}; + const tag = release.tag_name || "release"; + const titleTag = release.name || tag; + const author = release.author?.login || event.sender?.login || "unknown"; + + return { + title: `${titleTag} Released`, + description: formatReleaseNotes(release.body || "") || "No release notes provided.", + url: release.html_url || event.repository?.html_url, + color: 0x57f287, + fields: [ + { + name: "Repository", + value: link(getRepoName(event), event.repository?.html_url), + inline: true, + }, + { + name: "Tag", + value: tag, + inline: true, + }, + { + name: "Published by", + value: author, + inline: true, + }, + ], + }; + } + + function buildPushEmbed(event) { + const repo = getRepoName(event); + const branch = trimRef(event.ref); + const commits = Array.isArray(event.commits) ? event.commits : []; + const commitLines = commits.slice(0, 8).map(formatCommit).join("\n"); + const overflow = commits.length > 8 ? `\n...and ${commits.length - 8} more.` : ""; + const commitWord = commits.length === 1 ? "commit" : "commits"; + + return { + title: `${commits.length} ${commitWord} pushed to ${branch}`, + description: commitLines + ? `${commitLines}${overflow}` + : "No commit list was included in this event.", + url: event.compare || event.repository?.html_url, + color: 0x5865f2, + fields: [ + { + name: "Repository", + value: link(repo, event.repository?.html_url), + inline: true, + }, + { + name: "Branch", + value: branch, + inline: true, + }, + ], + }; + } + + function buildPullRequestEmbed(event) { + const pr = event.pull_request || {}; + const action = + event.action === "closed" && pr.merged ? "merged" : event.action || "updated"; + const number = pr.number ? `#${pr.number}` : ""; + + return { + title: `Pull request ${number} ${action}`, + description: link(pr.title || "Pull request", pr.html_url), + url: pr.html_url || event.repository?.html_url, + color: pr.merged ? 0x57f287 : 0xfee75c, + fields: [ + { + name: "Author", + value: pr.user?.login || event.sender?.login || "unknown", + inline: true, + }, + { + name: "Branch", + value: `${pr.head?.ref || "head"} -> ${pr.base?.ref || "base"}`, + inline: true, + }, + ], + }; + } + + function buildRefEmbed(eventName, event) { + const action = eventName === "create" ? "created" : "deleted"; + const type = event.ref_type || "ref"; + const ref = event.ref || "unknown"; + + return { + title: `${capitalize(type)} ${action}: ${ref}`, + description: `${getActor(event)} ${action} ${type} \`${ref}\`.`, + url: event.repository?.html_url, + color: eventName === "create" ? 0x57f287 : 0xed4245, + }; + } + + function formatCommit(commit) { + const shortSha = commit.id ? commit.id.slice(0, 7) : "commit"; + const message = truncate(oneLine(commit.message || "No commit message"), 140); + const author = commit.author?.name ? ` - ${commit.author.name}` : ""; + + return `- ${link(shortSha, commit.url)} ${message}${author}`; + } + + function formatReleaseNotes(value) { + return truncate( + value + .replace(/\r\n/g, "\n") + .trim() + .split("\n") + .map((line) => { + const heading = line.match(/^#{1,6}\s+(.+)$/); + return heading ? `**${heading[1].trim()}**` : line; + }) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim(), + 3900, + ); + } + + function getRepoName(event) { + return event.repository?.full_name || repoFullName; + } + + function getActor(event) { + return event.sender?.login || event.pusher?.name || context.actor || "GitHub"; + } + + function link(label, url) { + return url ? `[${label}](${url})` : label; + } + + function trimRef(ref) { + return (ref || "unknown").replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, ""); + } + + function oneLine(value) { + return value.replace(/\s+/g, " ").trim(); + } + + function truncate(value, maxLength) { + return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`; + } + + function capitalize(value) { + return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`; + }