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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-3379-1f3bc34058e3f3e45336bc5428f37b92649a608b
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git

commit e8ec7f8403554496db976700eb993459e0ab7f9b
Author: Danny C Jones <[email protected]>
AuthorDate: Fri Oct 9 22:31:27 2026 +0000

    chore(release): port the dependency license check from shell to Python 
(#3379)
    
    This change migrates from the old shell script for cargo-deny to a new 
Python script.
    This new script will contain logic for dealing with dependencies and their 
licenses. This commit simply translates the existing logic.
    
    In later changes, this script will be extended with:
    - Clarifications for existing dependencies where licenses are misreported, 
with additional verifications added in the script itself
    - Logic for generating the attribution documentation for the pyiceberg-core 
package.
---
 .github/workflows/ci.yml    |   6 +-
 Makefile                    |   2 +-
 dev/release/README.md       |   4 +-
 dev/release/create_rc.sh    |   3 +-
 dev/release/dependencies.sh | 148 --------------------------------------------
 dev/release/licenses.py     | 101 ++++++++++++++++++++++++++++++
 website/src/release.md      |   2 +-
 7 files changed, 113 insertions(+), 153 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c57b4799..9369a91e8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,6 +61,10 @@ jobs:
         with:
           tool: [email protected],[email protected],[email protected]
 
+      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # 
v7.0.0
+        with:
+          python-version: 3.12
+
       - name: Check License Header
         uses: 
apache/skywalking-eyes/header@a196742f472feaffafea537ce5a2a4c3c53a8de4 # v0.9.0
 
@@ -80,7 +84,7 @@ jobs:
         run: make check-license-notice
 
       - name: Check dependency licenses
-        run: dev/release/dependencies.sh check
+        run: python3 ./dev/release/licenses.py dependencies check
 
       # Trusted publishing cannot create a crate 
(https://crates.io/docs/trusted-publishing),
       # so a new publishable crate must be reserved on crates.io before it is 
merged.
diff --git a/Makefile b/Makefile
index 8733abbab..c596fc4bf 100644
--- a/Makefile
+++ b/Makefile
@@ -94,7 +94,7 @@ install-cargo-deny:
        cargo install --locked [email protected]
 
 check-dependency-licenses: install-cargo-deny
-       bash ./dev/release/dependencies.sh check
+       python3 ./dev/release/licenses.py dependencies check
 
 check: check-fmt check-clippy check-toml cargo-machete check-license-notice 
check-dependency-licenses
 
diff --git a/dev/release/README.md b/dev/release/README.md
index cf10a3aa3..4a9cecd8f 100644
--- a/dev/release/README.md
+++ b/dev/release/README.md
@@ -137,5 +137,7 @@ git push origin "v0.9.1"
 Verify every dependency's license is one the project allows:
 
 ```shell
-dev/release/dependencies.sh check
+python3 dev/release/licenses.py dependencies check
 ```
+
+CI also runs this check on every pull request.
diff --git a/dev/release/create_rc.sh b/dev/release/create_rc.sh
index ae3a5207e..99dd37565 100755
--- a/dev/release/create_rc.sh
+++ b/dev/release/create_rc.sh
@@ -307,7 +307,8 @@ check_rc_tag_available() {
 }
 
 check_dependency_licenses() {
-  "${SCRIPT_DIR}/dependencies.sh" check
+  require_command python3
+  python3 "${SCRIPT_DIR}/licenses.py" dependencies check
 }
 
 # Packages and builds every crate as `cargo publish` would, without uploading,
diff --git a/dev/release/dependencies.sh b/dev/release/dependencies.sh
deleted file mode 100755
index 36246a1cd..000000000
--- a/dev/release/dependencies.sh
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/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 -Eeuo pipefail
-
-# bash 3.2 (macOS default) does not run the ERR trap for a failing subshell:
-# the script still exits on failure but does not report which step failed.
-if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
-  echo "Warning: bash ${BASH_VERSION} will not print which step failed. Use 
bash 4 or newer to see it." >&2
-fi
-
-CURRENT_STEP=""
-
-on_error() {
-  local status=$?
-  if [ -n "${CURRENT_STEP}" ]; then
-    echo "FAILED: ${CURRENT_STEP}" >&2
-  else
-    echo "FAILED" >&2
-  fi
-  exit "${status}"
-}
-
-trap on_error ERR
-
-usage() {
-  cat <<USAGE
-Usage:
-  $0 <check>
-
-Commands:
-  check
-      Run cargo-deny license validation once at the root workspace.
-      Default command: none; this argument is required.
-
-Options:
-  -h, --help
-      Show this help message.
-
-Examples:
-  $0 check
-USAGE
-}
-
-show_help_if_requested() {
-  while [ "$#" -gt 0 ]; do
-    case "$1" in
-      -h | --help)
-        usage
-        exit 0
-        ;;
-    esac
-    shift
-  done
-}
-
-start_step() {
-  CURRENT_STEP="$1"
-  echo "==> ${CURRENT_STEP}"
-}
-
-finish_step() {
-  echo "OK: ${CURRENT_STEP}"
-  CURRENT_STEP=""
-}
-
-run_step() {
-  local step="$1"
-  shift
-  start_step "${step}"
-  "$@"
-  finish_step
-}
-
-require_command() {
-  local command_name="$1"
-  if ! command -v "${command_name}" >/dev/null 2>&1; then
-    echo "This script requires '${command_name}', but it is not installed." >&2
-    return 1
-  fi
-}
-
-require_cargo_deny() {
-  require_command cargo
-  if ! cargo deny --version >/dev/null 2>&1; then
-    echo "This script requires 'cargo-deny' for dependency license checks." >&2
-    echo "Install it with: cargo install --locked cargo-deny" >&2
-    return 1
-  fi
-}
-
-validate_args() {
-  if [ "$#" -ne 1 ]; then
-    usage
-    return 1
-  fi
-
-  case "$1" in
-    check) ;;
-    *)
-      usage
-      return 1
-      ;;
-  esac
-}
-
-check_deps_for_dir() {
-  local cargo_dir="$1"
-  require_cargo_deny
-  (
-    trap - ERR
-    cd "${cargo_dir}"
-    cargo deny check license
-  )
-}
-
-check_deps() {
-  run_step "Check dependency licenses in ${REPO_ROOT}" check_deps_for_dir 
"${REPO_ROOT}"
-}
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-COMMAND="${1:-}"
-
-show_help_if_requested "$@"
-run_step "Validate dependency command arguments" validate_args "$@"
-
-case "${COMMAND}" in
-  check)
-    check_deps
-    ;;
-esac
diff --git a/dev/release/licenses.py b/dev/release/licenses.py
new file mode 100755
index 000000000..2fc744987
--- /dev/null
+++ b/dev/release/licenses.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+# 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.
+"""
+CLI for assisting maintenance of the project's license checks.
+"""
+
+import argparse
+import subprocess
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent.parent
+
+
+class Failure(Exception):
+    """A user-facing error."""
+
+
+def step(message: str) -> None:
+    print(f"==> {message}", flush=True)
+
+
+def ok(message: str) -> None:
+    print(f"OK: {message}", flush=True)
+
+
+def require_cargo_deny() -> None:
+    """Assert cargo-deny is installed."""
+    try:
+        subprocess.run(
+            ["cargo", "deny", "--version"],
+            capture_output=True,
+            check=True,
+        )
+    except FileNotFoundError:
+        raise Failure("This script requires 'cargo', but it is not installed.")
+    except subprocess.CalledProcessError:
+        raise Failure(
+            "This script requires 'cargo-deny' for dependency license 
checks.\n"
+            "Install it with: make install-cargo-deny"
+        )
+
+
+def command_check_dependencies(_args: argparse.Namespace) -> None:
+    step("Check dependency licenses")
+    require_cargo_deny()
+
+    result = subprocess.run(
+        ["cargo", "deny", "check", "license"],
+        cwd=REPO_ROOT,
+        check=False,
+    )
+    if result.returncode != 0:
+        raise Failure("cargo-deny rejected the dependency licenses.")
+
+    ok("Check dependency licenses")
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__)
+    cmd_groups = parser.add_subparsers(dest="group", required=True)
+
+    dependencies = cmd_groups.add_parser(
+        "dependencies",
+        help="license policy for the Rust dependency graph, enforced through 
deny.toml",
+    )
+    dependencies_commands = dependencies.add_subparsers(dest="command", 
required=True)
+
+    check = dependencies_commands.add_parser(
+        "check",
+        help="assert every dependency's license is one deny.toml allows",
+    )
+    check.set_defaults(func=command_check_dependencies)
+
+    args = parser.parse_args()
+    try:
+        args.func(args)
+    except Failure as failure:
+        print(str(failure), file=sys.stderr)
+        print(f"FAILED: {args.group} {args.command}", file=sys.stderr)
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/website/src/release.md b/website/src/release.md
index 03fb66551..a519fce56 100644
--- a/website/src/release.md
+++ b/website/src/release.md
@@ -175,7 +175,7 @@ The changelog should reflect a summary of each commit in 
the new release.
 Run the following command to verify the licenses meet the project's policy.
 
 ```shell
-dev/release/dependencies.sh check
+python3 dev/release/licenses.py dependencies check
 ```
 
 #### Open pull request

Reply via email to