Files
coolify/scripts/plan-version.sh
T
peaklabs-dev 2b4a4bff08 fix(release): name the local tags that differ from origin
- a local tag that differs from origin, such as a tag moved on origin before the tag protection rule, failed the fetch with "could not fetch origin" and no reason, as --quiet also hid the rejected tags
2026-11-02 16:50:05 +01:00

365 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
OPTION="$*"
BRANCH="${BRANCH:-}"
PULL_REQUEST="${PULL_REQUEST:-}"
NIGHTLY_VERSION="${NIGHTLY_VERSION:-}"
NIGHTLY_REVISION="${NIGHTLY_REVISION:-}"
MAIN_VERSION="${MAIN_VERSION:-}"
MAIN_REVISION="${MAIN_REVISION:-}"
VERSION_FILE=release.json
ARTIFACT=coolify
UPSTREAM_FILE=upstream.json
UPSTREAM=
BOLD=$'\033[1m' BLUE=$'\033[34m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RED=$'\033[31m' RESET=$'\033[0m'
info() { [ -n "$OPTION" ] || printf ' %sℹ%s %s\n' "$BLUE" "$RESET" "$*" >&2; }
ok() { [ -n "$OPTION" ] || printf ' %s✔%s %s\n' "$GREEN" "$RESET" "$*" >&2; }
warn() { [ -n "$OPTION" ] || printf ' %s⚠%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
die() {
printf ' %s✖%s %s\n' "$RED" "$RESET" "$*" >&2
exit 1
}
cd "$(dirname "$0")/.."
main() {
[[ "$OPTION" =~ ^(--build|--release|--tags|--nightly|--main)?$ ]] || die "'$OPTION' is not an option, expected --build, --release, --tags, --nightly, --main or none"
read_version
print_build_version
fetch_origin
print_tags
print_next_build
check_branch
check_version
select_releases
check_unreleased
print_upcoming_versions
print_releases
}
read_version() {
branch="${BRANCH:-$(git branch --show-current)}"
channel=stable
[[ ! "$branch" =~ ^(next|release/v[0-9]+\.[0-9]+)$ ]] || channel=preview
version="$(artifact_version "$channel")"
[ -n "$version" ] || die "$VERSION_FILE has no $channel version of $ARTIFACT"
}
print_build_version() {
[ "$OPTION" = --build ] || return 0
if [ -n "$PULL_REQUEST" ] && [ "$channel" = preview ]; then
tags="$(git ls-remote --tags --refs origin | sed 's|.*refs/tags/||' | filter_release_tags | sort -V)" || die "could not list the tags of origin"
line="${branch#release/v}"
[ "$line" != "$branch" ] || line=""
revision=HEAD
select_newest_stable
select_preview_base
version="$base"
fi
echo "$version${PULL_REQUEST:+-pr.$PULL_REQUEST}"
exit 0
}
fetch_origin() {
local current behind fetched stale
[ "$(git rev-parse --is-shallow-repository)" = false ] || die "the clone is shallow, run git fetch --unshallow first"
fetched="$(git fetch --porcelain --no-progress --prune --tags origin)" || {
stale="$(awk '$1 == "!" { sub("^refs/tags/", "", $4); print $4 }' <<< "$fetched" | paste -sd ' ' -)"
[ -z "$stale" ] || die "the local tag(s) $stale differ from origin, check them with git ls-remote --tags origin, then run git fetch --tags --force origin"
die "could not fetch origin"
}
current="$(git branch --show-current)"
if [ -n "$current" ] && behind="$(git rev-list --count "HEAD..origin/$current" 2> /dev/null)"; then
[ "$behind" = 0 ] || die "$current is $behind commit(s) behind origin/$current, pull first"
fi
tags="$(release_tags | sort -V)"
}
print_tags() {
local released
[ "$OPTION" = --tags ] || return 0
released="$(release_tags --points-at HEAD)"
moved_tags "$(grep -vxF -f <(echo "$released") <<< "$tags" || true)" "$tags" \
| jq -Rnc '[inputs | split(" ") | {(.[0]): .[1]}] | add // {}'
exit 0
}
print_next_build() {
local build current_version current_revision
[[ "$OPTION" =~ ^--(nightly|main)$ ]] || return 0
if [ "$OPTION" = --main ]; then
branch=main
revision="$(git rev-parse origin/main)"
current_version="$MAIN_VERSION" current_revision="$MAIN_REVISION"
else
select_nightly_branch
current_version="$NIGHTLY_VERSION" current_revision="$NIGHTLY_REVISION"
fi
[ "$revision" != "$current_revision" ] || exit 0
if [ "$OPTION" = --main ]; then
base="$(artifact_version stable <(git show "$revision:$VERSION_FILE" 2> /dev/null))"
[ -n "$base" ] || die "main has no stable version of $ARTIFACT in $VERSION_FILE, expected the release process there"
else
select_preview_base
fi
[ -z "$UPSTREAM" ] || current_version="${current_version#*-v}"
build="$base.$(date -u +%Y%m%d)"
if [[ "$current_version" == "$build".* ]]; then
build+=".$((${current_version##*.} + 1))"
else
build+=".1"
fi
upstream_lines <(git show "$revision:$UPSTREAM_FILE") | while read -r name upstream; do
jq -nc --arg name "$name" --arg version "$upstream" --arg tag "v${upstream:+$upstream-v}$build" '{$name, $version, $tag}'
done | jq -sc --arg branch "$branch" --arg revision "$revision" --arg image "$build" '{$branch, $revision, $image, lines: .}'
exit 0
}
select_nightly_branch() {
select_newest_stable
line="$(git for-each-ref --format='%(refname:lstrip=4)' 'refs/remotes/origin/release/v*' | sed -n 's/^v\([0-9]*\.[0-9]*\)$/\1/p' | sort -V \
| while read -r release; do ! newer "$release.0" "$stable" || { echo "$release" && break; }; done)"
branch="${line:+release/v$line}"
branch="${branch:-next}"
revision="$(git rev-parse "origin/$branch")"
}
check_branch() {
[[ "$branch" =~ ^(main|next|v([0-9]+(\.[0-9]+)?)\.x|release/v([0-9]+\.[0-9]+))$ ]] || die "${branch:-a detached HEAD} releases nothing"
line="${BASH_REMATCH[2]}${BASH_REMATCH[4]}"
case "$branch" in
main) ok "main releases stable versions" ;;
next) ok "next releases prereleases" ;;
release/*) ok "$branch releases prereleases of $line.0" ;;
*) ok "$branch releases stable $line.x versions" ;;
esac
}
check_version() {
local from
if [ "$channel" = stable ]; then
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "$VERSION_FILE sets '$version', expected major.minor.patch"
[ -z "$line" ] || [[ "$version" == "$line".* ]] || die "$VERSION_FILE sets $version, expected a $line.x version"
else
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$ ]] \
|| die "$VERSION_FILE sets '$version', expected major.minor.patch with an optional -alpha.N, -beta.N or -rc.N"
[ -z "$line" ] || [ "${version%%-*}" = "$line.0" ] || die "$VERSION_FILE sets $version, expected a prerelease of $line.0"
fi
last="$(release_tags --merged HEAD | tag_versions | awk -v channel="$channel" 'channel == "preview" || !/-/' | newest)"
earlier="$(with_version "$version" <<< "$tags" | tail -n 1)"
publish=false
if newer "$last" "$version"; then
info "$VERSION_FILE sets $version, $branch released $last already"
elif [ "$version" = 0.0.0 ]; then
info "$VERSION_FILE sets 0.0.0, $branch releases nothing before scripts/release.sh sets the first version"
elif [ "$channel" = preview ] && [[ ! "$version" =~ -[a-z]+\.[1-9] ]]; then
info "$VERSION_FILE sets $version, $branch publishes numbered prereleases only"
elif [ "$channel" = preview ] && [ -n "$earlier" ]; then
ok "$VERSION_FILE sets $BOLD$version$RESET"
info "$earlier is published, $branch publishes a prerelease once"
else
from="${last:+$last → }"
[ "$version" != "$last" ] || from=
ok "$VERSION_FILE sets $from$BOLD$version$RESET"
publish=true
fi
if [ "$publish" = true ] && taken "${version%%-*}"; then
die "$VERSION_FILE sets $version, ${version%%-*} is taken by $holder"
fi
if [ "$publish" = true ] && in_progress "${version%%-*}"; then
warn "${version%%-*} is in progress, held by $holder"
fi
}
select_releases() {
local name sqlite tag previous older moves listed added=""
releases=
[ "$publish" = true ] || return 0
while read -r name sqlite; do
tag="v${sqlite:+$sqlite-v}$version"
if grep -qxF "$tag" <<< "$tags"; then
info "$tag is published"
continue
fi
previous=
while read -r older; do
! newer "$sqlite" "$older" || previous="$older"
done < <(with_version "$version" <<< "$tags" | sed -nE 's/^v(.*)-v.*/\1/p')
added+="$tag"$'\n'
releases+="$(jq -nc --arg name "$name" --arg version "$sqlite" --arg image "$version" --arg tag "$tag" --arg previous "$previous" \
'{$name, $version, $image, $tag, $previous}')"$'\n'
done < <(upstream_lines)
moves="$(moved_tags "$tags" "$tags"$'\n'"$added")"
for tag in $added; do
listed="$(moved_to "$tag")"
ok "$BOLD$tag$RESET is new, a push publishes it${listed:+ and moves $listed}"
done
awk 'NF { print $2 }' <<< "$moves" | sort -u | while read -r tag; do
grep -qxF "$tag" <<< "$added" || info "a push moves $(moved_to "$tag") to $tag"
done
}
check_unreleased() {
local count
[ -n "$releases" ] && [ -n "$earlier" ] || return 0
count="$(git rev-list --no-merges "$earlier..HEAD" -- src | grep -cvxF -f <(git rev-list "$earlier..HEAD" -- "$UPSTREAM_FILE") || true)"
[ "$count" = 0 ] || die "$count commit(s) since $earlier change src/ and would ship as $version, release them first with scripts/release.sh"
}
print_upcoming_versions() {
local base stable upcoming
[ -z "$releases" ] || return 0
if [ "$channel" = stable ]; then
base="$version"
! newer "$last" "$base" || base="$last"
case "$branch" in
main) upcoming="$(next_version "$base" patch), $(next_version "$base" minor) or $(next_version "$base" major)" ;;
v*.*.x) upcoming="$(next_version "$base" patch)" ;;
*) upcoming="$(next_version "$base" patch) or $(next_version "$base" minor)" ;;
esac
[ "$base" != 0.0.0 ] || upcoming="$(next_version "$base" minor) or $(next_version "$base" major)"
elif [ -n "$line" ]; then
upcoming="a prerelease of $line.0"
else
stable="$(release_tags --merged HEAD | tag_versions | awk '!/-/' | newest)"
stable="${stable:-$(artifact_version stable)}"
upcoming="a prerelease of $(next_version "$stable" minor) or $(next_version "$stable" major)"
fi
info "the upcoming version on $branch is $upcoming, scripts/release.sh sets it"
}
print_releases() {
[ "$OPTION" = --release ] || return 0
jq -sc . <<< "$releases"
}
select_newest_stable() {
stable="$(tag_versions <<< "$tags" | awk '!/-/' | newest)"
stable="${stable:-$(artifact_version stable)}"
}
select_preview_base() {
version="$(artifact_version preview <(git show "$revision:$VERSION_FILE"))"
if [ -n "$line" ]; then
base="$line.0-alpha.0"
[[ "$version" != "$line.0-"* ]] || base="$version"
elif newer "$version" "$stable"; then
base="$version"
[[ "$version" == *-* ]] || base="$version-alpha.0"
else
base="$(next_version "$stable" minor)-alpha.0"
fi
}
artifact_version() {
jq -r --arg channel "$1" --arg artifact "$ARTIFACT" '.[$channel][$artifact] // empty' "${2:-$VERSION_FILE}"
}
upstream_lines() {
if [ -z "$UPSTREAM" ]; then
echo latest
return
fi
jq -r --arg upstream "$UPSTREAM" '.[$upstream] | to_entries[] | "\(.key) \(.value.version)"' "${1:-$UPSTREAM_FILE}"
}
next_version() {
local major minor patch
IFS=. read -r major minor patch <<< "${1%%-*}"
while true; do
case "$2" in
patch) patch=$((patch + 1)) ;;
minor) minor=$((minor + 1)) patch=0 ;;
major) major=$((major + 1)) minor=0 patch=0 ;;
esac
taken "$major.$minor.$patch" || break
done
echo "$major.$minor.$patch"
}
taken() {
local tag
for tag in $(release_tags "*v$1"); do
holder="$tag, which $branch does not contain"
git merge-base --is-ancestor "$tag" HEAD || return 0
done
[ "$channel" = preview ] && in_progress "$1"
}
in_progress() {
local tag release="release/v${1%.0}"
if [ "$release" != "$branch" ] && git rev-parse --quiet --verify "origin/$release" > /dev/null; then
holder="$release"
[ "$channel" = stable ] || return 0
git merge-base --is-ancestor "origin/$release" HEAD || return 0
fi
for tag in $(release_tags "*v$1-*"); do
holder="$tag, which $branch does not contain"
git merge-base --is-ancestor "$tag" HEAD || return 0
done
return 1
}
moved_tags() {
moving_tags <<< "$2" | grep -vxF -f <(moving_tags <<< "$1") || true
}
moving_tags() {
local tags releases latest preview base image sqlite prefix seen=" "
tags="$(cat)"
releases="$(sed -nE 's/^v((.*)-v)?([^-]*)$/\3 \2/p' <<< "$tags" | sort -k1,1rV -k2,2rV)"
latest="$(awk 'NR == 1 && NF { print "v" ($2 == "" ? "" : $2 "-v") $1 }' <<< "$releases")"
preview="$latest"
while read -r base; do
if newer "$base" "${latest##*v}"; then
preview="$(tag_versions <<< "$tags" | grep "^$base-" | newest)"
preview="$(with_version "$preview" <<< "$tags" | sort -V | tail -n 1)"
break
fi
done < <(tag_versions <<< "$tags" | sed -n 's/-.*//p' | sort -uV)
[ -z "$latest" ] || echo "latest $latest"
[ -z "$preview" ] || echo "preview $preview"
while read -r image sqlite; do
prefix="$sqlite"
while [ -n "$prefix" ] && [[ "$seen" != *" $prefix "* ]]; do
echo "$prefix v$sqlite-v$image"
seen+="$prefix "
[[ "$prefix" == *.* ]] || break
prefix="${prefix%.*}"
done
done <<< "$releases"
}
release_tags() {
git tag --list "$@" | filter_release_tags
}
filter_release_tags() {
grep -E "^v${UPSTREAM:+[^-]+-v}[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$" || true
}
tag_versions() {
sed 's/.*v//'
}
with_version() {
awk -v version="$1" '{ tagged = $0; sub(/.*v/, "", tagged) } tagged == version'
}
moved_to() {
awk -v tag="$1" '$2 == tag { print $1 }' <<< "$moves" | paste -sd , - | sed 's/,/, /g; s/\(.*\), /\1 and /'
}
newer() {
[ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | newest)" = "$1" ]
}
newest() {
sed 's/-/~/' | sort -V | sed 's/~/-/' | tail -n 1
}
main