1
0

bump-version.sh 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env bash
  2. #
  3. # bump-version.sh — bump version numbers across all declared files,
  4. # with drift detection and repo-wide audit for missed files.
  5. #
  6. # Usage:
  7. # bump-version.sh <new-version> Bump all declared files to new version
  8. # bump-version.sh --check Report current versions (detect drift)
  9. # bump-version.sh --audit Check + grep repo for old version strings
  10. #
  11. set -euo pipefail
  12. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  13. REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
  14. CONFIG="$REPO_ROOT/.version-bump.json"
  15. if [[ ! -f "$CONFIG" ]]; then
  16. echo "error: .version-bump.json not found at $CONFIG" >&2
  17. exit 1
  18. fi
  19. # --- helpers ---
  20. # Read a dotted field path from a JSON file.
  21. # Handles both simple ("version") and nested ("plugins.0.version") paths.
  22. read_json_field() {
  23. local file="$1" field="$2"
  24. # Convert dot-path to jq path: "plugins.0.version" -> .plugins[0].version
  25. local jq_path
  26. jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
  27. jq -r "$jq_path" "$file"
  28. }
  29. # Write a dotted field path in a JSON file, preserving formatting.
  30. write_json_field() {
  31. local file="$1" field="$2" value="$3"
  32. local jq_path
  33. jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
  34. local tmp="${file}.tmp"
  35. jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file"
  36. }
  37. # Read the list of declared files from config.
  38. # Outputs lines of "path<TAB>field"
  39. declared_files() {
  40. jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG"
  41. }
  42. # Read the audit exclude patterns from config.
  43. audit_excludes() {
  44. jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null
  45. }
  46. # --- commands ---
  47. cmd_check() {
  48. local has_drift=0
  49. local versions=()
  50. echo "Version check:"
  51. echo ""
  52. while IFS=$'\t' read -r path field; do
  53. local fullpath="$REPO_ROOT/$path"
  54. if [[ ! -f "$fullpath" ]]; then
  55. printf " %-45s MISSING\n" "$path ($field)"
  56. has_drift=1
  57. continue
  58. fi
  59. local ver
  60. ver=$(read_json_field "$fullpath" "$field")
  61. printf " %-45s %s\n" "$path ($field)" "$ver"
  62. versions+=("$ver")
  63. done < <(declared_files)
  64. echo ""
  65. # Check if all versions match
  66. local unique
  67. unique=$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ')
  68. if [[ "$unique" -gt 1 ]]; then
  69. echo "DRIFT DETECTED — versions are not in sync:"
  70. printf '%s\n' "${versions[@]}" | sort | uniq -c | sort -rn | while read -r count ver; do
  71. echo " $ver ($count files)"
  72. done
  73. has_drift=1
  74. else
  75. echo "All declared files are in sync at ${versions[0]}"
  76. fi
  77. return $has_drift
  78. }
  79. cmd_audit() {
  80. # First run check
  81. cmd_check || true
  82. echo ""
  83. # Determine the current version (most common across declared files)
  84. local current_version
  85. current_version=$(
  86. while IFS=$'\t' read -r path field; do
  87. local fullpath="$REPO_ROOT/$path"
  88. [[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field"
  89. done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}'
  90. )
  91. if [[ -z "$current_version" ]]; then
  92. echo "error: could not determine current version" >&2
  93. return 1
  94. fi
  95. echo "Audit: scanning repo for version string '$current_version'..."
  96. echo ""
  97. # Build grep exclude args
  98. local -a exclude_args=()
  99. while IFS= read -r pattern; do
  100. exclude_args+=("--exclude=$pattern" "--exclude-dir=$pattern")
  101. done < <(audit_excludes)
  102. # Also always exclude binary files and .git
  103. exclude_args+=("--exclude-dir=.git" "--exclude-dir=node_modules" "--binary-files=without-match")
  104. # Get list of declared paths for comparison
  105. local -a declared_paths=()
  106. while IFS=$'\t' read -r path _field; do
  107. declared_paths+=("$path")
  108. done < <(declared_files)
  109. # Grep for the version string
  110. local found_undeclared=0
  111. while IFS= read -r match; do
  112. local match_file
  113. match_file=$(echo "$match" | cut -d: -f1)
  114. # Make path relative to repo root
  115. local rel_path="${match_file#$REPO_ROOT/}"
  116. # Check if this file is in the declared list
  117. local is_declared=0
  118. for dp in "${declared_paths[@]}"; do
  119. if [[ "$rel_path" == "$dp" ]]; then
  120. is_declared=1
  121. break
  122. fi
  123. done
  124. if [[ "$is_declared" -eq 0 ]]; then
  125. if [[ "$found_undeclared" -eq 0 ]]; then
  126. echo "UNDECLARED files containing '$current_version':"
  127. found_undeclared=1
  128. fi
  129. echo " $match"
  130. fi
  131. done < <(grep -rn "${exclude_args[@]}" -F "$current_version" "$REPO_ROOT" 2>/dev/null || true)
  132. if [[ "$found_undeclared" -eq 0 ]]; then
  133. echo "No undeclared files contain the version string. All clear."
  134. else
  135. echo ""
  136. echo "Review the above files — if they should be bumped, add them to .version-bump.json"
  137. echo "If they should be skipped, add them to the audit.exclude list."
  138. fi
  139. }
  140. cmd_bump() {
  141. local new_version="$1"
  142. # Validate semver-ish format
  143. if ! echo "$new_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
  144. echo "error: '$new_version' doesn't look like a version (expected X.Y.Z)" >&2
  145. exit 1
  146. fi
  147. echo "Bumping all declared files to $new_version..."
  148. echo ""
  149. while IFS=$'\t' read -r path field; do
  150. local fullpath="$REPO_ROOT/$path"
  151. if [[ ! -f "$fullpath" ]]; then
  152. echo " SKIP (missing): $path"
  153. continue
  154. fi
  155. local old_ver
  156. old_ver=$(read_json_field "$fullpath" "$field")
  157. write_json_field "$fullpath" "$field" "$new_version"
  158. printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version"
  159. done < <(declared_files)
  160. echo ""
  161. echo "Done. Running audit to check for missed files..."
  162. echo ""
  163. cmd_audit
  164. }
  165. # --- main ---
  166. case "${1:-}" in
  167. --check)
  168. cmd_check
  169. ;;
  170. --audit)
  171. cmd_audit
  172. ;;
  173. --help|-h|"")
  174. echo "Usage: bump-version.sh <new-version> | --check | --audit"
  175. echo ""
  176. echo " <new-version> Bump all declared files to the given version"
  177. echo " --check Show current versions, detect drift"
  178. echo " --audit Check + scan repo for undeclared version references"
  179. exit 0
  180. ;;
  181. --*)
  182. echo "error: unknown flag '$1'" >&2
  183. exit 1
  184. ;;
  185. *)
  186. cmd_bump "$1"
  187. ;;
  188. esac