Releases Index
GitHub Actions
View .md

Publish from GitHub Actions

If your release notes live in git (CHANGELOG.md, Keep a Changelog, a dated product log), a GitHub Action can create or update the matching releases in Releases Index the moment the file lands on your default branch. You do not wait for scrape polling.

Re-running the same commit is safe. Each section is keyed by a stable URL, and the batch upsert only writes when the body actually changed.

1. Create a write token

The Action calls POST /v1/sources/…/releases/batch, which needs a write-scoped machine token (relk_…).

Read-only user keys (relu_…, including releases login) are rejected. Store the token as a repository secret named RELEASES_API_TOKEN.

2. Point it at a source

You need the source the Action should write to — preferably the typed id (src_…) from the source page or releases admin source list. A slug works if you also pass the organization.

3. Add the workflow

Copy the committed example at actions/publish-changelog/examples/publish-changelog.yml into your repo's .github/workflows/. Replace the source id and RELEASES_API_TOKEN secret. Do not copy this repo's dogfood .github/workflows/changelog-publish.yml — that file pins our source and updates URL.

Checkout with full history so the Action can diff against the previous commit, then call the Action:

name: Publish changelog

on:
  push:
    branches: [main]
    paths:
      - CHANGELOG.md
      # or: docs/**, .changeset/**

permissions:
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
          persist-credentials: false

      - uses: buildinternet/releases/actions/publish-changelog@main
        with:
          source: src_…
          api-token: ${{ secrets.RELEASES_API_TOKEN }}

fetch-depth: 0 is required. A shallow clone cannot git show the file at github.event.before.

Inputs

InputRequiredNotes
sourceyessrc_… id, or a source slug (then set org)
orgnoOrganization slug for the org-scoped batch route
api-tokenyesWrite-scoped relk_…
api-urlnoDefaults to https://api.releases.sh
changelog-pathnoDefaults to CHANGELOG.md
working-directorynoSubdirectory in a monorepo
url-templatenoSee Stable URLs
generate-contentnoDefaults to true. Admin tokens regenerate summaries for edited sections; write-only tokens skip this (batch already queues the fill for new rows)

Path filters stay on your workflow on.push.paths — the Action does not decide which pushes run.

Changelog formats

The Action reads ## headings:

  • Versioned — Keep a Changelog / conventional-changelog, e.g. ## [1.4.0] - 2026-05-01. [Unreleased] is skipped.
  • Date-sectioned## June 10, 2026, newest first. This is the format releases.sh publishes.

Other heading styles are ignored. If the file changed but nothing parsed, the Action fails so a silent no-op cannot hide a format mistake.

Stable URLs

Idempotency is (source, url). The Action prefers a permalink already in the heading (common on conventional-changelog compare links). Otherwise it fills url-template.

Placeholders: {key} (version or YYYY-MM-DD), {version}, {date}, {path}.

Default when you omit the template:

https://github.com/<owner>/<repo>/blob/<branch>/<path>#{key}

To keep human URLs on a docs site:

url-template: https://example.com/changelog/{date}

Do not put the commit SHA in the URL. A later edit to the same version would insert a second release instead of updating the first.

What happens on the server

The Action does not invent a second write path. It posts the same batch body the rest of ingest uses:

POST /v1/sources/{source}/releases/batch
{ "mode": "upsert-content", "releases": [ { "title", "content", "url", "publishedAt", "type" } ] }

That upsert triggers the usual side effects: content generation for new rows, embeddings, live events, and web revalidation. An identical re-run returns inserted: 0.

Secrets

Treat RELEASES_API_TOKEN like a password. Rotate it if it leaks. The Action never prints the token.