diff --git a/.github/blocks/notify-discord/action.yaml b/.github/blocks/notify-discord/action.yaml new file mode 100644 index 0000000..f16b39b --- /dev/null +++ b/.github/blocks/notify-discord/action.yaml @@ -0,0 +1,110 @@ +name: "Notify Discord" +description: "Post a release announcement embed to a Discord channel webhook" + +inputs: + webhook-url: + description: "[secret] Discord channel webhook URL. Empty disables the block." + required: false + default: "" + service-name: + description: "[string] Display name of the released service" + required: true + version: + description: "[string] Version being released (e.g., 1.2.3)" + required: true + release-url: + description: "[string] URL to the GitHub Release" + required: true + release-notes: + description: "[string] Markdown release notes (truncated to 4096 chars)" + required: false + default: "" + prerelease: + description: "[boolean: 'true'/'false'] Whether this is a prerelease" + required: false + default: "false" + dry-run: + description: "[boolean: 'true'/'false'] Log payload without sending" + required: false + default: "false" + +runs: + using: composite + steps: + - name: Send Discord announcement + uses: actions/github-script@v7 + env: + WEBHOOK_URL: ${{ inputs.webhook-url }} + SERVICE_NAME: ${{ inputs.service-name }} + VERSION: ${{ inputs.version }} + RELEASE_URL: ${{ inputs.release-url }} + RELEASE_NOTES: ${{ inputs.release-notes }} + PRERELEASE: ${{ inputs.prerelease }} + DRY_RUN: ${{ inputs.dry-run }} + with: + script: | + const webhookUrl = process.env.WEBHOOK_URL; + if (!webhookUrl) { + core.info('Discord webhook not configured, skipping.'); + return; + } + core.setSecret(webhookUrl); + + const serviceName = process.env.SERVICE_NAME; + const version = process.env.VERSION; + const releaseUrl = process.env.RELEASE_URL; + const rawNotes = process.env.RELEASE_NOTES || ''; + const isPrerelease = process.env.PRERELEASE === 'true'; + const isDryRun = process.env.DRY_RUN === 'true'; + + const MAX_DESCRIPTION = 4096; + const description = rawNotes.length > MAX_DESCRIPTION + ? rawNotes.slice(0, MAX_DESCRIPTION - 1) + '\u2026' + : rawNotes; + + const titlePrefix = isPrerelease ? '[Pre-release] ' : ''; + const color = isPrerelease ? 0xF1C40F : 0x2ECC71; + + const payload = { + embeds: [ + { + title: `${titlePrefix}${serviceName} v${version}`, + url: releaseUrl, + description, + color, + timestamp: new Date().toISOString(), + footer: { text: context.payload.repository?.full_name || `${context.repo.owner}/${context.repo.repo}` }, + }, + ], + }; + + if (isDryRun) { + core.info('Dry run enabled, not sending. Payload:'); + core.info(JSON.stringify(payload, null, 2)); + return; + } + + const WEBHOOK_TIMEOUT_MS = 10000; + let response; + try { + response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS), + }); + } catch (error) { + if (error.name === 'AbortError' || error.name === 'TimeoutError') { + core.setFailed(`Discord webhook timed out after ${WEBHOOK_TIMEOUT_MS}ms.`); + return; + } + throw error; + } + + if (!response.ok) { + const body = await response.text(); + core.setFailed(`Discord webhook returned ${response.status} ${response.statusText}: ${body}`); + return; + } + + core.info(`Discord announcement posted (HTTP ${response.status}).`); diff --git a/README.md b/README.md index dafaa92..992eaa2 100644 --- a/README.md +++ b/README.md @@ -1273,6 +1273,59 @@ Posts or updates a single comment on a pull request. When a `comment-key` is pro ``` +--- + +### notify-discord + +**Path:** `.github/blocks/notify-discord/action.yaml` + +Posts a release announcement embed to a Discord channel webhook. The block no-ops when `webhook-url` is empty, so it can be wired into a pipeline unconditionally and stays dormant for repos that haven't configured `DISCORD_WEBHOOK_URL`. + +#### Setup (one-time, in Discord) + +1. Open the Discord server, then **Server Settings → Integrations → Webhooks → New Webhook**. +2. Pick the channel where releases should be announced. +3. Copy the webhook URL (looks like `https://discord.com/api/webhooks//`). +4. Add it to GitHub as a repo or org Actions secret named `DISCORD_WEBHOOK_URL`. + +The webhook URL is bound to the channel selected in step 2 — that is how "specific channel" is targeted; no channel ID is needed in the workflow. + +#### Inputs + +| Input | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `webhook-url` | secret | No | `""` | Discord channel webhook URL. Empty disables the block. | +| `service-name` | string | Yes | — | Display name of the released service | +| `version` | string | Yes | — | Version being released (e.g., `1.2.3`) | +| `release-url` | string | Yes | — | URL to the GitHub Release (use the `url` output of `create-github-release`) | +| `release-notes` | string | No | `""` | Markdown release notes (auto-truncated to Discord's 4096-char embed limit) | +| `prerelease` | boolean | No | `false` | Tags the embed as a prerelease (amber color, `[Pre-release]` title prefix) | +| `dry-run` | boolean | No | `false` | Logs the JSON payload without sending | + +#### Usage + +```yaml +- name: Create GitHub Release + id: gh-release + uses: photon-hq/buildspace/.github/blocks/create-github-release@main + with: + version: ${{ steps.release-info.outputs.version }} + title: "${{ inputs.service-name }} v${{ steps.release-info.outputs.version }}" + notes: ${{ steps.release-info.outputs.release_notes }} + prerelease: ${{ inputs.prerelease }} + +- name: Announce release on Discord + uses: photon-hq/buildspace/.github/blocks/notify-discord@main + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + service-name: ${{ inputs.service-name }} + version: ${{ steps.release-info.outputs.version }} + release-url: ${{ steps.gh-release.outputs.url }} + release-notes: ${{ steps.release-info.outputs.release_notes }} + prerelease: ${{ inputs.prerelease }} +``` + + --- ### update-docs