Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/scripts/announce-discord.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
// Post a release announcement to a Discord webhook. Formats mechanically from
// the GitHub Release: title + lead paragraph (word-boundary capped) + link.
// Transport-agnostic formatting lives here so it can move into a shared action
// verbatim when we extend to nimblebrain / mpak.
//
// Env:
// WEBHOOK_URL Discord webhook. Empty/unset -> print payload + skip (never
// fails a release, so merging before the secret exists is safe).
// REPO owner/name (GITHUB_REPOSITORY).
// RELEASE_JSON path to `gh release view ... --json tagName,name,body,url`.
// DRY_RUN "true" -> print payload, don't POST.
import { readFileSync } from "node:fs";

const webhook = (process.env.WEBHOOK_URL || "").trim();
const repoName = (process.env.REPO || "").split("/").pop() || "release";
const dryRun = process.env.DRY_RUN === "true";

const rel = JSON.parse(readFileSync(process.env.RELEASE_JSON || "release.json", "utf8"));
const tag = rel.tagName || rel.name || "";
const url = rel.url || "";

// Lead paragraph: skip leading blanks; take consecutive non-blank lines until a
// blank line, a heading (#), or a list marker (- / *).
const lines = (rel.body || "").replace(/\r/g, "").split("\n");
let i = 0;
while (i < lines.length && lines[i].trim() === "") i++;
const lead = [];
for (; i < lines.length; i++) {
const t = lines[i].trim();
if (t === "" || t.startsWith("#") || t.startsWith("- ") || t.startsWith("* ")) break;
lead.push(t);
}
let desc = lead.join(" ").trim();

// Word-boundary cap; always link out to the full notes.
const CAP = 280;
if (desc.length > CAP) {
const cut = desc.lastIndexOf(" ", CAP);
desc = desc.slice(0, cut > 0 ? cut : CAP).trimEnd() + "…";
}
if (!desc) {
// The parser found no prose lead under the version heading (e.g. a CHANGELOG
// that leads with `### Fixed`, or a manually-cut release using GitHub's
// auto-generated notes). Post a generic line but make the gap visible in the
// run log rather than silently shipping content-free copy.
console.log("::warning::No lead paragraph found in the release body β€” posting generic copy. Check the CHANGELOG format.");
}
const description = `${desc || "New release."}\n\n[Full release notes β†’](${url})`;

const payload = {
embeds: [{ title: `πŸš€ ${repoName} ${tag}`, url, description, color: 0x0055ff }],
};

if (!webhook || dryRun) {
console.log(webhook ? "DRY_RUN β€” payload:" : "::notice::WEBHOOK_URL unset β€” skipping post. Payload:");
console.log(JSON.stringify(payload, null, 2));
process.exit(0);
}
const res = await fetch(webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
console.error(`::error::Discord webhook ${res.status}: ${await res.text().catch(() => "")}`);
process.exit(1);
}
console.log(`Announced ${repoName} ${tag} to Discord (${res.status}).`);
52 changes: 52 additions & 0 deletions .github/workflows/announce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Announce release

# Posts a Discord announcement when a GitHub Release is published. Decoupled
# from publish.yml on purpose: it fires for any published release (including
# manually-cut ones), and a failure here never affects the publish path.
#
# Setup (one-time): create a webhook on the Discord #announcements channel and
# add its URL as the `DISCORD_ANNOUNCE_WEBHOOK` repo/org secret. Until that
# secret exists the job runs and cleanly skips (no post, green run) β€” so this
# can merge before the webhook is wired.

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to announce (e.g. v0.10.2)"
required: true
dry_run:
description: "Print the payload without posting"
type: boolean
default: false

permissions:
contents: read

jobs:
announce:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24

- name: Resolve release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name || github.event.inputs.tag }}
run: |
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json tagName,name,body,url > release.json

- name: Announce to Discord
env:
WEBHOOK_URL: ${{ secrets.DISCORD_ANNOUNCE_WEBHOOK }}
REPO: ${{ github.repository }}
RELEASE_JSON: release.json
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
run: node .github/scripts/announce-discord.mjs