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
55 changes: 34 additions & 21 deletions .github/scripts/announce-discord.mjs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
#!/usr/bin/env node
// Post a release announcement to a Discord webhook. Formats mechanically from
// the GitHub Release: title + the opening sentence(s) of the lead paragraph
// (the code-dense detail comes later, so the opener reads cleanly) + link.
// Transport-agnostic formatting lives here so it can move into a shared action
// verbatim when we extend to nimblebrain / mpak.
// Post a synapse release announcement to the NimbleBrain #announcements Discord
// webhook. Formats from the GitHub Release: a one-sentence teaser (synapse's
// CHANGELOG leads are code-dense, so the opening sentence reads far cleaner in
// Discord than the full body) plus links to the full notes and the compare diff.
//
// Synapse-specific by design — NOT a shared/reusable action. Other products get
// their own announce, tuned to their own changelog style.
//
// Env:
// WEBHOOK_URL Discord webhook. Empty/unset -> print payload + skip (never
// fails a release, so merging before the secret exists is safe).
// WEBHOOK_URL Discord webhook. Empty/unset -> print payload + skip (a release
// never fails on a missing secret).
// REPO owner/name (GITHUB_REPOSITORY).
// RELEASE_JSON path to `gh release view ... --json tagName,name,body,url`.
// RELEASE_JSON path to `gh release view ... --json tagName,name,body,url,publishedAt`.
// PREV_TAG previous release tag, for the compare link (optional).
// 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 repo = process.env.REPO || "NimbleBrainInc/synapse";
const repoName = repo.split("/").pop() || "synapse";
const prevTag = (process.env.PREV_TAG || "").trim();
const dryRun = process.env.DRY_RUN === "true";

const rel = JSON.parse(readFileSync(process.env.RELEASE_JSON || "release.json", "utf8"));
Expand All @@ -34,11 +39,9 @@ for (; i < lines.length; i++) {
}
const para = lead.join(" ").trim();

// Teaser = the first sentence (plus the second only if the first is terse and
// they still fit). CHANGELOG leads open with prose; the inline-code-heavy detail
// comes later, so an opening sentence renders cleanly in Discord instead of a
// wall of code pills. Sentence boundaries are detected backtick-aware, so a `.`
// inside an identifier like `tokens.bgSubtle` is never a false split.
// Teaser = the first sentence (+ the second only if the first is terse and they
// still fit). Sentence boundaries are detected backtick-aware, so a `.` inside an
// identifier like `tokens.bgSubtle` is never a false split.
function splitSentences(text) {
let inCode = false;
let start = 0;
Expand Down Expand Up @@ -72,17 +75,27 @@ if (desc.length > 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.");
console.log("::warning::No lead paragraph found in the release body — posting generic copy. Check CHANGELOG.md.");
}

const links = [`[Full release notes →](${url})`];
if (prevTag && tag) {
links.push(`[Changelog ${prevTag}…${tag} →](https://github.com/${repo}/compare/${prevTag}...${tag})`);
}
const description = `${desc || "New release."}\n\n[Full release notes →](${url})`;
const description = `${desc || "New release."}\n\n${links.join("\n")}`;

const payload = {
username: "NimbleBrain Releases",
embeds: [{ title: `🚀 ${repoName} ${tag}`, url, description, color: 0x0055ff }],
embeds: [
{
title: `🚀 ${repoName} ${tag}`,
url,
description,
color: 0x0055ff, // NimbleBrain brand blue
footer: { text: "@nimblebrain/synapse" },
...(rel.publishedAt ? { timestamp: rel.publishedAt } : {}),
},
],
};

if (!webhook || dryRun) {
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/announce.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ jobs:
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
--json tagName,name,body,url,publishedAt > release.json
# Previous release tag for the compare link: the newest non-current
# published release. Assumes single-track sequential releases (true for
# synapse — semver order matches publish order). A backport published
# after a higher version would yield a cosmetically backwards compare
# link; switch the pipe to `sort -V` if synapse ever ships out of order.
# Empty on the first release -> the script omits the compare link.
PREV=$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts \
--json tagName --jq '.[].tagName' | grep -vxF "$TAG" | head -n1 || true)
echo "PREV_TAG=$PREV" >> "$GITHUB_ENV"

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