From 9deac8139a8e5c678607f86d8e467d4ecc9c39df Mon Sep 17 00:00:00 2001 From: Yanick Fratantonio Date: Wed, 29 Oct 2025 10:41:16 +0000 Subject: [PATCH 01/13] python: rewrote script for pre-release checks --- .../check_release_candidate_python_package.py | 218 +++++++++++------- 1 file changed, 135 insertions(+), 83 deletions(-) diff --git a/python/scripts/check_release_candidate_python_package.py b/python/scripts/check_release_candidate_python_package.py index da824084..dcfcc2c1 100644 --- a/python/scripts/check_release_candidate_python_package.py +++ b/python/scripts/check_release_candidate_python_package.py @@ -13,6 +13,10 @@ # limitations under the License. +""" +It performs a number of checks to determine whether the package is ready for release. +""" + import re import subprocess import sys @@ -25,89 +29,84 @@ @click.command() @click.argument("expected_version") @click.option( - "--python-client", + "--use-python-client", is_flag=True, - help="Use the Python client instead of CLI for validation.", + help="Use the Python client instead of Rust CLI.", ) -def main(expected_version, python_client): +def main(expected_version: str, use_python_client: bool) -> None: """Checks version consistency for the `magika` package.""" - module_version, instance_version, py_errors = check_python_module_version() - for error in py_errors: - click.echo(error, err=True) - sys.exit(1) - - cli_version = instance_version if python_client else check_rust_cli_version() - - errors = validate_release_tag(module_version, expected_version) - errors += validate_release_tag(cli_version, expected_version) - - if errors: - for error in errors: - click.echo(error, err=True) - sys.exit(1) - - click.echo("✅ All versions match!") - -def validate_release_tag(extracted_version, release_tag): - """ - Ensures the extracted version matches the expected release tag. - - Args: - extracted_version (str or None): The version extracted from the package or CLI. - release_tag (str): The expected release tag version. - - Returns: - list: A list of error messages if the extracted version does not match the release tag. - """ - errors = [] - normalized_release_tag = re.sub(r"^python-v", "", release_tag) - if extracted_version and extracted_version != normalized_release_tag: - errors.append( - f"ERROR: Extracted version {extracted_version} does not match release tag {release_tag}." - ) - return errors - - -def check_python_module_version(): - """ - Checks the version of the Python `magika` package and its instance version. - - Returns: - tuple: A tuple containing: - - module_version (str or None): The `__version__` attribute of the `magika` package. - - instance_version (str or None): The version retrieved from `magika.Magika().get_version()`. - - errors (list): A list of error messages if any issues are found. - """ - errors = [] - module_version = getattr(magika, "__version__", None) + with_errors = False + # Get the versions + module_version = getattr(magika, "__version__", "") try: - instance_version = magika.Magika().get_version() - except Exception as e: - click.echo(f"ERROR: Failed to get instance version: {e}", err=True) - sys.exit(1) - - if module_version is None: - click.echo("ERROR: Could not retrieve Python package version.", err=True) + instance_version = magika.Magika().get_module_version() + except Exception: + instance_version = "" + + package_version = get_magika_package_version_via_pip_show() + + if use_python_client: + cli_version = instance_version + else: + cli_version = get_rust_cli_version() + + if module_version == "": + click.echo("ERROR: failed to get module_version.") + with_errors = True + if instance_version == "": + click.echo("ERROR: failed to get instance_version.") + with_errors = True + if package_version == "": + click.echo("ERROR: failed to get package_version.") + with_errors = True + if cli_version == "": + click.echo("ERROR: failed to get cli_version.") + with_errors = True + + click.echo( + f"Versions: {expected_version=}, {module_version=}, {instance_version=}, {package_version=}, {cli_version=}" + ) + + if module_version != expected_version: + click.echo(f"ERROR: {module_version=} != {expected_version=}") + with_errors = True + + if module_version != instance_version: + click.echo(f"ERROR: {instance_version=} != {module_version=}") + with_errors = True + + if module_version != package_version: + click.echo(f"ERROR: {module_version=} != {package_version=}") + with_errors = True + + # From now on, we assume all the python-related versions are the same. If + # they are not, we would have at least one error above. + + if not is_valid_python_version(module_version): + click.echo(f"ERROR: {module_version=} is not a valid python version") + with_errors = True + + if module_version.endswith("-dev") or cli_version.endswith("-dev"): + click.echo("ERROR: One of the versions is a -dev version.") + with_errors = True + + if cli_version.endswith("-rc") and not module_version.endswith("-rc"): + click.echo("ERROR: The CLI has an -rc version, but the python module does not.") + with_errors = True + + if with_errors: + click.echo("There was at least one error") sys.exit(1) + else: + click.echo("All tests pass!") - if "-dev" in module_version: - errors.append( - f"ERROR: Python package version {module_version} contains '-dev'." - ) - - return module_version, instance_version, errors +def get_rust_cli_version() -> str: + """Get the version of the Rust CLI `magika`. -def check_rust_cli_version(): - """ - Checks the version of the Rust CLI `magika` and ensures it is non-dev. - - Returns: - tuple: A tuple containing: - - cli_version (str or None): The version string extracted from `magika --version`. - - errors (list): A list of error messages if any issues are found. + Returns an empty string ("") if an error is encountered. """ try: result = subprocess.run( @@ -115,19 +114,72 @@ def check_rust_cli_version(): ) parts = result.stdout.strip().split() if len(parts) < 2: - click.echo("ERROR: Could not parse CLI version output.", err=True) - sys.exit(1) - + click.echo("ERROR: Could not parse CLI version output.") + return "" cli_version = parts[1] - if "-dev" in cli_version: - click.echo( - f"ERROR: Rust CLI version {cli_version} contains '-dev'.", err=True - ) - sys.exit(1) return cli_version except subprocess.CalledProcessError as e: - click.echo(f"ERROR: Could not retrieve CLI version: {e}", err=True) - sys.exit(1) + click.echo(f"ERROR: Could not retrieve CLI version: {e}") + return "" + + +def get_magika_package_version_via_pip_show() -> str: + try: + r = subprocess.run( + ["python3", "-m", "pip", "show", "magika"], capture_output=True, text=True + ) + lines = r.stdout.strip().split("\n") + for line in lines: + if line.startswith("Version: "): + return line.split(": ", 1)[1] + return "" + except subprocess.CalledProcessError as e: + click.echo(f"ERROR: Could not retrieve package version via pip show: {e}") + return "" + + +def is_valid_python_version(version: str) -> bool: + # Regex from PEP440: '[N!]N(.N)*[{a|b|rc}N][.postN][.devN]' + PEP440_CANONICAL_REGEX = re.compile( + r""" +^ +# Optional Epoch segment (e.g., 1!) +(?P\d+!)? + +# Required Release segment (e.g., 1.2.3) +(?P[0-9]+(?:\.[0-9]+)*) + +# Optional Pre-release segment (e.g., a1, b2, rc3) +(?P
+    (?:a|b|rc)
+    [0-9]+
+)?
+
+# Optional Post-release segment (e.g., .post4)
+(?P
+    (?:\.post[0-9]+)
+)?
+
+# Optional Development release segment (e.g., .dev5)
+(?P
+    (?:\.dev[0-9]+)
+)?
+$
+""",
+        re.VERBOSE | re.IGNORECASE,
+    )
+    return PEP440_CANONICAL_REGEX.fullmatch(version) is not None
+
+
+def test_is_valid_python_version() -> None:
+    assert is_valid_python_version("1.2.3") is True
+    assert is_valid_python_version("1.2.3.rc") is False
+    assert is_valid_python_version("1.2.3.rc0") is False
+    assert is_valid_python_version("1.2.3rc0") is True
+    assert is_valid_python_version("1.2.3rc1") is True
+    assert is_valid_python_version("1.2.3-dev") is False
+    assert is_valid_python_version("1.2.3.dev0") is True
+    assert is_valid_python_version("1.2.3-dev0") is False
 
 
 if __name__ == "__main__":

From 3945c1e6bc33d23aa816271481fc05b161a749c0 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 10:48:04 +0000
Subject: [PATCH 02/13] gh: new version of build GH action

- add pre-release-checks, gated by release tags
- run package checks on the actual artifacts (i.e., the packages), and not just from the source of the repo.
- add stub for actual release
- simplify "uv add magika" linux vs. windows, using shell: bash
- make uv version easily updatable
---
 .github/workflows/python-build-package.yml | 138 ++++++++++++++-------
 1 file changed, 93 insertions(+), 45 deletions(-)

diff --git a/.github/workflows/python-build-package.yml b/.github/workflows/python-build-package.yml
index fe3d6237..ed5cc712 100644
--- a/.github/workflows/python-build-package.yml
+++ b/.github/workflows/python-build-package.yml
@@ -5,6 +5,8 @@ on:
   push:
     branches:
       - "main"
+    tags:
+      - "test-python-v*"
   pull_request:
     paths:
       - "python/**"
@@ -20,8 +22,40 @@ on:
 permissions:
   contents: read
 
+# Note: This needs to match what specified in the on.push.tags trigger.
+env:
+  RELEASE_TAG_PREFIX: "test-python-v"
+  UV_VERSION: "0.9.5"
+
 jobs:
+  # This job acts as a gatekeeper for releases, which are triggered by a tag
+  # push. It performs critical pre-release checks. These checks are skipped for
+  # non-release pushes.
+  pre-release-checks:
+    name: Pre-release checks
+    runs-on: ubuntu-latest
+    # Note: the env context is not available for jobs.if:
+    # https://github.com/actions/runner/issues/1189#issuecomment-1129307280.
+    if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'test-python-v')
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v5
+      - name: Setup python
+        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # pin@v5
+        with:
+          python-version: "3.12"
+      - name: Install uv
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
+      - name: Check package for release
+        run: |
+          FULL_TAG_REF="${{ github.ref }}"
+          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          # Note: this uses the magika python package installed via uv.
+          uv run ./scripts/check_release_candidate_python_package.py ${TAG_VERSION}
+        working-directory: python
+
   build-wheels:
+    needs: [pre-release-checks]
     runs-on: ${{ matrix.platform.runner }}
     strategy:
       matrix:
@@ -39,7 +73,7 @@ jobs:
         with:
           python-version: "3.12"
       - name: Install uv
-        run: curl -LsSf https://astral.sh/uv/0.5.22/install.sh | sh
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
       - if: matrix.platform.runner == 'ubuntu-latest'
         uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
         with:
@@ -96,41 +130,43 @@ jobs:
         with:
           python-version: "${{ matrix.python-version }}"
       - name: Install uv
-        run: curl -LsSf https://astral.sh/uv/0.5.22/install.sh | sh
-      - if: matrix.platform.runner != 'windows-latest'
-        name: Check that `uv add magika.whl` works
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
+      # Attempt "uv add magika.whl", in a temporary directory
+      - name: Check that `uv add magika.whl` works
+        shell: bash
         run: |
           mkdir /tmp/test-uv
           cp -vR dist/*.whl /tmp/test-uv
           cd /tmp/test-uv
           uv init
           uv add ./$(\ls -1 *.whl | head -n 1)
-      - if: matrix.platform.runner == 'windows-latest'
-        name: Check that `uv add magika.whl` works
-        shell: pwsh
-        run: |
-          mkdir C:\test-uv
-          Copy-Item -Path dist\*.whl -Destination C:\test-uv
-          cd C:\test-uv
-          $env:PATH += ";$HOME/.local/bin"
-          uv init
-          $wheel = Get-ChildItem -Filter *.whl | Select-Object -ExpandProperty Name
-          uv add ".\$wheel"
-      - name: Install the wheel
+      # From now on, magika will be available in the global environment
+      - name: Install the wheel via pip
         run: python3 -m pip install $(python -c "import glob; print(glob.glob('dist/*.whl')[0])")
+
       - run: magika --version
       - run: "python3 -c 'import magika; m = magika.Magika(); print(m)'"
       - run: magika -r tests_data/basic
       - run: python3 ./python/scripts/run_quick_test_magika_cli.py
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
+      - name: Check package for release (if this is a release push)
+        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+        shell: bash
+        run: |
+          FULL_TAG_REF="${{ github.ref }}"
+          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          # Note: this uses the magika python package that was just built.
+          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION}
+        working-directory: python
 
   build-pure-python-wheel-and-sdist:
+    needs: [pre-release-checks]
     runs-on: ubuntu-latest
     if: github.event.schedule != '12 3 * * 1'
     steps:
       - uses: actions/checkout@v5
       - name: Install uv
-        run: curl -LsSf https://astral.sh/uv/0.5.22/install.sh | sh
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
       - run: uv run ./scripts/prepare_pyproject_for_pure_python_wheel.py
         working-directory: python
       - name: Build pure python wheel and source distribution
@@ -175,26 +211,17 @@ jobs:
         with:
           python-version: "${{ matrix.python-version }}"
       - name: Install uv
-        run: curl -LsSf https://astral.sh/uv/0.5.22/install.sh | sh
-      - if: matrix.platform.runner != 'windows-latest'
-        name: Check that `uv add magika.whl` works
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
+      # Attempt "uv add magika.whl", in a temporary directory
+      - name: Check that `uv add magika.whl` works
+        shell: bash
         run: |
           mkdir /tmp/test-uv
           cp -vR dist/*.whl /tmp/test-uv
           cd /tmp/test-uv
           uv init
           uv add ./$(\ls -1 *.whl | head -n 1)
-      - if: matrix.platform.runner == 'windows-latest'
-        name: Check that `uv add magika.whl` works
-        shell: pwsh
-        run: |
-          mkdir C:\test-uv
-          Copy-Item -Path dist\*.whl -Destination C:\test-uv
-          cd C:\test-uv
-          $env:PATH += ";$HOME/.local/bin"
-          uv init
-          $wheel = Get-ChildItem -Filter *.whl | Select-Object -ExpandProperty Name
-          uv add ".\$wheel"
+      # From now on, magika will be available in the global environment
       - name: Install the wheel
         run: python3 -m pip install $(python -c "import glob; print(glob.glob('dist/*.whl')[0])")
 
@@ -207,6 +234,15 @@ jobs:
       # Test the python module
       - run: "python3 -c 'import magika; m = magika.Magika(); print(m)'"
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
+      - name: Check package for release (if this is a release push)
+        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+        shell: bash
+        run: |
+          FULL_TAG_REF="${{ github.ref }}"
+          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          # Note: this uses the magika python package that was just built.
+          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --use-python-client
+        working-directory: python
 
   test-sdist:
     needs: [build-pure-python-wheel-and-sdist]
@@ -235,26 +271,17 @@ jobs:
         with:
           python-version: "${{ matrix.python-version }}"
       - name: Install uv
-        run: curl -LsSf https://astral.sh/uv/0.5.22/install.sh | sh
-      - if: matrix.platform.runner != 'windows-latest'
-        name: Check that `uv add magika.tar.gz` works
+        run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
+      # Attempt "uv add magika.whl", in a temporary directory
+      - name: Check that `uv add magika.tar.gz` works
+        shell: bash
         run: |
           mkdir /tmp/test-uv
           cp -vR dist/*.tar.gz /tmp/test-uv
           cd /tmp/test-uv
           uv init
           uv add ./$(\ls -1 *.tar.gz | head -n 1)
-      - if: matrix.platform.runner == 'windows-latest'
-        name: Check that `uv add magika.tar.gz` works
-        shell: pwsh
-        run: |
-          mkdir C:\test-uv
-          Copy-Item -Path dist\*.tar.gz -Destination C:\test-uv
-          cd C:\test-uv
-          $env:PATH += ";$HOME\.local\bin"
-          uv init
-          $sdist = Get-ChildItem -Filter *.tar.gz | Select-Object -ExpandProperty Name
-          uv add ".\$sdist"
+      # From now on, magika will be available in the global environment
       - name: Install the sdist
         run: python3 -m pip install $(python -c "import glob; print(glob.glob('dist/*.tar.gz')[0])")
 
@@ -267,3 +294,24 @@ jobs:
       # Test the python module
       - run: "python3 -c 'import magika; m = magika.Magika(); print(m)'"
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
+      - name: Check package for release (if this is a release push)
+        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+        shell: bash
+        run: |
+          FULL_TAG_REF="${{ github.ref }}"
+          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          # Note: this uses the magika python package that was just built.
+          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --use-python-client
+        working-directory: python
+
+  release:
+    needs: [test-wheels, test-pure-python-wheel, test-sdist]
+    name: Release to pypi
+    runs-on: ubuntu-latest
+    # Note: the env context is not available for jobs.if:
+    # https://github.com/actions/runner/issues/1189#issuecomment-1129307280.
+    if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'test-python-v')
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v5
+    # TODO: download artifacts, upload them to pypi
\ No newline at end of file

From 03b55c4f2e741770019efcc00b008fdcdc31d876 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 10:50:46 +0000
Subject: [PATCH 03/13] gh: avoid overtriggerig js import scenario

In GH actions, the "path" filter is not used for tags push. This means that any tag push would trigger this gh action, even if the push has nothing to do with it.

Fixed by triggering only for branch pushes to main (note: the branch filter is used for tags as well), and for any PR that relates to this.
---
 .github/workflows/js-check-import-scenarios.yml | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/js-check-import-scenarios.yml b/.github/workflows/js-check-import-scenarios.yml
index 163abd2a..16ddadd0 100644
--- a/.github/workflows/js-check-import-scenarios.yml
+++ b/.github/workflows/js-check-import-scenarios.yml
@@ -3,6 +3,9 @@ name: JS - check import scenarios
 on:
   workflow_dispatch:
   push:
+    branches:
+      - "main"
+  pull_request:
     paths:
       - 'js/simple_examples/**'
       - '.github/workflows/run-js-examples.yml'
@@ -24,7 +27,7 @@ jobs:
         with:
           node-version: "20.x"
           registry-url: "https://registry.npmjs.org"
-      
+
       - name: Install Magika dependencies
         run: yarn install --frozen-lockfile
         working-directory: js

From adc65e83b3e8fdae83ab1344a550a188c27aeb17 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 10:50:59 +0000
Subject: [PATCH 04/13] python: better error message for check release script

---
 python/scripts/check_release_candidate_python_package.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/python/scripts/check_release_candidate_python_package.py b/python/scripts/check_release_candidate_python_package.py
index dcfcc2c1..edce4a43 100644
--- a/python/scripts/check_release_candidate_python_package.py
+++ b/python/scripts/check_release_candidate_python_package.py
@@ -132,6 +132,9 @@ def get_magika_package_version_via_pip_show() -> str:
         for line in lines:
             if line.startswith("Version: "):
                 return line.split(": ", 1)[1]
+        click.echo(
+            f"ERROR: Could not extract the package version via pip show. Output from pip show: {r.stdout}"
+        )
         return ""
     except subprocess.CalledProcessError as e:
         click.echo(f"ERROR: Could not retrieve package version via pip show: {e}")

From 26db7e9e95038f5e3e0da434da274505e0c68c85 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 10:57:15 +0000
Subject: [PATCH 05/13] gh: execute pre-release-checks jobs even when the
 actual checks are not needed

if we don't do that, the build-wheels and other jobs will not run as the pre-release-checks is a dependency.
---
 .github/workflows/python-build-package.yml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.github/workflows/python-build-package.yml b/.github/workflows/python-build-package.yml
index ed5cc712..37b2e072 100644
--- a/.github/workflows/python-build-package.yml
+++ b/.github/workflows/python-build-package.yml
@@ -34,9 +34,6 @@ jobs:
   pre-release-checks:
     name: Pre-release checks
     runs-on: ubuntu-latest
-    # Note: the env context is not available for jobs.if:
-    # https://github.com/actions/runner/issues/1189#issuecomment-1129307280.
-    if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'test-python-v')
     steps:
       - name: Checkout code
         uses: actions/checkout@v5
@@ -47,6 +44,7 @@ jobs:
       - name: Install uv
         run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
       - name: Check package for release
+        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
         run: |
           FULL_TAG_REF="${{ github.ref }}"
           TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"

From 4a6b6a6f7226a38db452151ff41642a29cd76bc9 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 10:58:24 +0000
Subject: [PATCH 06/13] gh: avoid overtriggering python gh actions

---
 .github/workflows/python-build-package.yml | 2 +-
 .github/workflows/python-test-suite.yml    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/python-build-package.yml b/.github/workflows/python-build-package.yml
index 37b2e072..6ab240d9 100644
--- a/.github/workflows/python-build-package.yml
+++ b/.github/workflows/python-build-package.yml
@@ -12,7 +12,7 @@ on:
       - "python/**"
       - "rust/**"
       - "tests_data/**"
-      - ".github/workflows/python-*"
+      - ".github/workflows/python-build-package.yml"
   schedule:
     - cron: "12 3 * * 4" # Run everything once per week.
     - cron: "12 3 * * 1" # Refresh the cache an additional time.
diff --git a/.github/workflows/python-test-suite.yml b/.github/workflows/python-test-suite.yml
index 1372176e..9061eadd 100644
--- a/.github/workflows/python-test-suite.yml
+++ b/.github/workflows/python-test-suite.yml
@@ -10,7 +10,7 @@ on:
       - "python/**"
       - "rust/**"
       - "tests_data/**"
-      - ".github/workflows/python-*"
+      - ".github/workflows/python-test-suite.yml"
 
 permissions:
   contents: read

From 62408827bc271924738b6c567cbfd4c9922059bc Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 11:14:36 +0000
Subject: [PATCH 07/13] gh: skip pip show package version validation when
 installing via uv

---
 .github/workflows/python-build-package.yml    |  2 +-
 .../check_release_candidate_python_package.py | 27 +++++++++++++------
 2 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/python-build-package.yml b/.github/workflows/python-build-package.yml
index 6ab240d9..e5940178 100644
--- a/.github/workflows/python-build-package.yml
+++ b/.github/workflows/python-build-package.yml
@@ -49,7 +49,7 @@ jobs:
           FULL_TAG_REF="${{ github.ref }}"
           TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
           # Note: this uses the magika python package installed via uv.
-          uv run ./scripts/check_release_candidate_python_package.py ${TAG_VERSION}
+          uv run ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --skip-pip-show-package-version
         working-directory: python
 
   build-wheels:
diff --git a/python/scripts/check_release_candidate_python_package.py b/python/scripts/check_release_candidate_python_package.py
index edce4a43..f9ec061c 100644
--- a/python/scripts/check_release_candidate_python_package.py
+++ b/python/scripts/check_release_candidate_python_package.py
@@ -28,12 +28,19 @@
 
 @click.command()
 @click.argument("expected_version")
+@click.option(
+    "--skip-pip-show-package-version",
+    is_flag=True,
+    help="Do not attempt to get and validate the package version obtained via pip show.",
+)
 @click.option(
     "--use-python-client",
     is_flag=True,
     help="Use the Python client instead of Rust CLI.",
 )
-def main(expected_version: str, use_python_client: bool) -> None:
+def main(
+    expected_version: str, skip_pip_show_package_version: bool, use_python_client: bool
+) -> None:
     """Checks version consistency for the `magika` package."""
 
     with_errors = False
@@ -45,7 +52,10 @@ def main(expected_version: str, use_python_client: bool) -> None:
     except Exception:
         instance_version = ""
 
-    package_version = get_magika_package_version_via_pip_show()
+    if skip_pip_show_package_version:
+        pip_show_package_version = "skipped"
+    else:
+        pip_show_package_version = get_magika_package_version_via_pip_show()
 
     if use_python_client:
         cli_version = instance_version
@@ -58,15 +68,15 @@ def main(expected_version: str, use_python_client: bool) -> None:
     if instance_version == "":
         click.echo("ERROR: failed to get instance_version.")
         with_errors = True
-    if package_version == "":
-        click.echo("ERROR: failed to get package_version.")
+    if pip_show_package_version == "":
+        click.echo("ERROR: failed to get pip_show_package_version.")
         with_errors = True
     if cli_version == "":
         click.echo("ERROR: failed to get cli_version.")
         with_errors = True
 
     click.echo(
-        f"Versions: {expected_version=}, {module_version=}, {instance_version=}, {package_version=}, {cli_version=}"
+        f"Versions: {expected_version=}, {module_version=}, {instance_version=}, {pip_show_package_version=}, {cli_version=}"
     )
 
     if module_version != expected_version:
@@ -77,9 +87,10 @@ def main(expected_version: str, use_python_client: bool) -> None:
         click.echo(f"ERROR: {instance_version=} != {module_version=}")
         with_errors = True
 
-    if module_version != package_version:
-        click.echo(f"ERROR: {module_version=} != {package_version=}")
-        with_errors = True
+    if not skip_pip_show_package_version:
+        if module_version != pip_show_package_version:
+            click.echo(f"ERROR: {module_version=} != {pip_show_package_version=}")
+            with_errors = True
 
     # From now on, we assume all the python-related versions are the same. If
     # they are not, we would have at least one error above.

From 654925d3840cf72567e8b3d2cda89087ff843539 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 11:21:29 +0000
Subject: [PATCH 08/13] python: dump stderr in case of errors in version
 extraction

---
 python/scripts/check_release_candidate_python_package.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/python/scripts/check_release_candidate_python_package.py b/python/scripts/check_release_candidate_python_package.py
index f9ec061c..465198c0 100644
--- a/python/scripts/check_release_candidate_python_package.py
+++ b/python/scripts/check_release_candidate_python_package.py
@@ -144,7 +144,7 @@ def get_magika_package_version_via_pip_show() -> str:
             if line.startswith("Version: "):
                 return line.split(": ", 1)[1]
         click.echo(
-            f"ERROR: Could not extract the package version via pip show. Output from pip show: {r.stdout}"
+            f"ERROR: Could not extract the package version via pip show. Output from pip show:\nstdout={r.stdout}\n\nstderr={r.stderr}"
         )
         return ""
     except subprocess.CalledProcessError as e:

From fc76af880b60616ddd523f71d4d0137c81b698ac Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 11:26:08 +0000
Subject: [PATCH 09/13] gh: rename python build workflow

---
 ...hon-build-package.yml => python-build-and-release-package.yml} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename .github/workflows/{python-build-package.yml => python-build-and-release-package.yml} (100%)

diff --git a/.github/workflows/python-build-package.yml b/.github/workflows/python-build-and-release-package.yml
similarity index 100%
rename from .github/workflows/python-build-package.yml
rename to .github/workflows/python-build-and-release-package.yml

From 1c3bc4dbedddbbd1039af8a799f8291863840383 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Wed, 29 Oct 2025 11:33:29 +0000
Subject: [PATCH 10/13] gh: update name

---
 .github/workflows/python-build-and-release-package.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/python-build-and-release-package.yml b/.github/workflows/python-build-and-release-package.yml
index e5940178..c7d9c337 100644
--- a/.github/workflows/python-build-and-release-package.yml
+++ b/.github/workflows/python-build-and-release-package.yml
@@ -1,4 +1,4 @@
-name: Python - build package
+name: Python - build and release package
 
 on:
   workflow_dispatch:

From 8773e9484b4b4620c81abb0f199df203342c34d2 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Thu, 30 Oct 2025 09:34:02 +0000
Subject: [PATCH 11/13] python: improve pre-release checker CLI

---
 ...python_package.py => pre_release_check.py} | 63 ++++++++++++++-----
 1 file changed, 46 insertions(+), 17 deletions(-)
 rename python/scripts/{check_release_candidate_python_package.py => pre_release_check.py} (74%)

diff --git a/python/scripts/check_release_candidate_python_package.py b/python/scripts/pre_release_check.py
similarity index 74%
rename from python/scripts/check_release_candidate_python_package.py
rename to python/scripts/pre_release_check.py
index 465198c0..6422d9c1 100644
--- a/python/scripts/check_release_candidate_python_package.py
+++ b/python/scripts/pre_release_check.py
@@ -27,21 +27,49 @@
 
 
 @click.command()
-@click.argument("expected_version")
 @click.option(
-    "--skip-pip-show-package-version",
+    "--expected-version",
+    default="",
+    help="Expected version string (e.g., '1.2.3'). If provided, checks will be validated against this value.",
+)
+@click.option(
+    "--report-only",
+    is_flag=True,
+    help="Print errors without failing. (Default: Fails on errors)",
+)
+@click.option(
+    "--check-pip-show-package-version/--no-check-pip-show-package-version",
     is_flag=True,
-    help="Do not attempt to get and validate the package version obtained via pip show.",
+    default=True,
+    help="Enable/disable version check via 'pip show'. (Default: Enabled)",
 )
 @click.option(
     "--use-python-client",
     is_flag=True,
-    help="Use the Python client instead of Rust CLI.",
+    help="Use the Python client instead of Rust client. (Default: False)",
 )
 def main(
-    expected_version: str, skip_pip_show_package_version: bool, use_python_client: bool
+    expected_version: str,
+    report_only: bool,
+    check_pip_show_package_version: bool,
+    use_python_client: bool,
 ) -> None:
-    """Checks version consistency for the `magika` package."""
+    """Checks versions consistency for the `magika` package."""
+
+    if report_only:
+        click.echo('Running in "report only" mode.')
+    if not check_pip_show_package_version:
+        click.echo("Skipping checking package version via pip show.")
+    if use_python_client:
+        click.echo("Using python client instead of Rust client.")
+
+    strict_mode = not report_only
+    if strict_mode:
+        if expected_version == "":
+            click.secho(
+                "ERROR: when not using --report-only, --expected-version is required."
+            )
+            sys.exit(1)
 
     with_errors = False
 
@@ -52,10 +80,10 @@ def main(
     except Exception:
         instance_version = ""
 
-    if skip_pip_show_package_version:
-        pip_show_package_version = "skipped"
-    else:
+    if check_pip_show_package_version:
         pip_show_package_version = get_magika_package_version_via_pip_show()
+    else:
+        pip_show_package_version = ""
 
     if use_python_client:
         cli_version = instance_version
@@ -68,7 +96,7 @@ def main(
     if instance_version == "":
         click.echo("ERROR: failed to get instance_version.")
         with_errors = True
-    if pip_show_package_version == "":
+    if check_pip_show_package_version and pip_show_package_version == "":
         click.echo("ERROR: failed to get pip_show_package_version.")
         with_errors = True
     if cli_version == "":
@@ -76,10 +104,10 @@ def main(
         with_errors = True
 
     click.echo(
-        f"Versions: {expected_version=}, {module_version=}, {instance_version=}, {pip_show_package_version=}, {cli_version=}"
+        f"Extracted versions: {expected_version=}, {module_version=}, {instance_version=}, {pip_show_package_version=}, {cli_version=}."
     )
 
-    if module_version != expected_version:
+    if expected_version != "" and module_version != expected_version:
         click.echo(f"ERROR: {module_version=} != {expected_version=}")
         with_errors = True
 
@@ -87,7 +115,7 @@ def main(
         click.echo(f"ERROR: {instance_version=} != {module_version=}")
         with_errors = True
 
-    if not skip_pip_show_package_version:
+    if check_pip_show_package_version:
         if module_version != pip_show_package_version:
             click.echo(f"ERROR: {module_version=} != {pip_show_package_version=}")
             with_errors = True
@@ -96,7 +124,7 @@ def main(
     # they are not, we would have at least one error above.
 
     if not is_valid_python_version(module_version):
-        click.echo(f"ERROR: {module_version=} is not a valid python version")
+        click.echo(f"ERROR: {module_version=} is not a valid python version.")
         with_errors = True
 
     if module_version.endswith("-dev") or cli_version.endswith("-dev"):
@@ -108,10 +136,11 @@ def main(
         with_errors = True
 
     if with_errors:
-        click.echo("There was at least one error")
-        sys.exit(1)
+        click.secho("There was at least one error.", fg="red")
+        if strict_mode:
+            sys.exit(1)
     else:
-        click.echo("All tests pass!")
+        click.secho("All tests pass!", fg="green")
 
 
 def get_rust_cli_version() -> str:

From 17d71c4fd5c709b4ff6f85b5dd57ec073b70f166 Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Thu, 30 Oct 2025 09:35:23 +0000
Subject: [PATCH 12/13] gh: use new pre-release checker

---
 .../python-build-and-release-package.yml      | 65 +++++++++++++------
 1 file changed, 45 insertions(+), 20 deletions(-)

diff --git a/.github/workflows/python-build-and-release-package.yml b/.github/workflows/python-build-and-release-package.yml
index c7d9c337..362c27d2 100644
--- a/.github/workflows/python-build-and-release-package.yml
+++ b/.github/workflows/python-build-and-release-package.yml
@@ -44,12 +44,19 @@ jobs:
       - name: Install uv
         run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
       - name: Check package for release
-        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+        env:
+          IS_RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX) }}
         run: |
-          FULL_TAG_REF="${{ github.ref }}"
-          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
-          # Note: this uses the magika python package installed via uv.
-          uv run ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --skip-pip-show-package-version
+          if [[ "${{ env.IS_RELEASE_TAG }}" == 'true' ]]; then
+            FULL_TAG_REF="${{ github.ref }}"
+            TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+            CHECKER_OPTIONS="--expected-version ${TAG_VERSION}"
+          else
+            CHECKER_OPTIONS="--report-only"
+          fi
+          # Note: this uses the magika python package installed via uv. Also,
+          # pip is not available here, so we skip pip show check.
+          uv run ./scripts/pre_release_check.py $CHECKER_OPTIONS --no-check-pip-show-package-version
         working-directory: python
 
   build-wheels:
@@ -147,14 +154,20 @@ jobs:
       - run: magika -r tests_data/basic
       - run: python3 ./python/scripts/run_quick_test_magika_cli.py
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
-      - name: Check package for release (if this is a release push)
-        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+      - name: Check package for release readiness
+        env:
+          IS_RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX) }}
         shell: bash
         run: |
-          FULL_TAG_REF="${{ github.ref }}"
-          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          if [[ "${{ env.IS_RELEASE_TAG }}" == 'true' ]]; then
+            FULL_TAG_REF="${{ github.ref }}"
+            TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+            CHECKER_OPTIONS="--expected-version ${TAG_VERSION}"
+          else
+            CHECKER_OPTIONS="--report-only"
+          fi
           # Note: this uses the magika python package that was just built.
-          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION}
+          python3 ./scripts/pre_release_check.py $CHECKER_OPTIONS
         working-directory: python
 
   build-pure-python-wheel-and-sdist:
@@ -232,14 +245,20 @@ jobs:
       # Test the python module
       - run: "python3 -c 'import magika; m = magika.Magika(); print(m)'"
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
-      - name: Check package for release (if this is a release push)
-        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+      - name: Check package for release readiness
+        env:
+          IS_RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX) }}
         shell: bash
         run: |
-          FULL_TAG_REF="${{ github.ref }}"
-          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          if [[ "${{ env.IS_RELEASE_TAG }}" == 'true' ]]; then
+            FULL_TAG_REF="${{ github.ref }}"
+            TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+            CHECKER_OPTIONS="--expected-version ${TAG_VERSION}"
+          else
+            CHECKER_OPTIONS="--report-only"
+          fi
           # Note: this uses the magika python package that was just built.
-          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --use-python-client
+          python3 ./scripts/pre_release_check.py $CHECKER_OPTIONS --use-python-client
         working-directory: python
 
   test-sdist:
@@ -292,14 +311,20 @@ jobs:
       # Test the python module
       - run: "python3 -c 'import magika; m = magika.Magika(); print(m)'"
       - run: python3 ./python/scripts/run_quick_test_magika_module.py
-      - name: Check package for release (if this is a release push)
-        if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX)
+      - name: Check package for release readiness
+        env:
+          IS_RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, env.RELEASE_TAG_PREFIX) }}
         shell: bash
         run: |
-          FULL_TAG_REF="${{ github.ref }}"
-          TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+          if [[ "${{ env.IS_RELEASE_TAG }}" == 'true' ]]; then
+            FULL_TAG_REF="${{ github.ref }}"
+            TAG_VERSION="${FULL_TAG_REF#refs/tags/${{ env.RELEASE_TAG_PREFIX}}}"
+            CHECKER_OPTIONS="--expected-version ${TAG_VERSION}"
+          else
+            CHECKER_OPTIONS="--report-only"
+          fi
           # Note: this uses the magika python package that was just built.
-          python3 ./scripts/check_release_candidate_python_package.py ${TAG_VERSION} --use-python-client
+          python3 ./scripts/pre_release_check.py $CHECKER_OPTIONS --use-python-client
         working-directory: python
 
   release:

From a34f78c8dedb16ff4433ee85b2f174fdf43dcf9e Mon Sep 17 00:00:00 2001
From: Yanick Fratantonio 
Date: Thu, 30 Oct 2025 10:07:53 +0000
Subject: [PATCH 13/13] gh: add stub to publish to pypi test via trusted
 publishing

---
 .../python-build-and-release-package.yml      | 35 ++++++++++++++-----
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/python-build-and-release-package.yml b/.github/workflows/python-build-and-release-package.yml
index 362c27d2..3ed8a635 100644
--- a/.github/workflows/python-build-and-release-package.yml
+++ b/.github/workflows/python-build-and-release-package.yml
@@ -327,14 +327,33 @@ jobs:
           python3 ./scripts/pre_release_check.py $CHECKER_OPTIONS --use-python-client
         working-directory: python
 
-  release:
-    needs: [test-wheels, test-pure-python-wheel, test-sdist]
-    name: Release to pypi
-    runs-on: ubuntu-latest
+  # Adapted from https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+  publish-to-testpypi:
+    name: Publish to TestPyPI
     # Note: the env context is not available for jobs.if:
     # https://github.com/actions/runner/issues/1189#issuecomment-1129307280.
-    if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'test-python-v')
+    # TODO: re-enable the checker on the tag
+    # if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'test-python-v')
+    needs: [test-wheels, test-pure-python-wheel, test-sdist]
+    runs-on: ubuntu-latest
+
+    environment:
+      name: testpypi
+      url: https://test.pypi.org/p/magika
+
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+
     steps:
-      - name: Checkout code
-        uses: actions/checkout@v5
-    # TODO: download artifacts, upload them to pypi
\ No newline at end of file
+      - name: Download all the artifacts (binary wheels, pure python wheel, sdist)
+        uses: actions/download-artifact@v4
+        with:
+          path: artifacts/
+
+      - name: List all files (for debugging)
+        run: ls -alR artifacts/
+
+      # - name: Publish distribution to TestPyPI
+      #   uses: pypa/gh-action-pypi-publish@release/v1
+      #   with:
+      #     repository-url: https://test.pypi.org/legacy/