Skip to content
Closed
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
83 changes: 83 additions & 0 deletions .github/workflows/test-discord-product-news.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Test - Discord Product News

on:
pull_request:
paths:
- .github/workflows/test-discord-product-news.yml
- scripts/discord/**
- fern/changelog/**
workflow_dispatch:
inputs:
mode:
description: Preview the payload or send it to the test Discord server
required: true
type: choice
default: preview
options:
- preview
- send-test
changelog_file:
description: Optional fern/changelog/YYYY-MM-DD.mdx path; blank uses latest
required: false
type: string

permissions:
contents: read

jobs:
preview:
if: github.event_name == 'pull_request' || inputs.mode == 'preview'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Run tests
run: node --test scripts/discord/product-news.test.mjs

- name: Preview Discord payload
env:
CHANGELOG_FILE: ${{ inputs.changelog_file }}
run: |
args=(--prefix "[TEST]" --dry-run)
if [[ -n "$CHANGELOG_FILE" ]]; then
args+=(--file "$CHANGELOG_FILE")
fi
node scripts/discord/post-whats-new.mjs "${args[@]}"

send-test:
if: github.event_name == 'workflow_dispatch' && inputs.mode == 'send-test'
runs-on: ubuntu-latest
timeout-minutes: 5
environment: discord-product-news-test
concurrency:
group: discord-product-news-test
cancel-in-progress: false
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Run tests
run: node --test scripts/discord/product-news.test.mjs

- name: Post to test Discord server
env:
CHANGELOG_FILE: ${{ inputs.changelog_file }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
args=(--prefix "[TEST]")
if [[ -n "$CHANGELOG_FILE" ]]; then
args+=(--file "$CHANGELOG_FILE")
fi
node scripts/discord/post-whats-new.mjs "${args[@]}"
38 changes: 38 additions & 0 deletions scripts/discord/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Discord product-news test

This integration posts a dated `fern/changelog/YYYY-MM-DD.mdx` file to a
Discord channel through an incoming webhook. It is intentionally test-only:
there is no schedule, merge trigger, or production Discord destination.

## Local preview

```bash
node scripts/discord/post-whats-new.mjs \
--file fern/changelog/2026-09-14.mdx \
--prefix "[TEST]" \
--dry-run
```

If `--file` is omitted, the latest dated changelog file is selected.

Run the tests with:

```bash
node --test scripts/discord/product-news.test.mjs
```

## GitHub test environment

1. Create a Discord incoming webhook in the personal test server.
2. In the GitHub repository, create an environment named
`discord-product-news-test`.
3. Add the webhook URL to that environment as a secret named
`DISCORD_WEBHOOK_URL`.
4. Merge the test workflow into the default branch. GitHub only enables
manual `workflow_dispatch` runs for workflow files on the default branch.
5. Open **Actions → Test - Discord Product News**.
6. Run `preview` first, inspect the logs, then run `send-test`.

The script asks Discord to confirm each saved message, retries temporary API
and rate-limit failures, disables all mentions, and never prints the webhook
URL. Production scheduling and correction behavior are deliberately deferred.
76 changes: 76 additions & 0 deletions scripts/discord/post-whats-new.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node

import { buildProductNews, postProductNews } from "./product-news.mjs";

function parseArguments(arguments_) {
const options = {
dryRun: false,
prefix: "",
requestedPath: "",
username: "Product News Bot",
};

for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index];

if (argument === "--dry-run") {
options.dryRun = true;
continue;
}

if (["--file", "--prefix", "--username"].includes(argument)) {
const value = arguments_[index + 1];
if (!value) {
throw new Error(`${argument} requires a value`);
}
index += 1;

if (argument === "--file") options.requestedPath = value;
if (argument === "--prefix") options.prefix = value;
if (argument === "--username") options.username = value;
continue;
}

throw new Error(`Unknown argument: ${argument}`);
}

return options;
}

async function main() {
const options = parseArguments(process.argv.slice(2));
const productNews = await buildProductNews({
prefix: options.prefix,
requestedPath: options.requestedPath,
});

console.log(`Source: ${productNews.changelogPath}`);
console.log(`Docs: ${productNews.canonicalUrl}`);
console.log(`Discord messages: ${productNews.messages.length}`);

if (options.dryRun) {
productNews.messages.forEach((message, index) => {
console.log(`\n--- Message ${index + 1}/${productNews.messages.length} ---\n`);
console.log(message);
});
return;
}

const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error("DISCORD_WEBHOOK_URL is required unless --dry-run is used");
}

const postedMessages = await postProductNews({
webhookUrl,
messages: productNews.messages,
username: options.username,
});

console.log(`Posted ${postedMessages.length} Discord message(s) successfully.`);
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
Loading
Loading