|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +CLAUDE_CHANGELOG_URL=${CLAUDE_CHANGELOG_URL:-https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md} |
| 5 | +CODEX_RELEASES_API=${CODEX_RELEASES_API:-https://api.github.com/repos/openai/codex/releases/latest} |
| 6 | + |
| 7 | +section() { |
| 8 | + local title=$1 |
| 9 | + echo "$title" |
| 10 | + printf '%*s\n' "${#title}" '' | tr ' ' '-' |
| 11 | +} |
| 12 | + |
| 13 | +indent() { sed 's/^/ /'; } |
| 14 | + |
| 15 | +normalize_version() { |
| 16 | + local v=$1 |
| 17 | + v=${v#v} |
| 18 | + echo "$v" |
| 19 | +} |
| 20 | + |
| 21 | +get_latest_npm_version() { |
| 22 | + local pkg=$1 |
| 23 | + npm view "$pkg" version 2>/dev/null || echo "" |
| 24 | +} |
| 25 | + |
| 26 | +get_image_version() { |
| 27 | + local image=$1 label=$2 |
| 28 | + docker inspect "$image" 2>/dev/null | \ |
| 29 | + jq -r --arg k "$label" '.[0].Config.Labels[$k] // ""' 2>/dev/null || echo "" |
| 30 | +} |
| 31 | + |
| 32 | +compare_versions() { |
| 33 | + local current=$1 latest=$2 name=$3 |
| 34 | + current=$(normalize_version "$current") |
| 35 | + latest=$(normalize_version "$latest") |
| 36 | + |
| 37 | + if [[ -z $current ]]; then |
| 38 | + echo " $name: - -> v$latest (not built)" |
| 39 | + return 1 |
| 40 | + elif [[ $current == "$latest" ]]; then |
| 41 | + echo " $name: v$current (up-to-date)" |
| 42 | + return 0 |
| 43 | + else |
| 44 | + echo " $name: v$current -> v$latest (upgrade available)" |
| 45 | + return 1 |
| 46 | + fi |
| 47 | +} |
| 48 | + |
| 49 | +fetch_changelog_between() { |
| 50 | + local current=$1 latest=$2 |
| 51 | + current=$(normalize_version "$current") |
| 52 | + latest=$(normalize_version "$latest") |
| 53 | + |
| 54 | + if [[ -z $current ]] || [[ $current == "$latest" ]]; then |
| 55 | + return |
| 56 | + fi |
| 57 | + |
| 58 | + local data |
| 59 | + if ! data=$(curl -fsSL --max-time 10 --retry 2 "$CLAUDE_CHANGELOG_URL" 2>/dev/null); then |
| 60 | + return |
| 61 | + fi |
| 62 | + |
| 63 | + python3 -c 'import re, sys |
| 64 | +
|
| 65 | +def parse_version(v): |
| 66 | + """Parse version string to tuple of ints for comparison""" |
| 67 | + parts = re.findall(r"\d+", v) |
| 68 | + return tuple(int(p) for p in parts) if parts else (0,) |
| 69 | +
|
| 70 | +current = sys.argv[1] |
| 71 | +latest = sys.argv[2] |
| 72 | +text = sys.stdin.read() |
| 73 | +
|
| 74 | +try: |
| 75 | + cur_v = parse_version(current) |
| 76 | + lat_v = parse_version(latest) |
| 77 | +except: |
| 78 | + sys.exit(0) |
| 79 | +
|
| 80 | +# Match entire sections: ## heading followed by content until next ## or end |
| 81 | +sections = re.split(r"(?=^## )", text, flags=re.M) |
| 82 | +changes = [] |
| 83 | +
|
| 84 | +for section in sections: |
| 85 | + if not section.strip(): |
| 86 | + continue |
| 87 | +
|
| 88 | + # Extract version from heading |
| 89 | + match = re.match(r"^##\s+.*?(\d+\.\d+\.\d+)", section, re.M) |
| 90 | + if not match: |
| 91 | + continue |
| 92 | +
|
| 93 | + try: |
| 94 | + v = parse_version(match.group(1)) |
| 95 | + if cur_v < v <= lat_v: |
| 96 | + # Clean up section: remove heading marker, limit lines |
| 97 | + lines = section.strip().split("\n") |
| 98 | + if lines: |
| 99 | + # Keep version heading and first 10 content lines |
| 100 | + output = [lines[0].replace("## ", "")] |
| 101 | + content_lines = [l for l in lines[1:] if l.strip()][:10] |
| 102 | + output.extend(content_lines) |
| 103 | + changes.append("\n".join(output)) |
| 104 | + except: |
| 105 | + continue |
| 106 | +
|
| 107 | +if changes: |
| 108 | + for change in reversed(changes[-3:]): |
| 109 | + print(change) |
| 110 | + print() |
| 111 | +' "$current" "$latest" <<< "$data" 2>/dev/null || true |
| 112 | +} |
| 113 | + |
| 114 | +fetch_github_releases_between() { |
| 115 | + local current=$1 latest=$2 |
| 116 | + current=$(normalize_version "$current") |
| 117 | + latest=$(normalize_version "$latest") |
| 118 | + |
| 119 | + if [[ -z $current ]] || [[ $current == "$latest" ]]; then |
| 120 | + return |
| 121 | + fi |
| 122 | + |
| 123 | + local json |
| 124 | + if ! json=$(curl -fsSL --max-time 10 --retry 2 \ |
| 125 | + "https://api.github.com/repos/openai/codex/releases" 2>/dev/null); then |
| 126 | + return |
| 127 | + fi |
| 128 | + |
| 129 | + python3 -c 'import json, sys, re |
| 130 | +
|
| 131 | +def parse_version(v): |
| 132 | + """Parse version string to tuple of ints for comparison""" |
| 133 | + # Extract pure version numbers, ignore prefixes and suffixes |
| 134 | + match = re.search(r"(\d+)\.(\d+)\.(\d+)", v) |
| 135 | + if not match: |
| 136 | + return (0, 0, 0) |
| 137 | + return tuple(int(x) for x in match.groups()) |
| 138 | +
|
| 139 | +current = sys.argv[1] |
| 140 | +latest = sys.argv[2] |
| 141 | +releases = json.load(sys.stdin) |
| 142 | +
|
| 143 | +try: |
| 144 | + cur_v = parse_version(current) |
| 145 | + lat_v = parse_version(latest) |
| 146 | +except: |
| 147 | + sys.exit(0) |
| 148 | +
|
| 149 | +changes = [] |
| 150 | +seen_versions = set() |
| 151 | +
|
| 152 | +for rel in releases: |
| 153 | + # Skip prereleases (alpha, beta, rc) |
| 154 | + if rel.get("prerelease", False): |
| 155 | + continue |
| 156 | +
|
| 157 | + tag = rel.get("tag_name") or "" |
| 158 | + if not tag: |
| 159 | + continue |
| 160 | +
|
| 161 | + try: |
| 162 | + v = parse_version(tag) |
| 163 | +
|
| 164 | + # Skip duplicates and check version range |
| 165 | + if v in seen_versions or not (cur_v < v <= lat_v): |
| 166 | + continue |
| 167 | +
|
| 168 | + seen_versions.add(v) |
| 169 | +
|
| 170 | + # Extract clean version string |
| 171 | + ver_match = re.search(r"(\d+\.\d+\.\d+)", tag) |
| 172 | + ver = ver_match.group(1) if ver_match else tag |
| 173 | +
|
| 174 | + name = rel.get("name") or ver |
| 175 | + body = (rel.get("body") or "").strip() |
| 176 | + # Convert \r\n to \n |
| 177 | + body = body.replace("\r\n", "\n") |
| 178 | +
|
| 179 | + changes.append((v, ver, name, body)) |
| 180 | + except Exception: |
| 181 | + continue |
| 182 | +
|
| 183 | +# Sort by version tuple (newest first) and show up to 3 |
| 184 | +for v, ver, name, body in sorted(changes, key=lambda x: x[0], reverse=True)[:3]: |
| 185 | + print(f"{ver}") |
| 186 | + if body: |
| 187 | + lines = [l.rstrip() for l in body.split("\n") if l.strip()][:15] |
| 188 | + for line in lines: |
| 189 | + print(f" {line}") |
| 190 | + print() |
| 191 | +' "$current" "$latest" <<< "$json" 2>/dev/null || true |
| 192 | +} |
| 193 | + |
| 194 | +main() { |
| 195 | + local image=${MAIN_IMAGE:-ghcr.io/thevibeworks/deva:latest} |
| 196 | + |
| 197 | + section "Version Status" |
| 198 | + echo "Image: $image" |
| 199 | + |
| 200 | + # Get current versions from built image |
| 201 | + local cur_claude cur_codex image_exists=true |
| 202 | + if docker inspect "$image" >/dev/null 2>&1; then |
| 203 | + cur_claude=$(get_image_version "$image" "org.opencontainers.image.claude_code_version") |
| 204 | + cur_codex=$(get_image_version "$image" "org.opencontainers.image.codex_version") |
| 205 | + else |
| 206 | + echo " (image not built locally)" |
| 207 | + cur_claude="" |
| 208 | + cur_codex="" |
| 209 | + image_exists=false |
| 210 | + fi |
| 211 | + |
| 212 | + # Get latest versions |
| 213 | + local lat_claude=${CLAUDE_CODE_VERSION:-$(get_latest_npm_version "@anthropic-ai/claude-code")} |
| 214 | + local lat_codex=${CODEX_VERSION:-$(get_latest_npm_version "@openai/codex")} |
| 215 | + |
| 216 | + local needs_update=0 |
| 217 | + compare_versions "$cur_claude" "$lat_claude" "Claude Code" || needs_update=1 |
| 218 | + compare_versions "$cur_codex" "$lat_codex" "Codex" || needs_update=1 |
| 219 | + echo |
| 220 | + |
| 221 | + if [[ $needs_update -eq 1 ]]; then |
| 222 | + if [[ -n $cur_claude ]] && [[ $(normalize_version "$cur_claude") != $(normalize_version "$lat_claude") ]]; then |
| 223 | + section "Claude Code Changes" |
| 224 | + local changes |
| 225 | + changes=$(fetch_changelog_between "$cur_claude" "$lat_claude") |
| 226 | + if [[ -n $changes ]]; then |
| 227 | + echo "$changes" | indent |
| 228 | + else |
| 229 | + echo " (changelog unavailable)" |
| 230 | + fi |
| 231 | + echo |
| 232 | + fi |
| 233 | + |
| 234 | + if [[ -n $cur_codex ]] && [[ $(normalize_version "$cur_codex") != $(normalize_version "$lat_codex") ]]; then |
| 235 | + section "Codex Changes" |
| 236 | + local changes |
| 237 | + changes=$(fetch_github_releases_between "$cur_codex" "$lat_codex") |
| 238 | + if [[ -n $changes ]]; then |
| 239 | + echo "$changes" | indent |
| 240 | + else |
| 241 | + echo " (release notes unavailable)" |
| 242 | + fi |
| 243 | + echo |
| 244 | + fi |
| 245 | + |
| 246 | + if [[ $image_exists == true ]]; then |
| 247 | + echo "Run 'make versions-up' to upgrade" |
| 248 | + else |
| 249 | + echo "Run 'make build' to build images" |
| 250 | + fi |
| 251 | + else |
| 252 | + echo "All versions up-to-date" |
| 253 | + fi |
| 254 | +} |
| 255 | + |
| 256 | +main |
0 commit comments