Back to blog
GitHub automation12 min readPublished June 14, 2026

How to post GitHub releases to Mastodon

A correct, real recipe for posting GitHub releases to Mastodon: the API call to POST /api/v1/statuses with a Bearer token, the 500-character limit, content warnings, a GitHub Actions workflow, and the S2P approval path.

What this solves

Someone wants their GitHub releases to appear on Mastodon and needs a correct recipe for the Mastodon API plus honest guidance on fediverse etiquette.

How S2P helps

Ship a working recipe that posts each GitHub release to your Mastodon instance, respect the 500-character limit and content warnings, and know when a release-native tool is the better fit.

Key takeaways

  • Mastodon posting is a single authenticated POST to /api/v1/statuses on your own instance.
  • You get an access token from your instance's Development settings, not from a central provider.
  • Statuses are capped at 500 characters by default, so trim release notes deliberately.
  • Content warnings via spoiler_text are good etiquette for long or automated posts.

Section 1

How posting to Mastodon actually works

Mastodon is federated, so there is no single API endpoint for everyone. You post to your own instance, and the network distributes it. That one fact shapes the whole recipe.

Unlike a centralized network, Mastodon has no global API host. Every instance, whether it is mastodon.social, fosstodon.org, or your own self-hosted server, runs the same software and exposes the same REST API at its own domain. So the endpoint you call is not api.mastodon.com, it is your-instance.example/api/v1/statuses. The first thing to get right is which instance your account lives on, because that domain is the base for every call.

Authentication is per-instance too. You do not register a central app and get keys that work everywhere. Instead you create an application inside your own account's settings on your instance, which issues an access token scoped to that account on that server. That token is a Bearer credential: you send it in an Authorization header and the instance treats the request as coming from you. There is no OAuth redirect dance to manage if you only need to post as yourself, which keeps the DIY path approachable.

Posting itself is a single HTTP POST. You send a status field with the text of the toot, optionally a spoiler_text to put it behind a content warning, and optionally a visibility setting. The instance creates the post and federates it out to followers and connected servers. That is the entire model: get a token from your instance, POST a status to that instance, and the fediverse handles distribution.

  • There is no central API host; you call your own instance's domain.
  • Access tokens are issued per instance, scoped to your account.
  • The token is a Bearer credential sent in an Authorization header.
  • One POST to /api/v1/statuses creates the toot and federates it.

Section 2

Step 1: get an access token from your instance

You do not need a full OAuth flow to post as yourself. Your instance can hand you a ready-to-use access token in about a minute.

Log in to your Mastodon account in a browser and open Preferences, then Development. Create a new application, give it a recognizable name like github-release-bot, and grant it the write:statuses scope, which is the permission needed to publish toots. You can leave the redirect URI at its default value, because you are not building a third-party login flow, you just want a token for your own account.

After you create the application, open it and you will see three credentials: a client key, a client secret, and an access token. For posting as yourself, the access token is the only one you need. Copy it and store it as a GitHub repository secret, for example MASTODON_ACCESS_TOKEN, and also note your instance's base URL, for example https://fosstodon.org, as a second secret like MASTODON_INSTANCE. Never commit the token to your repository; it can post as you until you revoke it.

Treat that token with the same care as any password. If it leaks, anyone can post to your account on that instance, and the fix is to revoke the application in the same Development settings, which invalidates the token immediately. Because the token is scoped to write:statuses, the blast radius of a leak is limited to posting, not reading your private data or changing account settings, but it is still a credential to protect.

  • Open Preferences, then Development, and create a new application.
  • Grant the write:statuses scope so it can publish toots.
  • Copy the access token and store it as a GitHub secret.
  • Save your instance base URL as a secret too; the endpoint depends on it.

Section 3

Step 2: the GitHub Actions recipe

With a token and your instance URL in hand, a GitHub Action on the release event is the clean way to post each release automatically.

This workflow triggers when a release is published, builds a toot from the release name and URL, and POSTs it to your instance. It deliberately keeps the body short and links back to the full release notes on GitHub rather than dumping the entire changelog, because Mastodon caps a status at 500 characters and a wall of notes reads as spam in a timeline. The recipe truncates a short summary line and always appends the release URL so anyone interested can click through for the detail.

Two details make this correct rather than just plausible. First, the request goes to the MASTODON_INSTANCE host you stored as a secret, not to a hardcoded domain, so the same workflow works whatever instance you are on. Second, the status is sent as a form field, which is the simplest content type the statuses endpoint accepts, and the Authorization header carries your Bearer token. The spoiler_text field is included and set to a short content warning, which puts the automated post behind a collapsible CW, the polite default for an automated release feed.

