Automate your changelog with GitHub Actions (and any other CI)
Most abandoned changelogs didn't die because someone decided to stop. They died because posting was a separate chore that lived outside the release pipeline β and anything outside the pipeline gets skipped the first busy week, then forever. The fix is wiring, not willpower: make the changelog step part of the same automation that ships the code. This guide shows how to do that with GitHub Actions (the pattern ports to any CI), and β just as important β where to draw the line so the automation doesn't publish garbage on your behalf.
Automate the plumbing, not the judgment
There are two jobs hiding in "automate the changelog":
- Delivery β noticing that a release happened, assembling the raw material, getting an entry onto the page. Machines are perfect at this.
- Writing β deciding what matters to readers and saying it in
their words. Machines are terrible at this: a pipeline that publishes
fix: null check in parserverbatim ships you a commit dump, not a changelog.
The pattern that keeps both jobs honest: CI creates a draft, a human publishes it. The machine never forgets; the human never transcribes. Everything below builds toward that split β fully automatic publishing has its place (internal streams, mechanical per-package logs), but for a customer-facing changelog, curation is the feature.
Pick the trigger: announce when it's live
The single most consequential choice is which event runs the changelog step. The rule from announcing features applies to pipelines too: the entry should appear when users can actually touch the change.
- Tag push (
on: push: tags). Right for libraries, CLIs, and anything where the tag is the release β the momentv2.4.0exists, users can install it. The workflow below uses this. - Release published (
on: release: types: [published]). Right if your team already writes GitHub Releases: the human curation step already happened in the release body, so mirroring it outward is safe. Note this fires on publish, not on draft β which is exactly the draft-then-publish split, using GitHub's UI as the editor. - End of the deploy job. Right for SaaS: a tag means nothing to your users until the deploy finishes. Put the changelog step after the production deploy succeeds β same workflow, last step β so you never announce something that rolled back. If you use staged rollouts or feature flags, post a scheduled or draft entry and flip it when the rollout completes.
What's almost never right: per-merge to main. Merges are your team's heartbeat, not your users' β a changelog that posts on every merge is a firehose nobody subscribes to twice (ship rhythm β merge rhythm). Batch merges into releases, then announce the release.
The minimal workflow
One job, one curl. On every version tag, this drafts an entry whose body is the commit list since the previous tag β raw material waiting for an editor, not a public post:
# .github/workflows/changelog.yml
name: changelog
on:
push:
tags: ['v*']
jobs:
draft-entry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history, so the previous tag is reachable
- name: Draft changelog entry
env:
WAKELOG_TOKEN: ${{ secrets.WAKELOG_TOKEN }}
run: |
PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
BODY=$(git log --no-merges --pretty='- %s' ${PREV:+$PREV..}HEAD)
curl -fsS "https://YOUR-CHANGELOG-HOST/api/v1/my-app/posts" \
-H "Authorization: Bearer $WAKELOG_TOKEN" \
--data-urlencode "title=$GITHUB_REF_NAME" \
--data-urlencode "body=$BODY" \
--data-urlencode "draft=true"
Three details that save debugging time: fetch-depth: 0, because the default
shallow checkout can't see the previous tag and your "commits since last release" silently
becomes "the last commit"; --no-merges, because merge commits are plumbing;
and draft=true, because this body is a reminder of what shipped, not
prose anyone should read yet. The same step works in any CI β GitLab's version is a
rules: - if: $CI_COMMIT_TAG job with $CI_COMMIT_TAG as the
title; Jenkins, Buildkite, and a bare deploy.sh are the same curl.
Draft in CI, publish by hand
The two-step loop in practice: the tag lands, CI files a draft titled
v2.4.0 with the commit list. Sometime in the next hour, a human opens it,
deletes the chore: lines, renames it after
the change instead of the version, rewrites
three bullets in reader language β and publishes. Total human cost: five minutes,
with zero chance of forgetting, because the draft is already sitting there.
Two refinements once the loop works:
- Write the announcement before the release. For a launch you care about, draft the entry days early β by hand, in good prose β and let the pipeline (or a schedule) publish it. The CI draft then serves as a checklist to confirm nothing else snuck into the release.
- Schedule for rollout lag. If a release takes hours to reach users (app-store review, staged rollout), publish at the time users get it, not when CI finishes β a scheduled entry posted by the pipeline with a publish time attached does this without a human awake at rollout-complete o'clock (store lag is the classic case).
Make re-runs safe
CI steps run more than once: flaky runners get retried, workflows get re-run from the UI, a force-pushed tag fires twice. If the changelog step isn't idempotent, every hiccup double-posts. Three defenses, use any one:
- Dedupe at the receiving end. The best tools skip an incoming entry whose title already exists β then a re-run is a no-op by construction. (Wakelog's release-webhook and import paths work this way; a re-delivered webhook or re-run import can't double-post.)
- Check before posting. One GET for the latest entries, grep for the tag name, skip if present. Ten lines, works against any API.
- Draft-by-default as a backstop. If duplicates land as drafts, the human in the loop deletes one in passing β annoying, never public. This is the lazy option and it's genuinely fine at small scale.
Keep the deploy green
Decide explicitly: should a changelog failure fail the release? Almost always
no β you don't want a 500 from a notification endpoint rolling back a good deploy. Put
the changelog step in its own job (or mark it continue-on-error: true) so the
release pipeline stays green on its own merits. But pair that with visibility: a step that
fails silently for six weeks is how automated changelogs quietly die. A red job in the
Actions list you actually look at is enough; a CI-failure notification to the team chat is
better.
Secrets hygiene
- Store the token as a CI secret (repository or organization secret in GitHub, masked variable in GitLab) β never in the workflow file, never in the repo. Anyone who can read the repo can read the workflow.
- One token per pipeline. If the token only posts to the changelog, leaking it costs you spam cleanup, not your infrastructure. Don't reuse a deploy credential.
- Rotate on any doubt. A token that appeared in a log line is burned;
rotate it. (Curl with
-fsS, as above, won't echo headers into logs.) - Fork PRs don't get secrets β GitHub withholds secrets from
workflows triggered by pull requests from forks. Tag and release triggers come from
maintainers, so the patterns in this guide are naturally safe; just don't move the
posting step into a
pull_requestjob.
Already using GitHub Releases? Skip the YAML
If your release ritual already ends in a published GitHub Release, you may not need a
workflow at all: point a release webhook at your changelog tool and every
published release becomes an entry automatically β curation happens where you already
do it, in the release body
(how Releases and changelogs fit together).
GitHub's auto-generated release notes ("Generate release notes" button, or
gh release create --generate-notes) make a decent starting draft β treat
that text exactly like the commit list above: raw material to edit, not copy to publish.
Anti-patterns
- Publishing the commit dump. Automation that posts
git logoutput live is worse than no automation β it fills the page with noise that buries the entries that matter. Draft, then edit. - Post-per-merge. Twelve entries on a Tuesday teaches subscribers to unsubscribe. Announce releases, not merges.
- The commit-back loop. Workflows that edit
CHANGELOG.mdand push the commit back can re-trigger themselves (and fight human edits). If you must commit back, gate the workflow against its own commits and use[skip ci]β or keep the changelog out of the repo entirely and post to it instead. - Announcing before it's live. A tag-triggered post for a SaaS that deploys hourly later, or a store build still in review β readers click, find nothing changed, trust the page a little less. Match the trigger to user-visible reality.
- Hardcoded tokens. Covered above; listed again because it's the one that ends up in a security postmortem.
Where Wakelog fits
Wakelog is built for exactly this pipeline shape. Posting is one
POST /api/v1/<project>/posts with a bearer token β no SDK, no OAuth
dance β with draft=true for the draft-in-CI pattern and
publish_at for rollout-timed publishing. The wakelog CLI wraps
the same API for deploy scripts: wakelog post --from-git --draft turns
"commits since the last tag" into a draft in one line. Prefer zero YAML? Point a GitHub,
GitLab, or Gitea release webhook at your project and published releases
become entries β signature-verified, title-deduped, so redeliveries never double-post.
Publishing a draft is one click on the dashboard or one PATCH from anywhere.
Start your free changelog Next: changelogs from git commits β
Related guides
- Generate a changelog from git commits (without publishing garbage)
git log one-liners, Conventional Commits, git-cliff & friends β and the one rule that keeps auto-generated changelogs from being unreadable. - AI-generated release notes: what to automate (and what to never delegate)
What to hand the model (compression, rewording, consistency), the failure modes that publish fiction, the draft-then-gate pipeline, and the promises only humans get to write. - Machine-readable changelogs: feeds, APIs, and llms.txt for scripts and AI agents
Dependency bots, scripts, and AI agents read changelogs now. Write for humans β but expose structure (feeds, stable IDs, ISO dates, tags as data) so machines can read along.
Last updated 2026-07-27 Β· All guides