From d5d15e8f411d079fdbb785f32e5b1b9f6bc9d3e4 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Wed, 1 Jul 2026 20:02:28 -0700 Subject: [PATCH] Version Number Sync Adjustment Nucleic-Session: 6D0F2A29-65AB-40D8-BD83-A863DEE7E4AF Co-authored-by: Nucleic --- scripts/sync-version.sh | 165 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 scripts/sync-version.sh diff --git a/scripts/sync-version.sh b/scripts/sync-version.sh new file mode 100644 index 00000000..d23d7d53 --- /dev/null +++ b/scripts/sync-version.sh @@ -0,0 +1,165 @@ +#!/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; } + +# Compare two major.minor.patch strings; echo 1 (a>b), 0 (a==b) or -1 (a 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 </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 <