This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 50fe8ec77 feat(ci): add version bump tooling and consistency checks
(#2990)
50fe8ec77 is described below
commit 50fe8ec7747ca149993687d9ad5f53d73023ca79
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Wed Apr 1 17:01:41 2026 +0200
feat(ci): add version bump tooling and consistency checks (#2990)
---
.github/workflows/_common.yml | 31 +-
.pre-commit-config.yaml | 12 +-
scripts/bump-version.sh | 639 +++++++++++++++++++++
...-version-sync.sh => python-sdk-version-sync.sh} | 13 +-
scripts/ci/shellcheck.sh | 4 +-
scripts/extract-version.sh | 271 ++++++---
6 files changed, 879 insertions(+), 91 deletions(-)
diff --git a/.github/workflows/_common.yml b/.github/workflows/_common.yml
index 6b121d537..6dfdb9226 100644
--- a/.github/workflows/_common.yml
+++ b/.github/workflows/_common.yml
@@ -46,7 +46,26 @@ jobs:
- uses: actions/checkout@v4
- name: Check Python SDK versions are synchronized
- run: ./scripts/ci/python-version-sync.sh --check
+ run: ./scripts/ci/python-sdk-version-sync.sh --check
+
+ version-consistency:
+ name: Check version consistency
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup yq
+ run: |
+ if ! command -v yq >/dev/null 2>&1; then
+ YQ_VERSION="v4.47.1"
+
YQ_CHECKSUM="0fb28c6680193c41b364193d0c0fc4a03177aecde51cfc04d506b1517158c2fb"
+ curl -sSL -o /usr/local/bin/yq
https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64
+ echo "${YQ_CHECKSUM} /usr/local/bin/yq" | sha256sum -c - || exit 1
+ chmod +x /usr/local/bin/yq
+ fi
+
+ - name: Check version consistency
+ run: ./scripts/extract-version.sh --check
pr-title:
name: Check PR Title
@@ -255,6 +274,7 @@ jobs:
[
rust-versions,
python-versions,
+ version-consistency,
pr-title,
license-headers,
license-list,
@@ -295,6 +315,7 @@ jobs:
# Always-run checks
RUST_VERSIONS="${{ needs.rust-versions.result }}"
PYTHON_VERSIONS="${{ needs.python-versions.result }}"
+ VERSION_CONSISTENCY="${{ needs.version-consistency.result }}"
LICENSE_HEADERS="${{ needs.license-headers.result }}"
LICENSE_LIST="${{ needs.license-list.result }}"
MARKDOWN="${{ needs.markdown.result }}"
@@ -315,6 +336,14 @@ jobs:
echo "| ⏭️ Python SDK Versions | $PYTHON_VERSIONS | Check skipped
|" >> $GITHUB_STEP_SUMMARY
fi
+ if [ "$VERSION_CONSISTENCY" = "success" ]; then
+ echo "| ✅ Version Consistency | success | Group and workspace dep
versions match |" >> $GITHUB_STEP_SUMMARY
+ elif [ "$VERSION_CONSISTENCY" = "failure" ]; then
+ echo "| ❌ Version Consistency | failure | Version mismatch
detected |" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "| ⏭️ Version Consistency | $VERSION_CONSISTENCY | Check
skipped |" >> $GITHUB_STEP_SUMMARY
+ fi
+
if [ "$LICENSE_HEADERS" = "success" ]; then
echo "| ✅ License Headers | success | All files have Apache
headers |" >> $GITHUB_STEP_SUMMARY
elif [ "$LICENSE_HEADERS" = "failure" ]; then
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bf0c242cf..d47136dc2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -79,9 +79,9 @@ repos:
files: ^rust-toolchain\.toml$
pass_filenames: false
- - id: python-version-sync
+ - id: python-sdk-version-sync
name: python sdk version sync
- entry: ./scripts/ci/python-version-sync.sh
+ entry: ./scripts/ci/python-sdk-version-sync.sh
args: ["--fix"]
language: system
files: ^foreign/python/(Cargo\.toml|pyproject\.toml)$
@@ -95,6 +95,14 @@ repos:
files: ^(foreign|bdd|examples)/python/(pyproject\.toml|uv\.lock)$
pass_filenames: false
+ - id: version-consistency
+ name: version consistency
+ entry: ./scripts/extract-version.sh
+ args: ["--check"]
+ language: system
+ files:
(^Cargo\.toml$|^(core|foreign)/.*Cargo\.toml$|^foreign/node/package\.json$|^foreign/python/pyproject\.toml$|^foreign/csharp/.*\.csproj$|^foreign/java/gradle\.properties$|^foreign/go/contracts/version\.go$|^\.github/config/publish\.yml$)
+ pass_filenames: false
+
- id: trailing-whitespace
name: trailing whitespace
entry: ./scripts/ci/trailing-whitespace.sh
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
new file mode 100755
index 000000000..fd4582c6b
--- /dev/null
+++ b/scripts/bump-version.sh
@@ -0,0 +1,639 @@
+#!/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
+ return 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
+ return 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
+ return 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
+ return 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 "0,/\"version\": *\"[^\"]*\"/{s/\"version\":
*\"[^\"]*\"/\"version\": \"${translated}\"/}" "$abs_file" ;;
+ csproj)
+ sed -i
"0,/<Version>[^<]*<\/Version>/{s/<Version>[^<]*<\/Version>/<Version>${translated}<\/Version>/}"
"$abs_file" ;;
+ gradle)
+ sed -i "s/^version=.*/version=${translated}/" "$abs_file" ;;
+ go)
+ sed -i "0,/const Version = \"[^\"]*\"/{s/const Version =
\"[^\"]*\"/const Version = \"${translated}\"/}" "$abs_file" ;;
+ *)
+ echo -e "${RED}Unknown format: ${format}${NC}" >&2
+ return 1 ;;
+ 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 diff --quiet -- "$REPO_ROOT/$file" 2>/dev/null; then
+ dirty_files+=("$file")
+ fi
+ if ! git diff --cached --quiet -- "$REPO_ROOT/$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) 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: any edge operation on already-SNAPSHOT is a
no-op
+ # (SNAPSHOT has no counter, so --edge and --patch --edge both produce
SNAPSHOT)
+ if [[ "$component" == "sdk-java" ]] && [[ "$keyword" == "edge" ||
$add_edge -eq 1 ]]; 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
+
+ if [[ "${BUMP_MULTI:-0}" -eq 1 ]]; then
+ # In multi-component mode, accumulate files for a single commit
suggestion
+ for f in "${unique_files[@]}"; do
+ BUMP_MULTI_FILES+=("$f")
+ done
+ else
+ echo ""
+ echo "Next steps:"
+ echo " cargo generate-lockfile"
+ local git_add=" git add Cargo.lock"
+ 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
+ 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.
+# Collects all changed files and prints a single commit suggestion at the end.
+cmd_bump_multi() {
+ local components="$1"
+ shift
+ local failed=0
+ BUMP_MULTI=1
+ BUMP_MULTI_FILES=()
+
+ for comp in $components; do
+ echo -e "${CYAN}--- ${comp} ---${NC}"
+ if cmd_bump "$comp" "$@"; then
+ :
+ else
+ failed=$((failed + 1))
+ echo -e "${RED}FAILED ${comp}${NC}"
+ fi
+ echo ""
+ done
+
+ if [[ $failed -gt 0 ]]; then
+ echo -e "${RED}${failed} component(s) failed.${NC}"
+ echo -e "Use ${CYAN}--set${NC} to handle them individually."
+ echo -e "Run ${CYAN}git checkout${NC} on affected files to revert
partial changes."
+ BUMP_MULTI=0
+ return 1
+ fi
+
+ if [[ ${#BUMP_MULTI_FILES[@]} -gt 0 ]]; then
+ local unique_files
+ IFS=$'\n' read -r -d '' -a unique_files < <(printf '%s\n'
"${BUMP_MULTI_FILES[@]}" | sort -u && printf '\0') || true
+ echo "Next steps:"
+ echo " cargo generate-lockfile"
+ local git_add=" git add Cargo.lock"
+ for f in "${unique_files[@]}"; do
+ git_add+=" ${f}"
+ done
+ echo "$git_add"
+ echo " git commit -m \"chore(release): bump all components\""
+ fi
+
+ BUMP_MULTI=0
+}
+
+# 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" "$@" ;;
+ rust-all)
+ shift
+ cmd_bump_multi "$RUST_COMPONENTS" "$@" ;;
+ --*)
+ echo -e "${RED}Expected component name, got flag: ${1}${NC}" >&2
+ echo "Use: bump-version.sh <component> <flags>" >&2
+ exit 1 ;;
+ *)
+ COMPONENT="$1"
+ shift
+ cmd_bump "$COMPONENT" "$@" ;;
+esac
diff --git a/scripts/ci/python-version-sync.sh
b/scripts/ci/python-sdk-version-sync.sh
similarity index 92%
rename from scripts/ci/python-version-sync.sh
rename to scripts/ci/python-sdk-version-sync.sh
index 7abc966d0..3788573bc 100755
--- a/scripts/ci/python-version-sync.sh
+++ b/scripts/ci/python-sdk-version-sync.sh
@@ -87,16 +87,21 @@ if [ -z "$PYPROJECT_VERSION" ]; then
exit 1
fi
-# Normalize versions for comparison (both to PEP 440 format with .dev)
+# Normalize versions for comparison (both to strict PEP 440 format: X.Y.Z.devN)
+# Handles both old format (0.7.2-dev.1 / 0.7.2.dev.1) and new (0.7.2-dev1 /
0.7.2.dev1)
normalize_version() {
local v="$1"
- echo "${v//-dev/.dev}"
+ # -dev -> .dev (Cargo to PEP 440 separator)
+ v="${v//-dev/.dev}"
+ # .dev.N -> .devN (strip dot before counter if present)
+ v=$(echo "$v" | sed -E 's/\.dev\.([0-9])/\.dev\1/')
+ echo "$v"
}
-# Convert PEP 440 format to Cargo format
+# Convert PEP 440 format to Cargo format: .devN -> -devN
to_cargo_format() {
local v="$1"
- echo "${v//.dev/-dev}"
+ echo "$v" | sed -E 's/\.dev([0-9])/-dev\1/'
}
# Compare two versions, returns:
diff --git a/scripts/ci/shellcheck.sh b/scripts/ci/shellcheck.sh
index 2f769952a..c9cd209b6 100755
--- a/scripts/ci/shellcheck.sh
+++ b/scripts/ci/shellcheck.sh
@@ -86,7 +86,7 @@ if [ "$MODE" = "fix" ]; then
FAILED=0
while IFS= read -r -d '' script; do
echo "Checking: $script"
- if ! shellcheck -f gcc "$script"; then
+ if ! shellcheck -x -f gcc "$script"; then
FAILED=1
fi
echo ""
@@ -103,7 +103,7 @@ else
echo "🔍 Checking shell scripts..."
# Run shellcheck on all shell scripts (checks all severities: error,
warning, info, style)
- if find . -type f -name "*.sh" "${FIND_EXCLUDE_ARGS[@]}" -exec shellcheck {}
+; then
+ if find . -type f -name "*.sh" "${FIND_EXCLUDE_ARGS[@]}" -exec shellcheck -x
{} +; then
echo "✅ All shell scripts passed shellcheck"
else
echo ""
diff --git a/scripts/extract-version.sh b/scripts/extract-version.sh
index c76a3a069..d5ec54424 100755
--- a/scripts/extract-version.sh
+++ b/scripts/extract-version.sh
@@ -24,6 +24,8 @@
#
# Usage:
# ./extract-version.sh <component> [--tag]
+# ./extract-version.sh --all
+# ./extract-version.sh --check
#
# Examples:
# # Get version for Rust SDK
@@ -41,6 +43,12 @@
# # Get version for Go SDK
# ./extract-version.sh sdk-go # Output: 0.7.0
#
+# # List all components with versions
+# ./extract-version.sh --all
+#
+# # Validate version consistency across the workspace
+# ./extract-version.sh --check
+#
# The script uses the configuration from .github/config/publish.yml to
determine:
# - Where to find the version file (version_file)
# - What regex pattern to use for extraction (version_regex)
@@ -49,6 +57,11 @@
set -euo pipefail
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m'
+
# Check for required tools
if ! command -v yq &> /dev/null; then
echo "Error: yq is required but not installed" >&2
@@ -59,45 +72,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="$REPO_ROOT/.github/config/publish.yml"
-# Parse arguments
-COMPONENT="${1:-}"
-RETURN_TAG=false
-
-shift || true
-while [[ $# -gt 0 ]]; do
- case "$1" in
- --tag)
- RETURN_TAG=true
- shift
- ;;
- *)
- echo "Unknown option: $1" >&2
- exit 1
- ;;
- esac
-done
-
-if [[ -z "$COMPONENT" ]]; then
- echo "Usage: $0 <component> [--tag]" >&2
- echo "" >&2
- echo "Available components:" >&2
- yq eval '.components | keys | .[]' "$CONFIG_FILE" | sed 's/^/ - /' >&2
- exit 1
-fi
-
-# Check if component exists
-if ! yq eval ".components.\"$COMPONENT\"" "$CONFIG_FILE" | grep -q .; then
- echo "Error: Unknown component '$COMPONENT'" >&2
- echo "" >&2
- echo "Available components:" >&2
- yq eval '.components | keys | .[]' "$CONFIG_FILE" | sed 's/^/ - /' >&2
- exit 1
-fi
-
-# Extract component configuration
+# Extract a single config field for a given component
get_config() {
- local key="$1"
- yq eval ".components.\"$COMPONENT\".$key // \"\"" "$CONFIG_FILE"
+ local component="$1"
+ local key="$2"
+ yq eval ".components.\"$component\".$key // \"\"" "$CONFIG_FILE"
}
# Generic regex-based extraction
@@ -112,18 +91,13 @@ extract_version_with_regex() {
# Special handling for XML files (C# .csproj)
if [[ "$file" == *.csproj ]] || [[ "$file" == *.xml ]]; then
- # Extract version from XML tags like <PackageVersion> or <Version>
grep -E '<(PackageVersion|Version)>' "$REPO_ROOT/$file" | head -1 |
sed -E 's/.*<[^>]+>([^<]+)<.*/\1/' | tr -d ' '
elif command -v perl &> /dev/null; then
- # Use perl for more powerful regex support (supports multiline and
lookarounds)
- # Use m{} instead of // to avoid issues with slashes in regex
perl -0777 -ne "if (m{$regex}) { print \$1; exit; }" "$REPO_ROOT/$file"
else
- # Fallback to grep -P if available
if grep -P "" /dev/null 2>/dev/null; then
grep -Pzo "$regex" "$REPO_ROOT/$file" | grep -Pao
'[0-9]+\.[0-9]+\.[0-9]+[^"]*' | head -1
else
- # Basic fallback - may not work for all patterns
grep -E "$regex" "$REPO_ROOT/$file" | head -1 | sed -E
"s/.*$regex.*/\1/"
fi
fi
@@ -133,11 +107,12 @@ extract_version_with_regex() {
extract_cargo_version() {
local package="$1"
local cargo_file="$2"
+ local component="$3"
cd "$REPO_ROOT"
- # Try cargo metadata first (most reliable)
if command -v cargo &> /dev/null && command -v jq &> /dev/null; then
+ local version
version=$(cargo metadata --no-deps --format-version=1 2>/dev/null | \
jq -r --arg pkg "$package" '.packages[] | select(.name ==
$pkg) | .version' | \
head -1)
@@ -148,74 +123,206 @@ extract_cargo_version() {
fi
fi
- # Fallback to direct Cargo.toml parsing using the regex from config
local version_regex
- version_regex=$(get_config "version_regex")
+ version_regex=$(get_config "$component" "version_regex")
if [[ -n "$version_regex" && -f "$REPO_ROOT/$cargo_file" ]]; then
extract_version_with_regex "$cargo_file" "$version_regex"
fi
}
-# Main version extraction logic
-VERSION=""
-VERSION_FILE=$(get_config "version_file")
-VERSION_REGEX=$(get_config "version_regex")
-PACKAGE=$(get_config "package")
-
-# For Rust components with cargo metadata support
-if [[ "$COMPONENT" == rust-* ]] && [[ -n "$PACKAGE" ]]; then
- # Use package name from config if available
- VERSION=$(extract_cargo_version "$PACKAGE" "$VERSION_FILE")
-
- # Fallback to regex-based extraction if cargo metadata failed
- if [[ -z "$VERSION" ]] && [[ -n "$VERSION_FILE" ]] && [[ -n
"$VERSION_REGEX" ]]; then
- VERSION=$(extract_version_with_regex "$VERSION_FILE" "$VERSION_REGEX")
+# Extract version for a named component. Prints the version string to stdout.
+extract_component_version() {
+ local component="$1"
+ local version=""
+ local version_file
+ local version_regex
+ local package
+
+ version_file=$(get_config "$component" "version_file")
+ version_regex=$(get_config "$component" "version_regex")
+ package=$(get_config "$component" "package")
+
+ if [[ "$component" == rust-* ]] && [[ -n "$package" ]]; then
+ version=$(extract_cargo_version "$package" "$version_file"
"$component")
+
+ if [[ -z "$version" ]] && [[ -n "$version_file" ]] && [[ -n
"$version_regex" ]]; then
+ version=$(extract_version_with_regex "$version_file"
"$version_regex")
+ fi
+ elif [[ -n "$version_file" ]] && [[ -n "$version_regex" ]]; then
+ version=$(extract_version_with_regex "$version_file" "$version_regex")
fi
-# Generic extraction using version_file and version_regex
-elif [[ -n "$VERSION_FILE" ]] && [[ -n "$VERSION_REGEX" ]]; then
- VERSION=$(extract_version_with_regex "$VERSION_FILE" "$VERSION_REGEX")
-else
- echo "Error: No version extraction method available for component
'$COMPONENT'" >&2
+
+ echo "$version"
+}
+
+# ── --all mode: print a table of all components ──────────────────────────────
+handle_all() {
+ local components
+ components=$(yq eval '.components | keys | .[]' "$CONFIG_FILE")
+
+ printf "%-28s %s\n" "COMPONENT" "VERSION"
+ printf "%-28s %s\n" "---------" "-------"
+
+ while IFS= read -r comp; do
+ local version
+ version=$(extract_component_version "$comp")
+
+ if [[ -z "$version" ]]; then
+ version="(error)"
+ fi
+
+ printf "%-28s %s\n" "$comp" "$version"
+ done <<< "$components"
+}
+
+# ── --check mode: validate version consistency ───────────────────────────────
+handle_check() {
+ local errors=0
+ local passes=0
+
+ # --- Check 1: Workspace dep consistency ---
+ echo "=== Workspace dep consistency ==="
+ local ws_cargo="$REPO_ROOT/Cargo.toml"
+
+ while IFS= read -r line; do
+ local pkg_name dep_version
+ # Extract package name (left of '=') and version from the dep spec
+ pkg_name=$(echo "$line" | sed -E 's/^([a-z_-]+)\s*=.*/\1/')
+ dep_version=$(echo "$line" | sed -E
's/.*version\s*=\s*"([^"]+)".*/\1/')
+
+ if [[ -z "$pkg_name" ]] || [[ -z "$dep_version" ]]; then
+ continue
+ fi
+
+ # Find the matching component in publish.yml by its package field
+ local comp
+ comp=$(yq eval "[.components | to_entries[] | select(.value.package ==
\"$pkg_name\") | .key] | .[0] // \"\"" "$CONFIG_FILE")
+
+ if [[ -z "$comp" ]]; then
+ # No matching publish.yml component - skip silently
+ continue
+ fi
+
+ local comp_version
+ comp_version=$(extract_component_version "$comp")
+
+ if [[ "$dep_version" == "$comp_version" ]]; then
+ echo -e " ${GREEN}PASS${NC} $pkg_name: workspace=$dep_version,
component=$comp_version"
+ passes=$((passes + 1))
+ else
+ echo -e " ${RED}FAIL${NC} $pkg_name: workspace=$dep_version,
component($comp)=$comp_version"
+ errors=$((errors + 1))
+ fi
+ done < <(grep -E '^iggy[a-z_-]* = \{ path = .*, version = ".*" \}'
"$ws_cargo")
+
+ echo ""
+
+ # --- Check 2: Python dual-file sync ---
+ echo "=== Python dual-file sync ==="
+ local python_script="$SCRIPT_DIR/ci/python-sdk-version-sync.sh"
+ if [[ -x "$python_script" ]]; then
+ if "$python_script" --check; then
+ passes=$((passes + 1))
+ else
+ errors=$((errors + 1))
+ fi
+ else
+ echo -e " ${RED}FAIL${NC} python-sdk-version-sync.sh not found or not
executable"
+ errors=$((errors + 1))
+ fi
+
+ echo ""
+
+ # --- Summary ---
+ local total=$((passes + errors))
+ echo "=== Summary ==="
+ echo -e " Passed: ${GREEN}${passes}${NC}/${total}"
+ if [[ $errors -gt 0 ]]; then
+ echo -e " Failed: ${RED}${errors}${NC}/${total}"
+ exit 1
+ else
+ echo -e " ${GREEN}All checks passed.${NC}"
+ fi
+}
+
+# ── Argument parsing ─────────────────────────────────────────────────────────
+COMPONENT=""
+RETURN_TAG=false
+
+# Detect mode flags as first argument only
+case "${1:-}" in
+ --all) handle_all; exit 0;;
+ --check) handle_check; exit 0;;
+esac
+
+# Original single-component flow
+COMPONENT="${1:-}"
+RETURN_TAG=false
+
+shift || true
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --tag)
+ RETURN_TAG=true
+ shift
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "$COMPONENT" ]]; then
+ echo "Usage: $0 <component> [--tag]" >&2
+ echo " $0 --all" >&2
+ echo " $0 --check" >&2
+ echo "" >&2
+ echo "Available components:" >&2
+ yq eval '.components | keys | .[]' "$CONFIG_FILE" | sed 's/^/ - /' >&2
exit 1
fi
+# Check if component exists
+if ! yq eval ".components.\"$COMPONENT\"" "$CONFIG_FILE" | grep -q .; then
+ echo "Error: Unknown component '$COMPONENT'" >&2
+ echo "" >&2
+ echo "Available components:" >&2
+ yq eval '.components | keys | .[]' "$CONFIG_FILE" | sed 's/^/ - /' >&2
+ exit 1
+fi
+
+# Main version extraction logic
+VERSION=$(extract_component_version "$COMPONENT")
+
# Validate version was found
if [[ -z "$VERSION" ]]; then
echo "Error: Could not extract version for component '$COMPONENT'" >&2
- if [[ -n "$VERSION_FILE" ]]; then
- echo " Checked file: $VERSION_FILE" >&2
+ local_vf=$(get_config "$COMPONENT" "version_file")
+ local_vr=$(get_config "$COMPONENT" "version_regex")
+ if [[ -n "$local_vf" ]]; then
+ echo " Checked file: $local_vf" >&2
fi
- if [[ -n "$VERSION_REGEX" ]]; then
- echo " Using regex: $VERSION_REGEX" >&2
+ if [[ -n "$local_vr" ]]; then
+ echo " Using regex: $local_vr" >&2
fi
exit 1
fi
# Return tag or version based on flag
if [[ "$RETURN_TAG" == "true" ]]; then
- TAG_PATTERN=$(get_config "tag_pattern")
+ TAG_PATTERN=$(get_config "$COMPONENT" "tag_pattern")
if [[ -z "$TAG_PATTERN" ]]; then
echo "Error: No tag pattern defined for component '$COMPONENT'" >&2
exit 1
fi
- # Replace the capture group in the pattern with the actual version
- # The pattern has a capture group like
"^iggy-([0-9]+\\.[0-9]+\\.[0-9]+...)$"
- # We need to replace the (...) part with the actual version
-
- # Extract the prefix (everything before the first capture group)
PREFIX=$(echo "$TAG_PATTERN" | sed -E 's/^(\^?)([^(]*)\(.*/\2/')
-
- # Extract the suffix (everything after the capture group)
SUFFIX=$(echo "$TAG_PATTERN" | sed -E 's/.*\)[^)]*(\$?)$/\1/')
-
- # Build the tag
TAG="${PREFIX}${VERSION}${SUFFIX}"
-
- # Remove regex anchors if present
TAG=$(echo "$TAG" | sed 's/^\^//; s/\$$//')
echo "$TAG"
else
echo "$VERSION"
-fi
\ No newline at end of file
+fi