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
| Input | Required | Notes |
|---|---|---|
source | yes | src_… id, or a source slug (then set org) |
org | no | Organization slug for the org-scoped batch route |
api-token | yes | Write-scoped relk_… |
api-url | no | Defaults to https://api.releases.sh |
changelog-path | no | Defaults to CHANGELOG.md |
working-directory | no | Subdirectory in a monorepo |
url-template | no | See Stable URLs |
generate-content | no | Defaults 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.