This is an automated email from the ASF dual-hosted git repository.

hgruszecki pushed a commit to branch prek
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 202e5d783e544c3b4c10cdf01bc20d8bc0ea8558
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Thu Nov 20 13:23:20 2025 +0100

    feat(repo): introduce pre-commit hooks and refactor CI scripts
    
    Adds .pre-commit-config.yaml with prek-based hooks and extracts inline bash
    from GitHub Actions workflows into reusable scripts with --check/--fix 
modes.
---
 .github/workflows/_common.yml             | 253 ++++--------------------------
 .github/workflows/dependabot-licenses.yml |   2 +-
 .markdownlint.yml                         |  24 +++
 .pre-commit-config.yaml                   | 120 ++++++++++++++
 CONTRIBUTING.md                           |  34 ++++
 foreign/cpp/.pre-commit-config.yaml       | 107 -------------
 justfile                                  |   8 +-
 scripts/ci/license-headers.sh             | 136 ++++++++++++++++
 scripts/ci/licenses-list.sh               | 100 ++++++++++++
 scripts/ci/markdownlint.sh                |  63 ++++++++
 scripts/ci/shellcheck.sh                  | 111 +++++++++++++
 scripts/{ => ci}/sync-rust-version.sh     |   4 +-
 scripts/ci/trailing-newline.sh            | 188 ++++++++++++++++++++++
 scripts/ci/trailing-whitespace.sh         | 187 ++++++++++++++++++++++
 scripts/copy-latest-from-master.sh        |   4 +-
 scripts/licenses-list.sh                  | 110 -------------
 16 files changed, 1000 insertions(+), 451 deletions(-)

diff --git a/.github/workflows/_common.yml b/.github/workflows/_common.yml
index f4de395e3..1595affc2 100644
--- a/.github/workflows/_common.yml
+++ b/.github/workflows/_common.yml
@@ -39,18 +39,7 @@ jobs:
       - uses: actions/checkout@v4
 
       - name: Check Rust versions are synchronized
-        run: |
-          # Use the sync-rust-version.sh script in check mode
-          if ! bash scripts/sync-rust-version.sh --check; then
-            echo ""
-            echo "โŒ Rust versions are not synchronized!"
-            echo ""
-            echo "To fix this issue, run:"
-            echo "  ./scripts/sync-rust-version.sh --fix"
-            echo ""
-            echo "This script will automatically update all Dockerfiles to 
match rust-toolchain.toml"
-            exit 1
-          fi
+        run: ./scripts/ci/sync-rust-version.sh --check
 
   pr-title:
     name: Check PR Title
@@ -121,36 +110,7 @@ jobs:
       - uses: actions/checkout@v4
 
       - name: Check Apache license headers
-        run: |
-          echo "๐Ÿ” Checking license headers..."
-
-          # Pull the addlicense image
-          docker pull ghcr.io/google/addlicense:latest
-
-          # Run the check
-          if docker run --rm -v ${{ github.workspace }}:/src -w /src \
-            ghcr.io/google/addlicense:latest \
-            -check -f ASF_LICENSE.txt . > missing_files.txt 2>&1; then
-            echo "โœ… All files have proper license headers"
-          else
-            file_count=$(wc -l < missing_files.txt)
-            echo "โŒ Found $file_count files missing license headers:"
-            echo ""
-            cat missing_files.txt | sed 's/^/  โ€ข /'
-            echo ""
-            echo "๐Ÿ’ก Run 'addlicense -f ASF_LICENSE.txt .' to fix automatically"
-
-            # Add to summary
-            echo "## โŒ License Headers Missing" >> $GITHUB_STEP_SUMMARY
-            echo "" >> $GITHUB_STEP_SUMMARY
-            echo "The following files are missing Apache license headers:" >> 
$GITHUB_STEP_SUMMARY
-            echo '```' >> $GITHUB_STEP_SUMMARY
-            cat missing_files.txt >> $GITHUB_STEP_SUMMARY
-            echo '```' >> $GITHUB_STEP_SUMMARY
-            echo 'Please call `just licenses-fix` to fix automatically.' >> 
$GITHUB_STEP_SUMMARY
-
-            exit 1
-          fi
+        run: ./scripts/ci/license-headers.sh --check
 
   license-list:
     name: Check licenses list
@@ -163,9 +123,12 @@ jobs:
       - name: Setup Rust toolchain
         uses: ./.github/actions/utils/setup-rust-with-cache
         with:
-          enabled: "false"  # Don't need cache for just checking licenses
+          enabled: "false" # Don't need cache for just checking licenses
 
-      - run: scripts/licenses-list.sh --check
+      - name: Install cargo-license
+        run: cargo install cargo-license
+
+      - run: ./scripts/ci/licenses-list.sh --check
 
   markdown:
     name: Markdown lint
@@ -178,37 +141,13 @@ jobs:
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
-          node-version: '23'
+          node-version: "23"
 
       - name: Install markdownlint-cli
         run: npm install -g markdownlint-cli
 
       - name: Run markdownlint
-        run: |
-          echo "๐Ÿ” Checking markdown files..."
-
-          # Create config if it doesn't exist
-          if [ ! -f ".markdownlint.yml" ]; then
-            cat > .markdownlint.yml << 'EOF'
-          # Markdown lint configuration
-          default: true
-          MD013:
-            line_length: 120
-            tables: false
-          MD033:
-            allowed_elements: [details, summary, img]
-          MD041: false  # First line in file should be a top level heading
-          EOF
-          fi
-
-          # Run the linter
-          if markdownlint '**/*.md' --ignore-path .gitignore; then
-            echo "โœ… All markdown files are properly formatted"
-          else
-            echo "โŒ Markdown linting failed"
-            echo "๐Ÿ’ก Run 'markdownlint **/*.md --fix' to auto-fix issues"
-            exit 1
-          fi
+        run: ./scripts/ci/markdownlint.sh --check
 
   shellcheck:
     name: Shell scripts lint
@@ -221,26 +160,12 @@ jobs:
 
       - name: Install shellcheck
         run: |
-          sudo apt-get update --yes && sudo apt-get install --yes shellcheck
+          SHELLCHECK_VERSION="0.11.0"
+          wget -qO- 
"https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz";
 | tar -xJv
+          sudo cp "shellcheck-v${SHELLCHECK_VERSION}/shellcheck" 
/usr/local/bin/
 
       - name: Check shell scripts
