diff --git a/.github/workflows/notify-console-changelog.yml b/.github/workflows/notify-console-changelog.yml new file mode 100644 index 0000000..6be9e65 --- /dev/null +++ b/.github/workflows/notify-console-changelog.yml @@ -0,0 +1,66 @@ +name: Notify Console Changelog + +# When curated changelog content lands on main, push the changelog snapshot to +# the BitRouter console so its "What's new" panel updates right away (rather +# than waiting for the console to poll). The payload is HMAC-signed with +# CHANGELOG_WEBHOOK_SECRET — the same secret the console verifies with. +# +# Setup: +# • Actions secret CHANGELOG_WEBHOOK_SECRET — must equal the console's env +# value (generate once with `openssl rand -hex 32`, set in both). +# • Actions variable CONSOLE_URL (optional) — console origin; defaults to +# https://cloud.bitrouter.ai. +on: + push: + branches: [main] + paths: + - "content/changelog/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: notify-console-changelog + cancel-in-progress: true + +jobs: + notify: + name: Push changelog snapshot to the console + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + # Only js-yaml is needed to read the frontmatter; skip postinstall + # (fumadocs codegen) so this stays fast and can't fail on build concerns. + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Build changelog payload + run: node scripts/emit-changelog-json.mjs > changelog.json + + - name: Push signed snapshot to the console + env: + CHANGELOG_WEBHOOK_SECRET: ${{ secrets.CHANGELOG_WEBHOOK_SECRET }} + CONSOLE_URL: ${{ vars.CONSOLE_URL || 'https://cloud.bitrouter.ai' }} + run: | + if [ -z "$CHANGELOG_WEBHOOK_SECRET" ]; then + echo "::error::CHANGELOG_WEBHOOK_SECRET is not set in Actions secrets" + exit 1 + fi + # HMAC-SHA256 over the exact bytes we POST (openssl prints + # "HMAC-SHA256(changelog.json)= " — take the last field). + sig="sha256=$(openssl dgst -sha256 -hmac "$CHANGELOG_WEBHOOK_SECRET" changelog.json | awk '{print $NF}')" + curl -fsS -X POST "$CONSOLE_URL/api/webhooks/changelog" \ + -H "Content-Type: application/json" \ + -H "X-Changelog-Signature: $sig" \ + --data-binary @changelog.json + echo "Pushed changelog snapshot to $CONSOLE_URL" diff --git a/scripts/emit-changelog-json.mjs b/scripts/emit-changelog-json.mjs new file mode 100644 index 0000000..1106c65 --- /dev/null +++ b/scripts/emit-changelog-json.mjs @@ -0,0 +1,62 @@ +/** + * Emit the curated changelog as JSON — `{ items: ChangelogItem[] }` — for the + * BitRouter console's "What's New" webhook (POST /api/webhooks/changelog). + * + * Mirrors the fumadocs changelog source (`getChangelogItems` in lib/source.ts): + * one MDX file per entry under content/changelog, `url = /changelog/`, + * with the frontmatter fields declared in source.config.ts (title, description, + * date, version, tags, breaking). Kept dependency-light (js-yaml only, like + * generate-changelog-latest.mjs) so the notify workflow needs no build step. + * + * Writes JSON to stdout; diagnostics to stderr. + */ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import yaml from "js-yaml"; + +const DIR = "content/changelog"; +const BASE_URL = "/changelog"; + +function readFrontmatter(src) { + const match = src.match(/^---\n([\s\S]*?)\n---/); + return match ? (yaml.load(match[1]) ?? {}) : {}; +} + +let files = []; +try { + files = (await readdir(DIR)).filter( + // Default-locale entries only: skip `name..mdx` variants so the + // payload matches getChangelogItems("en"). + (f) => f.endsWith(".mdx") && !/\.[a-z]{2}\.mdx$/.test(f), + ); +} catch { + // No changelog dir yet — emit an empty snapshot. +} + +const items = []; +for (const file of files) { + const slug = file.replace(/\.mdx$/, ""); + const fm = readFrontmatter(await readFile(join(DIR, file), "utf8")); + if (typeof fm.title !== "string" || typeof fm.date !== "string") { + process.stderr.write(`skip ${file}: missing title/date\n`); + continue; + } + items.push({ + slug, + url: `${BASE_URL}/${slug}`, + title: fm.title, + description: typeof fm.description === "string" ? fm.description : undefined, + date: fm.date, + version: typeof fm.version === "string" ? fm.version : undefined, + tags: Array.isArray(fm.tags) + ? fm.tags.filter((t) => typeof t === "string") + : [], + breaking: fm.breaking === true, + }); +} + +// Newest first (ISO YYYY-MM-DD sorts lexicographically). +items.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); + +process.stderr.write(`emit-changelog-json: ${items.length} item(s)\n`); +process.stdout.write(JSON.stringify({ items }));