- Posted on
- • Artificial Intelligence
Artificial Intelligence Release Automation
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Release Automation with Bash: From Commit to Tagged Release
Shipping fast is easy. Shipping fast without breaking things is hard. If your team spends hours every week writing release notes, choosing a semver bump, and copy-pasting changelogs into GitHub/GitLab, you’re burning time on tasks AI can do well. In this post, you’ll learn how to bolt AI into a Linux + Bash workflow to automate release prep end‑to‑end—while keeping humans in the loop for the final “approve and ship” moment.
We’ll cover why AI is a natural fit for release automation, how to set up a lean toolchain on Linux, and provide a production‑ready Bash script that:
Summarizes changes since your last tag
Proposes a semver bump (major/minor/patch)
Drafts release notes and a risk level
Creates a signed tag and optional GitHub release
You can run it locally, in CI, or behind a protected branch. Let’s go.
Why AI for release automation?
Summarization is a sweet spot for LLMs. Turning a pile of commit messages and diffs into clean release notes is exactly what today’s models are good at.
AI augments, not replaces, your pipeline. You keep tests, linters, and approvals. AI just handles the boring parts quicker and more consistently.
Bash is the glue you already know. With
git,jq,curl, and the GitHub CLI, you can build a portable release workflow that works across distros and CI providers.
Prerequisites (Linux)
We’ll use: git, curl, jq, and optionally the GitHub CLI (gh) if you want to publish releases automatically.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git curl jq
# GitHub CLI (official repo)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install -y gh
- Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y git curl jq
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install -y gh
- openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y git curl jq
sudo rpm --import https://cli.github.com/packages/githubcli-archive-keyring.gpg
sudo zypper addrepo https://cli.github.com/packages/rpm/gh-cli.repo gh-cli
sudo zypper refresh
sudo zypper install -y gh
If you’ll publish GitHub releases, authenticate once:
gh auth login
Optional: prefer a local model? You can run a small LLM via Ollama and skip any external API:
curl -fsSL https://ollama.com/install.sh | sh
# then: ollama run llama3
Environment setup
Export your AI configuration as environment variables. Choose one of the two approaches below.
- OpenAI‑compatible API:
export AI_PROVIDER=openai
export AI_API_BASE=https://api.openai.com
export AI_MODEL=gpt-4o-mini # or another chat/completions model
export AI_API_KEY=sk-... # store securely in CI secrets or your shell keychain
- Local Ollama:
export AI_PROVIDER=ollama
export OLLAMA_MODEL=llama3
# ensure ollama is running: systemctl --user start ollama
The script: ai-release.sh
This Bash script:
Detects the last tag
Collects commits and a diff summary
Asks the model for: version bump, risk, and release notes (as JSON)
Applies a safe fallback if AI is unavailable
Writes a CHANGELOG entry, tags, pushes, and optionally creates a GitHub release
Save as ai-release.sh and make it executable with chmod +x ai-release.sh.
#!/usr/bin/env bash
set -euo pipefail
# Configuration
DEFAULT_BASE_BRANCH="${DEFAULT_BASE_BRANCH:-main}"
ALLOW_PUSH="${ALLOW_PUSH:-false}" # set true in CI or when ready to push
CREATE_GH_RELEASE="${CREATE_GH_RELEASE:-false}" # set true to publish via GitHub CLI
# Helpers
die() { echo "Error: $*" >&2; exit 1; }
need() {
command -v "$1" >/dev/null 2>&1 || die "Missing dependency: $1"
}
need git
need jq
need curl
if [ "${CREATE_GH_RELEASE}" = "true" ]; then
command -v gh >/dev/null 2>&1 || die "CREATE_GH_RELEASE=true but gh not installed"
fi
git fetch --tags --quiet || true
# Determine previous tag and commit range
if last_tag=$(git describe --tags --abbrev=0 2>/dev/null); then
range="${last_tag}..HEAD"
else
last_tag="0.0.0"
range=""
fi
commits=$(git log --no-merges --pretty=format:'%s' ${range})
details=$(git log --no-merges --pretty=format:'%h %s' ${range})
changed_files=$(git diff --name-status ${range})
diffstat=$(git diff --shortstat ${range})
branch=$(git rev-parse --abbrev-ref HEAD)
if [ -z "${commits}" ]; then
echo "No new commits since last tag (${last_tag}). Nothing to release."
exit 0
fi
# Conventional-commit-based fallback bump
fallback_bump() {
# major if BREAKING CHANGE or feat! or fix!
if echo "${commits}" | grep -Eiq "BREAKING CHANGE|!:"
then echo "major"; return; fi
# minor if feat
if echo "${commits}" | grep -Eiq "^feat(\(|:)"
then echo "minor"; return; fi
# else patch
echo "patch"
}
bump_semver() {
local ver="$1" inc="$2"
local prefix=""
# strip leading 'v' if present
if [[ "$ver" =~ ^v ]]; then prefix="v"; ver="${ver#v}"; fi
IFS=. read -r MA MI PA <<< "$ver"
MA=${MA:-0}; MI=${MI:-0}; PA=${PA:-0}
case "$inc" in
major) MA=$((MA+1)); MI=0; PA=0;;
minor) MI=$((MI+1)); PA=0;;
patch|*) PA=$((PA+1));;
esac
echo "${prefix}${MA}.${MI}.${PA}"
}
# Ask AI for release plan
ai_json=""
if [ "${AI_PROVIDER:-}" = "openai" ] && [ -n "${AI_API_KEY:-}" ] && [ -n "${AI_MODEL:-}" ]; then
prompt=$(cat <<'P'
You are a release automation assistant. Given commit subjects and a diff summary,
decide the semantic version bump, risk level, and produce clear markdown release notes.
Return STRICT JSON with keys: bump ("major"|"minor"|"patch"), risk ("low"|"medium"|"high"),
notes (markdown string). Do not include backticks or extra fields.
P
)
user=$(jq -n --arg c "${commits}" --arg d "${diffstat}" --arg f "${changed_files}" \
'{commits:$c, diffstat:$d, files:$f}' | jq -r tostring)
ai_json=$(curl -sS "${AI_API_BASE:-https://api.openai.com}/v1/chat/completions" \
-H "Authorization: Bearer ${AI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg model "${AI_MODEL}" --arg sys "${prompt}" --arg user "${user}" \
'{model:$model, temperature:0.2,
messages:[{"role":"system","content":$sys},{"role":"user","content":$user}]}')" \
| jq -r '.choices[0].message.content' 2>/dev/null || true)
elif [ "${AI_PROVIDER:-}" = "ollama" ] && command -v ollama >/dev/null 2>&1; then
prompt=$(cat <<'P'
You are a release automation assistant. Given commit subjects and a diff summary,
decide the semantic version bump, risk level, and produce clear markdown release notes.
Return STRICT JSON with keys: bump ("major"|"minor"|"patch"), risk ("low"|"medium"|"high"),
notes (markdown string). Do not include backticks or extra fields.
Input:
P
)
input=$(printf "%s\n\nCommits:\n%s\n\nDiffstat:\n%s\n\nFiles:\n%s\n" \
"$prompt" "$commits" "$diffstat" "$changed_files")
# Ollama's /api/generate streams responses; collect final JSON
ai_json=$(curl -sS http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d "$(jq -n --arg model "${OLLAMA_MODEL:-llama3}" --arg p "$input" \
'{model:$model, prompt:$p, stream:false}')" \
| jq -r '.response' 2>/dev/null || true)
fi
# Validate/normalize AI response or fallback
bump=""; risk=""; notes=""
if echo "${ai_json}" | jq -e . >/dev/null 2>&1; then
bump=$(echo "${ai_json}" | jq -r '.bump // empty')
risk=$(echo "${ai_json}" | jq -r '.risk // empty')
notes=$(echo "${ai_json}" | jq -r '.notes // empty')
fi
if [[ ! "${bump}" =~ ^(major|minor|patch)$ ]]; then
bump=$(fallback_bump)
fi
risk=${risk:-"medium"}
if [ -z "${notes}" ]; then
notes="### Changes
$(printf "%s\n" "${details}" | sed 's/^/- /')"
fi
new_tag=$(bump_semver "${last_tag}" "${bump}")
if [[ ! "${new_tag}" =~ ^v ]]; then new_tag="v${new_tag}"; fi
echo "Proposed release:"
echo " From tag: ${last_tag}"
echo " To branch: ${branch}"
echo " Bump: ${bump}"
echo " New tag: ${new_tag}"
echo " Risk: ${risk}"
echo
echo "Release notes preview:"
echo "----------------------"
echo "${notes}"
echo "----------------------"
echo
if [ "${ALLOW_PUSH}" != "true" ]; then
echo "Dry run complete. Set ALLOW_PUSH=true to tag and push."
exit 0
fi
# Write/update CHANGELOG and a release notes file
ts=$(date -u +%Y-%m-%d)
tmp_notes=".release-notes-${new_tag}.md"
echo -e "# ${new_tag} (${ts})\n\n${notes}\n" > "${tmp_notes}"
# Prepend to CHANGELOG.md
if [ -f CHANGELOG.md ]; then
cp CHANGELOG.md CHANGELOG.md.bak
{ echo -e "# Changelog\n"; cat "${tmp_notes}"; sed '1,1d' CHANGELOG.md.bak || true; } > CHANGELOG.md
else
{ echo -e "# Changelog\n"; cat "${tmp_notes}"; } > CHANGELOG.md
fi
git add CHANGELOG.md
git commit -m "docs: update changelog for ${new_tag} [ci skip]" || true
git tag -a "${new_tag}" -m "Release ${new_tag}"
git push --follow-tags
if [ "${CREATE_GH_RELEASE}" = "true" ]; then
gh release create "${new_tag}" -F "${tmp_notes}" -t "${new_tag}" || {
echo "gh release failed; tag was pushed. You can publish manually."
}
fi
echo "Release ${new_tag} created successfully."
How to run:
- Dry run (no tag/push):
./ai-release.sh
- Tag, push, and publish a GitHub Release:
ALLOW_PUSH=true CREATE_GH_RELEASE=true ./ai-release.sh
Notes:
Keep your API keys in CI secrets or a local keychain. Never commit them.
If you use GitLab, skip
ghand publish viacurlto the GitLab Releases API after tagging.
3–5 actionable patterns to make this production‑grade
1) Use human‑in‑the‑loop approvals
- In CI, run the script in dry mode, post the proposed bump/notes as a PR comment, and require a maintainer’s “approve” to re-run with
ALLOW_PUSH=true.
2) Pin and test your model
- Prompt tweaks matter. Pin a specific model (or a local one via Ollama), add a unit test that feeds a known commit set and asserts the JSON schema and bump type.
3) Conventional commits as a strong signal
- Even if you use AI, conventional commits make bumping deterministic. The script falls back to them if the AI returns invalid JSON.
4) Gate high‑risk releases
- If AI returns risk=high, require additional checks (end‑to‑end tests, canary). Example CI gate:
if [ "$RISK" = "high" ]; then
echo "High risk: running extended test job..."
# trigger extra CI stages here
fi
5) Keep everything observable
- Save AI input/output artifacts in CI logs or as workflow artifacts for traceability. Include the tag, commit range, and generated notes.
Real‑world example flow (GitHub Actions)
Job 1 (on push to main): run
ai-release.shin dry mode, add the proposed release notes as a PR/commit comment.Job 2 (manual dispatch): with approver input, run with
ALLOW_PUSH=true CREATE_GH_RELEASE=true.Protect your main branch and require approvals before Job 2.
Conclusion and next steps
AI won’t replace your tests, but it will replace tedious release paperwork. With a few portable tools and a Bash script, you can go from “what changed?” to a signed tag and polished notes in seconds—while keeping full control over when to ship.
Your next step:
Install the prerequisites for your distro
Configure your AI provider (API or local)
Drop
ai-release.shinto a repo and run a dry run
Then iterate: tune the prompt, wire into CI, and add gates for risk. Ship faster, with less toil.