-        run: |
-          echo "๐Ÿ” Checking shell scripts..."
-
-          # Find all shell scripts excluding certain directories
-          if find . -type f -name "*.sh" \
-            -not -path "./target/*" \
-            -not -path "./node_modules/*" \
-            -not -path "./.git/*" \
-            -not -path "./foreign/node/node_modules/*" \
-            -not -path "./foreign/python/.venv/*" \
-            -exec shellcheck -S warning {} +; then
-            echo "โœ… All shell scripts passed shellcheck"
-          else
-            echo "โŒ Shellcheck found issues in shell scripts"
-            echo "๐Ÿ’ก Fix the issues reported above"
-            exit 1
-          fi
+        run: ./scripts/ci/shellcheck.sh --check
 
   trailing-whitespace:
     name: Check trailing whitespace
@@ -251,76 +176,10 @@ jobs:
       - name: Checkout code
         uses: actions/checkout@v4
         with:
-          fetch-depth: 0  # Need full history to get diff
+          fetch-depth: 0 # Need full history to get diff
 
       - name: Check for trailing whitespace in changed files
-        run: |
-          echo "๐Ÿ” Checking for trailing whitespace in changed files..."
-
-          # Get list of changed files in PR
-          if [ "${{ github.event_name }}" = "pull_request" ]; then
-            git fetch --no-tags --depth=1 origin ${{ 
github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} 
|| true
-            BASE_SHA="${{ github.event.pull_request.base.sha }}"
-            CHANGED_FILES=$(git diff --name-only --diff-filter=ACM 
"$BASE_SHA"...HEAD || true)
-          else
-            CHANGED_FILES=$(git diff --name-only --diff-filter=ACM HEAD~1)
-          fi
-
-          if [ -z "$CHANGED_FILES" ]; then
-            echo "No files changed to check"
-            exit 0
-          fi
-
-          echo "Files to check:"
-          echo "$CHANGED_FILES" | sed 's/^/  โ€ข /'
-          echo ""
-
-          # Check each changed file for trailing whitespace
-          FILES_WITH_TRAILING=""
-          for file in $CHANGED_FILES; do
-            # Skip if file doesn't exist (might be deleted)
-            if [ ! -f "$file" ]; then
-              continue
-            fi
-
-            # Skip binary files
-            if file "$file" | grep -qE "binary|data|executable|compressed"; 
then
-              continue
-            fi
-
-            # Check for trailing whitespace
-            if grep -q '[[:space:]]$' "$file" 2>/dev/null; then
-              FILES_WITH_TRAILING="$FILES_WITH_TRAILING $file"
-            fi
-          done
-
-          if [ -z "$FILES_WITH_TRAILING" ]; then
-            echo "โœ… No trailing whitespace found in changed files"
-          else
-            echo "โŒ Found trailing whitespace in the following changed files:"
-            echo ""
-            for file in $FILES_WITH_TRAILING; do
-              echo "  โ€ข $file"
-              # Show lines with trailing whitespace (limit to first 5 
occurrences per file)
-              grep -n '[[:space:]]$' "$file" | head -5 | while IFS=: read -r 
line_num content; do
-                # Show the line with visible whitespace markers
-                visible_content=$(echo "$content" | sed 's/ /ยท/g; s/\t/โ†’/g')
-                echo "    Line $line_num: '${visible_content}'"
-              done
-              TOTAL_LINES=$(grep -c '[[:space:]]$' "$file")
-              if [ "$TOTAL_LINES" -gt 5 ]; then
-                echo "    ... and $((TOTAL_LINES - 5)) more lines"
-              fi
-              echo ""
-            done
-
-            echo "๐Ÿ’ก To fix trailing whitespace in these files:"
-            echo "   โ€ข VSCode: Enable 'files.trimTrailingWhitespace' setting"
-            echo "   โ€ข Fix specific file: sed -i 's/[[:space:]]*$//' 
<filename>"
-            echo "   โ€ข Fix all changed files:"
-            echo "     for f in$FILES_WITH_TRAILING; do sed -i 
's/[[:space:]]*$//' \$f; done"
-            exit 1
-          fi
+        run: ./scripts/ci/trailing-whitespace.sh --check --ci
 
   trailing-newline:
     name: Check trailing newline
@@ -331,80 +190,24 @@ jobs:
       - name: Checkout code
         uses: actions/checkout@v4
         with:
-          fetch-depth: 0  # Need full history to get diff
+          fetch-depth: 0 # Need full history to get diff
 
       - name: Check for trailing newline in changed text files
