atharvalade commented on code in PR #2990: URL: https://github.com/apache/iggy/pull/2990#discussion_r2991220516
########## scripts/bump-version.sh: ########## @@ -0,0 +1,605 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +usage() { + cat <<'EOF' +Usage: + bump-version.sh <component> <flags> + bump-version.sh --status [<component>] + +Components: + rust-sdk core/sdk + workspace dep + python iggy dep + rust-common core/common + workspace dep + rust-binary-protocol core/binary_protocol + workspace dep + rust-server core/server + rust-cli core/cli + workspace dep + rust-connector-sdk core/connectors/sdk + workspace dep + rust-all All Rust crates at once + --all All components (Rust + SDKs) + sdk-python foreign/python/Cargo.toml + foreign/python/pyproject.toml + sdk-node foreign/node/package.json + sdk-go foreign/go/contracts/version.go + sdk-csharp foreign/csharp/Iggy_SDK/Iggy_SDK.csproj + sdk-java foreign/java/gradle.properties + +Version flags (exactly one required): + --patch Bump patch version (0.9.3 -> 0.9.4) + --minor Bump minor version (0.9.3 -> 0.10.0) + --major Bump major version (0.9.3 -> 1.0.0) + --edge Increment edge counter (0.9.3-edge.1 -> 0.9.3-edge.2) + Or add -edge.1 when combined with --patch/--minor/--major + --strip-edge Remove edge suffix (0.9.3-edge.1 -> 0.9.3) + --set V Set explicit version V (use --force to bypass validation) + +Modifiers: + --dry-run Preview changes without writing (default: writes immediately) + --force Bypass validation (only with --set) + +Examples: + bump-version.sh rust-all --patch --edge # 0.9.3 -> 0.9.4-edge.1 + bump-version.sh rust-all --edge # 0.9.3-edge.1 -> 0.9.3-edge.2 + bump-version.sh rust-all --strip-edge # 0.9.3-edge.1 -> 0.9.3 + bump-version.sh rust-all --minor --edge --dry-run + bump-version.sh --all --strip-edge + bump-version.sh rust-sdk --patch + bump-version.sh sdk-python --set 0.8.0 + bump-version.sh --status +EOF +} + +RUST_COMPONENTS="rust-sdk rust-common rust-binary-protocol rust-server rust-cli rust-connector-sdk" +SDK_COMPONENTS="sdk-python sdk-node sdk-go sdk-csharp sdk-java" +ALL_COMPONENTS="${RUST_COMPONENTS} ${SDK_COMPONENTS}" + +# Returns "file:format" lines per component. +# Format keys: cargo, cargo-ws-dep:PKG, cargo-dep:PKG, python-cargo, pyproject, json, csproj, gradle, go +get_version_files() { + local component="$1" + case "$component" in + rust-sdk) + echo "core/sdk/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy" + echo "foreign/python/Cargo.toml:cargo-dep:iggy" + ;; + rust-common) + echo "core/common/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_common" + ;; + rust-binary-protocol) + echo "core/binary_protocol/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_binary_protocol" + ;; + rust-server) + echo "core/server/Cargo.toml:cargo" + ;; + rust-cli) + echo "core/cli/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy-cli" + ;; + rust-connector-sdk) + echo "core/connectors/sdk/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_connector_sdk" + ;; + sdk-python) + echo "foreign/python/Cargo.toml:python-cargo" + echo "foreign/python/pyproject.toml:pyproject" + ;; + sdk-node) + echo "foreign/node/package.json:json" + ;; + sdk-go) + echo "foreign/go/contracts/version.go:go" + ;; + sdk-csharp) + echo "foreign/csharp/Iggy_SDK/Iggy_SDK.csproj:csproj" + ;; + sdk-java) + echo "foreign/java/gradle.properties:gradle" + ;; + *) + echo -e "${RED}Unknown component: ${component}${NC}" >&2 + echo "Valid: rust-all ${ALL_COMPONENTS}" >&2 + return 1 + ;; + esac +} + +# Parse canonical semver into "base pre_type pre_num". +# "0.9.3-edge.1" -> "0.9.3 edge 1", "0.9.3" -> "0.9.3 stable 0" +parse_version() { + local ver="$1" + if [[ "$ver" =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "$ver stable 0" + elif [[ "$ver" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-edge\.([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]} edge ${BASH_REMATCH[2]}" + else + echo -e "${RED}Cannot parse version: ${ver}${NC}" >&2 + exit 1 + fi +} + +# Compute next version from current + keyword + flags. +# Args: current keyword add_edge(0|1) strip_edge(0|1) +compute_next_version() { + local current="$1" keyword="$2" add_edge="${3:-0}" strip_edge="${4:-0}" + local parsed base pre_type pre_num + parsed=$(parse_version "$current") || return 1 + read -r base pre_type pre_num <<< "$parsed" + + local major minor patch + IFS='.' read -r major minor patch <<< "$base" + + # --strip-edge: remove edge suffix + if [[ $strip_edge -eq 1 ]]; then + if [[ "$pre_type" != "edge" ]]; then + echo -e "${RED}Cannot --strip-edge: ${current} has no edge suffix${NC}" >&2 + return 1 + fi + echo "$base" + return 0 + fi + + # edge keyword: increment edge counter + if [[ "$keyword" == "edge" ]]; then + if [[ "$pre_type" != "edge" ]]; then + echo -e "${RED}Cannot 'edge' from stable version ${current}${NC}" >&2 + echo -e " Use ${GREEN}patch --edge${NC} or ${GREEN}minor --edge${NC} instead" >&2 + return 1 + fi + echo "${base}-edge.$((pre_num + 1))" + return 0 + fi + + # patch/minor/major: bump base version + local new_base + case "$keyword" in + patch) new_base="${major}.${minor}.$((patch + 1))" ;; + minor) new_base="${major}.$((minor + 1)).0" ;; + major) new_base="$((major + 1)).0.0" ;; + *) echo -e "${RED}Invalid keyword: ${keyword}${NC}" >&2; return 1 ;; + esac + + if [[ $add_edge -eq 1 ]]; then + echo "${new_base}-edge.1" + else + echo "${new_base}" + fi +} + +# Convert canonical semver to ecosystem-specific format. +translate_version() { + local canonical="$1" format="$2" + local parsed base pre_type pre_num + parsed=$(parse_version "$canonical" 2>/dev/null) || { echo "$canonical"; return 0; } + read -r base pre_type pre_num <<< "$parsed" + + case "$format" in + cargo|cargo-ws-dep:*|cargo-dep:*|json|csproj|go) + echo "$canonical" ;; + python-cargo) + case "$pre_type" in + edge) echo "${base}-dev${pre_num}" ;; + stable) echo "$base" ;; + esac ;; + pyproject) + case "$pre_type" in + edge) echo "${base}.dev${pre_num}" ;; + stable) echo "$base" ;; + esac ;; + gradle) + case "$pre_type" in + edge) echo "${base}-SNAPSHOT" ;; + stable) echo "$base" ;; + esac ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Reverse: read ecosystem format, return canonical semver. +canonicalize_version() { + local raw="$1" format="$2" + case "$format" in + cargo|cargo-ws-dep:*|cargo-dep:*|json|csproj|go) + echo "$raw" ;; + python-cargo) + # Handle both old (0.7.2-dev.1) and new (0.7.2-dev1) formats + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-dev\.?([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}-edge.${BASH_REMATCH[2]}" + else + echo "$raw" + fi ;; + pyproject) + # Handle both old (0.7.2.dev.1) and new (0.7.2.dev1) formats + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev\.?([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}-edge.${BASH_REMATCH[2]}" + else + echo "$raw" + fi ;; + gradle) + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-SNAPSHOT$ ]]; then + echo "${BASH_REMATCH[1]}-edge.0" + else + echo "$raw" + fi ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Extract version string from file based on format. +read_current_version() { + local file="$1" format="$2" + local abs_file="${REPO_ROOT}/${file}" + local fmt_base="${format%%:*}" + + case "$fmt_base" in + cargo) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + cargo-ws-dep) + local pkg="${format#cargo-ws-dep:}" + grep "^${pkg} = " "$abs_file" | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/' ;; + cargo-dep) + local pkg="${format#cargo-dep:}" + grep "^${pkg} = " "$abs_file" | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/' ;; + python-cargo) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + pyproject) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + json) + grep '"version"' "$abs_file" | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/' ;; + csproj) + grep '<Version>' "$abs_file" | head -1 | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/' | tr -d '[:space:]' ;; + gradle) + grep '^version=' "$abs_file" | head -1 | sed 's/version=//' ;; + go) + grep 'const Version' "$abs_file" | head -1 | sed 's/.*const Version = "\([^"]*\)".*/\1/' ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Write translated version into file using sed. +write_version() { + local file="$1" format="$2" new_canonical="$3" + local abs_file="${REPO_ROOT}/${file}" + local fmt_base="${format%%:*}" + local translated + translated=$(translate_version "$new_canonical" "$format") + + case "$fmt_base" in + cargo) + sed -i "1,/^version = \".*\"/s/^version = \".*\"/version = \"${translated}\"/" "$abs_file" ;; + cargo-ws-dep) + local pkg="${format#cargo-ws-dep:}" + sed -i "s/^\(${pkg} = .*version = \"\)[^\"]*/\1${translated}/" "$abs_file" ;; + cargo-dep) + local pkg="${format#cargo-dep:}" + sed -i "s/^\(${pkg} = .*version = \"\)[^\"]*/\1${translated}/" "$abs_file" ;; + python-cargo) + sed -i "1,/^version = \".*\"/s/^version = \".*\"/version = \"${translated}\"/" "$abs_file" ;; + pyproject) + sed -i '/^\[project\]/,/^\[/{ s/^version = ".*"/version = "'"${translated}"'"/ }' "$abs_file" ;; + json) + sed -i "s/\"version\": *\"[^\"]*\"/\"version\": \"${translated}\"/" "$abs_file" ;; + csproj) + sed -i "s/<Version>[^<]*<\/Version>/<Version>${translated}<\/Version>/" "$abs_file" ;; + gradle) + sed -i "s/^version=.*/version=${translated}/" "$abs_file" ;; + go) + sed -i "s/const Version = \"[^\"]*\"/const Version = \"${translated}\"/" "$abs_file" ;; + esac +} + +# --status: print current versions for all or one component. +cmd_status() { + local filter="${1:-}" + local components + if [[ "$filter" == "rust-all" ]]; then + components="$RUST_COMPONENTS" + elif [[ -n "$filter" ]]; then + components="$filter" + else + components="$ALL_COMPONENTS" + fi + + for comp in $components; do + echo -e "${CYAN}${comp}${NC}" + local first_canonical="" + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw canonical + raw=$(read_current_version "$file" "$format") + canonical=$(canonicalize_version "$raw" "$format") + printf " %-55s %s" "$file" "$raw" + if [[ "$raw" != "$canonical" ]]; then + printf " (canonical: %s)" "$canonical" + fi + printf "\n" + if [[ -z "$first_canonical" ]]; then + first_canonical="$canonical" + fi + done < <(get_version_files "$comp") + echo "" + done +} + +# Collect unique files from version entries for dirty check. +collect_files() { + local component="$1" + get_version_files "$component" | while IFS= read -r entry; do + echo "${entry%%:*}" + done | sort -u +} + +# Pre-flight: check for uncommitted changes in version files. +preflight_dirty_check() { + local component="$1" + local dirty_files=() + while IFS= read -r file; do + if ! git -C "$REPO_ROOT" diff --quiet -- "$file" 2>/dev/null; then + dirty_files+=("$file") + fi + if ! git -C "$REPO_ROOT" diff --cached --quiet -- "$file" 2>/dev/null; then + dirty_files+=("$file (staged)") + fi + done < <(collect_files "$component") + + if [[ ${#dirty_files[@]} -gt 0 ]]; then + echo -e "${YELLOW}Warning: uncommitted changes in version files:${NC}" >&2 + for f in "${dirty_files[@]}"; do + echo -e " ${f}" >&2 + done + fi +} + +# Pre-flight: verify grouped components have consistent canonical versions. +# Returns the canonical version (or exits on inconsistency). +preflight_consistency_check() { + local component="$1" + local first_canonical="" first_file="" + local inconsistent=0 + + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw canonical + raw=$(read_current_version "$file" "$format") + canonical=$(canonicalize_version "$raw" "$format") + + if [[ -z "$first_canonical" ]]; then + first_canonical="$canonical" + first_file="$file" + elif [[ "$canonical" != "$first_canonical" ]]; then + if [[ $inconsistent -eq 0 ]]; then + echo -e "${RED}Inconsistent versions in ${component}:${NC}" >&2 + echo -e " ${first_file}: ${first_canonical}" >&2 + inconsistent=1 + fi + echo -e " ${file}: ${canonical}" >&2 + fi + done < <(get_version_files "$component") + + if [[ $inconsistent -ne 0 ]]; then + echo -e "${RED}Fix version inconsistencies before bumping, or use --set --force.${NC}" >&2 + return 1 + fi + + echo "$first_canonical" +} + +# Main bump logic. +cmd_bump() { + local component="$1" + shift + local apply=1 set_ver="" force=0 has_edge=0 strip_edge=0 keyword="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --patch) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --patch${NC}" >&2; return 1; } + keyword="patch" ;; + --minor) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --minor${NC}" >&2; return 1; } + keyword="minor" ;; + --major) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --major${NC}" >&2; return 1; } + keyword="major" ;; + --edge) has_edge=1 ;; + --strip-edge) strip_edge=1 ;; + --dry-run|--dry) apply=0 ;; + --set) + [[ $# -lt 2 ]] && { echo -e "${RED}--set requires a version argument${NC}" >&2; return 1; } + set_ver="$2"; shift ;; + --force) force=1 ;; + *) echo -e "${RED}Unknown flag: ${1}${NC}" >&2; return 1 ;; + esac + shift + done + + # Resolve --edge: standalone = increment counter, combined with bump = add suffix + local add_edge=0 + if [[ $has_edge -eq 1 ]]; then + if [[ -n "$keyword" ]]; then + add_edge=1 + else + keyword="edge" + fi + fi + + # Validate: need exactly one action + if [[ -z "$set_ver" && -z "$keyword" && $strip_edge -eq 0 ]]; then + echo -e "${RED}Missing version flag. Use --patch, --minor, --major, --edge, --strip-edge, or --set${NC}" >&2 + return 1 + fi + + preflight_dirty_check "$component" + local current + current=$(preflight_consistency_check "$component") + + local new_ver + if [[ -n "$set_ver" ]]; then + if [[ $force -eq 0 ]]; then + if ! parse_version "$set_ver" > /dev/null 2>&1; then + echo -e "${RED}Invalid version format: ${set_ver}${NC}" >&2 + echo "Expected: X.Y.Z or X.Y.Z-edge.N (use --force to bypass)" >&2 + return 1 + fi + fi + if [[ $force -eq 0 && "$set_ver" == "$current" ]]; then + echo -e "${YELLOW}Already at ${set_ver}, nothing to do.${NC}" + return 0 + fi + new_ver="$set_ver" + else + new_ver=$(compute_next_version "$current" "${keyword:-_none}" "$add_edge" "$strip_edge") || return 1 + fi + + # Java SNAPSHOT idempotency: edge on already-SNAPSHOT is a no-op + if [[ "$component" == "sdk-java" && "$keyword" == "edge" ]]; then + local raw_java + raw_java=$(read_current_version "foreign/java/gradle.properties" "gradle") + if [[ "$raw_java" == *-SNAPSHOT ]]; then + echo -e "${GREEN}sdk-java already at SNAPSHOT (${raw_java}), nothing to do.${NC}" + return 0 + fi + fi + + echo -e "${CYAN}Component:${NC} ${component}" + echo -e "${CYAN}Current:${NC} ${current}" + echo -e "${CYAN}New:${NC} ${new_ver}" + echo "" + + local changed_files=() + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw_old translated_new + raw_old=$(read_current_version "$file" "$format") + translated_new=$(translate_version "$new_ver" "$format") + + if [[ "$raw_old" == "$translated_new" ]]; then + printf " %-55s %s (unchanged)\n" "$file" "$raw_old" + else + printf " %-55s %s -> ${GREEN}%s${NC}\n" "$file" "$raw_old" "$translated_new" + changed_files+=("$file") + fi + + if [[ $apply -eq 1 ]]; then + write_version "$file" "$format" "$new_ver" + local verify + verify=$(read_current_version "$file" "$format") + if [[ "$verify" != "$translated_new" ]]; then + echo -e "${RED}FAILED to write ${file}: expected '${translated_new}', got '${verify}'${NC}" >&2 + return 1 + fi + fi + done < <(get_version_files "$component") + + echo "" + if [[ $apply -eq 1 ]]; then + echo -e "${GREEN}Applied.${NC}" + else + echo -e "${YELLOW}Dry run. Remove --dry-run to write changes.${NC}" + fi + + if [[ ${#changed_files[@]} -gt 0 ]]; then + local unique_files + IFS=$'\n' read -r -d '' -a unique_files < <(printf '%s\n' "${changed_files[@]}" | sort -u && printf '\0') || true + echo "" + echo "To commit:" + local git_add=" git add" + for f in "${unique_files[@]}"; do + git_add+=" ${f}" + done + echo "$git_add" + echo " git commit -m \"chore(release): bump ${component} to ${new_ver}\"" + fi +} + +# --- Argument parsing --- + +if [[ $# -eq 0 ]]; then + usage + exit 0 +fi + +case "$1" in + -h|--help) + usage + exit 0 ;; + --status) + cmd_status "${2:-}" + exit 0 ;; +esac + +# Bump multiple components with the same flags. +cmd_bump_multi() { + local components="$1" + shift + local failed=0 + for comp in $components; do + echo -e "${CYAN}--- ${comp} ---${NC}" + if cmd_bump "$comp" "$@"; then + : + else + failed=$((failed + 1)) + echo -e "${YELLOW}Skipped ${comp} (transition not valid)${NC}" + fi + echo "" + done + if [[ $failed -gt 0 ]]; then + echo -e "${YELLOW}${failed} component(s) skipped due to invalid transitions.${NC}" + echo -e "Use ${CYAN}--set${NC} to handle them individually." + fi +} Review Comment: `md_bump_multi` counts failures in failed but never returns a non-zero status, so the function always completes “successfully” from the shell’s point of view even when one or more `cmd_bump` calls failed ########## scripts/bump-version.sh: ########## @@ -0,0 +1,605 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +usage() { + cat <<'EOF' +Usage: + bump-version.sh <component> <flags> + bump-version.sh --status [<component>] + +Components: + rust-sdk core/sdk + workspace dep + python iggy dep + rust-common core/common + workspace dep + rust-binary-protocol core/binary_protocol + workspace dep + rust-server core/server + rust-cli core/cli + workspace dep + rust-connector-sdk core/connectors/sdk + workspace dep + rust-all All Rust crates at once + --all All components (Rust + SDKs) + sdk-python foreign/python/Cargo.toml + foreign/python/pyproject.toml + sdk-node foreign/node/package.json + sdk-go foreign/go/contracts/version.go + sdk-csharp foreign/csharp/Iggy_SDK/Iggy_SDK.csproj + sdk-java foreign/java/gradle.properties + +Version flags (exactly one required): + --patch Bump patch version (0.9.3 -> 0.9.4) + --minor Bump minor version (0.9.3 -> 0.10.0) + --major Bump major version (0.9.3 -> 1.0.0) + --edge Increment edge counter (0.9.3-edge.1 -> 0.9.3-edge.2) + Or add -edge.1 when combined with --patch/--minor/--major + --strip-edge Remove edge suffix (0.9.3-edge.1 -> 0.9.3) + --set V Set explicit version V (use --force to bypass validation) + +Modifiers: + --dry-run Preview changes without writing (default: writes immediately) + --force Bypass validation (only with --set) + +Examples: + bump-version.sh rust-all --patch --edge # 0.9.3 -> 0.9.4-edge.1 + bump-version.sh rust-all --edge # 0.9.3-edge.1 -> 0.9.3-edge.2 + bump-version.sh rust-all --strip-edge # 0.9.3-edge.1 -> 0.9.3 + bump-version.sh rust-all --minor --edge --dry-run + bump-version.sh --all --strip-edge + bump-version.sh rust-sdk --patch + bump-version.sh sdk-python --set 0.8.0 + bump-version.sh --status +EOF +} + +RUST_COMPONENTS="rust-sdk rust-common rust-binary-protocol rust-server rust-cli rust-connector-sdk" +SDK_COMPONENTS="sdk-python sdk-node sdk-go sdk-csharp sdk-java" +ALL_COMPONENTS="${RUST_COMPONENTS} ${SDK_COMPONENTS}" + +# Returns "file:format" lines per component. +# Format keys: cargo, cargo-ws-dep:PKG, cargo-dep:PKG, python-cargo, pyproject, json, csproj, gradle, go +get_version_files() { + local component="$1" + case "$component" in + rust-sdk) + echo "core/sdk/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy" + echo "foreign/python/Cargo.toml:cargo-dep:iggy" + ;; + rust-common) + echo "core/common/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_common" + ;; + rust-binary-protocol) + echo "core/binary_protocol/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_binary_protocol" + ;; + rust-server) + echo "core/server/Cargo.toml:cargo" + ;; + rust-cli) + echo "core/cli/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy-cli" + ;; + rust-connector-sdk) + echo "core/connectors/sdk/Cargo.toml:cargo" + echo "Cargo.toml:cargo-ws-dep:iggy_connector_sdk" + ;; + sdk-python) + echo "foreign/python/Cargo.toml:python-cargo" + echo "foreign/python/pyproject.toml:pyproject" + ;; + sdk-node) + echo "foreign/node/package.json:json" + ;; + sdk-go) + echo "foreign/go/contracts/version.go:go" + ;; + sdk-csharp) + echo "foreign/csharp/Iggy_SDK/Iggy_SDK.csproj:csproj" + ;; + sdk-java) + echo "foreign/java/gradle.properties:gradle" + ;; + *) + echo -e "${RED}Unknown component: ${component}${NC}" >&2 + echo "Valid: rust-all ${ALL_COMPONENTS}" >&2 + return 1 + ;; + esac +} + +# Parse canonical semver into "base pre_type pre_num". +# "0.9.3-edge.1" -> "0.9.3 edge 1", "0.9.3" -> "0.9.3 stable 0" +parse_version() { + local ver="$1" + if [[ "$ver" =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "$ver stable 0" + elif [[ "$ver" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-edge\.([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]} edge ${BASH_REMATCH[2]}" + else + echo -e "${RED}Cannot parse version: ${ver}${NC}" >&2 + exit 1 + fi +} + +# Compute next version from current + keyword + flags. +# Args: current keyword add_edge(0|1) strip_edge(0|1) +compute_next_version() { + local current="$1" keyword="$2" add_edge="${3:-0}" strip_edge="${4:-0}" + local parsed base pre_type pre_num + parsed=$(parse_version "$current") || return 1 + read -r base pre_type pre_num <<< "$parsed" + + local major minor patch + IFS='.' read -r major minor patch <<< "$base" + + # --strip-edge: remove edge suffix + if [[ $strip_edge -eq 1 ]]; then + if [[ "$pre_type" != "edge" ]]; then + echo -e "${RED}Cannot --strip-edge: ${current} has no edge suffix${NC}" >&2 + return 1 + fi + echo "$base" + return 0 + fi + + # edge keyword: increment edge counter + if [[ "$keyword" == "edge" ]]; then + if [[ "$pre_type" != "edge" ]]; then + echo -e "${RED}Cannot 'edge' from stable version ${current}${NC}" >&2 + echo -e " Use ${GREEN}patch --edge${NC} or ${GREEN}minor --edge${NC} instead" >&2 + return 1 + fi + echo "${base}-edge.$((pre_num + 1))" + return 0 + fi + + # patch/minor/major: bump base version + local new_base + case "$keyword" in + patch) new_base="${major}.${minor}.$((patch + 1))" ;; + minor) new_base="${major}.$((minor + 1)).0" ;; + major) new_base="$((major + 1)).0.0" ;; + *) echo -e "${RED}Invalid keyword: ${keyword}${NC}" >&2; return 1 ;; + esac + + if [[ $add_edge -eq 1 ]]; then + echo "${new_base}-edge.1" + else + echo "${new_base}" + fi +} + +# Convert canonical semver to ecosystem-specific format. +translate_version() { + local canonical="$1" format="$2" + local parsed base pre_type pre_num + parsed=$(parse_version "$canonical" 2>/dev/null) || { echo "$canonical"; return 0; } + read -r base pre_type pre_num <<< "$parsed" + + case "$format" in + cargo|cargo-ws-dep:*|cargo-dep:*|json|csproj|go) + echo "$canonical" ;; + python-cargo) + case "$pre_type" in + edge) echo "${base}-dev${pre_num}" ;; + stable) echo "$base" ;; + esac ;; + pyproject) + case "$pre_type" in + edge) echo "${base}.dev${pre_num}" ;; + stable) echo "$base" ;; + esac ;; + gradle) + case "$pre_type" in + edge) echo "${base}-SNAPSHOT" ;; + stable) echo "$base" ;; + esac ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Reverse: read ecosystem format, return canonical semver. +canonicalize_version() { + local raw="$1" format="$2" + case "$format" in + cargo|cargo-ws-dep:*|cargo-dep:*|json|csproj|go) + echo "$raw" ;; + python-cargo) + # Handle both old (0.7.2-dev.1) and new (0.7.2-dev1) formats + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-dev\.?([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}-edge.${BASH_REMATCH[2]}" + else + echo "$raw" + fi ;; + pyproject) + # Handle both old (0.7.2.dev.1) and new (0.7.2.dev1) formats + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev\.?([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}-edge.${BASH_REMATCH[2]}" + else + echo "$raw" + fi ;; + gradle) + if [[ "$raw" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-SNAPSHOT$ ]]; then + echo "${BASH_REMATCH[1]}-edge.0" + else + echo "$raw" + fi ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Extract version string from file based on format. +read_current_version() { + local file="$1" format="$2" + local abs_file="${REPO_ROOT}/${file}" + local fmt_base="${format%%:*}" + + case "$fmt_base" in + cargo) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + cargo-ws-dep) + local pkg="${format#cargo-ws-dep:}" + grep "^${pkg} = " "$abs_file" | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/' ;; + cargo-dep) + local pkg="${format#cargo-dep:}" + grep "^${pkg} = " "$abs_file" | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/' ;; + python-cargo) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + pyproject) + grep '^version = ' "$abs_file" | head -1 | sed 's/version = "\(.*\)"/\1/' ;; + json) + grep '"version"' "$abs_file" | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/' ;; + csproj) + grep '<Version>' "$abs_file" | head -1 | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/' | tr -d '[:space:]' ;; + gradle) + grep '^version=' "$abs_file" | head -1 | sed 's/version=//' ;; + go) + grep 'const Version' "$abs_file" | head -1 | sed 's/.*const Version = "\([^"]*\)".*/\1/' ;; + *) + echo -e "${RED}Unknown format: ${format}${NC}" >&2 + exit 1 ;; + esac +} + +# Write translated version into file using sed. +write_version() { + local file="$1" format="$2" new_canonical="$3" + local abs_file="${REPO_ROOT}/${file}" + local fmt_base="${format%%:*}" + local translated + translated=$(translate_version "$new_canonical" "$format") + + case "$fmt_base" in + cargo) + sed -i "1,/^version = \".*\"/s/^version = \".*\"/version = \"${translated}\"/" "$abs_file" ;; + cargo-ws-dep) + local pkg="${format#cargo-ws-dep:}" + sed -i "s/^\(${pkg} = .*version = \"\)[^\"]*/\1${translated}/" "$abs_file" ;; + cargo-dep) + local pkg="${format#cargo-dep:}" + sed -i "s/^\(${pkg} = .*version = \"\)[^\"]*/\1${translated}/" "$abs_file" ;; + python-cargo) + sed -i "1,/^version = \".*\"/s/^version = \".*\"/version = \"${translated}\"/" "$abs_file" ;; + pyproject) + sed -i '/^\[project\]/,/^\[/{ s/^version = ".*"/version = "'"${translated}"'"/ }' "$abs_file" ;; + json) + sed -i "s/\"version\": *\"[^\"]*\"/\"version\": \"${translated}\"/" "$abs_file" ;; + csproj) + sed -i "s/<Version>[^<]*<\/Version>/<Version>${translated}<\/Version>/" "$abs_file" ;; + gradle) + sed -i "s/^version=.*/version=${translated}/" "$abs_file" ;; + go) + sed -i "s/const Version = \"[^\"]*\"/const Version = \"${translated}\"/" "$abs_file" ;; + esac +} + +# --status: print current versions for all or one component. +cmd_status() { + local filter="${1:-}" + local components + if [[ "$filter" == "rust-all" ]]; then + components="$RUST_COMPONENTS" + elif [[ -n "$filter" ]]; then + components="$filter" + else + components="$ALL_COMPONENTS" + fi + + for comp in $components; do + echo -e "${CYAN}${comp}${NC}" + local first_canonical="" + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw canonical + raw=$(read_current_version "$file" "$format") + canonical=$(canonicalize_version "$raw" "$format") + printf " %-55s %s" "$file" "$raw" + if [[ "$raw" != "$canonical" ]]; then + printf " (canonical: %s)" "$canonical" + fi + printf "\n" + if [[ -z "$first_canonical" ]]; then + first_canonical="$canonical" + fi + done < <(get_version_files "$comp") + echo "" + done +} + +# Collect unique files from version entries for dirty check. +collect_files() { + local component="$1" + get_version_files "$component" | while IFS= read -r entry; do + echo "${entry%%:*}" + done | sort -u +} + +# Pre-flight: check for uncommitted changes in version files. +preflight_dirty_check() { + local component="$1" + local dirty_files=() + while IFS= read -r file; do + if ! git -C "$REPO_ROOT" diff --quiet -- "$file" 2>/dev/null; then + dirty_files+=("$file") + fi + if ! git -C "$REPO_ROOT" diff --cached --quiet -- "$file" 2>/dev/null; then + dirty_files+=("$file (staged)") + fi + done < <(collect_files "$component") + + if [[ ${#dirty_files[@]} -gt 0 ]]; then + echo -e "${YELLOW}Warning: uncommitted changes in version files:${NC}" >&2 + for f in "${dirty_files[@]}"; do + echo -e " ${f}" >&2 + done + fi +} + +# Pre-flight: verify grouped components have consistent canonical versions. +# Returns the canonical version (or exits on inconsistency). +preflight_consistency_check() { + local component="$1" + local first_canonical="" first_file="" + local inconsistent=0 + + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw canonical + raw=$(read_current_version "$file" "$format") + canonical=$(canonicalize_version "$raw" "$format") + + if [[ -z "$first_canonical" ]]; then + first_canonical="$canonical" + first_file="$file" + elif [[ "$canonical" != "$first_canonical" ]]; then + if [[ $inconsistent -eq 0 ]]; then + echo -e "${RED}Inconsistent versions in ${component}:${NC}" >&2 + echo -e " ${first_file}: ${first_canonical}" >&2 + inconsistent=1 + fi + echo -e " ${file}: ${canonical}" >&2 + fi + done < <(get_version_files "$component") + + if [[ $inconsistent -ne 0 ]]; then + echo -e "${RED}Fix version inconsistencies before bumping, or use --set --force.${NC}" >&2 + return 1 + fi + + echo "$first_canonical" +} + +# Main bump logic. +cmd_bump() { + local component="$1" + shift + local apply=1 set_ver="" force=0 has_edge=0 strip_edge=0 keyword="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --patch) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --patch${NC}" >&2; return 1; } + keyword="patch" ;; + --minor) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --minor${NC}" >&2; return 1; } + keyword="minor" ;; + --major) + [[ -n "$keyword" ]] && { echo -e "${RED}Conflicting flags: --${keyword} and --major${NC}" >&2; return 1; } + keyword="major" ;; + --edge) has_edge=1 ;; + --strip-edge) strip_edge=1 ;; + --dry-run|--dry) apply=0 ;; + --set) + [[ $# -lt 2 ]] && { echo -e "${RED}--set requires a version argument${NC}" >&2; return 1; } + set_ver="$2"; shift ;; + --force) force=1 ;; + *) echo -e "${RED}Unknown flag: ${1}${NC}" >&2; return 1 ;; + esac + shift + done + + # Resolve --edge: standalone = increment counter, combined with bump = add suffix + local add_edge=0 + if [[ $has_edge -eq 1 ]]; then + if [[ -n "$keyword" ]]; then + add_edge=1 + else + keyword="edge" + fi + fi + + # Validate: need exactly one action + if [[ -z "$set_ver" && -z "$keyword" && $strip_edge -eq 0 ]]; then + echo -e "${RED}Missing version flag. Use --patch, --minor, --major, --edge, --strip-edge, or --set${NC}" >&2 + return 1 + fi + + preflight_dirty_check "$component" + local current + current=$(preflight_consistency_check "$component") + + local new_ver + if [[ -n "$set_ver" ]]; then + if [[ $force -eq 0 ]]; then + if ! parse_version "$set_ver" > /dev/null 2>&1; then + echo -e "${RED}Invalid version format: ${set_ver}${NC}" >&2 + echo "Expected: X.Y.Z or X.Y.Z-edge.N (use --force to bypass)" >&2 + return 1 + fi + fi + if [[ $force -eq 0 && "$set_ver" == "$current" ]]; then + echo -e "${YELLOW}Already at ${set_ver}, nothing to do.${NC}" + return 0 + fi + new_ver="$set_ver" + else + new_ver=$(compute_next_version "$current" "${keyword:-_none}" "$add_edge" "$strip_edge") || return 1 + fi + + # Java SNAPSHOT idempotency: edge on already-SNAPSHOT is a no-op + if [[ "$component" == "sdk-java" && "$keyword" == "edge" ]]; then + local raw_java + raw_java=$(read_current_version "foreign/java/gradle.properties" "gradle") + if [[ "$raw_java" == *-SNAPSHOT ]]; then + echo -e "${GREEN}sdk-java already at SNAPSHOT (${raw_java}), nothing to do.${NC}" + return 0 + fi + fi + + echo -e "${CYAN}Component:${NC} ${component}" + echo -e "${CYAN}Current:${NC} ${current}" + echo -e "${CYAN}New:${NC} ${new_ver}" + echo "" + + local changed_files=() + while IFS= read -r entry; do + local file="${entry%%:*}" + local format="${entry#*:}" + local raw_old translated_new + raw_old=$(read_current_version "$file" "$format") + translated_new=$(translate_version "$new_ver" "$format") + + if [[ "$raw_old" == "$translated_new" ]]; then + printf " %-55s %s (unchanged)\n" "$file" "$raw_old" + else + printf " %-55s %s -> ${GREEN}%s${NC}\n" "$file" "$raw_old" "$translated_new" + changed_files+=("$file") + fi + + if [[ $apply -eq 1 ]]; then + write_version "$file" "$format" "$new_ver" + local verify + verify=$(read_current_version "$file" "$format") + if [[ "$verify" != "$translated_new" ]]; then + echo -e "${RED}FAILED to write ${file}: expected '${translated_new}', got '${verify}'${NC}" >&2 + return 1 + fi + fi + done < <(get_version_files "$component") + + echo "" + if [[ $apply -eq 1 ]]; then + echo -e "${GREEN}Applied.${NC}" + else + echo -e "${YELLOW}Dry run. Remove --dry-run to write changes.${NC}" + fi + + if [[ ${#changed_files[@]} -gt 0 ]]; then + local unique_files + IFS=$'\n' read -r -d '' -a unique_files < <(printf '%s\n' "${changed_files[@]}" | sort -u && printf '\0') || true + echo "" + echo "To commit:" + local git_add=" git add" + for f in "${unique_files[@]}"; do + git_add+=" ${f}" + done + echo "$git_add" + echo " git commit -m \"chore(release): bump ${component} to ${new_ver}\"" + fi +} + +# --- Argument parsing --- + +if [[ $# -eq 0 ]]; then + usage + exit 0 +fi + +case "$1" in + -h|--help) + usage + exit 0 ;; + --status) + cmd_status "${2:-}" + exit 0 ;; +esac + +# Bump multiple components with the same flags. +cmd_bump_multi() { + local components="$1" + shift + local failed=0 + for comp in $components; do + echo -e "${CYAN}--- ${comp} ---${NC}" + if cmd_bump "$comp" "$@"; then + : + else + failed=$((failed + 1)) + echo -e "${YELLOW}Skipped ${comp} (transition not valid)${NC}" + fi + echo "" + done + if [[ $failed -gt 0 ]]; then + echo -e "${YELLOW}${failed} component(s) skipped due to invalid transitions.${NC}" + echo -e "Use ${CYAN}--set${NC} to handle them individually." + fi +} + +# Resolve component target: --all, rust-all, or named component. +# Remaining args are flags passed to cmd_bump. +case "$1" in + --all) + shift + cmd_bump_multi "$ALL_COMPONENTS" "$@" + exit 0 ;; + rust-all) + shift + cmd_bump_multi "$RUST_COMPONENTS" "$@" + exit 0 ;; Review Comment: The `--all` and `rust-all `branches always run exit 0 after `cmd_bump_multi`, so the process exit code is 0 even when `failed > 0`. That makes `rust-all / --all` unsafe for release automation or any script that relies on `$? `(partial bumps look like full success). Suggested direction: have `cmd_bump_multi` return non-zero when failed > 0, and replace the unconditional exit 0 with propagating that status (e.g. `cmd_bump_multi` `...; exit $?` or `exit 1` when any component failed). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
