Files
nucleic/scripts/sync-version.sh
T

166 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Sync the release version (./VERSION) DOWN the channel chain so no less-stable
# branch ever trails a more-stable one.
#
# scripts/sync-version.sh # dry run: print what would change
# scripts/sync-version.sh --apply # write a VERSION-bump commit on each branch that is behind
#
# Channels flow feature -> dev -> canary -> staging -> rc -> main (least -> most stable;
# see BUILD.md). Each branch feeds one channel and is released independently, so a more-stable
# branch can ship a newer version than a less-stable one that hasn't been released in a while.
# That is the case this fixes: a branch's marketing version must never be *behind* a more-stable
# branch above it.
#
# staging released 0.3.1 while dev sat at 0.2.1 -> dev is fast-forwarded to 0.3.1
# (build number raised too, so
# CFBundleVersion stays monotonic)
# dev already ahead at 0.3.2 vs staging 0.3.1 -> dev is left untouched (never downgraded)
#
# Each branch is fast-forwarded to the highest marketing version among every branch listed
# before it (more stable) in the chain. Only ./VERSION is rewritten, one commit per branch:
# a non-checked-out branch is updated in place via git plumbing (no working-tree churn); the
# currently checked-out branch, if it needs a bump, is committed normally (needs a clean tree).
# Nothing is pushed — push the updated branches with your usual pushes.
#
# Branch order (most -> least stable) can be overridden with NUCLEIC_CHANNEL_BRANCHES.
# Missing branches, and branches without a readable ./VERSION, are skipped.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
APPLY=0
case "${1:-}" in
""|-n|--dry-run) APPLY=0 ;;
--apply) APPLY=1 ;;
-h|--help)
echo "usage: $0 [--dry-run|--apply]"
echo " (no args / --dry-run) show which branches would be fast-forwarded"
echo " --apply write the VERSION-bump commits"
exit 0 ;;
*) echo "usage: $0 [--dry-run|--apply]" >&2; exit 2 ;;
esac
# Most-stable -> least-stable. Each branch is fast-forwarded up to the max marketing version
# of every branch before it in this list.
# shellcheck disable=SC2206
BRANCHES=(${NUCLEIC_CHANNEL_BRANCHES:-main rc staging canary dev})
CUR="$(git symbolic-ref --quiet --short HEAD || true)"
branch_exists() { git show-ref --verify --quiet "refs/heads/$1"; }
read_field() { git show "refs/heads/$1:VERSION" 2>/dev/null | sed -n "s/^$2=[[:space:]]*//p" | tail -n1 || true; }
# Compare two major.minor.patch strings; echo 1 (a>b), 0 (a==b) or -1 (a<b).
ver_cmp() {
local IFS=.
# shellcheck disable=SC2206
local -a A=($1) B=($2)
local i x y
for i in 0 1 2; do
x=${A[i]:-0}; y=${B[i]:-0}
if ((10#$x > 10#$y)); then echo 1; return; fi
if ((10#$x < 10#$y)); then echo -1; return; fi
done
echo 0
}
# Build the plan: for each branch, if its marketing version trails the running-max ("floor")
# of the more-stable branches above it, record a fast-forward. Never lowers a branch that is
# already ahead, and never touches the floor-setting (more-stable) branches themselves.
PLAN="" # rows: branch|oldmkt|oldbuild|newmkt|newbuild|src
CUR_IN_PLAN=0
FMKT=""; FBUILD=""; FSRC="" # floor: highest marketing version + build seen so far, and its source branch
for b in "${BRANCHES[@]}"; do
if ! branch_exists "$b"; then
echo " . $b: no local branch, skipping" >&2
continue
fi
mkt="$(read_field "$b" NUCLEIC_MARKETING_VERSION)"
build="$(read_field "$b" NUCLEIC_BUILD_NUMBER)"
if [ -z "$mkt" ] || [ -z "$build" ]; then
echo " . $b: no readable VERSION, skipping" >&2
continue
fi
if [ -z "$FMKT" ]; then # seed the floor from the most-stable branch that has a VERSION
FMKT="$mkt"; FBUILD="$build"; FSRC="$b"
continue
fi
if [ "$(ver_cmp "$mkt" "$FMKT")" = "-1" ]; then # behind the floor -> fast-forward up to it
newmkt="$FMKT"
if [ "$build" -gt "$FBUILD" ]; then newbuild="$build"; else newbuild="$FBUILD"; fi
PLAN="${PLAN}${b}|${mkt}|${build}|${newmkt}|${newbuild}|${FSRC}
"
if [ "$b" = "$CUR" ]; then CUR_IN_PLAN=1; fi
fi
# Advance the floor to the running max (a branch that is ahead becomes the new floor for those below it).
if [ "$(ver_cmp "$mkt" "$FMKT")" = "1" ]; then FMKT="$mkt"; FSRC="$b"; fi
if [ "$build" -gt "$FBUILD" ]; then FBUILD="$build"; fi
done
if [ -z "$PLAN" ]; then
echo "OK All channel branches are in version sync — nothing to do."
exit 0
fi
printf '%-9s %-18s %-18s %s\n' "branch" "current" "-> new" "(catch up to)"
while IFS='|' read -r b omkt obuild nmkt nbuild src; do
[ -n "$b" ] || continue
printf '%-9s %-18s -> %-18s %s\n' "$b" "$omkt ($obuild)" "$nmkt ($nbuild)" "$src"
done <<EOF
$PLAN
EOF
if [ "$APPLY" != "1" ]; then
echo
echo "Dry run. Re-run with --apply (or 'make sync-versions APPLY=1') to write these commits."
exit 0
fi
# Applying. If the checked-out branch itself needs a bump we commit it through the working
# tree, so require a clean tree in that case.
if [ "$CUR_IN_PLAN" = "1" ] && ! git diff-index --quiet HEAD -- 2>/dev/null; then
echo "sync-version: working tree is dirty and the current branch ($CUR) needs a bump — commit or stash first." >&2
exit 1
fi
echo
while IFS='|' read -r b omkt obuild nmkt nbuild src; do
[ -n "$b" ] || continue
msg="sync($b): VERSION $omkt ($obuild) -> $nmkt ($nbuild) — catch up to $src"
if [ "$b" = "$CUR" ]; then
sed -E -i.bak \
-e "s/^NUCLEIC_MARKETING_VERSION=.*/NUCLEIC_MARKETING_VERSION=$nmkt/" \
-e "s/^NUCLEIC_BUILD_NUMBER=.*/NUCLEIC_BUILD_NUMBER=$nbuild/" VERSION
rm -f VERSION.bak
git add VERSION
git commit -q -m "$msg"
else
# Rewrite VERSION on the branch tip without touching the working tree or index.
mode="$(git ls-tree "refs/heads/$b" VERSION | awk '{print $1}')"; [ -n "$mode" ] || mode=100644
blob="$(git show "refs/heads/$b:VERSION" \
| sed -E -e "s/^NUCLEIC_MARKETING_VERSION=.*/NUCLEIC_MARKETING_VERSION=$nmkt/" \
-e "s/^NUCLEIC_BUILD_NUMBER=.*/NUCLEIC_BUILD_NUMBER=$nbuild/" \
| git hash-object -w --stdin)"
tmpidx="$(mktemp)"
GIT_INDEX_FILE="$tmpidx" git read-tree "refs/heads/$b"
GIT_INDEX_FILE="$tmpidx" git update-index --cacheinfo "$mode,$blob,VERSION"
tree="$(GIT_INDEX_FILE="$tmpidx" git write-tree)"
rm -f "$tmpidx"
commit="$(git commit-tree "$tree" -p "refs/heads/$b" -m "$msg")"
git update-ref "refs/heads/$b" "$commit" "refs/heads/$b"
fi
echo " OK $b -> $nmkt ($nbuild)"
done <<EOF
$PLAN
EOF
echo
echo "Version-sync commits written. Push the updated branches to publish them (e.g. git push origin dev canary)."