-        run: |
-          echo "๐Ÿ” Checking for trailing newline in changed text files..."
-
-          # Get list of changed files in PR
-          if [ "${{ github.event_name }}" = "pull_request" ]; then
-            git fetch --no-tags --depth=1 origin ${{ 
github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} 
|| true
-            BASE_SHA="${{ github.event.pull_request.base.sha }}"
-            CHANGED_FILES=$(git diff --name-only --diff-filter=ACM 
"$BASE_SHA"...HEAD || true)
-          else
-            CHANGED_FILES=$(git diff --name-only --diff-filter=ACM HEAD~1)
-          fi
-
-          if [ -z "$CHANGED_FILES" ]; then
-            echo "No files changed to check"
-            exit 0
-          fi
-
-          echo "Files to check:"
-          echo "$CHANGED_FILES" | sed 's/^/  โ€ข /'
-          echo ""
-
-          # Check each changed file for missing trailing newline
-          FILES_WITHOUT_NEWLINE=""
-          for file in $CHANGED_FILES; do
-            # Skip if file doesn't exist (might be deleted)
-            if [ ! -f "$file" ]; then
-              continue
-            fi
-
-            # Skip binary files
-            if file "$file" | grep -qE "binary|data|executable|compressed"; 
then
-              continue
-            fi
-
-            # Skip empty files
-            if [ ! -s "$file" ]; then
-              continue
-            fi
-
-            # Check if file ends with a newline
-            # Use tail to get last byte and od to check if it's a newline 
(0x0a)
-            if [ -n "$(tail -c 1 "$file" | od -An -tx1 | grep -v '0a')" ]; then
-              FILES_WITHOUT_NEWLINE="$FILES_WITHOUT_NEWLINE $file"
-            fi
-          done
-
-          if [ -z "$FILES_WITHOUT_NEWLINE" ]; then
-            echo "โœ… All changed text files have trailing newlines"
-          else
-            echo "โŒ Found text files without trailing newline:"
-            echo ""
-            for file in $FILES_WITHOUT_NEWLINE; do
-              echo "  โ€ข $file"
-              # Show last few characters of the file for context
-              echo -n "    Last characters: '"
-              tail -c 20 "$file" | tr '\n' 'โ†ต' | sed 's/\t/โ†’/g'
-              echo "'"
-              echo ""
-            done
-
-            echo "๐Ÿ’ก To add trailing newlines to these files:"
-            echo "   โ€ข VSCode: Enable 'files.insertFinalNewline' setting"
-            echo "   โ€ข Fix specific file: echo >> <filename>"
-            echo "   โ€ข Fix all files:"
-            echo "     for f in$FILES_WITHOUT_NEWLINE; do [ -n \"\$(tail -c 1 
\"\$f\")\" ] && echo >> \"\$f\"; done"
-            exit 1
-          fi
+        run: ./scripts/ci/trailing-newline.sh --check --ci
 
   summary:
     name: Common checks summary
-    needs: [rust-versions, pr-title, license-headers, license-list, markdown, 
shellcheck, trailing-whitespace, trailing-newline]
+    needs:
+      [
+        rust-versions,
+        pr-title,
+        license-headers,
+        license-list,
+        markdown,
+        shellcheck,
+        trailing-whitespace,
+        trailing-newline,
+      ]
     if: always()
     runs-on: ubuntu-latest
     env:
diff --git a/.github/workflows/dependabot-licenses.yml 
b/.github/workflows/dependabot-licenses.yml
index d58f91aeb..8b433feb4 100644
--- a/.github/workflows/dependabot-licenses.yml
+++ b/.github/workflows/dependabot-licenses.yml
@@ -50,7 +50,7 @@ jobs:
         run: cargo install cargo-license
 
       - name: Update DEPENDENCIES.md
-        run: ./scripts/licenses-list.sh --update
+        run: ./scripts/ci/licenses-list.sh --update
 
       - name: Commit changes
         run: |
diff --git a/.markdownlint.yml b/.markdownlint.yml
new file mode 100644
index 000000000..88d9d9b03
--- /dev/null
+++ b/.markdownlint.yml
@@ -0,0 +1,24 @@
+# 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.
+
+default: true
+MD013:
+  line_length: 120
+  tables: false
+MD033:
+  allowed_elements: [details, summary, img]
+MD041: false # First line in file should be a top level heading
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..e0919768b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,120 @@
+# 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.
+
+default_stages: [pre-commit]
+default_install_hook_types: [pre-commit, pre-push]
+
+repos:
+  # Standard pre-commit hooks
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v6.0.0
+    hooks:
+      - id: check-yaml
+      - id: mixed-line-ending
+
+  # CI scripts
+  - repo: local
+    hooks:
+      - id: markdownlint
+        name: markdownlint
+        entry: ./scripts/ci/markdownlint.sh
+        args: ["--fix", "--staged"]
+        language: system
+        types: [markdown]
+        pass_filenames: false
+
+      - id: shellcheck
+        name: shellcheck
+        entry: ./scripts/ci/shellcheck.sh
+        args: ["--fix", "--staged"]
+        language: system
+        types: [shell]
+        pass_filenames: false
+
+      - id: license-headers
+        name: license headers
+        entry: ./scripts/ci/license-headers.sh
+        args: ["--fix"]
+        language: system
+        pass_filenames: false
+
+      - id: licenses-list
+        name: licenses list
+        entry: ./scripts/ci/licenses-list.sh
+        args: ["--fix"]
+        language: system
+        files: ^Cargo\.lock$
+        pass_filenames: false
+
+      - id: rust-version-sync
+        name: rust version sync
+        entry: ./scripts/ci/sync-rust-version.sh
+        args: ["--fix"]
+        language: system
+        files: ^rust-toolchain\.toml$
+        pass_filenames: false
+
+      - id: trailing-whitespace
+        name: trailing whitespace
+        entry: ./scripts/ci/trailing-whitespace.sh
+        args: ["--fix", "--staged"]
+        language: system
+        types: [text]
+        pass_filenames: false
+
+      - id: trailing-newline
+        name: trailing newline
+        entry: ./scripts/ci/trailing-newline.sh
+        args: ["--fix", "--staged"]
+        language: system
+        types: [text]
+        pass_filenames: false
+
+  # Rust
+  - repo: local
+    hooks:
+      - id: cargo-fmt
+        name: cargo fmt
+        entry: cargo fmt --all
+        language: system
+        types: [rust]
+        pass_filenames: false
+
+      - id: cargo-clippy
+        name: cargo clippy
+        entry: cargo clippy
+        args:
+          [
+            "--all-features",
+            "--all-targets",
+            "--manifest-path",
+            "Cargo.toml",
+            "--",
+            "-D",
+            "warnings",
+          ]
+        language: system
+        types: [rust]
+        pass_filenames: false
+        stages: [pre-push]
+
+        # TODO(hubcio): add fast checks, linters, formatters for other 
languages
+        # - python (maturin, pytest, ruff, black)
+        # - java
+        # - go
+        # - csharp
+        # - js
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 31696169a..8964a23e9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -72,6 +72,40 @@ $ cargo version
 cargo 1.86.0 (adf9b6ad1 2025-02-28)
 ```
 
+### Pre-commit Hooks (Recommended)
+
+Iggy uses [prek](https://github.com/9999years/prek) for pre-commit hooks to 
ensure code quality before commits. Setting up hooks is optional but strongly 
recommended to catch issues early.
+
+#### Setup
+
+```shell
+cargo install prek
+prek install
+```
+
+This will install git hooks that automatically run on `pre-commit` and 
`pre-push` events.
+
+#### Manual hook execution
+
+You can manually run specific hooks:
+
+```shell
+# Run all pre-commit hooks
+prek run
+
+# Run specific hook
+prek run cargo-fmt
+prek run cargo-clippy
+```
+
+#### Skip hooks (when necessary)
+
+If you need to skip hooks for a specific commit:
+
+```shell
+git commit --no-verify -m "your message"
+```
+
 ## How to build
 
 See [Quick 
Start](https://github.com/apache/iggy?tab=readme-ov-file#quick-start)
diff --git a/foreign/cpp/.pre-commit-config.yaml 
b/foreign/cpp/.pre-commit-config.yaml
deleted file mode 100644
index e3a87f017..000000000
--- a/foreign/cpp/.pre-commit-config.yaml
+++ /dev/null
@@ -1,107 +0,0 @@
-// 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.
----
-repos:
-    - repo: https://github.com/adrienverge/yamllint.git
-      rev: v1.21.0
-      hooks:
-          - id: yamllint
-            args: [--format, parsable, --strict]
-    - repo: https://github.com/cmake-lint/cmake-lint
-      rev: 1.4.2
-      hooks:
-          - id: cmakelint
-            args: [--filter, -linelength]
-    - repo: https://github.com/cpplint/cpplint
-      rev: 1.6.1
-      hooks:
-          - id: cpplint
-    - repo: https://github.com/gitleaks/gitleaks
-      rev: v8.16.1
-      hooks:
-          - id: gitleaks
-    - repo: https://github.com/hadolint/hadolint
-      rev: v2.12.0
-      hooks:
-          - id: hadolint
-            args: [--config, .github/linters/.hadolint.yaml]
-    - repo: https://github.com/igorshubovych/markdownlint-cli
-      rev: v0.39.0
-      hooks:
-          - id: markdownlint
-            args: [--config, .github/linters/.markdown-lint.yml]
-    - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt
-      rev: 0.2.3
-      hooks:
-          - id: yamlfmt
-    - repo: https://github.com/pre-commit/pre-commit-hooks
-      rev: v2.3.0
-      hooks:
-          - id: check-added-large-files
-          - id: check-json
-          - id: check-merge-conflict
-          - id: check-yaml
-          - id: end-of-file-fixer
-          - id: trailing-whitespace
-    - repo: https://github.com/pre-commit/mirrors-clang-format
-      rev: v18.1.0
-      hooks:
-          - id: clang-format
-    - repo: https://github.com/scop/pre-commit-shfmt
-      rev: v3.8.0-1
-      hooks:
-          - id: shfmt
-    - repo: local
-      hooks:
-          - id: cppcheck
-            name: cppcheck
-            entry: cppcheck --enable=style,performance,warning --inline-suppr 
--std=c++20 -x c++
-            language: system
-            files: \.(cc|h)$
-            pass_filenames: true
-            always_run: false
-          - id: flawfinder
-            name: flawfinder
-            entry: flawfinder --quiet -S -D --error-level=1
-            language: system
-            files: \.(cc|h)$
-            pass_filenames: true
-            always_run: false
-          - id: lizard
-            name: Check code cyclomatic complexity
-            entry: lizard -C 15 -i 0 -w -l c,cc,cpp,h,hpp
-            language: system
-            pass_filenames: true
-            always_run: false
-          - id: build
-            name: Build
-            entry: cmake --build build
-            language: system
-            pass_filenames: false
-            always_run: true
-          - id: run-unit-tests
-            name: Run Unit Tests
-            entry: ./build/tests/Debug/iggy_cpp_test
-            language: system
-            pass_filenames: false
-            always_run: true
-          - id: e2e-tests
-            name: Run E2E Tests
-            entry: ./build/tests/Debug/iggy_e2e_test
-            language: system
-            pass_filenames: false
-            always_run: true
diff --git a/justfile b/justfile
index 7b7a14b9e..578d0e4dd 100644
--- a/justfile
+++ b/justfile
@@ -67,16 +67,16 @@ profile-io-client:
   ./scripts/profile.sh iggy-bench io
 
 licenses-fix:
-  docker run --rm -v $(pwd):/src -w /src ghcr.io/google/addlicense:latest -f 
ASF_LICENSE.txt .
+  ./scripts/ci/license-headers.sh --fix
 
 licenses-check:
-  docker run --rm -v $(pwd):/src -w /src ghcr.io/google/addlicense:latest 
-check -f ASF_LICENSE.txt .
+  ./scripts/ci/license-headers.sh --check
 
 licenses-list-check:
-  ./scripts/licenses-list.sh --check
+  ./scripts/ci/licenses-list.sh --check
 
 licenses-list-fix:
-  ./scripts/licenses-list.sh --update
+  ./scripts/ci/licenses-list.sh --fix
 
 markdownlint:
   markdownlint '**/*.md' --ignore-path .gitignore
diff --git a/scripts/ci/license-headers.sh b/scripts/ci/license-headers.sh
new file mode 100755
index 000000000..7a002d5b8
--- /dev/null
+++ b/scripts/ci/license-headers.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env 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
+
+# Parse arguments
+MODE="check"
+if [ $# -gt 0 ]; then
+  case "$1" in
+    --check)
+      MODE="check"
+      ;;
+    --fix)
+      MODE="fix"
+      ;;
+    *)
+      echo "Usage: $0 [--check|--fix]"
+      echo "  --check  Check files for Apache license headers (default)"
+      echo "  --fix    Add Apache license headers to files missing them"
+      exit 1
+      ;;
+  esac
+fi
+
+# Check if docker is available
+if ! command -v docker &> /dev/null; then
+  echo "โŒ docker command not found"
+  echo "๐Ÿ’ก Install Docker to run license header checks"
+  exit 1
+fi
+
+# Get the repository root
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+# Check if ASF_LICENSE.txt exists
+if [ ! -f "ASF_LICENSE.txt" ]; then
+  echo "โŒ ASF_LICENSE.txt not found in repository root"
+  exit 1
+fi
+
+# Pull the addlicense image
+echo "Pulling addlicense Docker image..."
+docker pull ghcr.io/google/addlicense:latest >/dev/null 2>&1
+
+# Common patterns to ignore (build artifacts, dependencies, IDE files)
+IGNORE_PATTERNS=(
+  "**/target/**"
+  "target/**"
+  "**/node_modules/**"
+  "node_modules/**"
+  "**/.venv/**"
+  ".venv/**"
+  "**/venv/**"
+  "venv/**"
+  "**/dist/**"
+  "dist/**"
+  "**/build/**"
+  "build/**"
+  "**/.idea/**"
+  ".idea/**"
+  "**/.vscode/**"
+  ".vscode/**"
+  "**/.gradle/**"
+  ".gradle/**"
+  "**/bin/**"
+  "**/obj/**"
+  "**/local_data*/**"
+  "**/performance_results*/**"
+)
+
+# Build ignore flags for addlicense
+IGNORE_FLAGS=()
+for pattern in "${IGNORE_PATTERNS[@]}"; do
+  IGNORE_FLAGS+=("-ignore" "$pattern")
+done
+
+if [ "$MODE" = "fix" ]; then
+  echo "๐Ÿ”ง Adding license headers to files..."
+
+  # Run addlicense without -check to fix files
+  docker run --rm -v "$REPO_ROOT:/src" -w /src \
+    ghcr.io/google/addlicense:latest \
+    -f ASF_LICENSE.txt "${IGNORE_FLAGS[@]}" .
+
+  echo "โœ… License headers have been added to files"
+else
+  echo "๐Ÿ” Checking license headers..."
+
+  # Run the check and capture output
+  TEMP_FILE=$(mktemp)
+  trap 'rm -f "$TEMP_FILE"' EXIT
+
+  if docker run --rm -v "$REPO_ROOT:/src" -w /src \
+    ghcr.io/google/addlicense:latest \
+    -check -f ASF_LICENSE.txt "${IGNORE_FLAGS[@]}" . > "$TEMP_FILE" 2>&1; then
+    echo "โœ… All files have proper license headers"
+  else
+    file_count=$(wc -l < "$TEMP_FILE")
+    echo "โŒ Found $file_count files missing license headers:"
+    echo ""
+    cat "$TEMP_FILE" | sed 's/^/  โ€ข /'
+    echo ""
+    echo "๐Ÿ’ก Run '$0 --fix' to add license headers automatically"
+
+    # Add to GitHub Actions summary if running in CI
+    if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
+      {
+        echo "## โŒ License Headers Missing"
+        echo ""
+        echo "The following files are missing Apache license headers:"
+        echo '```'
+        cat "$TEMP_FILE"
+        echo '```'
+        echo "Please run \`./scripts/ci/license-headers.sh --fix\` to fix 
automatically."
+      } >> "$GITHUB_STEP_SUMMARY"
+    fi
+
+    exit 1
+  fi
+fi
diff --git a/scripts/ci/licenses-list.sh b/scripts/ci/licenses-list.sh
new file mode 100755
index 000000000..2372ff3b3
--- /dev/null
+++ b/scripts/ci/licenses-list.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env 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
+
+# Default mode
+MODE="check"
+
+# Parse arguments
+if [ $# -gt 0 ]; then
+  case "$1" in
+    --check)
+      MODE="check"
+      ;;
+    --update|--fix)
+      MODE="update"
+      ;;
+    *)
+      echo "Usage: $0 [--check|--update|--fix]"
+      echo "  --check   Check if DEPENDENCIES.md is up to date (default)"
+      echo "  --update  Update DEPENDENCIES.md with current dependencies"
+      echo "  --fix     Alias for --update"
+      exit 1
+      ;;
+  esac
+fi
+
+# Check if cargo-license is installed
+if ! command -v cargo-license &>/dev/null; then
+  echo "โŒ cargo-license command not found"
+  echo "๐Ÿ’ก Install it using: cargo install cargo-license"
+  exit 1
+fi
+
+# Check if DEPENDENCIES.md exists
+if [ ! -f "DEPENDENCIES.md" ] && [ "$MODE" = "check" ]; then
+  echo "โŒ DEPENDENCIES.md does not exist"
+  echo "๐Ÿ’ก Run '$0 --fix' to create it"
+  exit 1
+fi
+
+# Generate current dependencies
+TEMP_FILE=$(mktemp)
+trap 'rm -f "$TEMP_FILE"' EXIT
+
+echo "Generating current dependencies list..."
+cargo license --color never --do-not-bundle --all-features >"$TEMP_FILE"
+
+# Update mode
+if [ "$MODE" = "update" ]; then
+  echo "๐Ÿ”ง Updating DEPENDENCIES.md..."
+  {
+    echo "# Dependencies"
+    echo ""
+    cat "$TEMP_FILE"
+  } >DEPENDENCIES.md
+  echo "โœ… DEPENDENCIES.md has been updated"
+  exit 0
+fi
+
+# Check mode
+if [ "$MODE" = "check" ]; then
+  echo "๐Ÿ” Checking if DEPENDENCIES.md is up to date..."
+  # Create expected format for comparison
+  EXPECTED_FILE=$(mktemp)
+  trap 'rm -f "$TEMP_FILE" "$EXPECTED_FILE"' EXIT
+  {
+    echo "# Dependencies"
+    echo ""
+    cat "$TEMP_FILE"
+  } >"$EXPECTED_FILE"
+
+  if ! diff -q "$EXPECTED_FILE" DEPENDENCIES.md >/dev/null; then
+    echo "โŒ DEPENDENCIES.md is out of date"
+    echo ""
+    echo "Diff:"
+    diff -u DEPENDENCIES.md "$EXPECTED_FILE" || true
+    echo ""
+    echo "๐Ÿ’ก Run '$0 --fix' to update it"
+    exit 1
+  else
+    echo "โœ… DEPENDENCIES.md is up to date"
+  fi
+fi
diff --git a/scripts/ci/markdownlint.sh b/scripts/ci/markdownlint.sh
new file mode 100755
index 000000000..61ba62467
--- /dev/null
+++ b/scripts/ci/markdownlint.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env 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
+
+# Parse arguments
+MODE="check"
+if [ $# -gt 0 ]; then
+  case "$1" in
+    --check)
+      MODE="check"
+      ;;
+    --fix)
+      MODE="fix"
+      ;;
+    *)
+      echo "Usage: $0 [--check|--fix]"
+      echo "  --check  Check markdown files for issues (default)"
+      echo "  --fix    Automatically fix markdown issues"
+      exit 1
+      ;;
+  esac
+fi
+
+# Check if markdownlint is installed
+if ! command -v markdownlint &> /dev/null; then
+  echo "โŒ markdownlint command not found"
+  echo "๐Ÿ’ก Install it using: npm install -g markdownlint-cli"
+  exit 1
+fi
+
+# Files to ignore (in addition to .gitignore)
+IGNORE_FILES="CLAUDE.md"
+
+if [ "$MODE" = "fix" ]; then
+  echo "๐Ÿ”ง Fixing markdown files..."
+  markdownlint '**/*.md' --ignore-path .gitignore --ignore "$IGNORE_FILES" 
--fix
+  echo "โœ… Markdown files have been fixed"
+else
+  echo "๐Ÿ” Checking markdown files..."
+  if markdownlint '**/*.md' --ignore-path .gitignore --ignore "$IGNORE_FILES"; 
then
+    echo "โœ… All markdown files are properly formatted"
+  else
+    echo "โŒ Markdown linting failed"
+    echo "๐Ÿ’ก Run '$0 --fix' to auto-fix issues"
+    exit 1
+  fi
+fi
diff --git a/scripts/ci/shellcheck.sh b/scripts/ci/shellcheck.sh
new file mode 100755
index 000000000..e4b1e0f32
--- /dev/null
+++ b/scripts/ci/shellcheck.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env 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
+
+# Parse arguments
+MODE="check"
+if [ $# -gt 0 ]; then
+  case "$1" in
+    --check)
+      MODE="check"
+      ;;
+    --fix)
+      MODE="fix"
+      ;;
+    *)
+      echo "Usage: $0 [--check|--fix]"
+      echo "  --check  Check shell scripts for issues (default)"
+      echo "  --fix    Show detailed suggestions for fixes"
+      exit 1
+      ;;
+  esac
+fi
+
+# Check if shellcheck is installed
+if ! command -v shellcheck &> /dev/null; then
+  echo "โŒ shellcheck command not found"
+  echo "๐Ÿ’ก Install it using:"
+  echo "   โ€ข Ubuntu/Debian: sudo apt-get install shellcheck"
+  echo "   โ€ข macOS: brew install shellcheck"
+  echo "   โ€ข Or visit: https://www.shellcheck.net/";
+  exit 1
+fi
+
+echo "shellcheck version: $(shellcheck --version)"
+
+# Get repository root
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+# Directories to exclude
+EXCLUDE_PATHS=(
+  "./target/*"
+  "./node_modules/*"
+  "./.git/*"
+  "./foreign/node/node_modules/*"
+  "./foreign/python/.venv/*"
+  "./.venv/*"
+  "./venv/*"
+  "./build/*"
+  "./dist/*"
+)
+
+# Build find exclusion arguments
+FIND_EXCLUDE_ARGS=()
+for path in "${EXCLUDE_PATHS[@]}"; do
+  FIND_EXCLUDE_ARGS+=("-not" "-path" "$path")
+done
+
+if [ "$MODE" = "fix" ]; then
+  echo "๐Ÿ”ง Running shellcheck with detailed suggestions..."
+  echo ""
+  echo "Note: shellcheck does not support automatic fixing."
+  echo "Please review the suggestions below and fix issues manually."
+  echo ""
+
+  # Run with detailed format
+  FAILED=0
+  while IFS= read -r -d '' script; do
+    echo "Checking: $script"
+    if ! shellcheck -f gcc "$script"; then
+      FAILED=1
+    fi
+    echo ""
+  done < <(find . -type f -name "*.sh" "${FIND_EXCLUDE_ARGS[@]}" -print0)
+
+  if [ $FAILED -eq 1 ]; then
+    echo "โŒ Found issues in shell scripts"
+    echo "๐Ÿ’ก Fix the issues reported above manually"
+    exit 1
+  else
+    echo "โœ… All shell scripts passed shellcheck"
+  fi
+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
+    echo "โœ… All shell scripts passed shellcheck"
+  else
+    echo ""
+    echo "โŒ Shellcheck found issues in shell scripts"
+    echo "๐Ÿ’ก Run '$0 --fix' to see detailed suggestions"
+    exit 1
+  fi
+fi
diff --git a/scripts/sync-rust-version.sh b/scripts/ci/sync-rust-version.sh
similarity index 97%
rename from scripts/sync-rust-version.sh
rename to scripts/ci/sync-rust-version.sh
index ccaa8012f..45b056f7f 100755
--- a/scripts/sync-rust-version.sh
+++ b/scripts/ci/sync-rust-version.sh
@@ -64,8 +64,8 @@ if [ -z "$MODE" ]; then
     exit 1
 fi
 