This is a minimal, dependable feed with no approval step, which is exactly right for your own project account where an automatic toot per release is what followers expect. It is not the right tool the moment you want a human to glance at the post before it goes public, or you want the same release to also reach LinkedIn, Bluesky, or Discord with copy that fits each. That is where a release-native tool starts to earn its place over the raw API call.

.github/workflows/mastodon-on-release.yml

name: Post release to Mastodon

on:
  release:
    types: [published]

jobs:
  toot:
    runs-on: ubuntu-latest
    steps:
      - name: Post status to Mastodon
        env:
          INSTANCE: ${{ secrets.MASTODON_INSTANCE }}
          TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
          NAME: ${{ github.event.release.name }}
          TAG: ${{ github.event.release.tag_name }}
          URL: ${{ github.event.release.html_url }}
        run: |
          # Keep well under the 500-char limit; link out for the full notes.
          STATUS="Shipped $NAME ($TAG). Full release notes: $URL"
          curl -sS -X POST "$INSTANCE/api/v1/statuses" \
            -H "Authorization: Bearer $TOKEN" \
            --data-urlencode "status=$STATUS" \
            --data-urlencode "spoiler_text=New release" \
            --data-urlencode "visibility=public"

Section 4

Step 3: test it in ten seconds with curl

Before you trust a workflow, confirm the token and endpoint work with a single curl call from your terminal.

You do not need GitHub Actions to verify the recipe. Any terminal that can make an HTTPS request can post a status, which makes this the fastest way to prove your token is valid and you have the right instance host before building anything around it. Replace the instance and token with your real values and run the command; a toot appears on your timeline immediately.

This minimal version sends just a status field. Once it works, you know the GitHub Actions recipe will work too, because it is the same POST with the values filled in from the release event. If you get a 401, the token is wrong or lacks write:statuses; if you get a 404, you are pointing at the wrong host, which usually means the instance domain is off. A 422 with a message about length means your status went over 500 characters and needs trimming.

When the basic call works, add the spoiler_text field to see the content warning behavior, and try visibility=unlisted if you want the post to reach followers and the link but stay off the public local and federated timelines. Unlisted is a considerate default for high-frequency automated posts, because it keeps your release feed from flooding the firehose that everyone on the instance sees.

Minimal Mastodon status test (curl)

curl -sS -X POST "https://your-instance.example/api/v1/statuses" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  --data-urlencode "status=Shipped v2.4.0. Approval rules now route each release to the right reviewer. https://github.com/your-org/your-repo/releases/tag/v2.4.0" \
  --data-urlencode "spoiler_text=New release" \
  --data-urlencode "visibility=unlisted"

Section 5

The 500-character limit and content warnings

Mastodon is not Twitter with a higher ceiling. The 500-character limit and the content-warning culture both shape how a release post should read.

The default character limit on a Mastodon status is 500, though some instances configure a higher value. You should write for 500 and not assume more, because your followers may be on instances that enforce the standard cap and a status that is too long is rejected with a 422 error rather than silently truncated. The practical implication is the same one good release posts follow everywhere: lead with the one thing that changed and why it matters, then link to the full notes rather than pasting them. A release toot is a headline plus a link, not a changelog dump.

Content warnings are a genuine part of fediverse culture, not a nuisance to route around. A CW is set with the spoiler_text field and collapses your post behind a short label that the reader taps to expand. For an automated release feed, putting posts behind a brief warning like New release is a courteous default, because it lets people who follow you for other things skip your shipping log without unfollowing. It signals that you respect the shared timeline rather than treating it as a broadcast channel.

None of this is enforced by code; it is etiquette, and the fediverse notices when an account ignores it. The honest framing is that Mastodon rewards posting like a participant, not a billboard. A short, linked, optionally CW-wrapped release toot at a sane cadence reads as a member of the community sharing their work. A long, frequent, unwarned firehose reads as automation that does not care, and people quietly mute or block it.

  • Write for 500 characters; do not assume an instance allows more.
  • Lead with the change and link out; never paste the whole changelog.
  • Use spoiler_text as a content warning for automated or long posts.
  • A status over the limit is rejected with a 422, not truncated.

Section 6

Fediverse etiquette: post like a member, not a billboard

The fediverse has a strong culture and a long memory for accounts that automate carelessly. Honest etiquette is part of the recipe, not an afterthought.

Mastodon communities are deliberately non-algorithmic and tend to value real participation over reach hacking. An account that only ever fires automated release toots, never replies, and never engages is tolerated at best and muted at worst. If you want your releases to actually land, the bot feed should be one part of a real presence: reply to people, boost others' work, and let the automated posts sit alongside human ones rather than being the entire account. Automation is fine; automation with no human behind it is what the culture pushes back on.

