Customer changelog: GitHub → Notion → Slack
GitHub's release notes rewritten for customers, published to the web, announced in Slack.
GitHub's "Generate release notes" button already produces a technical changelog the moment you tag a release — merged PRs, contributors, a diff link. Customers want two or three sentences about what changed, not that. This workflow takes GitHub's notes as raw input, has AI rewrite them into customer language, and files a Draft entry in Notion. A human flips it to Published before it's customer-visible — a hallucination in front of customers is worse than a PR list nobody reads. Slack announces it with links to the release and changelog page.
How it flows
- 01
Release published
Webhook fires with action published; release.body already holds GitHub's generated PR list if the generation step ran.
- 02
tag_name, body and html_url pulled from the payload
The technical raw material for the rewrite — no second API call needed when the release already used native generation.
- 03
AI rewrites the technical body into customer language
Two or three sentences about what customers will notice — no PR numbers, no internal component names. This step drafts; it does not decide what customers see.
- 04
Entry lands in Notion as Draft
New database row, Status: Draft, Source release linking back to the GitHub release.
- 05
A human flips Status to Published
Nothing appears on the public page until someone does this by hand — an AI hallucination in front of every customer is a worse failure than a technical PR list nobody reads.
- 06
Slack gets the announcement
One message with the GitHub release link, the live Notion changelog link and the AI's summary line — self-built here, or Notion's own "Page added → Send Slack notification" automation on Plus and above.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
GitHub
Publishes releases, and already writes the technical changelog
- 01
Turn on native release-note generation
Every release needs GitHub's own changelog before this workflow has anything to rewrite. Repo → Releases → Draft a new release → click Generate release notes, just above the Description box — it fills in merged PRs, contributors and a full diff link. If releases are cut by CI instead of by hand, set generate_release_notes: true on the create-release call and GitHub does the same thing server-side, or call the generate-notes endpoint directly if you want the text before the release exists. Either path needs a fine-grained PAT with Contents: Write; reading an existing release back only needs Contents: Read.
POST /repos/{owner}/{repo}/releases/generate-notescurl -L -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "X-GitHub-Api-Version: 2026-03-10" \ https://api.github.com/repos/OWNER/REPO/releases/generate-notes \ -d '{ "tag_name": "v1.4.0", "target_commitish": "main", "previous_tag_name": "v1.3.0" }' # response: { "name": "...", "body": "<markdown: PR list + contributors + Full Changelog link>" } - 02
(Optional) Sort the PR list into categories first
Commit .github/release.yml with a changelog.categories array mapping PR labels to sections, and a "*" wildcard as the catch-all for anything unlabeled. Skipping this step does not break the workflow — the AI rewrite just works from one flat PR list instead of pre-sorted sections, and the customer narrative loses a bit of structure.
- 03
Subscribe the webhook to Releases
Repo → Settings → Webhooks → Add webhook. Content type application/json, set a secret, then "Let me select individual events" and tick Releases (not "Send me everything"). The event GitHub actually fires is named release with action published — if your account's checkbox wording differs slightly, that event name is what to look for. Adding the webhook itself needs your account to have admin access on the repo; it is not an API token or OAuth scope.
- 04
Read release.body straight off the webhook
The payload arrives with the generated notes already inside release.body — if the previous generation step ran (button or API flag), there is no need to call generate-notes a second time, just read the field. Pull tag_name, name, body and html_url; body is the Markdown the AI rewrite step consumes.
release webhook payload (action=published, trimmed){ "action": "published", "release": { "tag_name": "v1.4.0", "name": "Version 1.4.0", "body": "## What's Changed\n* Faster search index by @dana-w in #482\n* Fix CSV export dropping last row by @sam-k in #486\n\n**Full Changelog**: https://github.com/acme/web/compare/v1.3.0...v1.4.0", "draft": false, "prerelease": false, "published_at": "2026-08-08T12:00:00Z", "author": { "login": "dana-w" }, "html_url": "https://github.com/acme/web/releases/tag/v1.4.0" }, "repository": { "full_name": "acme/web" } } - 05
Fallback: poll the Atom feed with no webhook receiver
If your automation platform only has an RSS/polling trigger rather than a public webhook endpoint, GitHub's releases Atom feed is public, needs no auth token, and its rendered content field already holds the same generated notes — poll it on a schedule instead of standing up a receiver.
Fallback poll (zero auth)GET https://github.com/OWNER/REPO/releases.atom # <content type="html"> is the release body, already rendered — feed it to # the AI rewrite step the same way you would release.body from the webhook.
Notion
Hosts the customer-visible changelog page
- 01
Create the "Customer Changelog" database
Properties: Title (Title), Version (Text), Release date (Date), Summary (Text — the AI-rewritten customer narrative), Source release (URL, back to the GitHub release), Status (Select: Draft / Published). Status is the human review gate: nothing with Status: Draft belongs in the view published next.
- 02
Connect the internal integration
app.notion.com/developers/connections → Build → Internal connections → Create a new connection, keep both Insert content and Read content on. Then open the database → ••• (top right) → Connections → Add connection → your connection. Skip that last click and page creation 404s on a database you can plainly see.
- 03
Know the page-create payload
POST to /v1/pages with Notion-Version 2026-03-11. Retrieve the database once for its data source id and parent on that rather than on the database_id — the same data-source migration note as every other Notion step in this catalogue. New entries default to Status: Draft; nothing flips them to Published but a human.
POST https://api.notion.com/v1/pages · Notion-Version: 2026-03-11{ "parent": { "type": "data_source_id", "data_source_id": "3af9c2e1d0b74a6f9c8e1d2c3b4a5f6e" }, "properties": { "Title": { "title": [{ "text": { "content": "v1.4.0 — Faster search, export bug fixed" } }] }, "Version": { "rich_text": [{ "text": { "content": "v1.4.0" } }] }, "Release date": { "date": { "start": "2026-08-08" } }, "Summary": { "rich_text": [{ "text": { "content": "Search results now load about twice as fast, and CSV export no longer drops the last row for large organizations." } }] }, "Source release": { "url": "https://github.com/acme/web/releases/tag/v1.4.0" }, "Status": { "select": { "name": "Draft" } } } } - 04
Publish the Published-only view to the web
Build a database view filtered to Status = Published, then Share → Publish → Publish. That gives a public notion.site URL that only ever shows reviewed entries. The free plan covers this; a custom domain, search-engine discoverability and analytics on top are paid-plan extras, not required for the page to be public.
- 05
Poll for newly-Published entries (Option A only)
If you are self-building the Slack step rather than using the native automation from the previous app's Slack section, add an hourly (or webhook-driven, via a Property-edited automation that just calls out to your endpoint) sweep: POST /v1/data_sources/{data_source_id}/query filtered to Status = Published and a "Slack notified" checkbox still unset. For each match, fire the chat.postMessage call, then PATCH the row to flip "Slack notified" so the same entry never re-announces on the next poll.
Slack
Announces the published entry — self-built so it stays on the free plan and carries both links
- 01
Create the bot, and know the native alternative
api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes: chat:write, plus chat:write.public if you don't want to manually invite the bot to every channel → Install to Workspace. Notion's own database automations already offer a Property edited trigger — condition it on the Status property changing to Published — paired with a 'Send Slack notification to' action; that combination covers the moment this step needs (a Page added trigger would fire the instant the Draft row is created, before any human review, which is the exact moment this workflow must stay silent) — but that action is gated to Plus and above, and it sends Notion's fixed template rather than a message you control. Building the call yourself keeps this off Notion's paid tier and lets the message carry the GitHub link, the live changelog link and the AI's summary line together. On Notion Plus or above, you can skip this app entirely and wire that native automation (Property edited → Status becomes Published) to this database instead.
- 02
Create #product-changelog
One channel, not one per repo. Invite the bot with /invite if you skipped chat:write.public.
- 03
Know the announcement payload
chat.postMessage with blocks carrying both links plus the AI's summary sentence, so nobody has to open two tabs to sanity-check what went out.
POST https://slack.com/api/chat.postMessage{ "channel": "C0PRODUCTCL", "text": "v1.4.0 is live", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*v1.4.0 is live* — faster search, export bug fixed.\n<https://acme-changelog.notion.site/v1-4-0|Read the changelog> · <https://github.com/acme/web/releases/tag/v1.4.0|Full release on GitHub>" } } ] }