A GitHub Action that calculates the next semantic version from your git history using Conventional Commits. No Node.js, no Docker — pure bash.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required: full history + tags
- uses: logi-camp/conventioanl-semver@v1
id: semver
- run: echo "Next version is ${{ steps.semver.outputs.version_tag }}"
fetch-depth: 0is required. The default shallow clone has no tags or history, so the action cannot determine the previous version.
| Input | Description | Default |
|---|---|---|
prefix |
Tag prefix | v |
default_bump |
Bump type when no conventional commits are found (major / minor / patch / none) |
patch |
initial_version |
Starting version when the repo has no tags yet | 0.0.0 |
prerelease |
Prerelease identifier (rc, alpha, beta, etc.). When set, produces prerelease tags |
(empty) |
prerelease_identifier |
How to generate prerelease identifier: numbered (rc.1, rc.2) or sha (rc.a1b2c3d) |
numbered |
include_sha |
Append short commit SHA to tag (auto / true / false) |
auto |
| Output | Description | Example |
|---|---|---|
version |
Next semantic version | 1.4.0 or 1.3.0-rc.1-a1b2c3d or 1.3.0-rc.a1b2c3d |
version_tag |
Next version with prefix | v1.4.0 or v1.3.0-rc.a1b2c3d |
previous_version |
Previous version without prefix | 1.3.0 |
previous_version_tag |
Previous version tag, empty if no tags existed | v1.3.0 |
base_version |
Base version without prerelease suffix or SHA | 1.4.0 |
base_version_tag |
Base version tag with prefix | v1.4.0 |
changelog |
Markdown changelog grouped by commit type | (see below) |
bump |
Bump type applied | minor |
last_commit |
SHA of HEAD at calculation time | 49d6690... |
rc_number |
Prerelease identifier (number or SHA). Only set when prerelease is used |
1 or a1b2c3d |
| Commit pattern | Bump |
|---|---|
type!: … or type(scope)!: … |
major |
BREAKING CHANGE: in commit body |
major |
feat: … / feat(scope): … |
minor |
fix: … / fix(scope): … |
patch |
perf: … / perf(scope): … |
patch |
| anything else | value of default_bump input |
The highest bump found across all commits since the last tag wins (major > minor > patch).
The changelog output is a markdown string grouped by commit type:
[Breaking Changes]
- feat!: redesign API response format
[Features]
- feat: add dark mode toggle
- feat(api): add pagination support
[Bug Fixes]
- fix: resolve null pointer on login
[Performance Improvements]
- perf: cache database queriesUse the prerelease input to produce sequential pre-release tags. This is useful for release candidate (RC), alpha, or beta builds on staging/release branches.
The algorithm differs based on the prerelease_identifier input:
- The action computes the base version from conventional commits (same as normal mode).
- It scans existing tags matching
{base_version}-{prerelease}.*to find the highest RC number. - It outputs the next sequential prerelease tag.
Important: For the numbered mode to correctly increment RC numbers, you must create and push the tags after each run. If tags are not pushed, the action cannot see previous RC tags and will always output
rc.1. Use the SHA-based mode if you don't want to manage tag synchronization.
- The action computes the base version from conventional commits (same as normal mode).
- It uses the current commit's SHA as the prerelease identifier.
- Each commit automatically gets a unique version without requiring tag tracking.
Last tag: v1.2.3
Commits: feat: new endpoint
Base version: v1.3.0
Existing RCs: v1.3.0-rc.1, v1.3.0-rc.2 ← tags must exist in repo
Output: v1.3.0-rc.3-a1b2c3d
Last tag: v1.2.3
Commits: feat: new endpoint
Base version: v1.3.0
Output: v1.3.0-rc.a1b2c3d
Each push to the release branch produces a unique version automatically in both modes.
When using prerelease_identifier: numbered, by default (include_sha: auto), prerelease tags include a 7-char commit SHA suffix:
v1.3.0-rc.1-a1b2c3d ← with SHA suffix (default for prerelease)
v1.3.0-rc.1 ← without SHA suffix
v1.3.0 ← final releases never include SHA (unless overridden)
The include_sha input controls this:
| Value | Behavior |
|---|---|
auto |
SHA included for numbered prereleases only |
true |
SHA always appended (including final releases) |
false |
SHA never appended |
Note: When using prerelease_identifier: sha, the commit SHA is already used as the prerelease identifier, so include_sha has no effect.
name: Release Candidate
on:
push:
branches:
- release/**
jobs:
version:
runs-on: ubuntu-latest
outputs:
rc_tag: ${{ steps.semver.outputs.version_tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: logi-camp/conventioanl-semver@v1
id: semver
with:
prerelease: rc
prerelease_identifier: sha
- name: Print version
run: |
echo "RC tag: ${{ steps.semver.outputs.version_tag }}"
echo "Base: ${{ steps.semver.outputs.base_version }}"
echo "SHA: ${{ steps.semver.outputs.rc_number }}"
build:
runs-on: ubuntu-latest
needs: version
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t registry/app:${{ needs.version.outputs.rc_tag }} .
docker push registry/app:${{ needs.version.outputs.rc_tag }}This approach ensures each commit on a release branch gets a unique identifier without requiring tags to be pushed or tracked in the repository.
- uses: logi-camp/conventioanl-semver@v1
id: semver
with:
prerelease: alpha
include_sha: 'false' # clean tags: v1.3.0-alpha.1release/** branch main branch
──────────────── ────────────
push → v1.3.0-rc.a1b2c3d (no action)
push → v1.3.0-rc.e5f6g7h (no action)
push → v1.3.0-rc.i8j9k0l (no action)
merge ─────────────────────────────→ v1.3.0 (final release)
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: logi-camp/conventioanl-semver@v1
id: semver
- name: Create tag
if: steps.semver.outputs.bump != 'none'
run: |
git tag ${{ steps.semver.outputs.version_tag }}
git push origin ${{ steps.semver.outputs.version_tag }}
- name: Create GitHub Release
if: steps.semver.outputs.bump != 'none'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.semver.outputs.version_tag }}
body: ${{ steps.semver.outputs.changelog }}jobs:
version:
runs-on: ubuntu-latest
outputs:
rc_tag: ${{ steps.semver.outputs.version_tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: logi-camp/conventioanl-semver@v1
id: semver
- name: Tag commit
if: steps.semver.outputs.bump != 'none'
run: |
git config user.email "ci@your-domain.com"
git config user.name "CI Bot"
TAG_MSG=$(printf "%s\n\n%s" "${{ steps.semver.outputs.version_tag }}" "${{ steps.semver.outputs.changelog }}")
git tag -a ${{ steps.semver.outputs.version_tag }} -m "$TAG_MSG"
git push https://${{ secrets.ROBOT_USERNAME }}:${{ secrets.ROBOT_TOKEN }}@${{ vars.GIT_SERVER }}/${{ github.repository }}.git ${{ steps.semver.outputs.version_tag }}