Generating Release Notes from Git Log Automatically
AI generated
$_
#!/
Bash · Git · Release Management · Real World
Generating Release Notes from Git Log Automatically
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.

18 min read git log · git shortlog · curl · GitHub API Bash 4.x · 5.x · Git 2.3x

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.

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.

11. FAQ: Generating release notes from git log

1Difference from a changelog?
A changelog documents continuously for developers, release notes summarize for users at a point in time.
2How is the commit range determined?
Via the last two tags, sorted by version number using git for-each-ref.
3How are contributors listed?
Via git log by author name, sorted and deduplicated, without merge commits.
4How are highlights selected?
Through filtering on feat commits, technical commit types are left out.
5Why group differently than a changelog?
Technical commit types say little to users, release notes group by noticeable effects instead.
6What is the comparison link for?
It lets technical readers view the full diff without overloading the notes themselves.
7How is publishing done?
Via curl against the release API, with jq -Rs . correctly escaping the text as a JSON string.
8Isn't GitHub's built in feature enough?
Often yes for pure PR workflows, a custom script pays off for custom categories or multiple channels.
9How does the tone stay consistent?
Through a fixed skeleton with slightly varying intro sentences.
10Does this work with lightweight tags?
Yes, as long as git for-each-ref is used instead of git describe.