mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
feat(release): write the changelog from change files
This commit is contained in:
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CHANGE_DIRECTORY=.changes
|
||||
TYPES=(added changed deprecated removed fixed security maintenance)
|
||||
SECTIONS=('Breaking Changes' Security Added Changed Deprecated Removed Fixed Maintenance)
|
||||
|
||||
BOLD=$'\033[1m' GREEN=$'\033[32m' RED=$'\033[31m' DIM=$'\033[2m' RESET=$'\033[0m'
|
||||
[ -t 1 ] || BOLD='' GREEN='' RED='' DIM='' RESET=''
|
||||
|
||||
heading() { printf '\n %s%s%s\n' "$BOLD" "$*" "$RESET"; }
|
||||
ok() { printf ' %s✔%s %s\n' "$GREEN" "$RESET" "$*"; }
|
||||
die() {
|
||||
printf ' %s✖%s %s\n' "$RED" "$RESET" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
confirm() {
|
||||
local answer
|
||||
printf '\n %s [y/N] ' "$*"
|
||||
read -r answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
trap 'tput cnorm 2> /dev/null || true' EXIT
|
||||
|
||||
main() {
|
||||
read_arguments "$@"
|
||||
if [ -n "$notes" ]; then
|
||||
print_notes
|
||||
return
|
||||
fi
|
||||
[ -n "$kind" ] && [ -n "$summary" ] || ask_questions
|
||||
write_change
|
||||
}
|
||||
|
||||
read_arguments() {
|
||||
kind='' summary='' commit='' breaking='' notes='' details=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--notes) notes=true ;;
|
||||
--breaking) breaking=true ;;
|
||||
--type | --summary | --detail | --commit)
|
||||
[ $# -ge 2 ] || die "$1 has no value, expected one"
|
||||
case "$1" in
|
||||
--type) kind="$2" ;;
|
||||
--summary) summary="$2" ;;
|
||||
--detail) details+=("$2") ;;
|
||||
--commit) commit="$2" ;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
*) die "'$1' is not an option, expected --type, --summary, --detail, --breaking, --commit or --notes" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
ask_questions() {
|
||||
local selected detail
|
||||
[ -t 0 ] || die "--type and --summary are required without a terminal"
|
||||
if [ -z "$kind" ]; then
|
||||
heading "Select the type"
|
||||
selected="$(menu 0 "${TYPES[@]}")"
|
||||
kind="${TYPES[$selected]}"
|
||||
fi
|
||||
if [ -z "$summary" ]; then
|
||||
heading "Summary, one sentence with the heading"
|
||||
printf ' '
|
||||
read -r summary
|
||||
fi
|
||||
heading "Details, an empty line ends them"
|
||||
while printf ' - ' && read -r detail && [ -n "$detail" ]; do
|
||||
details+=("$detail")
|
||||
done
|
||||
if confirm "Breaking change?"; then
|
||||
breaking=true
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
write_change() {
|
||||
local path detail
|
||||
printf '%s\n' "${TYPES[@]}" | grep -qx -- "$kind" || die "the type is '$kind', expected ${TYPES[*]}"
|
||||
[ -n "$summary" ] || die "the summary is empty, expected one sentence"
|
||||
if [ -n "$commit" ]; then
|
||||
git rev-parse --verify --quiet "$commit^{commit}" > /dev/null || die "$commit is not a commit of this repository"
|
||||
commit="$(git rev-parse --short "$commit")"
|
||||
fi
|
||||
mkdir -p "$CHANGE_DIRECTORY"
|
||||
path="$CHANGE_DIRECTORY/$(file_name).md"
|
||||
{
|
||||
printf -- '---\ntype: %s\n' "$kind"
|
||||
[ -z "$breaking" ] || printf 'breaking: true\n'
|
||||
[ -z "$commit" ] || printf 'commit: %s\n' "$commit"
|
||||
printf -- '---\n%s\n' "$summary"
|
||||
[ "${#details[@]}" = 0 ] || printf '\n'
|
||||
for detail in ${details[@]+"${details[@]}"}; do
|
||||
printf -- '- %s\n' "$detail"
|
||||
done
|
||||
} > "$path"
|
||||
ok "$path describes the $BOLD$kind$RESET change"
|
||||
}
|
||||
|
||||
print_notes() {
|
||||
local section entries
|
||||
compgen -G "$CHANGE_DIRECTORY/*.md" > /dev/null || die "$CHANGE_DIRECTORY has no change files"
|
||||
for section in "${SECTIONS[@]}"; do
|
||||
entries="$(awk -v section="$section" '
|
||||
FNR == 1 { fences = 0; type = ""; breaking = 0 }
|
||||
fences < 2 {
|
||||
if ($0 == "---") fences++
|
||||
else if ($1 == "type:") type = $2
|
||||
else if ($0 == "breaking: true") breaking = 1
|
||||
next
|
||||
}
|
||||
NF && tolower(section) == (breaking ? "breaking changes" : type) {
|
||||
prefix = /^- / ? " " : "- "
|
||||
print prefix $0
|
||||
}
|
||||
' "$CHANGE_DIRECTORY"/*.md)"
|
||||
[ -z "$entries" ] || printf '### %s\n\n%s\n\n' "$section" "$entries"
|
||||
done
|
||||
}
|
||||
|
||||
file_name() {
|
||||
local stem name number=2
|
||||
stem="$(printf '%s' "$summary" | LC_ALL=C tr -cs '[:alnum:]' '\n' | LC_ALL=C tr '[:upper:]' '[:lower:]' | awk 'NF && ++count <= 6' | paste -sd - -)"
|
||||
name="$stem"
|
||||
while [ -e "$CHANGE_DIRECTORY/$name.md" ]; do
|
||||
name="$stem-$number"
|
||||
number=$((number + 1))
|
||||
done
|
||||
echo "$name"
|
||||
}
|
||||
|
||||
menu() {
|
||||
local selected="$1" options key i
|
||||
shift
|
||||
options=("$@")
|
||||
tput civis >&2 2> /dev/null || true
|
||||
while true; do
|
||||
for i in "${!options[@]}"; do
|
||||
if [ "$i" = "$selected" ]; then
|
||||
printf ' %s❯ %s%s\n' "$GREEN" "${options[$i]}" "$RESET" >&2
|
||||
else
|
||||
printf ' %s %s%s\n' "$DIM" "${options[$i]}" "$RESET" >&2
|
||||
fi
|
||||
done
|
||||
IFS= read -rsn1 key
|
||||
if [ "$key" = $'\x1b' ]; then
|
||||
IFS= read -rsn2 key
|
||||
if [ "$key" = '[A' ] && [ "$selected" -gt 0 ]; then
|
||||
selected=$((selected - 1))
|
||||
elif [ "$key" = '[B' ] && [ "$selected" -lt $((${#options[@]} - 1)) ]; then
|
||||
selected=$((selected + 1))
|
||||
fi
|
||||
elif [ -z "$key" ]; then
|
||||
break
|
||||
fi
|
||||
printf '\033[%dA\033[J' "${#options[@]}" >&2
|
||||
done
|
||||
tput cnorm >&2 2> /dev/null || true
|
||||
echo "$selected"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+63
-24
@@ -3,9 +3,10 @@ set -euo pipefail
|
||||
|
||||
OPTION="$*"
|
||||
VERSION_FILE=release.json
|
||||
CHANGE_DIRECTORY=.changes
|
||||
SECTIONS=('Breaking Changes' Security Added Changed Deprecated Removed Fixed Maintenance)
|
||||
ARTIFACT=coolify
|
||||
UPSTREAM=
|
||||
BREAKING_CHANGE='^([a-z]+(\([^)]*\))?!|BREAKING[ -]CHANGE): '
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
BOLD=$'\033[1m' BLUE=$'\033[34m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RED=$'\033[31m' DIM=$'\033[2m' RESET=$'\033[0m'
|
||||
@@ -38,6 +39,7 @@ main() {
|
||||
select_version
|
||||
heading "Releasing v$new_version"
|
||||
update_changelog
|
||||
update_change_files
|
||||
update_version
|
||||
commit
|
||||
push
|
||||
@@ -64,9 +66,9 @@ check_repository() {
|
||||
read -r behind ahead <<< "$counts"
|
||||
[ "$behind$ahead" = 00 ] || die "$branch is $ahead commit(s) ahead of and $behind behind origin/$branch, expected it in sync"
|
||||
ok "$branch is clean and in sync with origin/$branch"
|
||||
entries="$(awk '/^## \[Unreleased\]$/ { inside = 1; next } /^## / { inside = 0 } inside && /^- / { count++ } END { print count + 0 }' CHANGELOG.md)"
|
||||
[ "$entries" != 0 ] || die "CHANGELOG.md has no entries under ## [Unreleased], describe the changes first"
|
||||
ok "CHANGELOG.md lists $BOLD$entries$RESET change(s) under [Unreleased], the notes of this release"
|
||||
entries="$(git ls-files "$CHANGE_DIRECTORY" | wc -l | tr -d ' ')"
|
||||
[ "$entries" != 0 ] || die "$CHANGE_DIRECTORY has no change files, describe the changes first with scripts/change.sh"
|
||||
ok "$CHANGE_DIRECTORY lists $BOLD$entries$RESET change(s), the notes of this release"
|
||||
}
|
||||
|
||||
read_versions() {
|
||||
@@ -81,19 +83,17 @@ read_versions() {
|
||||
}
|
||||
|
||||
read_changes() {
|
||||
local tag range hash subject
|
||||
tag="$(git describe --tags --abbrev=0 --match "v${UPSTREAM:+*-v}$stable" 2> /dev/null)" || true
|
||||
range="${tag:+$tag..}HEAD"
|
||||
local file
|
||||
required="patch"
|
||||
if [ -n "$(git log -E --grep="$BREAKING_CHANGE" --format=%h "$range")" ]; then
|
||||
if grep -qx 'breaking: true' "$CHANGE_DIRECTORY"/*.md; then
|
||||
required=major
|
||||
[ "${stable%%.*}" != 0 ] || required=minor
|
||||
elif git log --format=%s "$range" | awk '/^feat(\([^)]*\))?!?: / && !/^feat\(release\): bump / { found = 1 } END { exit !found }'; then
|
||||
elif grep -qxE 'type: (added|changed|deprecated|removed)' "$CHANGE_DIRECTORY"/*.md; then
|
||||
required=minor
|
||||
fi
|
||||
info "the commits since ${tag:-the first commit} need a $BOLD$required$RESET release"
|
||||
git log -E --grep="$BREAKING_CHANGE" --format='%h %s' "$range" | while read -r hash subject; do
|
||||
warn "$DIM$hash$RESET $subject is a breaking change"
|
||||
info "the change files need a $BOLD$required$RESET release"
|
||||
for file in $(grep -lx 'breaking: true' "$CHANGE_DIRECTORY"/*.md || true); do
|
||||
warn "$file is a breaking change"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -152,19 +152,58 @@ select_version() {
|
||||
}
|
||||
|
||||
update_changelog() {
|
||||
local today
|
||||
if [ "$channel" = preview ]; then
|
||||
info "CHANGELOG.md keeps the entries under [Unreleased] until the stable release dates them"
|
||||
return
|
||||
fi
|
||||
today="$(date +%Y-%m-%d)"
|
||||
awk -v heading="## [v$new_version] - $today" '
|
||||
/^#/ && NR > 1 && previous != "" { print "" }
|
||||
{ print; previous = $0 }
|
||||
/^## \[Unreleased\]$/ { print ""; print heading; previous = heading }
|
||||
local heading=''
|
||||
[ "$channel" = preview ] || heading="## [v$new_version] - $(date +%Y-%m-%d)"
|
||||
print_notes > "$WORK_DIR/notes.md"
|
||||
awk -v heading="$heading" -v notes="$WORK_DIR/notes.md" '
|
||||
/^## / { inside = 0 }
|
||||
/^## \[Unreleased\]$/ {
|
||||
inside = 1
|
||||
print
|
||||
print ""
|
||||
if (heading != "") { print heading; print "" }
|
||||
while ((getline line < notes) > 0) print line
|
||||
next
|
||||
}
|
||||
!inside
|
||||
' CHANGELOG.md > "$WORK_DIR/CHANGELOG.md"
|
||||
cp "$WORK_DIR/CHANGELOG.md" CHANGELOG.md
|
||||
ok "CHANGELOG.md dates the unreleased entries as ${BOLD}v$new_version$RESET, $today"
|
||||
ok "CHANGELOG.md lists the changes under $BOLD${heading:-## [Unreleased]}$RESET"
|
||||
}
|
||||
|
||||
print_notes() {
|
||||
local section entries
|
||||
for section in "${SECTIONS[@]}"; do
|
||||
entries="$(awk -v section="$section" '
|
||||
FNR == 1 { fences = 0; type = ""; breaking = 0 }
|
||||
fences < 2 {
|
||||
if ($0 == "---") fences++
|
||||
else if ($1 == "type:") type = $2
|
||||
else if ($0 == "breaking: true") breaking = 1
|
||||
next
|
||||
}
|
||||
NF && tolower(section) == (breaking ? "breaking changes" : type) {
|
||||
prefix = /^- / ? " " : "- "
|
||||
print prefix $0
|
||||
}
|
||||
' "$CHANGE_DIRECTORY"/*.md)"
|
||||
[ -z "$entries" ] || printf '### %s\n\n%s\n\n' "$section" "$entries"
|
||||
done
|
||||
}
|
||||
|
||||
update_change_files() {
|
||||
local file
|
||||
if [ "$channel" = stable ]; then
|
||||
rm "$CHANGE_DIRECTORY"/*.md
|
||||
ok "$CHANGE_DIRECTORY is empty, CHANGELOG.md lists the changes now"
|
||||
return
|
||||
fi
|
||||
for file in "$CHANGE_DIRECTORY"/*.md; do
|
||||
! grep -q '^version: ' "$file" || continue
|
||||
awk -v version="$new_version" '/^---$/ && ++fences == 2 { print "version: " version } { print }' "$file" > "$WORK_DIR/change.md"
|
||||
cp "$WORK_DIR/change.md" "$file"
|
||||
done
|
||||
ok "$CHANGE_DIRECTORY names ${BOLD}v$new_version$RESET in the change files that had no version"
|
||||
}
|
||||
|
||||
update_version() {
|
||||
@@ -174,7 +213,7 @@ update_version() {
|
||||
}
|
||||
|
||||
commit() {
|
||||
git add "$VERSION_FILE" CHANGELOG.md
|
||||
git add --all -- "$VERSION_FILE" CHANGELOG.md "$CHANGE_DIRECTORY"
|
||||
git commit --quiet -S --message "feat(release): bump $ARTIFACT version to v$new_version"
|
||||
ok "signed commit $DIM$(git rev-parse --short HEAD)$RESET $(git log -1 --format=%s)"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
||||
|
||||
OPTION="$*"
|
||||
VERSION_FILE=release.json
|
||||
CHANGE_DIRECTORY=.changes
|
||||
|
||||
BOLD=$'\033[1m' BLUE=$'\033[34m' GREEN=$'\033[32m' RED=$'\033[31m' DIM=$'\033[2m' RESET=$'\033[0m'
|
||||
|
||||
@@ -26,7 +27,7 @@ main() {
|
||||
[ -z "$OPTION" ] || die "'$OPTION' is not an option, expected none"
|
||||
check_repository
|
||||
heading "Undoing $(git log -1 --format='%h %s')"
|
||||
info "resets the commit and restores $VERSION_FILE and CHANGELOG.md"
|
||||
info "resets the commit and restores $VERSION_FILE, CHANGELOG.md and $CHANGE_DIRECTORY"
|
||||
confirm "Continue?" || die "aborted, nothing changed"
|
||||
undo_commit
|
||||
}
|
||||
@@ -35,18 +36,18 @@ check_repository() {
|
||||
local branch
|
||||
git rev-parse --is-inside-work-tree > /dev/null 2>&1 || die "$PWD is not a git repository"
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
git diff --quiet HEAD~1 HEAD -- . ":(exclude)$VERSION_FILE" ":(exclude)CHANGELOG.md" || die "the last commit changes more than $VERSION_FILE and CHANGELOG.md, expected a release commit"
|
||||
git diff --quiet HEAD~1 HEAD -- . ":(exclude)$VERSION_FILE" ":(exclude)CHANGELOG.md" ":(exclude)$CHANGE_DIRECTORY" || die "the last commit changes more than $VERSION_FILE, CHANGELOG.md and $CHANGE_DIRECTORY, expected a release commit"
|
||||
[ "$(versions_at HEAD~1)" != "$(versions_at HEAD)" ] || die "the last commit keeps the versions in $VERSION_FILE at $(versions_at HEAD), expected a release commit"
|
||||
[ -z "$(git status --porcelain -- "$VERSION_FILE" CHANGELOG.md)" ] || die "$VERSION_FILE or CHANGELOG.md has uncommitted changes, commit or stash them first"
|
||||
[ -z "$(git status --porcelain -- "$VERSION_FILE" CHANGELOG.md "$CHANGE_DIRECTORY")" ] || die "$VERSION_FILE, CHANGELOG.md or $CHANGE_DIRECTORY has uncommitted changes, commit or stash them first"
|
||||
git fetch --quiet origin || die "could not fetch origin"
|
||||
[ "$(git rev-list --count "origin/$branch..HEAD")" -ge 1 ] || die "the release commit is on origin/$branch already, nothing to undo locally"
|
||||
}
|
||||
|
||||
undo_commit() {
|
||||
git reset --quiet --soft HEAD~1
|
||||
git restore --staged --worktree "$VERSION_FILE" CHANGELOG.md
|
||||
git restore --staged --worktree "$VERSION_FILE" CHANGELOG.md "$CHANGE_DIRECTORY"
|
||||
ok "reset to $DIM$(git rev-parse --short HEAD)$RESET $(git log -1 --format=%s)"
|
||||
ok "$VERSION_FILE is back to $BOLD$(versions_at HEAD)$RESET, the entries are back under [Unreleased]"
|
||||
ok "$VERSION_FILE is back to $BOLD$(versions_at HEAD)$RESET, the change files are back"
|
||||
}
|
||||
|
||||
versions_at() {
|
||||
|
||||
Reference in New Issue
Block a user