readable announcements instead of raw commit lists
Release notes speak to users and stakeholders, not developers, and therefore need a different tone than a technical changelog. This article shows how a Bash script automatically builds readable release notes with contributors, highlighted key changes, and a comparison link from the commit range between two tags, and pushes them directly to the GitHub or GitLab Release API.
Table of contents
- 1. Release notes are not a changelog
- 2. Determining the commit range between two tags
- 3. Listing contributors automatically
- 4. Highlighting the most important changes
- 5. Grouping by user relevant categories
- 6. Adding a comparison link to the previous release
- 7. Text building blocks for a consistent tone
- 8. Publishing release notes to the release API
- 9. Release notes generator compared to alternatives
- 10. Summary
- 11. FAQ
1. Release notes are not a changelog
A changelog continuously logs every single technical change of a project, often in one single, steadily growing file. Release notes, on the other hand, are a point in time summary of exactly one release, written for users, customers, or management who do not want to read a commit history. Ignoring this distinction produces either release notes that are too technical or a changelog that reads like a marketing message.
The core of a good script for release notes lies in producing two completely different outputs from the same git history: technical details for developers stay in the changelog, while the release notes only highlight the changes visible and noticeable to users, enriched with context such as contributors and a direct comparison link.
This article builds a Bash script that implements exactly this separation: it reads the same commit range as a changelog generator, but applies different filtering and formatting rules to end up producing release notes that can be published directly on a GitHub or GitLab release page.
2. Determining the commit range between two tags
Unlike a continuously updated changelog, release notes always refer to exactly one closed time window, usually the range between two consecutive tags. The script therefore has to reliably determine both the current and the previous tag, typically via git tag --sort=-v:refname, to sort by version number rather than creation date.
A common pitfall with release notes: lightweight tags without annotation behave differently in some git commands than annotated tags. A robust script should therefore explicitly use git for-each-ref, which handles both tag types uniformly, instead of relying solely on git describe, which is primarily meant for annotated tags.
#!/usr/bin/env bash
# determine-range.sh — find the commit range for release notes
set -euo pipefail
determine_release_range() {
local -a tags=()
while IFS= read -r tag; do
tags+=("$tag")
done < <(git for-each-ref --sort=-v:refname --format='%(refname:short)' refs/tags)
if [[ ${#tags[@]} -lt 1 ]]; then
echo "[ERROR] No tags found in repository" >&2
exit 1
fi
local current_tag="${tags[0]}"
local previous_tag="${tags[1]:-}"
echo "current=${current_tag}"
echo "previous=${previous_tag}"
echo "range=${previous_tag:+${previous_tag}..}${current_tag}"
}
determine_release_range
3. Listing contributors automatically
One trait that distinguishes release notes from a technical changelog is crediting contributors by name. Especially in open source projects or larger teams, a list of authors per release creates recognition and transparency about who was involved in a version. git shortlog already delivers this information aggregated and sorted by commit count.
For release notes, the list should also be deduplicated in case the same person committed under different email addresses, for example a personal and a work address. A simple normalization by name instead of email address reduces duplicate entries without having to maintain a full mailmap file.
#!/usr/bin/env bash
# list-contributors.sh — list unique contributors for the release notes
set -euo pipefail
list_contributors() {
local range="$1"
git log "$range" --no-merges --format='%an' | sort -u
}
range="${1:?Usage: list-contributors.sh <range>}"
echo "## Contributors"
echo
list_contributors "$range" | while IFS= read -r name; do
echo "- ${name}"
done
4. Highlighting the most important changes
Simply printing every commit subject line would make release notes too unstructured, since not every change is equally relevant to users. A practical heuristic: commits with the feat: prefix count as potential highlights, while chore:, test:, and ci: are irrelevant to users and should be filtered out of the release notes, even though they are perfectly fine to appear in the technical changelog.
It is also worth looking at the length and level of detail of the commit message: messages with an extensive body often already contain the exact description that can be carried over into the release notes almost unchanged, while terse one liners are often phrased too technically to communicate directly to end users.
#!/usr/bin/env bash
# extract-highlights.sh — pick user-facing changes for release notes
set -euo pipefail
extract_highlights() {
local range="$1"
git log "$range" --no-merges --format='%s' |
grep -E '^feat(\(.+\))?!?: ' |
sed -E 's/^feat(\(.+\))?!?: //'
}
range="${1:?Usage: extract-highlights.sh <range>}"
echo "## Highlights"
echo
extract_highlights "$range" | while IFS= read -r line; do
echo "- ${line^}"
done
5. Grouping by user relevant categories
While a changelog generator groups strictly by commit type, good release notes are organized around user relevant categories such as new features, improvements, and fixed issues. These categories overlap with the technical commit types but are deliberately reworded to stay understandable for readers without a technical background.
One example: a perf: commit is listed as a performance optimization in the technical changelog, but in the release notes it might be summarized as an improvement under faster load times, together with other performance relevant changes that appear to users as one single, noticeable benefit.
6. Adding a comparison link to the previous release
Even though release notes are primarily intended for non technical readers, a link to the full diff between the current and the previous tag should always appear at the end. Interested technical users, such as partner developers or the own support team, can then dig deeper into the actual code changes whenever needed, without the release notes themselves having to be overloaded with detail.
The comparison link can be assembled directly from the two tag names and the known repository URL, for instance in the form https://github.com/org/repo/compare/v1.4.0...v1.5.0. For GitLab hosted projects only the URL scheme differs, the principle stays identical.
7. Text building blocks for a consistent tone
Automatically generated release notes quickly feel robotic if every version shows the exact same structure without any variation. A simple trick: an array with several phrasing variants for the introduction, from which the script randomly picks one variant per run, so consecutive release notes do not begin with the exact same wording every time.
More important than stylistic variation, though, is a consistent basic skeleton: introduction, highlights, full category list, contributors, comparison link, always in the same order. This consistency ensures readers can immediately find their way around the release notes across several versions, regardless of the content details of each individual version.
#!/usr/bin/env bash
# build-release-notes.sh — assemble the final release notes document
set -euo pipefail
version="$1"
range="$2"
intros=(
"This release focuses on"
"In this update, we shipped"
"Here is what changed in"
)
intro="${intros[$((RANDOM % ${#intros[@]}))]}"
{
echo "# ${version}"
echo
echo "${intro} ${version}."
echo
bash extract-highlights.sh "$range"
echo
bash list-contributors.sh "$range"
echo
echo "**Full diff:** https://github.com/mironsoft/example-repo/compare/${range/../.../.../}"
} > release-notes.md
echo "[OK] release-notes.md generated for ${version}"
8. Publishing release notes to the release API
Instead of manually copying the finished release notes into the GitHub or GitLab web interface, the entire process can be automated through the respective release API. For GitHub, a curl call against /repos/{owner}/{repo}/releases with the generated Markdown text in the body field of the JSON payload is enough.
Important for automation: special characters and line breaks in the generated text must be correctly escaped for JSON, for which jq -Rs . works well, reliably turning a multi line text file into a valid JSON string without manually escaping quotes or newlines.
#!/usr/bin/env bash
# publish-release.sh — push release notes to the GitHub Releases API
set -euo pipefail
readonly REPO="mironsoft/example-repo"
readonly TOKEN="${GITHUB_TOKEN:?Set GITHUB_TOKEN}"
version="$1"
body_json="$(jq -Rs . < release-notes.md)"
payload=$(jq -n \
--arg tag "$version" \
--argjson body "$body_json" \
'{ tag_name: $tag, name: $tag, body: $body, draft: false, prerelease: false }')
curl -sf -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "$payload" \
"https://api.github.com/repos/${REPO}/releases" > /dev/null
echo "[OK] Release ${version} published with generated release notes"
9. Release notes generator compared to alternatives
GitHub offers automatically generated release notes as a built in feature that groups pull request titles. For projects working consistently through pull requests instead of direct commits, this built in tool is often enough. A custom Bash script pays off once individual categories, contributor formatting, or cross platform publishing across several release channels are needed.
| Approach | Adaptability | Contributor list | Suited for |
|---|---|---|---|
| Bash release notes generator | Full | Custom formatted | Cross platform, custom categories |
| GitHub auto generated notes | Limited configurability | By PR author | Pure GitHub PR workflows |
| Manual release notes | Full | Manual, error prone | Very infrequent releases |
| Release Please (Google) | Configurable via presets | Limited | Node heavy, Google style projects |
For teams with their own release channel, for example an internal status page alongside GitHub, a self built approach to release notes is often the only way to distribute the same text consistently to several targets at once.
Mironsoft
Shell automation, release management, and deployment infrastructure
Release notes that write themselves?
We build a script that produces readable release notes with contributors, highlights, and a comparison link from your git history, and pushes them directly to GitHub, GitLab, or your own status page.
Script development
A custom release notes generator matching your tone
API integration
Automatic publishing to GitHub, GitLab, or your own channels
Release process
Integration into existing CI/CD pipelines and tag workflows
10. Summary
Automatically generated release notes deliberately differ from a technical changelog: they speak to users, highlight key changes instead of every single change, and credit contributors by name. A Bash script that reads the commit range between two tags, filters feat commits, and adds a comparison link provides the foundation for consistent release notes with every release.
The final step, publishing directly through the GitHub or GitLab release API, turns the script into a complete part of the release process. Instead of manually formatting release notes and copying them into a web interface, a single command emerges that generates the text and publishes it at the same time.
Release notes from git log, the essentials at a glance
Distinction from a changelog
Release notes speak to users, a changelog to developers, but both are built from the same history.
Highlights over completeness
feat commits are filtered and highlighted, technical chore and ci commits are left out.
Contributors
git shortlog automatically delivers a deduplicated list of authors per release.
Publishing
Direct submission to the GitHub or GitLab release API with correctly escaped JSON text.