A GitHub Action that creates a git tag and a GitHub release generated from the commits since the previous tag — and automatically rolls back the tag and release if anything goes wrong partway through.
- 🏷️ Creates and pushes an annotated git tag
- 📝 Builds release notes from the commits since the previous tag, grouped into Features, Bug Fixes, Documentation, CI, … by their Conventional Commits prefix
- 🔀 Lists either the merged pull requests or the individual commits
- 🚀 Publishes a GitHub release for that tag
- 🔗 Optionally creates alias tags (e.g.
v1,v1.0) pointing to the same commit - ↩️ Rolls back the tag, alias tags, and release on any failure
- 🖥️ Implemented in plain Python — no extra runtime beyond what
setup-pythonprovides
name: Create Release
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to create (e.g. v1.2.3)'
required: true
type: string
target:
description: 'Branch or commit to tag'
required: false
default: 'main'
type: string
prerelease:
description: 'Mark as a pre-release'
required: false
default: false
type: boolean
aliases:
description: 'Space-separated alias tags (e.g. "v1 v1.0")'
required: false
default: ''
type: string
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0 # full history + tags are needed to build the changelog
ref: ${{ inputs.target }}
- name: Tag and release
id: release
uses: carlspring/release-action@v1
with:
tag: ${{ inputs.tag }}
target: ${{ inputs.target }}
prerelease: ${{ inputs.prerelease }}
aliases: ${{ inputs.aliases }} # e.g. "v1 v1.0"
- name: Show result
run: echo "Release created at ${{ steps.release.outputs.release_url }}"A ready-to-run copy of this workflow is in
.github/workflows/example-release.yml.
Trigger it from the Actions tab → Create Release → Run workflow.
Note: replace
your-org/tag-and-release-action@v1withuses: ./if you're calling the action from within this same repository rather than as a published dependency.
| Name | Required | Default | Description |
|---|---|---|---|
tag |
Yes | — | Tag to create, e.g. v1.2.3 |
target |
No | main |
Branch or commit the tag should point to |
draft |
No | false |
Create the release as a draft |
prerelease |
No | false |
Mark the release as a pre-release |
aliases |
No | '' |
Space-separated alias tags, e.g. "v1 v1.0" |
group-by |
No | pull-request |
List pull-requests or commits in the notes |
github-token |
No | ${{ github.token }} |
Token used to push the tag and manage the release |
| Name | Description |
|---|---|
tag |
The tag that was created |
release_id |
ID of the created GitHub release |
release_url |
HTML URL of the created GitHub release |
The job calling this action needs:
permissions:
contents: writeso the default GITHUB_TOKEN can push tags and create releases.
The release notes list the changes since the previous tag, grouped into
sections by their Conventional Commits
prefix (type(scope): subject):
| Prefix | Section |
|---|---|
feat |
Features |
fix |
Bug Fixes |
perf |
Performance |
refactor |
Code Refactoring |
docs |
Documentation |
test |
Tests |
build |
Build |
ci |
CI |
style |
Styles |
revert |
Reverts |
chore |
Chores |
Anything without one of these prefixes goes under Other Changes. Breaking
changes (feat!: … or a BREAKING CHANGE: footer) are listed once, in a
⚠ Breaking Changes section at the top. Empty sections are left out.
The group-by input chooses what each line represents:
-
pull-request(default): the merged pull requests that contain the commits since the previous tag, categorised by their title:### Features - **tracing**: wire OpenTelemetry export ([#42](https://github.com/org/repo/pull/42)) (@carlspring)
If no pull requests are found, or there is no previous tag, the commits are listed instead.
-
commit: the individual (non-merge) commits, categorised by their message:### Bug Fixes - **publishing**: declare explicit dependency versions ([6840708a](https://github.com/org/repo/commit/6840708a91b2...))
The action tracks what it has created as it goes:
- Creates and pushes the git tag.
- Creates the GitHub release from that tag.
- Creates any alias tags (e.g.
v1,v1.0).
If step 2 or 3 (or anything else after the tag is pushed) raises an error, the action:
- Deletes the GitHub release, if one was created.
- Deletes the git tag, both on the remote and locally.
- Deletes any alias tags that were already created, both on the remote and locally.
This keeps a failed run from leaving a dangling tag or an incomplete release behind. Rollback is best-effort: failures encountered while cleaning up are logged rather than raised, so the original error is what surfaces in the workflow.
.
├── action.yml # Action metadata (inputs/outputs, runs config)
├── requirements.txt # Python dependencies
├── scripts/
│ ├── release.py # Core logic: tag, release, rollback
│ └── release_notes.py # Groups commits/PRs into release-note sections
├── tests/ # Unit tests (unittest)
└── .github/workflows/
├── ci.yml # Lints/validates the action's source
└── release.yml # Example consumer workflow
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Run the script directly against a real repo/token for local testing
export GITHUB_TOKEN=ghp_xxx
export GITHUB_REPOSITORY=your-org/your-repo
python scripts/release.py --tag v0.0.1-test --target mainRun the tests (standard-library unittest, no extra dependencies):
python -m unittest discover -s tests -vCI (.github/workflows/ci.yml) compiles, lints and tests the scripts on every
push and pull request against main.