From fde5507785a86fb2f4dfbe4a5f828484575b6e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Mon, 2 Nov 2026 14:03:13 -0500 Subject: [PATCH 1/2] feat: add slack notification --- .github/scripts/notify-app-build.mjs | 120 +++++++++++++++++++++++++++ .github/workflows/theseus-build.yml | 21 +++++ 2 files changed, 141 insertions(+) create mode 100644 .github/scripts/notify-app-build.mjs diff --git a/.github/scripts/notify-app-build.mjs b/.github/scripts/notify-app-build.mjs new file mode 100644 index 00000000000..0cb33698ae9 --- /dev/null +++ b/.github/scripts/notify-app-build.mjs @@ -0,0 +1,120 @@ +const { + BUILD_ENVIRONMENT, + GITHUB_API_URL, + GITHUB_REF_NAME, + GITHUB_REPOSITORY, + GITHUB_RUN_ID, + GITHUB_SERVER_URL, + GITHUB_SHA, + GITHUB_TOKEN, + GITHUB_TRIGGERING_ACTOR, + SLACK_WEBHOOK_URL, +} = process.env + +/** Job and artifact names must match the matrix in `.github/workflows/theseus-build.yml`. */ +const PLATFORMS = [ + { + label: 'Windows', + job: 'Build (namespace-profile-medium-amd64-windows)', + artifact: 'App bundle (x86_64-pc-windows-msvc)', + }, + { + label: 'MacOS', + job: 'Build (namespace-profile-medium-amd64-macos)', + artifact: 'App bundle (universal-apple-darwin)', + }, + { + label: 'Linux', + job: 'Build (namespace-profile-medium-amd64)', + artifact: 'App bundle (x86_64-unknown-linux-gnu)', + }, +] + +const runUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}` + +async function fetchRunResource(resource) { + const response = await fetch( + `${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/${resource}?per_page=100`, + { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${GITHUB_TOKEN}`, + }, + }, + ) + if (!response.ok) { + throw new Error(`Failed to fetch ${resource}: ${response.status} ${await response.text()}`) + } + return response.json() +} + +/** Slack treats these characters as control sequences, even inside inline code. */ +function escapeMrkdwn(text) { + return text.replace(/&/g, '&').replace(//g, '>') +} + +function mrkdwnSection(text) { + return { type: 'section', text: { type: 'mrkdwn', text } } +} + +function mrkdwnContext(...elements) { + return { type: 'context', elements: elements.map((text) => ({ type: 'mrkdwn', text })) } +} + +/** A build without an artifact is reported as failed, as there is nothing to download. */ +function summarizeBuild({ label, job: jobName, artifact: artifactName }, jobs, artifacts) { + const job = jobs.find(({ name }) => name === jobName) + if (!job) { + throw new Error(`No job named "${jobName}" in this run`) + } + + const artifact = artifacts.find(({ name }) => name === artifactName) + if (job.conclusion !== 'success' || !artifact) { + const failedStep = job.steps.find(({ conclusion }) => conclusion === 'failure') + const logsUrl = failedStep ? `${job.html_url}#step:${failedStep.number}:1` : job.html_url + return { succeeded: false, line: `:x: ${label} build *FAILED* (<${logsUrl}|view logs>)` } + } + + const sizeMiB = Math.round(artifact.size_in_bytes / 1024 / 1024) + const downloadUrl = `${runUrl}/artifacts/${artifact.id}` + return { + succeeded: true, + line: `:white_check_mark: ${label} build *SUCCESS* (<${downloadUrl}|download> ${sizeMiB}MiB)`, + } +} + +const [{ jobs }, { artifacts }] = await Promise.all([ + fetchRunResource('jobs'), + fetchRunResource('artifacts'), +]) + +const builds = PLATFORMS.map((platform) => summarizeBuild(platform, jobs, artifacts)) +const statusEmoji = builds.every(({ succeeded }) => succeeded) ? ':white_check_mark:' : ':x:' +const headline = `A new Modrinth app build has been completed ${statusEmoji}` + +const now = new Date() +const timestamp = `` + +const response = await fetch(SLACK_WEBHOOK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: headline, + blocks: [ + mrkdwnSection(`:modrinth: ${headline}`), + mrkdwnContext( + `Environment: \`${BUILD_ENVIRONMENT}\``, + `Branch: \`${escapeMrkdwn(GITHUB_REF_NAME)}\``, + `Commit: \`${GITHUB_SHA.slice(0, 8)}\``, + `Manually run by <${GITHUB_SERVER_URL}/${GITHUB_TRIGGERING_ACTOR}|${GITHUB_TRIGGERING_ACTOR}>`, + ), + mrkdwnSection(builds.map(({ line }) => line).join('\n')), + { type: 'divider' }, + mrkdwnContext(`<${runUrl}|View on GitHub> • ${timestamp}`), + ], + }), +}) + +if (!response.ok) { + throw new Error(`Failed to post to Slack: ${response.status} ${await response.text()}`) +} diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index d4903a4cbbd..cd8b5188a85 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -225,3 +225,24 @@ jobs: target/release/bundle/nsis/Modrinth App_*-setup.exe* target/release/bundle/nsis/Modrinth App (dev-*)_*-setup.exe* target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip* + + notify: + name: Notify Slack + needs: build + if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: .github/scripts + + - name: Send Slack notification + run: node .github/scripts/notify-app-build.mjs + env: + BUILD_ENVIRONMENT: ${{ inputs.environment || 'prod' }} + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_APP_BUILDS_WEBHOOK_URL }} From af4c978b28069b795625b882eb929b9c9273b20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Mon, 2 Nov 2026 14:44:40 -0500 Subject: [PATCH 2/2] fix display --- .github/scripts/notify-app-build.mjs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/scripts/notify-app-build.mjs b/.github/scripts/notify-app-build.mjs index 0cb33698ae9..6bb743b826b 100644 --- a/.github/scripts/notify-app-build.mjs +++ b/.github/scripts/notify-app-build.mjs @@ -57,8 +57,8 @@ function mrkdwnSection(text) { return { type: 'section', text: { type: 'mrkdwn', text } } } -function mrkdwnContext(...elements) { - return { type: 'context', elements: elements.map((text) => ({ type: 'mrkdwn', text })) } +function mrkdwnContext(text) { + return { type: 'context', elements: [{ type: 'mrkdwn', text }] } } /** A build without an artifact is reported as failed, as there is nothing to download. */ @@ -103,10 +103,12 @@ const response = await fetch(SLACK_WEBHOOK_URL, { blocks: [ mrkdwnSection(`:modrinth: ${headline}`), mrkdwnContext( - `Environment: \`${BUILD_ENVIRONMENT}\``, - `Branch: \`${escapeMrkdwn(GITHUB_REF_NAME)}\``, - `Commit: \`${GITHUB_SHA.slice(0, 8)}\``, - `Manually run by <${GITHUB_SERVER_URL}/${GITHUB_TRIGGERING_ACTOR}|${GITHUB_TRIGGERING_ACTOR}>`, + [ + `Environment: \`${BUILD_ENVIRONMENT}\``, + `Branch: \`${escapeMrkdwn(GITHUB_REF_NAME)}\``, + `Commit: \`${GITHUB_SHA.slice(0, 8)}\``, + `Manually run by <${GITHUB_SERVER_URL}/${GITHUB_TRIGGERING_ACTOR}|${GITHUB_TRIGGERING_ACTOR}>`, + ].join('\n'), ), mrkdwnSection(builds.map(({ line }) => line).join('\n')), { type: 'divider' },