roots/discourse-topic-github-release-action: The GitHub Action That Makes Release Notes Feel Human

It does more than post changelogs to Discourse. It pulls org-wide username mappings from `.github`, assigns the right release author, and turns automation into a community-facing voice.

6 to 8 min read • View on GitHub • More from roots

A release note travels from a GitHub-style machine press on the left into a Discourse-style town bulletin on the right. In the middle, a clerk opens a ledger and stamps a human name onto the post before it is published. The scene explains that the action is really about identity resolution, not just message delivery.
The action sits between two systems and decides who the release should be from before it posts anything.
Key Takeaways

Release Automation With a Face

Most release automation is invisible plumbing. This repo makes the plumbing matter because it decides whether a Discourse topic looks like it came from a bot or from the person who actually cut the release. That subtle shift changes the tone of the announcement from machine output to community message.

Post published GitHub releases from a repository to a Discourse instance.

roots/discourse-topic-github-release-action README, Project Documentation · roots/discourse-topic-github-release-action

That one sentence is the surface description. The real trick is farther down, where the action uses release metadata, looks up a username mapping, and posts under a human identity when it can. In open source communities, that is not a cosmetic detail. It is part of the product.

The Org-Level Phonebook Hidden in `.github`

The interesting architecture is not the post request itself. It is where identity gets resolved before the request goes out.

A close-up of a shared filing cabinet labeled .github. Inside are index cards for many repositories, but a central ledger labeled discourse.yml contains name mappings between GitHub handles and Discourse usernames. A thin pipeline leads from the cabinet into a Discourse post editor where the author field changes from system to a named person. The image explains how one org-level file can govern identity across many repos.
The shared mapping lives once at the organization level and applies everywhere the action runs.

This is the project’s sharpest architectural idea. Most GitHub Actions keep configuration local to the repository. This one reaches into the organization’s shared `.github` repo and treats identity as a central resource instead of a per-repo one.

ApproachSetup effortIdentity handlingWhere config livesNeeds Discourse admin access?Reusable across repos
Generic script with `curl` or `actions/github-script`Low at first, higher over timeUsually manual or ad hocEach repoUsually noLimited
`roots/discourse-topic-github-release-action`LowShared mapping plus fallbackOrganization `.github`NoHigh
Discourse plugin or server-side integrationHighDeep platform-level controlDiscourse serverYesHigh, but heavier

The comparison is not really about features. It is about trust boundaries. Scripts put the burden in each repository. Plugins move it onto the Discourse server. This action puts the important part, the identity map, into the GitHub organization where release maintainers already work.

How the Action Decides Who to Be

const release = github.context.payload.release;
const repo = github.context.repo.repo;
const releaseAuthor = release.author.login;

let discourseUsername = core.getInput('discourse-author-username');

if (!discourseUsername) {
  const usernameMapping = await fetchUsernameMapping();
  discourseUsername = usernameMapping[releaseAuthor] || 'system';
}

const body = `\n${release.body}\n\nSee the release on GitHub: ${release.html_url}`;

await client.postJson('/posts.json', {
  title: `${repo} ${release.tag_name} released`,
  raw: body,
  category: parseInt(core.getInput('discourse-category'), 10),
  tags: core.getMultilineInput('discourse-tags')
}, {
  headers: { 'Api-Username': discourseUsername }
});

The sequence is straightforward, which is part of the appeal. First comes a manual override. If that is absent, the action tries the shared mapping file. If that fails too, it falls back to `system` so the release pipeline does not break over missing identity data.

The action automatically maps GitHub usernames to Discourse usernames by reading a discourse.yml file from your organization's .github repository.

roots/discourse-topic-github-release-action README, Project Documentation · roots/discourse-topic-github-release-action - Username Mapping

That fallback order is the story. The action is not trying to be clever for its own sake. It is trying to keep release communication flowing while still preserving a person-shaped author field whenever the mapping exists.

Why This Is Better Than a Shell Script

A shell script can absolutely post to Discourse. So can `actions/github-script`. But those approaches make every repository re-encode the same choices: how to build the body, which category to use, which tags to apply, and how to decide who the post should come from. This action packages those decisions once.

It also gives downstream steps a usable `topic-url` output, which matters if release publishing is part of a larger workflow. The post does not just happen. It becomes a linkable artifact that other automation can reuse.

TraitShell scriptGeneric GitHub ActionThis action
Release body handlingHand-builtHand-builtBuilt in
Identity mappingCustom workCustom workShared org phonebook
Output URLUsually manualUsually manualProvided as `topic-url`
Operational scopePer repoPer repoPer organization
Community toneEasy to flatten into bot outputEasy to flatten into bot outputDesigned to preserve authorship

A Discourse plugin could go deeper, but that depth comes with server access, maintenance, and ownership on the forum side. This repo makes a different trade. It is lighter, easier to adopt, and good enough for the workflow most open source orgs actually need.

What the Maintainers Optimized For

The implementation choices point to discipline. It uses `@actions/http-client` rather than a heavier API wrapper, bundles with `@vercel/ncc`, and targets Node 24 in the action manifest. That keeps the runtime small and predictable.

The code also favors defensive defaults. Missing mappings do not explode the workflow. The action logs clearly, falls back cleanly, and keeps the release moving. That is what a production utility should do when its job is to support communication, not become the headline.

runs:
  using: node24
  main: dist/index.js

outputs:
  topic-url:
    description: URL of the created Discourse topic

The minimalist shape is not a lack of ambition. It is the point. This is a small tool with a precise job, and it avoids turning that job into an ecosystem.

The Real Product Is Community Continuity

Seen one way, this repo posts release notes to a forum. Seen the right way, it keeps the release voice continuous across GitHub and Discourse without reducing people to a generic bot account. That is a small technical choice with a big social effect.

For community-driven orgs, that matters. Releases are not just changelogs. They are public moments, and this action helps those moments keep a human face.