-# Get the repository root (parent of scripts directory)
-REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+# Get the repository root (two levels up from scripts/ci/)
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
 cd "$REPO_ROOT"
 
 # Extract Rust version from rust-toolchain.toml
diff --git a/scripts/ci/trailing-newline.sh b/scripts/ci/trailing-newline.sh
new file mode 100755
index 000000000..bbab2e564
--- /dev/null
+++ b/scripts/ci/trailing-newline.sh
@@ -0,0 +1,188 @@
+#!/usr/bin/env 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
+
+# Default values
+MODE="check"
+FILE_MODE="staged"
+FILES=()
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+  case "$1" in
+    --check)
+      MODE="check"
+      shift
+      ;;
+    --fix)
+      MODE="fix"
+      shift
+      ;;
+    --staged)
+      FILE_MODE="staged"
+      shift
+      ;;
+    --ci)
+      FILE_MODE="ci"
+      shift
+      ;;
+    --all)
+      FILE_MODE="all"
+      shift
+      ;;
+    --help|-h)
+      echo "Usage: $0 [--check|--fix] [--staged|--ci|--all] [files...]"
+      echo ""
+      echo "Modes:"
+      echo "  --check   Check for missing trailing newlines (default)"
+      echo "  --fix     Add trailing newlines to files"
+      echo ""
+      echo "File selection:"
+      echo "  --staged  Check staged files (default, for git hooks)"
+      echo "  --ci      Check files changed in PR (for CI)"
+      echo "  --all     Check all tracked files"
+      echo "  [files]   Check specific files"
+      exit 0
+      ;;
+    -*)
+      echo "Unknown option: $1"
+      echo "Use --help for usage information"
+      exit 1
+      ;;
+    *)
+      # Treat as file argument
+      FILES+=("$1")
+      shift
+      ;;
+  esac
+done
+
+# Get files to check based on mode
+get_files() {
+  case "$FILE_MODE" in
+    staged)
+      # Get staged files for git hooks
+      git diff --cached --name-only --diff-filter=ACM
+      ;;
+    ci)
+      # Get files changed in PR for CI
+      if [ -n "${GITHUB_BASE_REF:-}" ]; then
+        # GitHub Actions PR context
+        git fetch --no-tags --depth=1 origin 
"${GITHUB_BASE_REF}:${GITHUB_BASE_REF}" 2>/dev/null || true
+        git diff --name-only --diff-filter=ACM "${GITHUB_BASE_REF}...HEAD"
+      elif [ -n "${CI:-}" ]; then
+        # Generic CI - compare with HEAD~1
+        git diff --name-only --diff-filter=ACM HEAD~1
+      else
+        # Fallback to staged files
+        git diff --cached --name-only --diff-filter=ACM
+      fi
+      ;;
+    all)
+      # Get all tracked files
+      git ls-files
+      ;;
+  esac
+}
+
+# If files were provided as arguments, use them
+if [ ${#FILES[@]} -gt 0 ]; then
+  CHANGED_FILES=("${FILES[@]}")
+else
+  # Get files based on mode
+  CHANGED_FILES=()
+  while IFS= read -r file; do
+    CHANGED_FILES+=("$file")
+  done < <(get_files)
+fi
+
+# Exit early if no files to check
+if [ ${#CHANGED_FILES[@]} -eq 0 ]; then
+  echo "No files to check"
+  exit 0
+fi
+
+echo "Checking ${#CHANGED_FILES[@]} file(s) for trailing newlines..."
+
+# Track files with issues
+FILES_WITHOUT_NEWLINE=()
+
+# Check each file
+for file in "${CHANGED_FILES[@]}"; do
+  # Skip if file doesn't exist (might be deleted)
+  if [ ! -f "$file" ]; then
+    continue
+  fi
+
+  # Skip binary files
+  if file "$file" | grep -qE "binary|data|executable|compressed"; then
+    continue
+  fi
+
+  # Skip empty files
+  if [ ! -s "$file" ]; then
+    continue
+  fi
+
+  # Check if file ends with a newline
+  # Use tail to get last byte and od to check if it's a newline (0x0a)
+  if ! tail -c 1 "$file" | od -An -tx1 | grep -q '0a'; then
+    FILES_WITHOUT_NEWLINE+=("$file")
+  fi
+done
+
+# Fix mode
+if [ "$MODE" = "fix" ]; then
+  if [ ${#FILES_WITHOUT_NEWLINE[@]} -eq 0 ]; then
+    echo "โœ… All files have trailing newlines"
+    exit 0
+  fi
+
+  echo "๐Ÿ”ง Adding trailing newlines to ${#FILES_WITHOUT_NEWLINE[@]} file(s)..."
+  for file in "${FILES_WITHOUT_NEWLINE[@]}"; do
+    # Add newline if file doesn't end with one
+    if [ -n "$(tail -c 1 "$file")" ]; then
+      echo >> "$file"
+      echo "  Fixed: $file"
+    fi
+  done
+  echo "โœ… Trailing newlines added to ${#FILES_WITHOUT_NEWLINE[@]} file(s)"
+  exit 0
+fi
+
+# Check mode
+if [ ${#FILES_WITHOUT_NEWLINE[@]} -eq 0 ]; then
+  echo "โœ… All text files have trailing newlines"
+  exit 0
+fi
+
+echo "โŒ Found ${#FILES_WITHOUT_NEWLINE[@]} file(s) without trailing newline:"
+echo ""
+
+for file in "${FILES_WITHOUT_NEWLINE[@]}"; do
+  echo "  โ€ข $file"
+  # Show last few characters of the file for context
+  echo -n "    Last characters: '"
+  tail -c 20 "$file" | tr '\n' 'โ†ต' | sed 's/\t/โ†’/g'
+  echo "'"
+  echo ""
+done
+
+echo "๐Ÿ’ก Run '$0 --fix' to add trailing newlines automatically"
+exit 1
diff --git a/scripts/ci/trailing-whitespace.sh 
b/scripts/ci/trailing-whitespace.sh
new file mode 100755
index 000000000..b2e066021
--- /dev/null
+++ b/scripts/ci/trailing-whitespace.sh
@@ -0,0 +1,187 @@
+#!/usr/bin/env 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
+
+# Default values
+MODE="check"
+FILE_MODE="staged"
+FILES=()
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+  case "$1" in
+    --check)
+      MODE="check"
+      shift
+      ;;
+    --fix)
+      MODE="fix"
+      shift
+      ;;
+    --staged)
+      FILE_MODE="staged"
+      shift
+      ;;
+    --ci)
+      FILE_MODE="ci"
+      shift
+      ;;
+    --all)
+      FILE_MODE="all"
+      shift
+      ;;
+    --help|-h)
+      echo "Usage: $0 [--check|--fix] [--staged|--ci|--all] [files...]"
+      echo ""
+      echo "Modes:"
+      echo "  --check   Check for trailing whitespace (default)"
+      echo "  --fix     Remove trailing whitespace"
+      echo ""
+      echo "File selection:"
+      echo "  --staged  Check staged files (default, for git hooks)"
+      echo "  --ci      Check files changed in PR (for CI)"
+      echo "  --all     Check all tracked files"
+      echo "  [files]   Check specific files"
+      exit 0
+      ;;
+    -*)
+      echo "Unknown option: $1"
+      echo "Use --help for usage information"
+      exit 1
+      ;;
+    *)
+      # Treat as file argument
+      FILES+=("$1")
+      shift
+      ;;
+  esac
+done
+
+# Get files to check based on mode
+get_files() {
+  case "$FILE_MODE" in
+    staged)
+      # Get staged files for git hooks
+      git diff --cached --name-only --diff-filter=ACM
+      ;;
+    ci)
+      # Get files changed in PR for CI
+      if [ -n "${GITHUB_BASE_REF:-}" ]; then
+        # GitHub Actions PR context
+        git fetch --no-tags --depth=1 origin 
"${GITHUB_BASE_REF}:${GITHUB_BASE_REF}" 2>/dev/null || true
+        git diff --name-only --diff-filter=ACM "${GITHUB_BASE_REF}...HEAD"
+      elif [ -n "${CI:-}" ]; then
+        # Generic CI - compare with HEAD~1
+        git diff --name-only --diff-filter=ACM HEAD~1
+      else
+        # Fallback to staged files
+        git diff --cached --name-only --diff-filter=ACM
+      fi
+      ;;
+    all)
+      # Get all tracked files
+      git ls-files
+      ;;
+  esac
+}
+
+# If files were provided as arguments, use them
+if [ ${#FILES[@]} -gt 0 ]; then
+  CHANGED_FILES=("${FILES[@]}")
+else
+  # Get files based on mode
+  CHANGED_FILES=()
+  while IFS= read -r file; do
+    CHANGED_FILES+=("$file")
+  done < <(get_files)
+fi
+
+# Exit early if no files to check
+if [ ${#CHANGED_FILES[@]} -eq 0 ]; then
+  echo "No files to check"
+  exit 0
+fi
+
+echo "Checking ${#CHANGED_FILES[@]} file(s) for trailing whitespace..."
+
+# Track files with issues
+FILES_WITH_TRAILING=()
+
+# Check each file
+for file in "${CHANGED_FILES[@]}"; do
+  # Skip if file doesn't exist (might be deleted)
+  if [ ! -f "$file" ]; then
+    continue
+  fi
+
+  # Skip binary files
+  if file "$file" | grep -qE "binary|data|executable|compressed"; then
+    continue
+  fi
+
+  # Check for trailing whitespace
+  if grep -q '[[:space:]]$' "$file" 2>/dev/null; then
+    FILES_WITH_TRAILING+=("$file")
+  fi
+done
+
+# Fix mode
+if [ "$MODE" = "fix" ]; then
+  if [ ${#FILES_WITH_TRAILING[@]} -eq 0 ]; then
+    echo "โœ… No trailing whitespace found"
+    exit 0
+  fi
+
+  echo "๐Ÿ”ง Removing trailing whitespace from ${#FILES_WITH_TRAILING[@]} 
file(s)..."
+  for file in "${FILES_WITH_TRAILING[@]}"; do
+    # Remove trailing whitespace (in-place)
+    sed -i 's/[[:space:]]*$//' "$file"
+    echo "  Fixed: $file"
+  done
+  echo "โœ… Trailing whitespace removed from ${#FILES_WITH_TRAILING[@]} file(s)"
+  exit 0
+fi
+
+# Check mode
+if [ ${#FILES_WITH_TRAILING[@]} -eq 0 ]; then
+  echo "โœ… No trailing whitespace found"
+  exit 0
+fi
+
+echo "โŒ Found trailing whitespace in ${#FILES_WITH_TRAILING[@]} file(s):"
+echo ""
+
+for file in "${FILES_WITH_TRAILING[@]}"; do
+  echo "  โ€ข $file"
+  # Show lines with trailing whitespace (limit to first 3 occurrences per file)
+  grep -n '[[:space:]]$' "$file" | head -3 | while IFS=: read -r line_num 
content; do
+    # Show the line with visible whitespace markers
+    visible_content=$(echo "$content" | sed 's/ /ยท/g; s/\t/โ†’/g')
+    echo "    Line $line_num: '${visible_content}'"
+  done
+
+  TOTAL_LINES=$(grep -c '[[:space:]]$' "$file")
+  if [ "$TOTAL_LINES" -gt 3 ]; then
+    echo "    ... and $((TOTAL_LINES - 3)) more lines"
+  fi
+  echo ""
+done
+
+echo "๐Ÿ’ก Run '$0 --fix' to remove trailing whitespace automatically"
+exit 1
diff --git a/scripts/copy-latest-from-master.sh 
b/scripts/copy-latest-from-master.sh
index a6c2fa6c9..35665cf58 100755
--- a/scripts/copy-latest-from-master.sh
+++ b/scripts/copy-latest-from-master.sh
@@ -107,7 +107,7 @@ case "$COMMAND" in
         # Iterate through all saved files
         find "$TMP_SAVED" -type f | while read -r saved_file; do
             # Get relative path by removing the tmp prefix
-            rel_path="${saved_file#$TMP_SAVED/}"
+            rel_path="${saved_file#"$TMP_SAVED"/}"
             dest_dir=$(dirname "$rel_path")
 
             # Create destination directory if it doesn't exist
@@ -136,4 +136,4 @@ case "$COMMAND" in
         echo "Valid commands: save, apply"
         exit 1
         ;;
-esac
\ No newline at end of file
+esac
diff --git a/scripts/licenses-list.sh b/scripts/licenses-list.sh
deleted file mode 100755
index 8739383d9..000000000
--- a/scripts/licenses-list.sh
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/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.
-
-# This script is used to generate Cross.toml file for user which executes
-# this script. This is needed since Cross.toml build.dockerfile.build-args
-# section requires statically defined Docker build arguments and parameters
-# like current UID or GID must be entered (cannot be generated or fetched
-# during cross execution time).
-
-set -euo pipefail
-
-# Default mode
-MODE="help"
-
-# Parse arguments
-while [[ $# -gt 0 ]]; do
-    case "$1" in
-    --update)
-        MODE="update"
-        shift
-        ;;
-    --check)
-        MODE="check"
-        shift
-        ;;
-    *)
-        echo "Unknown option: $1"
-        MODE="help"
-        shift
-        ;;
-    esac
-done
-
-# Display usage if no valid arguments provided
-if [ "$MODE" = "help" ]; then
-    echo "Usage: $0 [OPTIONS]"
-    echo "Options:"
-    echo "  --check    Check if DEPENDENCIES.md is up to date"
-    echo "  --update   Update DEPENDENCIES.md with current dependencies"
-    exit 0
-fi
-
-# Check if cargo-license is installed
-if ! command -v cargo-license &>/dev/null; then
-    echo "Installing cargo-license..."
-    cargo install cargo-license
-fi
-
-# Check if DEPENDENCIES.md exists
-if [ ! -f "DEPENDENCIES.md" ] && [ "$MODE" = "check" ]; then
-    echo "Error: DEPENDENCIES.md does not exist."
-    exit 1
-fi
-
-# Generate current dependencies
-TEMP_FILE=$(mktemp)
-trap 'rm -f "$TEMP_FILE"' EXIT
-
-echo "Generating current dependencies list..."
-cargo license --color never --do-not-bundle --all-features >"$TEMP_FILE"
-
-# Update mode
-if [ "$MODE" = "update" ]; then
-    echo "Updating DEPENDENCIES.md..."
-    {
-        echo "# Dependencies"
-        echo ""
-        cat "$TEMP_FILE"
-    } >DEPENDENCIES.md
-    echo "DEPENDENCIES.md has been updated."
-    exit 0
-fi
-
-# Check mode
-if [ "$MODE" = "check" ]; then
-    echo "Checking if DEPENDENCIES.md is up to date..."
-    # Create expected format for comparison
-    EXPECTED_FILE=$(mktemp)
-    trap 'rm -f "$TEMP_FILE" "$EXPECTED_FILE"' EXIT
-    {
-        echo "# Dependencies"
-        echo ""
-        cat "$TEMP_FILE"
-    } >"$EXPECTED_FILE"
-
-    if ! diff -q "$EXPECTED_FILE" DEPENDENCIES.md >/dev/null; then
-        echo "Error: DEPENDENCIES.md is out of date. Please run '$0 --update' 
to update it."
-        echo "Diff:"
-        diff -u DEPENDENCIES.md "$EXPECTED_FILE"
-        exit 1
-    else
-        echo "DEPENDENCIES.md is up to date."
-    fi
-fi

Reply via email to