Be deliberate about hashtags and visibility. Hashtags are how discovery works on Mastodon since there is no algorithmic feed, so one or two relevant, specific tags help the right people find a release, while a pile of generic tags reads as spam. Unlisted visibility for routine automated posts keeps your release log from dominating the public local timeline that everyone on your instance shares, which is a small courtesy that goes a long way. Save public visibility for releases you genuinely want to surface to the whole network.

The bigger point is that the fediverse rewards restraint. Posting every patch release, pinging with no content warning, and never engaging is a fast way to get filtered out. Posting your meaningful releases, behind a light CW, at a human cadence, from an account that also acts like a person, is how a dev tool builds a real following there. This is the same cadence discipline that applies on every channel, just with a community that enforces it more actively than most.

  • Pair the bot feed with real engagement; do not run a reply-never account.
  • Use one or two specific hashtags for discovery, not a generic pile.
  • Default routine automated posts to unlisted to respect the shared timeline.
  • Post meaningful releases at a human cadence, not every patch.

Section 7

When the API call is not enough: the S2P path

The raw API is great for a single account with no review. The moment you want approval or more than one channel, the trade-off shifts.

The DIY recipe above is the right answer for an automatic, no-review release feed into one Mastodon account, and this guide recommends it for exactly that. Its limits show up when your needs grow past that one job. There is no approval step, so whatever the workflow builds posts instantly, which is fine for a project account and riskier when the same content represents a brand. And the call only knows about Mastodon, so if you also want the release on LinkedIn, Bluesky, or Discord with copy that fits each, you are back to maintaining a separate path per channel.

This is where a release-native tool complements rather than replaces the API idea. S2P detects your GitHub release once and can draft channel-native posts for Mastodon and your other destinations together, respecting the 500-character limit and offering a content warning on the fediverse post, then route them through approval before anything publishes. You get the same from-the-release behavior, with a human able to glance at the toot first and one release fanning out to every channel in the right voice, instead of an API call per platform and no review anywhere.

The honest split mirrors the rest of our how-to guides. If you want a dependable release feed on one Mastodon account and nothing more, the recipe above is genuinely the best tool and you should use it. If you want approval, multiple channels from one release, and a record of what went where, that is the job a release-native tool is built for. Many teams do both: a raw API workflow for their own account, and S2P for the public, multichannel announcements that need review.

  • The raw API is ideal for one account with no review needed.
  • S2P adds an approval step before a brand toot goes live.
  • One detected release fans out to Mastodon plus other channels in fitting copy.
  • Common pattern: raw workflow for your own account, S2P for public multichannel posts.

FAQ

Questions this article answers

How do I post a GitHub release to Mastodon?

Create an application in your Mastodon instance's Preferences, then Development, with the write:statuses scope to get an access token. Store that token and your instance URL as GitHub secrets, then POST to your-instance/api/v1/statuses with an Authorization Bearer header and a status field whenever a release publishes, either from a GitHub Action on the release event or from any script using curl.

What is the Mastodon API endpoint for posting?

There is no central endpoint because Mastodon is federated. You POST to /api/v1/statuses on your own instance's domain, for example https://fosstodon.org/api/v1/statuses. The status text goes in a status field, and you authenticate with a Bearer access token issued by that instance.

What is the character limit for a Mastodon post?

The default limit is 500 characters, though some instances configure more. Write for 500 to be safe, because followers may be on instances that enforce the standard cap. A status that exceeds the limit is rejected with a 422 error rather than truncated, so lead with the key change and link out to the full release notes instead of pasting them.

Should I use a content warning on automated release posts?

It is good etiquette. Set the spoiler_text field to a short label like New release to put the post behind a collapsible content warning. This lets people who follow you for other things skip your shipping log without unfollowing, and it signals respect for the shared timeline, which the fediverse community values.

Is automating Mastodon posts against the rules?

Automation itself is allowed, but the fediverse culture rewards participation over broadcasting. A pure bot account that never engages tends to get muted. Pair the automated feed with real replies and boosts, use unlisted visibility for routine posts, add a content warning, and post at a human cadence rather than firing on every patch release.

What does S2P add over the raw Mastodon API?

The raw API posts to one account with no review. S2P detects the GitHub release once, drafts a Mastodon toot that respects the 500-character limit and can carry a content warning, drafts channel-native posts for your other destinations, and routes them for approval before publishing. You get the same from-the-release behavior plus a human review step and one release fanning out to multiple channels.

Related guides and pages

Where to go next

Hand-picked pages that go deeper on the workflow, channels, and tooling covered above.

Ship 2 Post

Stop writing release posts.

Your engineers already commit. Now those commits become content - in your voice, on every channel.