github-tag-action turns conventional commits into a SemVer tag and a release
A Github Action to automatically bump and tag master, on merge, with the latest SemVer formatted version. Works on any platform.
At a glance
- What is it?
- This GitHub Action reads the commits since the last tag, works out the next semantic version with the semantic-release conventions, pushes a lightweight tag and exposes a changelog for the release step. It is powerful mainly in its inputs, and the v7 line requires a runner with Node 24.
- Who is it for?
- Adopt github-tag-action if your team already writes conventional commit messages and wants versioning and releases to happen without a human bumping a version file. Do not adopt it if your commit history is free form, because the release type is derived from commit text and a `fix` or `feat` prefix is what makes the numbers meaningful.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A push to master becomes a tag and a version
github-tag-action does one job: when a merge lands on a release branch, it calculates the next semantic version, creates a tag for it, and tells the rest of your workflow what it decided. The repository description says it automatically bumps and tags master, on merge, with the latest SemVer formatted version, and the README adds that it works on GitHub Actions runners supporting Node 24.
The action is written in TypeScript and published on the GitHub Marketplace as github-tag. The licence is MIT, the project is not archived, and the last push was on 2026-10-17. The current major line is v7, published on 2026-09-23, following v6.2 from 2024-03-13 and v6.1 from 2022-10-26.
The point of the tool is that the version number is never typed by a person. You commit with a conventional message, the action derives the release type from it, and the tag follows. For teams already writing `feat:` and `fix:` prefixes, that removes the recurring argument about which number changed. For teams that do not, it just moves the problem, because the action has nothing to read.
The implementation is a normal TypeScript GitHub Action. The repository has an action.yml at the root, source under src and types under types, tests under tests, and a dist directory that holds the compiled bundle, which is the file GitHub actually runs.
Conventional commits decide the release type
The bumping logic is borrowed from semantic-release. The action parses the new commits since the last tag using the semantic-release conventions, and semantic-release uses the commit messages to determine the type of change and therefore the next semantic version number.
The default parser is conventionalcommits, shared by both the bump analysis and the changelog generation. Under it, a fix prefix gives a patch, a feat prefix gives a minor, and a breaking change gives a major. The README is explicit that both `feat!: breaking change` and a `BREAKING CHANGE:` footer trigger a major release, and it gives three concrete examples. A fix scoped to a component produces a patch release:
fix(pencil): stop graphite breaking when too much pressure appliedA feature produces a minor release:
feat(pencil): add 'graphiteWidth' optionAnd a change that removes an option is a major, because the message carries the footer:
BREAKING CHANGE: The graphiteWidth option has been removed.
The default graphite width of 10mm is always used for performance reasons.If you prefer Angular's parsing rules, the action exposes a commit_analyzer_preset input that can be set to angular, and the conventional-changelog-angular package is a declared dependency.
There is a fallback that matters. If eligible commits exist but none of them selects a bump, default_bump is used, which by default is patch. With no eligible commits at all, no new version is calculated, unless custom_tag is set or a force-bump input is supplied.
The inputs decide which branches tag, and under what name
The input list is long, and it is where the real behaviour lives. A few entries matter to almost everyone.
github_token is required, and it is needed to read repository history, including in dry runs. Publishing needs contents: write permission, which is why the example workflow declares a permissions block. The commit_sha input is optional and, when set, is used as the commit the tag points at instead of GITHUB_SHA, which is useful when a previous step merged a branch into github.ref.
Branch selection is regex based. release_branches takes a comma separated list of JavaScript regular expressions and defaults to `^master$,^main$`. Branches that match generate release tags, while other branches and pull requests generate versions postfixed with the commit hash and no repository tag. pre_release_branches works the same way for pre-release tags, with the note that pull request events always calculate preview versions without publishing, and that tag triggered runs are skipped.
The naming of the tag is configurable too. tag_prefix defaults to v, append_to_pre_release_tag defaults to `<branch>`, create_annotated_tag chooses between an annotated and a lightweight tag and defaults to false, which is why the README notes that this action creates a lightweight tag by default. custom_tag overrides the bump settings entirely with a name you supply.
Two inputs are about safety rather than naming. fetch_all_tags defaults to true, and with false a full 100-tag page fails explicitly, because selecting a version from incomplete results would be unsafe. And dry_run does not perform tagging at all, it just calculates the next version and changelog and exits, which is the input to reach for when you are trying this on a repository that already has tags.
The outputs feed a separate release step
The action does not create a GitHub release itself. It exposes outputs, and a separate action turns them into a release. The workflow in the README wires the two together, first with the checkout and the tag step:
name: Bump version
on:
push:
branches:
- master
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v7and then with the release creation, guarded on the tag actually having been created:
- name: Create a GitHub release
if: steps.tag_version.outputs.new_tag != ''
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: Release ${{ steps.tag_version.outputs.new_tag }}
body: ${{ steps.tag_version.outputs.changelog }}
prerelease: ${{ steps.tag_version.outputs.prerelease == 'true' }}The guard exists because new_tag is undefined when there has been no new commit, and an empty string check is how the workflow skips the release in that case.
The outputs themselves are straightforward. new_tag is the calculated tag, new_version the same value without the prefix, previous_tag and previous_version the values before the bump, with v0.0.0 and 0.0.0 as the fallback when there is no prior tag. release_type is the computed type, major, minor, patch or custom, which can be prefixed with pre. And changelog holds the conventional changelog since the previous tag, which is what the release body is filled from.
Custom release rules let one keyword mean a different bump
The conventional commit parser has a fixed vocabulary, and real repositories rarely match it. The custom_release_rules input exists for that gap. It takes a comma separated list of rules in the format `<keyword>:<release_type>:<changelog_section>`, where the changelog section is optional and defaults to Angular's conventions.
Two examples from the README show the range of uses. The first maps two different keywords to different release types:
1. `hotfix:patch,pre-feat:preminor`,
2. `bug:patch:Bug Fixes,chore:patch:Chores`The first example treats a hotfix as a patch and a pre-feat as a preminor, so a feature being prepared for a release does not immediately bump the minor number. The second example groups two keywords into named changelog sections, Bug Fixes and Chores, so the release notes are organised by your own vocabulary rather than by the preset.
This is the input that turns the action from a convention follower into something you can align with an existing process. It is also the input most worth reading carefully, because the changelog sections you name are the sections your release notes will have.
The bump defaults are configurable in the same spirit. default_bump decides what happens on a release branch when no commit selects a bump, with patch as the default and the option to set it to false to avoid generating a tag at all. default_prerelease_bump does the same for pre-release branches, defaulting to prerelease and accepting prerelease, prepatch, preminor or premajor.
path_filter and scopes keep parts of the repo out of the bump
Not every commit should change the version. A monorepo often has components that version independently, and a documentation-only change should not produce a release. Two inputs handle this, and both are globs.
path_filter takes comma separated repository relative directories or file globs, with `packages/web,shared/**` given as the example. The README notes two details that matter in practice. Both the old and the new paths of a renamed file count, so a file moved out of a filtered path still triggers the release it should. And each commit's files are fetched separately, with a truncated file list failing the run rather than silently omitting changes.
That failure behaviour is the important half. A filter that quietly ignores a file it could not list would produce a version bump computed from incomplete information, which is the kind of bug that surfaces weeks later as a release with a missing entry in the changelog. Failing loudly is the right trade for this input.
scopes works on the commit scope rather than the path, taking comma separated conventional commit scope globs. When scopes and paths are combined, the README says both filters must match, which makes the combination an AND rather than an OR. scopes is how you require that a change to packages/web also carries a web scope in its message.
A third input removes commits outright. ignore_keywords takes comma separated literal strings that exclude entire commits from both the bump and the changelog, and the README gives a default of `[no-release],[skip-rel` before the text is cut off. The default value is therefore not fully readable in the README as published, which is a small documentation gap worth checking in the source before you rely on it.
Node 24, the v7 migration, and how the action is built
Version 7 is a platform change more than a feature change. The README describes v7 and tells you to read the migration guide at docs/migrating-to-v7.md before upgrading, and the first line of the README says it works on runners supporting Node 24. The package manifest agrees, declaring an engines requirement of node >=24:
"engines": {
"node": ">=24"
},That matters if your self-hosted runners are older than the GitHub hosted ones, because the action will refuse to run rather than misbehave.
The build and check scripts show how the action is produced. TypeScript is compiled and then bundled, tests run under jest, formatting is checked with prettier, types are checked with tsc, and there is a smoke script:
"build": "tsc && node scripts/build.cjs",
"test": "jest --watchman=false --testTimeout 10000",
"check": "prettier --check .",
"typecheck": "tsc --noEmit",
"smoke": "node scripts/smoke.cjs"The runtime dependencies tell you what the action leans on: the official actions core and exec packages, the semantic-release commit analyzer and release notes generator, the conventionalcommits changelog preset and the angular one, semver, minimatch for the glob filters, and undici for HTTP. The main entry point in the manifest is dist/index.js, which confirms the compiled bundle in dist is what runs.
The repository also keeps a CHANGELOG.md and a docs directory alongside the source, so the upgrade path for a team pinning v6 is written down rather than implied.
Where github-tag-action is the wrong tool
The first wrong case is a repository whose commit messages are not conventional. The release type is derived from commit text, so a team writing free form messages gets a version that does not reflect the change. The action would still produce a tag, and that tag would be wrong in a way that is hard to notice until someone tries to work out why a patch release contained a feature. The fix is a commit message convention, not a configuration flag.
The second is a repository that versions several components separately. This action computes one version for the repository, and while path_filter and scopes can narrow which commits count, the output is still a single new_tag and a single version number. If your packages need independent version streams, you need one workflow per component, each with its own filters, and you have to accept that they are not coordinated.
The third is a project that wants annotated tags or signed tags by policy. The default is a lightweight tag, and while create_annotated_tag turns it into an annotated one, the action does not mention signing tags at all, so a policy that requires a signature is outside what it does.
Finally, the actions that already do this. The semantic-release ecosystem includes a maintained semantic-release action, and the difference in approach is real rather than cosmetic. github-tag-action computes a version and a changelog and leaves tagging, releases and publishing to steps you write, which keeps the pieces small and lets you slot in the release action of your choice, as the README example does with ncipollo/release-action. A full semantic-release setup goes further and also drives the publish step and the branch and plugin model. Choose the smaller tool when you want the version step separated from the release step, and the larger one when you want one configuration to drive the whole pipeline.
Editorial conclusion
Adopt github-tag-action if your team already writes conventional commit messages and wants versioning and releases to happen without a human bumping a version file. Do not adopt it if your commit history is free form, because the release type is derived from commit text and a `fix` or `feat` prefix is what makes the numbers meaningful. Before enabling it on a release branch, run it once with dry_run to see the computed version and changelog, decide whether you want lightweight or annotated tags with create_annotated_tag, and read docs/migrating-to-v7.md, since the README describes v7 and requires a runner with Node 24.
Frequently asked questions
What does mathieudutour/github-tag-action do?
It calculates the next semantic version from the commits since the last tag, using the semantic-release conventions, pushes a tag for it on a release branch, and exposes the result as outputs. The release itself is created by a separate step in your workflow.
Which permissions does github-tag-action need?
The github_token input is required to read repository history, including in dry runs, and publishing needs contents: write permission. The README's example workflow declares permissions with contents: write and passes ${{ secrets.GITHUB_TOKEN }}.
How do I test github-tag-action without creating a tag?
Use the dry_run input. It does not perform tagging, it only calculates the next version and the changelog and then exits, so you can see what the action would do before it publishes anything.
How do I change the tag name or use annotated tags?
tag_prefix sets the prefix and defaults to v, append_to_pre_release_tag sets the suffix and defaults to the branch name, and custom_tag overrides the bump settings entirely with a name you supply. create_annotated_tag is a boolean that creates an annotated rather than a lightweight tag and defaults to false.
Which commit messages cause a major release in github-tag-action?
Under the default conventionalcommits parser, both a feat! marker with a breaking change and a BREAKING CHANGE: footer trigger a major release. A fix prefix gives a patch, a feat prefix gives a minor, and the commit_analyzer_preset input can be set to angular for Angular's parsing rules.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mathieudutour-github-tag-action)Was this useful?