11#! /usr/bin/env bash
22# update-version-pins.sh - Refresh shared version pins from upstream sources
3+ #
4+ # Verbose TUI with real-time fetch progress, grouped version comparison,
5+ # and optional changelog display for updated tools.
36
47set -euo pipefail
58
69SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
710# shellcheck disable=SC1091
811source " $SCRIPT_DIR /version-pins.sh"
12+ # shellcheck disable=SC1091
13+ source " $SCRIPT_DIR /release-utils.sh"
914
1015DRY_RUN=0
16+ SHOW_CHANGELOG=0
17+ IS_TTY=0
18+ [[ -t 1 ]] && IS_TTY=1
19+
20+ N_UPDATED=0
21+ N_UNCHANGED=0
22+ N_FAILED=0
23+ UPDATED_VARS=()
1124
1225usage () {
1326 cat << 'EOF '
14- Usage: update-version-pins.sh [--dry-run ]
27+ Usage: update-version-pins.sh [OPTIONS ]
1528
1629Refresh shared version pins from upstream sources and rewrite versions.env.
17- Only versions we intentionally pin are updated here.
30+
31+ Options:
32+ --dry-run Preview changes without writing versions.env
33+ --changelog Show changelogs for updated tools
34+ -h, --help Show this help
1835EOF
1936}
2037
38+ # ── Fetch helpers (soft-fail: empty string on error) ─────────────────────
39+
2140fetch_npm_version () {
2241 curl -fsSL --max-time 10 \
2342 " https://registry.npmjs.org/-/package/$1 /dist-tags" 2> /dev/null | \
@@ -43,13 +62,130 @@ fetch_latest_commit() {
4362 git ls-remote " $1 " " $2 " 2> /dev/null | awk ' NR == 1 { print $1 }'
4463}
4564
46- refresh_pin () {
47- local var_name=$1 fetched=$2
48- if [[ -n " $fetched " ]]; then
49- printf -v " $var_name " ' %s' " $fetched "
65+ # ── Pin: fetch one version and display result ────────────────────────────
66+ # Usage: pin NAME VAR_NAME FETCH_TYPE [FETCH_ARGS...]
67+
68+ pin () {
69+ local name=$1 var=$2 fetch_type=$3
70+ shift 3
71+ local old_val=${! var:- }
72+ local pad
73+ pad=$( printf ' %-16s' " $name " )
74+
75+ # Live progress (tty only -- overwritten when done)
76+ if [[ $IS_TTY -eq 1 ]]; then
77+ echo -en " ${CYAN} │${RESET} ${DIM} ⟳ ${pad} fetching...${RESET} "
78+ fi
79+
80+ # Fetch upstream. `|| true` is load-bearing: a failing command
81+ # substitution in a plain assignment takes its exit status, and under
82+ # `set -e` that aborts the whole run -- which would make the fetch-failed
83+ # branch below unreachable and kill the soft-fail contract. One dead
84+ # registry must degrade to a warning, not end the sweep.
85+ local new_val=" "
86+ case $fetch_type in
87+ go) new_val=$( fetch_go_version) || true ;;
88+ npm) new_val=$( fetch_npm_version " $1 " ) || true ;;
89+ git-tag) new_val=$( fetch_latest_git_tag " $1 " ) || true ;;
90+ git-commit) new_val=$( fetch_latest_commit " $1 " " $2 " ) || true ;;
91+ esac
92+
93+ # Clear fetching line
94+ if [[ $IS_TTY -eq 1 ]]; then
95+ echo -en " \r\033[K"
96+ fi
97+
98+ # Fetch failed -- keep old value, warn
99+ if [[ -z " $new_val " ]]; then
100+ echo -e " ${CYAN} │${RESET} ${YELLOW} !${RESET} ${WHITE}${pad}${RESET} ${DIM}${old_val:- ?}${RESET} ${YELLOW} (fetch failed)${RESET} "
101+ N_FAILED=$(( N_FAILED + 1 ))
102+ return
103+ fi
104+
105+ # Update variable in caller's scope
106+ printf -v " $var " ' %s' " $new_val "
107+
108+ # Short hash for commits, semver for everything else
109+ local old_disp=$old_val new_disp=$new_val
110+ if [[ $fetch_type == " git-commit" ]]; then
111+ old_disp=" ${old_val: 0: 7} "
112+ new_disp=" ${new_val: 0: 7} "
113+ fi
114+
115+ if [[ " $old_val " == " $new_val " ]]; then
116+ echo -e " ${CYAN} │${RESET} ${DIM} · ${pad} ${new_disp} (up-to-date)${RESET} "
117+ N_UNCHANGED=$(( N_UNCHANGED + 1 ))
118+ else
119+ echo -e " ${CYAN} │${RESET} ${GREEN} ▲${RESET} ${WHITE}${pad}${RESET} ${RED}${old_disp:- new}${RESET} ${DIM} ->${RESET} ${GREEN}${new_disp}${RESET} "
120+ N_UPDATED=$(( N_UPDATED + 1 ))
121+ UPDATED_VARS+=(" ${var} |${old_val} |${new_val} " )
122+ fi
123+ }
124+
125+ # ── Changelog display ────────────────────────────────────────────────────
126+
127+ registry_tool () {
128+ case $1 in
129+ CLAUDE_CODE_VERSION) echo " claude-code" ;;
130+ CCTRACE_VERSION) echo " cctrace" ;;
131+ CODEX_VERSION) echo " codex" ;;
132+ GEMINI_CLI_VERSION) echo " gemini-cli" ;;
133+ GROK_CLI_VERSION) echo " grok-cli" ;;
134+ KIMI_CODE_VERSION) echo " kimi-code" ;;
135+ CCX_VERSION) echo " ccx" ;;
136+ COPILOT_API_VERSION) echo " copilot-api" ;;
137+ PLAYWRIGHT_VERSION) echo " playwright" ;;
138+ esac
139+ }
140+
141+ show_changelogs () {
142+ local shown=0
143+ for entry in ${UPDATED_VARS[@]+" ${UPDATED_VARS[@]} " } ; do
144+ IFS=' |' read -r var old new <<< " $entry"
145+
146+ local tool
147+ tool=$( registry_tool " $var " )
148+ [[ -z " $tool " ]] && continue
149+
150+ local changelog_source
151+ changelog_source=$( get_tool_field " $tool " changelog 2> /dev/null) || true
152+ [[ -z " $changelog_source " ]] && continue
153+
154+ local name
155+ name=$( get_display_name " $tool " )
156+
157+ [[ $shown -eq 0 ]] && echo " "
158+ shown=1
159+
160+ section " $name ${old} -> ${new} "
161+
162+ if [[ $IS_TTY -eq 1 ]]; then
163+ echo -en " ${DIM} fetching changelog...${RESET} "
164+ fi
165+
166+ local changes
167+ changes=$( fetch_changelog " $tool " " $old " " $new " )
168+
169+ if [[ $IS_TTY -eq 1 ]]; then
170+ echo -en " \r\033[K"
171+ fi
172+
173+ if [[ -n " $changes " ]]; then
174+ echo " $changes " | indent
175+ else
176+ echo -e " ${DIM} (changelog unavailable)${RESET} "
177+ fi
178+ echo " "
179+ done
180+
181+ if [[ $shown -eq 0 ]]; then
182+ echo -e " ${DIM} No changelogs available for updated tools.${RESET} "
183+ echo " "
50184 fi
51185}
52186
187+ # ── Write versions.env ───────────────────────────────────────────────────
188+
53189write_version_pins () {
54190 cat > " $VERSION_PINS_FILE " << EOF
55191# Shared image version pins for local and release builds.
@@ -82,16 +218,13 @@ RUST_TARGETS=$RUST_TARGETS
82218EOF
83219}
84220
221+ # ── Arg parsing ──────────────────────────────────────────────────────────
222+
85223while [[ $# -gt 0 ]]; do
86224 case $1 in
87- --dry-run)
88- DRY_RUN=1
89- shift
90- ;;
91- -h|--help)
92- usage
93- exit 0
94- ;;
225+ --dry-run) DRY_RUN=1; shift ;;
226+ --changelog) SHOW_CHANGELOG=1; shift ;;
227+ -h|--help) usage; exit 0 ;;
95228 * )
96229 echo " error: unknown option: $1 " >&2
97230 usage >&2
@@ -100,28 +233,88 @@ while [[ $# -gt 0 ]]; do
100233 esac
101234done
102235
236+ # ── Main ─────────────────────────────────────────────────────────────────
237+
103238main () {
104239 load_version_pins
105240
106- refresh_pin GO_VERSION " $( fetch_go_version) "
107- refresh_pin DELTA_VERSION " $( fetch_latest_git_tag https://github.com/dandavison/delta.git) "
108- refresh_pin CLAUDE_CODE_VERSION " $( fetch_npm_version @anthropic-ai/claude-code) "
109- refresh_pin CCTRACE_VERSION " $( fetch_npm_version @thevibeworks/cctrace) "
110- refresh_pin CODEX_VERSION " $( fetch_npm_version @openai/codex) "
111- refresh_pin GEMINI_CLI_VERSION " $( fetch_npm_version @google/gemini-cli) "
112- refresh_pin GROK_CLI_VERSION " $( fetch_npm_version @xai-official/grok) "
113- refresh_pin KIMI_CODE_VERSION " $( fetch_npm_version @moonshot-ai/kimi-code) "
114- refresh_pin CCX_VERSION " $( fetch_latest_git_tag https://github.com/thevibeworks/ccx.git) "
115- refresh_pin COPILOT_API_VERSION " $( fetch_latest_commit https://github.com/ericc-ch/copilot-api.git refs/heads/master) "
116- refresh_pin PLAYWRIGHT_VERSION " $( fetch_npm_version playwright) "
117- refresh_pin CLOAKBROWSER_WRAPPER_VERSION " $( fetch_npm_version cloakbrowser) "
241+ echo -e " ${CYAN}${BOLD} ╔══════════════════════════════════════════════════╗${RESET} "
242+ echo -e " ${CYAN}${BOLD} ║ Refreshing Version Pins ║${RESET} "
243+ echo -e " ${CYAN}${BOLD} ╚══════════════════════════════════════════════════╝${RESET} "
244+ echo -e " ${DIM} $( date ' +%Y-%m-%d %H:%M:%S' ) ${RESET} "
245+ [[ $DRY_RUN -eq 1 ]] && echo -e " ${YELLOW} (dry run)${RESET} "
246+ echo " "
247+
248+ # ── Toolchains ───────────────────────────────────────────────────────
249+ echo -e " ${CYAN} ┌─${BOLD} Toolchains ${RESET}${CYAN} ──────────────────────────────────────${RESET} "
250+
251+ pin " Go" GO_VERSION go
252+ pin " delta" DELTA_VERSION git-tag " https://github.com/dandavison/delta.git"
253+
254+ # ── Agent CLIs ───────────────────────────────────────────────────────
255+ echo -e " ${CYAN} │${RESET} "
256+ echo -e " ${CYAN} ├─${BOLD} Agent CLIs ${RESET}${CYAN} ──────────────────────────────────────${RESET} "
257+
258+ pin " Claude Code" CLAUDE_CODE_VERSION npm " @anthropic-ai/claude-code"
259+ pin " cctrace" CCTRACE_VERSION npm " @thevibeworks/cctrace"
260+ pin " Codex" CODEX_VERSION npm " @openai/codex"
261+ pin " Gemini CLI" GEMINI_CLI_VERSION npm " @google/gemini-cli"
262+ pin " Grok CLI" GROK_CLI_VERSION npm " @xai-official/grok"
263+ pin " Kimi Code" KIMI_CODE_VERSION npm " @moonshot-ai/kimi-code"
264+ pin " CCX" CCX_VERSION git-tag " https://github.com/thevibeworks/ccx.git"
265+ pin " Copilot API" COPILOT_API_VERSION git-commit " https://github.com/ericc-ch/copilot-api.git" " refs/heads/master"
266+
267+ # ── Browser Tools ────────────────────────────────────────────────────
268+ echo -e " ${CYAN} │${RESET} "
269+ echo -e " ${CYAN} ├─${BOLD} Browser Tools ${RESET}${CYAN} ───────────────────────────────────${RESET} "
270+
271+ pin " Playwright" PLAYWRIGHT_VERSION npm " playwright"
272+ pin " CloakBrowser" CLOAKBROWSER_WRAPPER_VERSION npm " cloakbrowser"
118273
274+ # ── Summary footer ───────────────────────────────────────────────────
275+ echo -e " ${CYAN} │${RESET} "
276+
277+ local parts=()
278+ [[ $N_UPDATED -gt 0 ]] && parts+=(" ${GREEN}${N_UPDATED} updated${RESET} " )
279+ [[ $N_UNCHANGED -gt 0 ]] && parts+=(" ${DIM}${N_UNCHANGED} unchanged${RESET} " )
280+ [[ $N_FAILED -gt 0 ]] && parts+=(" ${YELLOW}${N_FAILED} failed${RESET} " )
281+ local summary=" "
282+ for i in " ${! parts[@]} " ; do
283+ [[ $i -gt 0 ]] && summary+=" , "
284+ summary+=" ${parts[$i]} "
285+ done
286+ echo -e " ${CYAN} └─${RESET} ${summary} ${CYAN} ──────────────────────────────────────${RESET} "
287+ echo " "
288+
289+ # ── Changelogs (opt-in) ──────────────────────────────────────────────
290+ if [[ $SHOW_CHANGELOG -eq 1 ]] && [[ $N_UPDATED -gt 0 ]]; then
291+ show_changelogs
292+ fi
293+
294+ # ── Result ───────────────────────────────────────────────────────────
119295 if [[ $DRY_RUN -eq 1 ]]; then
296+ echo -e " ${YELLOW} Dry run — would write:${RESET} "
297+ echo " "
120298 emit_version_pins
121299 return 0
122300 fi
123301
302+ # Write even when nothing moved. The heredoc in write_version_pins is a
303+ # second copy of the file layout, so a rewrite is what proves no pin was
304+ # dropped from it; skipping the write when N_UPDATED=0 would leave that
305+ # guard (tests/version-upgrade.sh) unexercised on exactly the runs where
306+ # every fetch failed.
124307 write_version_pins
308+ if [[ $N_UPDATED -eq 0 ]]; then
309+ if [[ $N_FAILED -gt 0 ]]; then
310+ echo -e " ${YELLOW} No pins moved, but ${N_FAILED} fetch(es) failed -- upstream state unknown for those.${RESET} "
311+ else
312+ echo -e " ${GREEN} All pins up-to-date.${RESET} "
313+ fi
314+ return 0
315+ fi
316+ echo -e " ${GREEN} Updated ${VERSION_PINS_FILE##*/ }${RESET} "
317+ echo -e " ${DIM} Run 'make versions-up' to rebuild images.${RESET} "
125318}
126319
127320main " $@ "
0 commit comments