Naming release versions manually is prone to inconsistency and errors. In this episode we automate semantic versioning from Conventional Commits commit messages, then create GitHub Releases with a changelog and artifacts every time code enters main.

In episode 17 we installed environment protection rules and an approval gateway. All that security ultimately leads to one important moment: the release. Manually deciding versions like v1.2.3 then v1.2.4 is often miscounted, inconsistent between developers, and lets changelogs pile up uncontrolled. In this episode we hand version numbering, changelogs, and GitHub Release creation entirely over to automation.
In this episode we discuss:
SemVer uses the MAJOR.MINOR.PATCH format. The analogy: MAJOR is like a car generation — going from generation 1 to 2 can mean a totally different engine; MINOR is like added features within the same generation; PATCH is like small fixes to existing features. These numbers aren't decoration; they tell consumers how "dangerous" the update is for them.
| Component | Increments when | Example |
|---|---|---|
| MAJOR | There's a change that breaks the old API | 1.0.0 to 2.0.0 |
| MINOR | New backward-compatible feature | 1.0.0 to 1.1.0 |
| PATCH | Backward-compatible bug fix | 1.0.0 to 1.0.1 |
If a version is bumped incorrectly, consumers can be disappointed. Raising MAJOR without reason forces them to re-review the entire API; skipping it silently breaks their applications on upgrade.
Automation needs "feed" that's machine-readable. Conventional Commits is a commit message format convention that exposes the type of change via a prefix:
| Prefix | Type of change | Version impact |
|---|---|---|
| feat: | New feature | MINOR |
| fix: | Bug fix | PATCH |
breaking change or feat!: | API change | MAJOR |
| chore:, docs:, refactor: | No behavior change | No release |
Examples: feat: tambah endpoint healthcheck, fix: perbaiki memory leak pada worker, and feat!: ganti format response API. Because it's these prefixes the versioner reads, commit message discipline becomes an absolute prerequisite. Without it, the pipeline is "deaf" to code changes and versions never go up.
Tip
Install commit linting (for example commitlint) as a status check on pull requests. Commit messages that don't follow the convention get held back early, so version automation never receives messy input.
Two tools dominate the ecosystem: release-please (from Google) and semantic-release (community). Both read Conventional Commits, compute the next version, and write a changelog. The difference is in their working style:
| Aspect | release-please | semantic-release |
|---|---|---|
| Release style | Release PR that waits for review | Releases immediately on push to main |
| Changelog | Automatic from commit history | Automatic from commit history |
| Complexity | Simple | Flexible with many plugins |
| Official action | google-github-actions/release-please-action@v4 | Via the npm semantic-release |
release-please opens a pull request containing changes to the CHANGELOG.md file and version bumps in the manifest. Once that PR is merged, it creates the tag and GitHub Release — humans still hold control over when a release happens. semantic-release is the opposite: the moment code enters main, it immediately tags the version and releases. Suitable for teams that want full automation without extra review.
The following workflow runs on every push to main. On a feature commit push, it creates or updates the Release PR; on a push of the merged Release PR, it publishes the tag and GitHub Release:
name: Release Automation
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- name: Jalankan release-please
uses: google-github-actions/release-please-action@v4
with:
release-type: node
token: ${{ secrets.GITHUB_TOKEN }}Things to note:
permissions: contents: write and pull-requests: write are needed to create tags, releases, and Release PRs.release-type: node tailors the behavior to Node.js projects; python, go, java, and simple are also available.secrets.GITHUB_TOKEN context, so all its changes are audited as part of the repository.How the cycle works: developer merges a feature → the action creates a "chore(main): release v1.1.0" PR → the team reviews → the PR is merged → the action detects its release PR was merged → creates the v1.1.0 tag and GitHub Release. As a result, releases are always documented and easy to trace.
For binaries, installers, or other assets, softprops/action-gh-release@v2 attaches files to a release. This workflow is triggered by a v-prefixed tag and uploads the build output:
name: Attach Release Assets
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Build artifact
run: npm ci && npm run build
- name: Buat/update GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/*.zip
generate_release_notes: trueExplanation:
generate_release_notes: true makes GitHub compile release notes from the commits merged since the previous release.files: dist/*.zip attaches all zip files in the dist folder; it can also be a glob array.tags: ['v*'] event makes this workflow run only when a v tag is created, so it never triggers double releases.A common production pattern: release-please creates the tag and release, then the tag-triggered workflow attaches artifacts to the existing release. Both run sequentially without overwriting each other.
Warning
Don't leave a release in draft status without an overseer. Workflows that upload artifacts to a release that hasn't been published will fail. Make the publish step explicit, or set draft: true only on steps that are indeed scheduled for manual completion.
In this episode we automated release management & semantic versioning:
MAJOR.MINOR.PATCH) provides a version language understood by both machines and humans.A good production pipeline doesn't just run smoothly — it also has to be easy to trace when it fails. In episode 19 we discuss troubleshooting, debugging, and custom action development, including enabling debug logging and SSH-ing into the runner. See you there!