Learn GitHub Actions - Automated Release Management & Semantic Versioning
Episode 18 of 21

Learn GitHub Actions - Automated Release Management & Semantic Versioning

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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:

  1. The concepts of Semantic Versioning (SemVer) and Conventional Commits.
  2. Version numbering automation with release-please and semantic-release.
  3. A release workflow that creates a GitHub Release with changelog and artifacts.

Semantic Versioning: The Agreed Version-Naming Language

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.

ComponentIncrements whenExample
MAJORThere's a change that breaks the old API1.0.0 to 2.0.0
MINORNew backward-compatible feature1.0.0 to 1.1.0
PATCHBackward-compatible bug fix1.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.

Conventional Commits: The Source of Truth for Version Numbering

Automation needs "feed" that's machine-readable. Conventional Commits is a commit message format convention that exposes the type of change via a prefix:

PrefixType of changeVersion impact
feat:New featureMINOR
fix:Bug fixPATCH
breaking change or feat!:API changeMAJOR
chore:, docs:, refactor:No behavior changeNo 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:

Aspectrelease-pleasesemantic-release
Release styleRelease PR that waits for reviewReleases immediately on push to main
ChangelogAutomatic from commit historyAutomatic from commit history
ComplexitySimpleFlexible with many plugins
Official actiongoogle-github-actions/release-please-action@v4Via 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.

Automatic Release Workflow with release-please

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:

release.yml - automatic releases with release-please
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.
  • The action uses a token from the 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.

Attaching Artifacts to GitHub Releases with softprops

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:

release-assets.yml - attach artifacts to a release
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: true

Explanation:

  • 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.
  • The 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.

Conclusion

In this episode we automated release management & semantic versioning:

  • SemVer (MAJOR.MINOR.PATCH) provides a version language understood by both machines and humans.
  • Conventional Commits become the source of truth for determining the next version.
  • release-please uses a Release PR; semantic-release releases directly from main.
  • A workflow on push to main generates tags and GitHub Releases automatically.
  • softprops/action-gh-release attaches artifacts to tag-triggered releases.

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!

Learn GitHub Actions - Automated Release Management & Semantic Versioning | Learn GitHub Actions