NuvioMobile/scripts/generate-release-notes.sh
2026-07-20 18:57:42 +05:30

136 lines
4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
from_ref=""
to_ref="HEAD"
repository="${GITHUB_REPOSITORY:-}"
offline=false
exclude_commits="${RELEASE_NOTES_EXCLUDE_COMMITS:-}"
usage() {
echo "Usage: $0 --from <commit> [--to <commit>] [--repository <owner/repo>] [--exclude <hashes>] [--offline]" >&2
}
while [[ $# -gt 0 ]]; do
case "$1" in
--from)
from_ref="${2:-}"
shift 2
;;
--to)
to_ref="${2:-}"
shift 2
;;
--repository)
repository="${2:-}"
shift 2
;;
--exclude)
exclude_commits="${exclude_commits} ${2:-}"
shift 2
;;
--offline)
offline=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 1
;;
esac
done
if [[ -z "$from_ref" ]]; then
usage
exit 1
fi
git cat-file -e "${from_ref}^{commit}" 2>/dev/null || {
echo "Unknown starting commit: ${from_ref}" >&2
exit 1
}
git cat-file -e "${to_ref}^{commit}" 2>/dev/null || {
echo "Unknown ending commit: ${to_ref}" >&2
exit 1
}
is_excluded_hash() {
local commit="$1"
local excluded
for excluded in ${exclude_commits//,/ }; do
[[ -n "$excluded" ]] || continue
if [[ "$commit" == "$excluded"* ]]; then
return 0
fi
done
return 1
}
is_release_note() {
local subject_lower
local version_bump_pattern='^(bump([[:space:]].*)?version|version[[:space:]]+bump)([[:space:]].*)?$'
local cleanup_pattern='^cleanup([[:space:][:punct:]].*)?$'
local conventional_noise_pattern='^(build|chore|ci|docs|style|test)(\([^)]*\))?:'
subject_lower="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
[[ "$subject_lower" != *"[skip release notes]"* ]] || return 1
[[ ! "$subject_lower" =~ $version_bump_pattern ]] || return 1
[[ ! "$subject_lower" =~ $cleanup_pattern ]] || return 1
[[ ! "$subject_lower" =~ $conventional_noise_pattern ]] || return 1
return 0
}
resolve_username() {
local commit="$1"
local author_name="$2"
local author_email="$3"
local username=""
if [[ "$author_email" =~ ^[0-9]+\+([^@]+)@users\.noreply\.github\.com$ ]]; then
username="${BASH_REMATCH[1]}"
elif [[ "$author_email" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then
username="${BASH_REMATCH[1]}"
elif [[ "$offline" == false && -n "$repository" && -n "${GH_TOKEN:-}" ]] && command -v gh >/dev/null 2>&1; then
username="$(gh api "repos/${repository}/commits/${commit}" --jq '.author.login // empty' 2>/dev/null || true)"
if [[ -z "$username" ]]; then
username="$(
gh api \
-H 'Accept: application/vnd.github+json' \
"repos/${repository}/commits/${commit}/pulls" \
--jq '.[0].user.login // empty' \
2>/dev/null \
|| true
)"
fi
fi
if [[ -z "$username" ]]; then
username="$(printf '%s' "$author_name" | tr -cd '[:alnum:]_-')"
fi
printf '%s' "${username:-unknown}"
}
seen_subjects=$'\n'
separator=$'\x1f'
while IFS="$separator" read -r commit short_hash subject author_name author_email; do
[[ -n "$commit" ]] || continue
is_excluded_hash "$commit" && continue
is_release_note "$subject" || continue
normalized_subject="$(printf '%s' "$subject" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]]+/ /g; s/[[:space:].]+$//')"
[[ "$seen_subjects" != *$'\n'"$normalized_subject"$'\n'* ]] || continue
seen_subjects+="${normalized_subject}"$'\n'
display_subject="$(printf '%s' "$subject" | sed -E 's/[[:space:]]+$//; s/\.$//')"
username="$(resolve_username "$commit" "$author_name" "$author_email")"
printf '%s %s @%s \n' "$short_hash" "$display_subject" "$username"
done < <(
git log "${from_ref}..${to_ref}" --no-merges \
--format="%H${separator}%h${separator}%s${separator}%an${separator}%ae"
)