Ctrl K
ComunidadGuías…

scripts/plan-version.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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