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 gh and publish via curl to 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.sh in 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.sh into 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.