diff --git a/.github/scripts/announce-discord.mjs b/.github/scripts/announce-discord.mjs index 06d933c..77b150d 100644 --- a/.github/scripts/announce-discord.mjs +++ b/.github/scripts/announce-discord.mjs @@ -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")); @@ -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; @@ -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) { diff --git a/.github/workflows/announce.yml b/.github/workflows/announce.yml index f7218bb..8098b30 100644 --- a/.github/workflows/announce.yml +++ b/.github/workflows/announce.yml @@ -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