浏览代码

Backport simplified build process (#2575)

* Backport simplified build process
Jean-Robin 1 月之前
父节点
当前提交
df33e991cc
共有 36 个文件被更改,包括 886 次插入560 次删除
  1. 85 61
      .github/workflows/build-and-release-single-package.yml
  2. 116 91
      .github/workflows/build-and-release.yml
  3. 二进制
      readme_img/gui_creation.webp
  4. 二进制
      readme_img/readme_app.gif
  5. 二进制
      readme_img/readme_cloud_demo.gif
  6. 二进制
      readme_img/readme_demo_studio.gif
  7. 二进制
      readme_img/readme_exec_graph.png
  8. 二进制
      readme_img/readme_logo.png
  9. 二进制
      readme_img/scenario_and_data_mgt.gif
  10. 二进制
      readme_img/taipy-github-optimized.png
  11. 二进制
      readme_img/taipy_banner.png
  12. 二进制
      readme_img/taipy_github_GUI_video.gif
  13. 二进制
      readme_img/taipy_github_data_support.png
  14. 二进制
      readme_img/taipy_github_scenario.png
  15. 二进制
      readme_img/taipy_github_scenarios_video.gif
  16. 二进制
      readme_img/tiny_demo_readme.gif
  17. 1 2
      taipy/common/pyproject.toml
  18. 1 2
      taipy/core/pyproject.toml
  19. 1 2
      taipy/gui/pyproject.toml
  20. 1 2
      taipy/rest/pyproject.toml
  21. 1 2
      taipy/templates/pyproject.toml
  22. 14 14
      tests/tools/release/test_version.py
  23. 1 1
      tools/packages/taipy-common/setup.py
  24. 1 1
      tools/packages/taipy-core/setup.py
  25. 1 1
      tools/packages/taipy-gui/setup.py
  26. 1 1
      tools/packages/taipy-rest/setup.py
  27. 1 1
      tools/packages/taipy-templates/setup.py
  28. 1 1
      tools/packages/taipy/setup.py
  29. 48 37
      tools/release/build_package_structure.py
  30. 47 0
      tools/release/bump_patch_version.py
  31. 181 54
      tools/release/common.py
  32. 98 0
      tools/release/delete_dev_releases.py
  33. 0 148
      tools/release/fetch_latest_versions.py
  34. 0 101
      tools/release/setup_version.py
  35. 192 0
      tools/release/setup_versions.py
  36. 94 38
      tools/release/update_setup_requirements.py

+ 85 - 61
.github/workflows/build-and-release-single-package.yml

@@ -1,4 +1,4 @@
-name: Build and release one taipy package
+name: Build one taipy sub-package release
 
 on:
   workflow_dispatch:
@@ -39,11 +39,13 @@ env:
 
 permissions:
   contents: write
+  pull-requests: write
 
 jobs:
-  fetch-versions:
+  setup-versions:
     runs-on: ubuntu-latest
     outputs:
+        branch: ${{ steps.version-setup.outputs.branch }}
         common_VERSION: ${{ steps.version-setup.outputs.common_VERSION }}
         core_VERSION: ${{ steps.version-setup.outputs.core_VERSION }}
         gui_VERSION: ${{ steps.version-setup.outputs.gui_VERSION }}
@@ -53,56 +55,43 @@ jobs:
         LATEST_TAIPY_VERSION: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
     steps:
       - uses: actions/checkout@v4
-      - name: Extract branch name
-        id: extract_branch
-        shell: bash
-        run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
+
+      - name: Validate target version
+        run: |
+          version="${{ github.event.inputs.target_version }}"
+          if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+            echo "❌ Invalid version format: '$version' - <M>.<m>.<patch> is mandatory."
+            exit 1
+          fi
+          echo "✅ Valid target version: $version"
 
       - name: Install mandatory Python packages
         run: |
           python -m pip install --upgrade pip
           pip install requests
 
-      - name: Check package version
-        run: |
-          python tools/release/check_package_version.py \
-          ${{ github.event.inputs.target_package }} \
-          ${{ github.event.inputs.target_version }}
-
-      - name: Setup Version
+      - name: Setup versions
         id: version-setup
         run: |
-          python tools/release/fetch_latest_versions.py \
+          python tools/release/setup_versions.py \
           ${{ github.event.inputs.target_package }} \
-          ${{ github.event.inputs.target_version }} \
-          ${{ github.event.inputs.release_type }} \
-          ${{ github.event.inputs.sub_packages_location }}  \
-          ${{ github.repository }} >>$GITHUB_OUTPUT
-
-      - name: Verify versions
-        run: |
-          echo 'common' version: ${{ steps.version-setup.outputs.common_VERSION }}
-          echo 'core' version: ${{ steps.version-setup.outputs.core_VERSION }}
-          echo 'gui' version: ${{ steps.version-setup.outputs.gui_VERSION }}
-          echo 'rest' version: ${{ steps.version-setup.outputs.rest_VERSION }}
-          echo 'templates' version: ${{ steps.version-setup.outputs.templates_VERSION }}
-          echo 'taipy' version: ${{ steps.version-setup.outputs.taipy_VERSION }}
-          echo Latest 'taipy' version: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
-
-  build-and-release-package:
-    needs: fetch-versions
+          -v ${{ github.event.inputs.target_version }} \
+          -t ${{ github.event.inputs.release_type }} \
+          -r ${{ github.repository }} >>$GITHUB_OUTPUT
+          echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >>$GITHUB_OUTPUT
+
+  build-package-release:
+    needs: setup-versions
     timeout-minutes: 20
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-        with:
-          ssh-key: ${{ secrets.DEPLOY_KEY }}
       - uses: actions/setup-python@v5
         with:
           python-version: 3.9
       - uses: actions/setup-node@v4
         with:
-          node-version: '20'
+          node-version: "20"
 
       - name: Extract commit hash
         id: extract_hash
@@ -114,29 +103,34 @@ jobs:
           python -m pip install --upgrade pip
           pip install requests
 
-      - name: Set build variables for ${{ github.event.inputs.target_package }} ${{ github.event.inputs.target_version }}
-        id: set-variables
-        shell: bash
-        run: |
-          echo "package_version=${{ github.event.inputs.target_version }}" >> $GITHUB_OUTPUT
-          if [ "${{ github.event.inputs.target_package }}" == "taipy" ]; then
-            echo "release_name=${{ github.event.inputs.target_version }}" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-${{ github.event.inputs.target_version }}.tar.gz" >> $GITHUB_OUTPUT
-          else
-            echo "release_name=${{ github.event.inputs.target_version }}-${{ github.event.inputs.target_package }}" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-${{ github.event.inputs.target_package }}-${{ github.event.inputs.target_version }}.tar.gz" >> $GITHUB_OUTPUT
-          fi
-
       - name: Update setup.requirements.txt
         run: |
           python tools/release/update_setup_requirements.py ${{ github.event.inputs.target_package }} \
-            ${{ needs.fetch-versions.outputs.common_VERSION }} \
-            ${{ needs.fetch-versions.outputs.core_VERSION }} \
-            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
-            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
-            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
-            ${{ github.event.inputs.sub_packages_location }} \
-            ${{ github.repository }}
+            ${{ needs.setup-versions.outputs.common_VERSION }} \
+            ${{ needs.setup-versions.outputs.core_VERSION }} \
+            ${{ needs.setup-versions.outputs.gui_VERSION }} \
+            ${{ needs.setup-versions.outputs.rest_VERSION }} \
+            ${{ needs.setup-versions.outputs.templates_VERSION }} \
+            -deps ${{ github.event.inputs.sub_packages_location }} \
+            -r ${{ github.repository }}
+
+      - name: Set package version for ${{ github.event.inputs.target_package }} ${{ github.event.inputs.target_version }}
+        id: package-version
+        shell: bash
+        run: |
+          if [ "${{ github.event.inputs.target_package }}" == "common" ]; then
+            echo "version=${{ needs.setup-versions.outputs.common_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ github.event.inputs.target_package }}" == "core" ]; then
+            echo "version=${{ needs.setup-versions.outputs.core_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ github.event.inputs.target_package }}" == "gui" ]; then
+            echo "version=${{ needs.setup-versions.outputs.gui_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ github.event.inputs.target_package }}" == "rest" ]; then
+            echo "version=${{ needs.setup-versions.outputs.rest_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ github.event.inputs.target_package }}" == "templates" ]; then
+            echo "version=${{ needs.setup-versions.outputs.templates_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ github.event.inputs.target_package }}" == "taipy" ]; then
+            echo "version=${{ needs.setup-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
+          fi
 
       - name: Install dependencies
         run: |
@@ -149,7 +143,7 @@ jobs:
           pipenv install --dev
 
       - name: Generate GUI pyi file
-        if: github.event.inputs.target_package == 'gui'
+        if: ${{ github.event.inputs.target_package == 'gui' }}
         run: |
           pipenv run python tools/gui/generate_pyi.py
 
@@ -165,29 +159,59 @@ jobs:
 
       - name: Build Package Structure
         run: |
-          python tools/release/build_package_structure.py ${{ github.event.inputs.target_package }}
+          python tools/release/build_package_structure.py ${{ github.event.inputs.target_package }} ${{ steps.package-version.outputs.version }}
 
       - name: Build package
         working-directory: "build_${{ github.event.inputs.target_package }}"
         run: |
           python -m build
-          for file in ./dist/*; do mv "$file" "${file//_/-}"; done
+          if compgen -G "./dist/*_*" > /dev/null; then
+            for file in ./dist/*_*; do mv "$file" "${file//_/-}"; done
+          fi
 
       - name: Create tag and release
         working-directory: "build_${{ github.event.inputs.target_package }}"
         run: |
+          package_suffix=""
+          if [ "${{ github.event.inputs.target_package }}" != "taipy" ]; then
+            package_suffix="-${{ github.event.inputs.target_package }}"
+          fi
+          release_name="${{ steps.package-version.outputs.version }}$package_suffix"
+          tar_path="./dist/taipy$package_suffix-${{ steps.package-version.outputs.version }}.tar.gz"
           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title $release_name --notes "Dev Release $release_name"
           else
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --title $release_name --notes "Release $release_name"
           fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Ensure the latest 'taipy' production release is marked as *latest* no matter what
+      - name: Bump patch version
+        if: ${{ github.event.inputs.release_type == 'production' }}
         run: |
-          gh release edit ${{ needs.fetch-versions.outputs.LATEST_TAIPY_VERSION }} --latest
+          python tools/release/bump_patch_version.py ${{ github.event.inputs.target_package }}
+
+      - uses: stefanzweifel/git-auto-commit-action@v5
+        if: ${{ github.event.inputs.release_type == 'production' }}
+        with:
+          branch: "devops/bump-patch-version-for-${{ github.event.inputs.target_package }}-${{ github.run_id }}"
+          create_branch: "true"
+          file_pattern: "**/version.json"
+          commit_message: Bump patch versions for ${{ github.event.inputs.target_package }}
+
+      - name: Create pull request
+        if: ${{ github.event.inputs.release_type == 'production' }}
+        run: gh pr create -B "${{ needs.setup-versions.outputs.branch }}" -H "devops/bump-patch-version-for-${{ github.event.inputs.target_package }}-${{ github.run_id }}" --title "Bump patch version" --body "Created by GitHub action build-and-release-single-package"
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      # Ensure the latest 'taipy' production release, if there is one, is marked as *latest* no matter what
+      - name: Force latest 'taipy' production release
+        run: |
+          if [ "${{ needs.setup-versions.outputs.LATEST_TAIPY_VERSION }}" != "0.0.0" ]; then
+            gh release edit ${{ needs.setup-versions.outputs.LATEST_TAIPY_VERSION }} --latest
+          fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 116 - 91
.github/workflows/build-and-release.yml

@@ -1,4 +1,4 @@
-name: Build all taipy packages and release them
+name: Build all taipy package releases
 
 on:
   workflow_dispatch:
@@ -28,62 +28,63 @@ env:
 
 permissions:
   contents: write
+  pull-requests: write
 
 jobs:
-  fetch-versions:
+  setup-versions:
     runs-on: ubuntu-latest
     outputs:
+        branch: ${{ steps.version-setup.outputs.branch }}
         common_VERSION: ${{ steps.version-setup.outputs.common_VERSION }}
-        NEXT_common_VERSION: ${{ steps.version-setup.outputs.NEXT_common_VERSION }}
         core_VERSION: ${{ steps.version-setup.outputs.core_VERSION }}
-        NEXT_core_VERSION: ${{ steps.version-setup.outputs.NEXT_core_VERSION }}
         gui_VERSION: ${{ steps.version-setup.outputs.gui_VERSION }}
-        NEXT_gui_VERSION: ${{ steps.version-setup.outputs.NEXT_gui_VERSION }}
         rest_VERSION: ${{ steps.version-setup.outputs.rest_VERSION }}
-        NEXT_rest_VERSION: ${{ steps.version-setup.outputs.NEXT_rest_VERSION }}
         templates_VERSION: ${{ steps.version-setup.outputs.templates_VERSION }}
-        NEXT_templates_VERSION: ${{ steps.version-setup.outputs.NEXT_templates_VERSION }}
         taipy_VERSION: ${{ steps.version-setup.outputs.taipy_VERSION }}
-        NEXT_taipy_VERSION: ${{ steps.version-setup.outputs.NEXT_taipy_VERSION }}
         LATEST_TAIPY_VERSION: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
-        BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
     steps:
       - uses: actions/checkout@v4
-      - name: Extract branch name
-        id: extract_branch
-        shell: bash
-        run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
+
+      - name: Validate target version
+        run: |
+          version="${{ github.event.inputs.target_version }}"
+          if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+            echo "❌ Invalid version format: '$version' - <M>.<m>.<patch> is mandatory."
+            exit 1
+          fi
+          echo "✅ Valid target version: $version"
 
       - name: Install mandatory Python packages
         run: |
           python -m pip install --upgrade pip
           pip install requests
 
-      - name: Setup Version
+      - name: Setup versions
         id: version-setup
         run: |
-          python tools/release/setup_version.py ALL ${{ github.event.inputs.release_type }} \
-          ${{ github.event.inputs.target_version }} \
-          ${{ steps.extract_branch.outputs.branch }} >> $GITHUB_OUTPUT
-
-  build-and-release-taipy-packages:
-    needs: [fetch-versions]
-    runs-on: ubuntu-latest
+          python tools/release/setup_versions.py \
+          all \
+          -v ${{ github.event.inputs.target_version }} \
+          -t ${{ github.event.inputs.release_type }} \
+          -r ${{ github.repository }} | tee -a >>$GITHUB_OUTPUT
+          echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >>$GITHUB_OUTPUT
+
+  build-sub-packager-releases:
+    needs: setup-versions
     timeout-minutes: 20
+    runs-on: ubuntu-latest
     strategy:
       matrix:
         package: [common, core, gui, rest, templates]
       max-parallel: 1
     steps:
       - uses: actions/checkout@v4
-        with:
-          ssh-key: ${{ secrets.DEPLOY_KEY }}
       - uses: actions/setup-python@v5
         with:
           python-version: 3.9
       - uses: actions/setup-node@v4
         with:
-          node-version: '20'
+          node-version: "20"
 
       - name: Extract commit hash
         id: extract_hash
@@ -95,70 +96,62 @@ jobs:
           python -m pip install --upgrade pip
           pip install requests
 
-      - name: Set Build Variables
-        id: set-variables
+      - name: Update setup.requirements.txt
+        run: |
+          python tools/release/update_setup_requirements.py ${{ matrix.package }} \
+            ${{ needs.setup-versions.outputs.common_VERSION }} \
+            ${{ needs.setup-versions.outputs.core_VERSION }} \
+            ${{ needs.setup-versions.outputs.gui_VERSION }} \
+            ${{ needs.setup-versions.outputs.rest_VERSION }} \
+            ${{ needs.setup-versions.outputs.templates_VERSION }} \
+            -deps ${{ github.event.inputs.sub_packages_location }} \
+            -r ${{ github.repository }}
+
+      - name: Set package version for ${{ matrix.package }} ${{ github.event.inputs.target_version }}
+        id: package-version
         shell: bash
         run: |
           if [ "${{ matrix.package }}" == "common" ]; then
-            echo "package_version=${{ needs.fetch-versions.outputs.common_VERSION }}" >> $GITHUB_OUTPUT
-            echo "release_name=${{ needs.fetch-versions.outputs.common_VERSION }}-common" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-common-${{ needs.fetch-versions.outputs.common_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+            echo "version=${{ needs.setup-versions.outputs.common_VERSION }}" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "core" ]; then
-            echo "package_version=${{ needs.fetch-versions.outputs.core_VERSION }}" >> $GITHUB_OUTPUT
-            echo "release_name=${{ needs.fetch-versions.outputs.core_VERSION }}-core" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-core-${{ needs.fetch-versions.outputs.core_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+            echo "version=${{ needs.setup-versions.outputs.core_VERSION }}" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "gui" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.gui_VERSION}}" >> $GITHUB_OUTPUT
-            echo "release_name=${{ needs.fetch-versions.outputs.gui_VERSION }}-gui" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-gui-${{ needs.fetch-versions.outputs.gui_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+            echo "version=${{ needs.setup-versions.outputs.gui_VERSION }}" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "rest" ]; then
-            echo "package_version=${{ needs.fetch-versions.outputs.rest_VERSION }}" >> $GITHUB_OUTPUT
-            echo "release_name=${{ needs.fetch-versions.outputs.rest_VERSION }}-rest" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-rest-${{ needs.fetch-versions.outputs.rest_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+            echo "version=${{ needs.setup-versions.outputs.rest_VERSION }}" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "templates" ]; then
-            echo "package_version=${{ needs.fetch-versions.outputs.templates_VERSION }}" >> $GITHUB_OUTPUT
-            echo "release_name=${{ needs.fetch-versions.outputs.templates_VERSION }}-templates" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-templates-${{ needs.fetch-versions.outputs.templates_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+            echo "version=${{ needs.setup-versions.outputs.templates_VERSION }}" >> $GITHUB_OUTPUT
+          elif [ "${{ matrix.package }}" == "taipy" ]; then
+            echo "version=${{ needs.setup-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
           fi
 
-      - name: Update setup.requirements.txt
-        run: |
-          python tools/release/update_setup_requirements.py ${{ matrix.package }} \
-            ${{ needs.fetch-versions.outputs.common_VERSION }} \
-            ${{ needs.fetch-versions.outputs.core_VERSION }} \
-            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
-            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
-            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
-            ${{ github.event.inputs.sub_packages_location }} \
-            ${{ github.repository }}
-
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
           pip install build wheel pipenv mypy black isort
 
       - name: Build GUI front-end
-        if: matrix.package == 'gui'
+        if: ${{ matrix.package == 'gui' }}
         run: |
           pipenv install --dev
           pipenv run python tools/gui/generate_pyi.py
           python tools/frontend/bundle_build.py gui
 
       - name: Archive the GUI front-end
-        if: matrix.package == 'gui'
+        if: ${{ matrix.package == 'gui' }}
         run: |
           tar -czf gui-frontend.tar.gz taipy/gui/webapp
 
       - name: Upload front-end archive as an artifact
-        if: matrix.package == 'gui'
+        if: ${{ matrix.package == 'gui' }}
         uses: actions/upload-artifact@v4
         with:
           name: gui-frontend
           path: gui-frontend.tar.gz
 
-      - name: Build Package Structure
+      - name: Build package structure
         run: |
-          python tools/release/build_package_structure.py ${{ matrix.package }}
+          python tools/release/build_package_structure.py ${{ matrix.package }} ${{ steps.package-version.outputs.version }}
 
       - name: Build package
         working-directory: "build_${{ matrix.package }}"
@@ -166,53 +159,59 @@ jobs:
           python -m build
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
-      - name: Create tag and release ${{ steps.set-variables.outputs.release_name }}
+      - name: Create tag and release
         working-directory: "build_${{ matrix.package }}"
         run: |
+          package_suffix="-${{ matrix.package }}"
+          release_name="${{ steps.package-version.outputs.version }}$package_suffix"
+          tar_path="./dist/taipy$package_suffix-${{ steps.package-version.outputs.version }}.tar.gz"
           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title $release_name --notes "Dev Release $release_name"
           else
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --title $release_name --notes "Release $release_name"
           fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-  build-and-release-taipy:
-    needs: [build-and-release-taipy-packages, fetch-versions]
+  build-taipy-release:
     runs-on: ubuntu-latest
+    needs: [setup-versions, build-sub-packager-releases]
     timeout-minutes: 20
     steps:
       - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: 3.9
+      - uses: actions/setup-node@v4
+        with:
+          node-version: "20"
 
       - name: Extract commit hash
         id: extract_hash
         shell: bash
         run: echo "HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
 
-      - name: Set Build Variables
-        id: set-variables
-        shell: bash
+      - name: Install mandatory Python packages
         run: |
-          echo "package_version=${{ needs.fetch-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
-          echo "release_name=${{ needs.fetch-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
-          echo "tar_path=./dist/${{ github.event.repository.name }}-${{ needs.fetch-versions.outputs.taipy_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
+          python -m pip install --upgrade pip
+          pip install requests
 
       - name: Update setup.requirements.txt
         run: |
           python tools/release/update_setup_requirements.py taipy \
-            ${{ needs.fetch-versions.outputs.common_VERSION }} \
-            ${{ needs.fetch-versions.outputs.core_VERSION }} \
-            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
-            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
-            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
-            ${{ github.event.inputs.sub_packages_location }} \
-            ${{ github.repository }}
+            ${{ needs.setup-versions.outputs.common_VERSION }} \
+            ${{ needs.setup-versions.outputs.core_VERSION }} \
+            ${{ needs.setup-versions.outputs.gui_VERSION }} \
+            ${{ needs.setup-versions.outputs.rest_VERSION }} \
+            ${{ needs.setup-versions.outputs.templates_VERSION }} \
+            -deps ${{ github.event.inputs.sub_packages_location }} \
+            -r ${{ github.repository }}
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install build wheel
+          pip install build wheel pipenv mypy black isort
 
       - uses: actions/download-artifact@v4
         with:
@@ -222,26 +221,31 @@ jobs:
       - name: Retrieve the GUI front-end
         run: tar -xzf gui-frontend.tar.gz
 
-      - name: Build taipy front-end
+      - name: Build Taipy front-end
         run: |
           python tools/frontend/bundle_build.py taipy
 
-      - name: Build taipy Package Structure
+      - name: Build package structure
         run: |
-          python tools/release/build_package_structure.py taipy
+          python tools/release/build_package_structure.py taipy ${{ needs.setup-versions.outputs.taipy_VERSION }}
 
-      - name: Build taipy package
+      - name: Build package
         working-directory: "build_taipy"
         run: |
           python -m build
+          if compgen -G "./dist/*_*" > /dev/null; then
+            for file in ./dist/*_*; do mv "$file" "${file//_/-}"; done
+          fi
 
       - name: Create tag and release Taipy
         working-directory: "build_taipy"
         run: |
+          release_name="${{ needs.setup-versions.outputs.taipy_VERSION }}"
+          tar_path="./dist/taipy-$release_name.tar.gz"
           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title $release_name --notes "Dev Release $release_name"
           else
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
+            gh release create $release_name $tar_path --target ${{ steps.extract_hash.outputs.HASH }} --title $release_name --notes "Release $release_name"
           fi
         shell: bash
         env:
@@ -250,25 +254,46 @@ jobs:
       - name: Download packages
         working-directory: "build_taipy"
         run: |
-          gh release download ${{ needs.fetch-versions.outputs.common_VERSION }}-common --skip-existing --dir dist
-          gh release download ${{ needs.fetch-versions.outputs.core_VERSION }}-core --skip-existing --dir dist
-          gh release download ${{ needs.fetch-versions.outputs.gui_VERSION }}-gui --skip-existing --dir dist
-          gh release download ${{ needs.fetch-versions.outputs.rest_VERSION }}-rest --skip-existing --dir dist
-          gh release download ${{ needs.fetch-versions.outputs.templates_VERSION }}-templates --skip-existing --dir dist
+          gh release download ${{ needs.setup-versions.outputs.common_VERSION }}-common --skip-existing --dir dist
+          gh release download ${{ needs.setup-versions.outputs.core_VERSION }}-core --skip-existing --dir dist
+          gh release download ${{ needs.setup-versions.outputs.gui_VERSION }}-gui --skip-existing --dir dist
+          gh release download ${{ needs.setup-versions.outputs.rest_VERSION }}-rest --skip-existing --dir dist
+          gh release download ${{ needs.setup-versions.outputs.templates_VERSION }}-templates --skip-existing --dir dist
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Bundle all packages in main release tag
         working-directory: "build_taipy"
         run: |
-          find dist -type f -print0 | xargs -r0 gh release upload ${{ needs.fetch-versions.outputs.taipy_VERSION }} --clobber
+          find dist -type f -print0 | xargs -r0 gh release upload ${{ needs.setup-versions.outputs.taipy_VERSION }} --clobber
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Bump patch versions
+        if: ${{ github.event.inputs.release_type == 'production' }}
+        run: |
+          python tools/release/bump_patch_version.py all
+
+      - uses: stefanzweifel/git-auto-commit-action@v5
+        if: ${{ github.event.inputs.release_type == 'production' }}
+        with:
+          branch: "devops/bump-patch-version-${{ github.run_id }}"
+          create_branch: "true"
+          file_pattern: "**/version.json"
+          commit_message: Bump patch versions for ${{ needs.setup-versions.outputs.taipy_VERSION }}
+
+      - name: Create pull request
+        if: ${{ github.event.inputs.release_type == 'production' }}
+        run: gh pr create -B "${{ needs.setup-versions.outputs.branch }}" -H "devops/bump-patch-version-${{ github.run_id }}" --title "Bump patch version" --body "Created by GitHub action build-and-release"
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Ensure the latest 'taipy' production release is marked as *latest*
-        if: github.event.inputs.release_type == 'dev'
+      # Ensure the latest 'taipy' production release, if there is one, is marked as *latest* no matter what
+      - name: Force latest 'taipy' production release
         run: |
-          gh release edit ${{ needs.fetch-versions.outputs.LATEST_TAIPY_VERSION }} --latest
+          if [ "${{ needs.setup-versions.outputs.LATEST_TAIPY_VERSION }}" != "0.0.0" ]; then
+            gh release edit ${{ needs.setup-versions.outputs.LATEST_TAIPY_VERSION }} --latest
+          fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

二进制
readme_img/gui_creation.webp


二进制
readme_img/readme_app.gif


二进制
readme_img/readme_cloud_demo.gif


二进制
readme_img/readme_demo_studio.gif


二进制
readme_img/readme_exec_graph.png


二进制
readme_img/readme_logo.png


二进制
readme_img/scenario_and_data_mgt.gif


二进制
readme_img/taipy-github-optimized.png


二进制
readme_img/taipy_banner.png


二进制
readme_img/taipy_github_GUI_video.gif


二进制
readme_img/taipy_github_data_support.png


二进制
readme_img/taipy_github_scenario.png


二进制
readme_img/taipy_github_scenarios_video.gif


二进制
readme_img/tiny_demo_readme.gif


+ 1 - 2
taipy/common/pyproject.toml

@@ -7,12 +7,11 @@ name = "taipy-common"
 description = "A Taipy package dedicated to provide common data structures, types, classes and functions."
 readme = "package_desc.md"
 requires-python = ">=3.9,<3.13"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy", "taipy-common"]
 classifiers = [
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
     "Natural Language :: English",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",

+ 1 - 2
taipy/core/pyproject.toml

@@ -7,12 +7,11 @@ name = "taipy-core"
 description = "A Python library to build powerful and customized data-driven back-end applications."
 readme = "package_desc.md"
 requires-python = ">=3.9,<3.13"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy", "taipy-core"]
 classifiers = [
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
     "Natural Language :: English",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",

+ 1 - 2
taipy/gui/pyproject.toml

@@ -7,12 +7,11 @@ name = "taipy-gui"
 description = "Low-code library to create graphical user interfaces on the Web for your Python applications."
 readme = "package_desc.md"
 requires-python = ">=3.9,<3.13"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy", "gui", "taipy-gui"]
 classifiers = [
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
     "Natural Language :: English",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",

+ 1 - 2
taipy/rest/pyproject.toml

@@ -7,12 +7,11 @@ name = "taipy-rest"
 description = "Library to expose taipy-core REST APIs."
 readme = "package_desc.md"
 requires-python = ">=3.9,<3.13"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy", "rest", "taipy-rest"]
 classifiers = [
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
     "Natural Language :: English",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",

+ 1 - 2
taipy/templates/pyproject.toml

@@ -7,12 +7,11 @@ name = "taipy-templates"
 description = "An open-source package holding Taipy application templates."
 readme = "package_desc.md"
 requires-python = ">=3.9,<3.13"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy", "taipy-templates"]
 classifiers = [
     "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
     "Natural Language :: English",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.9",

+ 14 - 14
tests/tools/release/test_version.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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
@@ -48,15 +48,15 @@ def test_from_string():
 
 def test_extension():
     version = Version.from_string("1.2.3")
-    extension = version._split_ext()
+    extension = version.split_ext()
     assert extension == ("", -1)
 
     version = Version.from_string("1.2.3.some_ext")
-    extension = version._split_ext()
+    extension = version.split_ext()
     assert extension == ("some_ext", -1)
 
     version = Version.from_string("1.2.3.some_ext123")
-    extension = version._split_ext()
+    extension = version.split_ext()
     assert extension == ("some_ext", 123)
 
 
@@ -142,15 +142,15 @@ def test_order():
     assert v1 < v2, "Version 1.0.0 is older than 1.0.1"
     assert v2 > v1, "Version 1.0.1 is newer than 1.0.0"
 
-    versions = [Version(1, 0), Version(2, 1), Version(3, 4), Version(2, 0)]
-    assert max(versions) == Version(3, 4), "Cannot find max in Version list"
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=1, minor=2, patch=3, ext="dev0")
+    assert v1 > v2, "Version 1.2.3 is newer than 1.2.3.dev0"
+    assert v2 < v1, "Version 1.2.3.dev0 is older than 1.2.3"
 
+    v1 = Version(major=1, minor=2, patch=3, ext="dev0")
+    v2 = Version(major=1, minor=2, patch=3, ext="dev1")
+    assert v1 < v2, "Version 1.2.3.dev0 is older than 1.2.3.dev1"
+    assert v2 > v1, "Version 1.2.3.dev1 is newer 1.2.3.dev0"
 
-def test_bump_ext():
-    version = Version(major=1, minor=2, patch=3)
-    new_version = version.bump_ext_version()
-    assert version == new_version
-
-    version = Version(major=1, minor=2, patch=3, ext="ext0")
-    new_version = version.bump_ext_version()
-    assert new_version.ext == "ext1"
+    versions = [Version(1, 0), Version(2, 1), Version(3, 4), Version(2, 0)]
+    assert max(versions) == Version(3, 4), "Cannot find max in Version list"

+ 1 - 1
tools/packages/taipy-common/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 1 - 1
tools/packages/taipy-core/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 1 - 1
tools/packages/taipy-gui/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 1 - 1
tools/packages/taipy-rest/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 1 - 1
tools/packages/taipy-templates/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 1 - 1
tools/packages/taipy/setup.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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

+ 48 - 37
tools/release/build_package_structure.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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
@@ -14,14 +14,14 @@
 # Invoked by the workflow files build-and-release-single-package.yml and build-and-release.yml.
 # Working directory must be '[checkout-root]'.
 # --------------------------------------------------------------------------------------------------
-
+import argparse
+import json
 import os
 import re
 import shutil
-import sys
 from pathlib import Path
 
-from common import PACKAGES, Package
+from common import Package, Version
 
 # Base build directory name
 DEST_ROOT = "build_"
@@ -38,7 +38,6 @@ SKIP_ITEMS = {
         "build_taipy",
         "doc",
         "frontend",
-        "readme_img",
         "tests",
         "tools",
         ".git",
@@ -52,7 +51,7 @@ SKIP_ITEMS = {
 }
 
 # Regexp identifying subpackage directories in taipy hierarchy
-packages = "|".join(PACKAGES)
+packages = "|".join(Package.NAMES)
 SUB_PACKAGE_DIR_PATTERN = re.compile(rf"taipy/(?:{packages})")
 
 
@@ -73,18 +72,39 @@ def skip_path(path: str, package: Package, parent: str) -> bool:
     return False
 
 
-def usage() -> None:
-    print(f"Usage: {sys.argv[0]} <package>")  # noqa: T201
-    packages = "', '".join(PACKAGES)
-    print(f"   <package> must be one of '{packages}', or 'taipy'.")  # noqa: T201
-
-
-if __name__ == "__main__":
-    if len(sys.argv) < 2:
-        usage()
-        raise ValueError("Missing arguments.")
-
-    package = Package(sys.argv[1])
+def recursive_copy(package: Package, source, dest, *, parent: str = "", skip_root: bool = False):
+    dest_path = dest if skip_root else os.path.join(dest, os.path.basename(source))
+    if not skip_root:
+        os.makedirs(dest_path, exist_ok=True)
+
+    for item in os.listdir(source):
+        src_item = os.path.join(source, item)
+        dest_item = os.path.join(dest_path, item)
+        if not skip_path(src_item, package, parent):
+            if os.path.isfile(src_item):
+                shutil.copy2(src_item, dest_item)
+            elif os.path.isdir(src_item):
+                if (s := src_item.replace("\\", "/")).startswith("./"):
+                    s = s[2:]
+                recursive_copy(package, src_item, dest_path, parent=s)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Creates the directory structure to build a Taipy package.",
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    parser.add_argument(
+        "package",
+        type=Package.check_argument,
+        action="store",
+        help="""The name of the package to setup the build version for.
+This must be the short name of a Taipy package (common, core...) or 'taipy'.
+""",
+    )
+    parser.add_argument("version", type=Version.check_argument, action="store", help="Version of the package to build.")
+    args = parser.parse_args()
+    package = Package(args.package)
 
     if package.name == "taipy":
         # Check that gui_core bundle was built
@@ -105,24 +125,8 @@ if __name__ == "__main__":
         dest_dir = build_dir / "taipy"
     dest_dir.mkdir(parents=True, exist_ok=True)
 
-    def recursive_copy(source, dest, *, parent: str = "", skip_root: bool = False):
-        dest_path = dest if skip_root else os.path.join(dest, os.path.basename(source))
-        if not skip_root:
-            os.makedirs(dest_path, exist_ok=True)
-
-        for item in os.listdir(source):
-            src_item = os.path.join(source, item)
-            dest_item = os.path.join(dest_path, item)
-            if not skip_path(src_item, package, parent):
-                if os.path.isfile(src_item):
-                    shutil.copy2(src_item, dest_item)
-                elif os.path.isdir(src_item):
-                    if (s := src_item.replace("\\", "/")).startswith("./"):
-                        s = s[2:]
-                    recursive_copy(src_item, dest_path, parent=s)
-
     # Copy the package build files from taipy[/package] to build_<package>/taipy
-    recursive_copy("." if package.name == "taipy" else package.package_dir, dest_dir)
+    recursive_copy(package, "." if package.name == "taipy" else package.package_dir, dest_dir)
 
     # This is needed for local builds (i.e. not in a Github workflow)
     if package.name == "taipy":
@@ -131,7 +135,7 @@ if __name__ == "__main__":
         tools_dir.mkdir(parents=True, exist_ok=True)
         shutil.copy2("tools/frontend/bundle_build.py", tools_dir)
         # Copy the build files from tools/packages/taipy to build_taipy
-        recursive_copy(Path("tools") / "packages" / "taipy", build_dir, skip_root=True)
+        recursive_copy(package, Path("tools") / "packages" / "taipy", build_dir, skip_root=True)
     else:
         build_package_dir = build_dir / package.package_dir
         # Copy build files from package to build dir
@@ -141,7 +145,7 @@ if __name__ == "__main__":
         for filename in BUILD_MV_FILES:
             shutil.move(build_package_dir / filename, build_dir)
         # Copy the build files from tools/packages/taipy-<package> to build_<package>
-        recursive_copy(Path("tools") / "packages" / f"taipy-{package.short_name}", build_dir, skip_root=True)
+        recursive_copy(package, Path("tools") / "packages" / f"taipy-{package.short_name}", build_dir, skip_root=True)
 
     # Check that versions were set in setup.requirements.txt
     with open(build_dir / "setup.requirements.txt") as requirements_file:
@@ -150,7 +154,14 @@ if __name__ == "__main__":
                 if not match[2]:  # Version not updated
                     print(f"setup.requirements.txt not up-to-date in 'tools/packages/{package.short_name}'.")  # noqa: T201
                     raise SystemError(f"Version for dependency on {match[1]} is missing.")
+    # Update package's version.json
+    with open(build_dir / package.package_dir / "version.json", "w") as version_file:
+        json.dump(args.version.to_dict(), version_file)
 
     # Copy topmost __init__
     if package.name != "taipy":
         shutil.copy2(Path("taipy") / "__init__.py", dest_dir)
+
+
+if __name__ == "__main__":
+    main()

+ 47 - 0
tools/release/bump_patch_version.py

@@ -0,0 +1,47 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed 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.
+# --------------------------------------------------------------------------------------------------
+# Increments the patch version number in all the packages' version.json file.
+#
+# Invoked from the workflow in build-and-release.yml when releasing production packages.
+# --------------------------------------------------------------------------------------------------
+
+import argparse
+
+from common import Package, Version
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Increments the patch version number of a package.",
+                                     formatter_class=argparse.RawTextHelpFormatter)
+    # <package> argument
+    def _check_package(value: str) -> str:
+        n_value = value.lower()
+        if n_value in Package.names(True) or value == "all":
+            return n_value
+        raise argparse.ArgumentTypeError(f"'{value}' is not a valid Taipy package name.")
+    parser.add_argument("package",
+                        type=_check_package,
+                        action="store",  help="""The name of the package to increment the patch version number.
+This should be the short name of a Taipy package (common, core...) or 'taipy'.
+If can also be set to 'ALL' then all packages are impacted.
+""")
+    args = parser.parse_args()
+
+    for package_name in [args.package] if args.package != "all" else Package.names(True):
+        package = Package(package_name)
+        version = package.load_version()
+        if version.ext:
+            raise ValueError(f"Package version for '{package.name}' has an extension ({version.full_name}).")
+        package.save_version(Version(version.major, version.minor, version.patch + 1))
+
+if __name__ == "__main__":
+    main()

+ 181 - 54
tools/release/common.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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
@@ -11,22 +11,21 @@
 # --------------------------------------------------------------------------------------------------
 # Common artifacts used by the other scripts located in this directory.
 # --------------------------------------------------------------------------------------------------
+import argparse
+import json
 import os
 import re
 import subprocess
 import typing as t
 from dataclasses import asdict, dataclass
-
+from datetime import datetime
+from pathlib import Path
+from functools import total_ordering
 import requests
 
-# These are the base name of the sub packages taipy-*
-# They also are the names of the directory where their code belongs, under the 'taipy' directory
-# in the root of the Taipy repository.
-PACKAGES = ["common", "core", "gui", "rest", "templates"]
-
 
 # --------------------------------------------------------------------------------------------------
-@dataclass(order=True)
+@dataclass(frozen=True)
 class Version:
     """Helps manipulate version numbers."""
 
@@ -35,6 +34,11 @@ class Version:
     patch: int = 0
     ext: t.Optional[str] = None
 
+    # Matching level
+    MAJOR: t.ClassVar[int] = 1
+    MINOR: t.ClassVar[int] = 2
+    PATCH: t.ClassVar[int] = 3
+
     # Unknown version constant
     UNKNOWN: t.ClassVar["Version"]
 
@@ -87,22 +91,20 @@ class Version:
         """Returns this Version as a dictionary."""
         return {k: v for k, v in asdict(self).items() if v is not None}
 
-    def bump_ext_version(self) -> "Version":
-        """Returns a new Version object where the extension part version was incremented.
-
-        If this Version has no extension part, this method returns *self*.
-        """
-        if not self.ext or (m := re.search(r"([0-9]+)$", self.ext)) is None:
-            return self
-
-        ext_ver = int(m[1]) + 1
-        return Version(self.major, self.minor, self.patch, f"{self.ext[: m.start(1)]}{ext_ver}")
+    @staticmethod
+    def check_argument(value: str) -> "Version":
+        """Checks version parameter in an argparse context."""
+        try:
+            version = Version.from_string(value)
+        except Exception as e:
+            raise argparse.ArgumentTypeError(f"'{value}' is not a valid version number.") from e
+        return version
 
     def validate_extension(self, ext="dev"):
         """Returns True if the extension part of this Version is the one queried."""
-        return self._split_ext()[0] == ext
+        return self.split_ext()[0] == ext
 
-    def _split_ext(self) -> t.Tuple[str, int]:
+    def split_ext(self) -> t.Tuple[str, int]:
         """Splits extension into the (identifier, index) tuple
 
         Returns:
@@ -160,14 +162,68 @@ class Version:
             return False
 
         # Both have extensions → check identifiers. Dissimilar identifiers → Not compatible
-        self_prefix, _ = self._split_ext()
-        other_prefix, _ = version._split_ext()
+        self_prefix, _ = self.split_ext()
+        other_prefix, _ = version.split_ext()
         if self_prefix != other_prefix:
             return False
 
         # Same identifiers → Compatible
         return True
 
+    def matches(self, version: "Version", level: int = PATCH) -> bool:
+        """Checks whether this version matches another, up to some level.
+
+        Arguments:
+            version: The version to check against.
+            level: The level of precision for the match:
+            - Version.MAJOR: compare only the major version;
+            - Version.MINOR: compare major and minor versions;
+            - Version.PATCH: compare major, minor, and patch versions.
+
+        Returns:
+            True if the versions match up to the given level, False otherwise.
+        """
+        if self.major != version.major:
+            return False
+        if level >= self.MINOR and self.minor != version.minor:
+            return False
+        if level >= self.PATCH and self.patch != version.patch:
+            return False
+        return True
+
+    def __lt__(self, other: "Version") -> bool:
+        if not isinstance(other, Version):
+            return NotImplemented
+
+        # Compare major, minor, patch
+        self_tuple = (self.major, self.minor, self.patch)
+        other_tuple = (other.major, other.minor, other.patch)
+        if self_tuple != other_tuple:
+            return self_tuple < other_tuple
+
+        # Same version number, now compare extensions
+        return self._ext_sort_key() < other._ext_sort_key()
+
+    def _ext_sort_key(self) -> t.Tuple[int, str, int]:
+        """
+        Defines ordering for extensions.
+        Final versions (None) are considered greater than prereleases.
+
+        Example sort order:
+        1.0.0.dev1 < 1.0.0.rc1 < 1.0.0 < 1.0.1
+        """
+        if self.ext is None:
+            return (2, "", 0)  # Final release — highest priority
+
+        # Parse extension like "dev1" into prefix + number
+        match = re.match(r"([a-zA-Z]+)(\d*)", self.ext)
+        if match:
+            label, num = match.groups()
+            num_val = int(num) if num else 0
+            return (1, label, num_val)  # Pre-release
+        else:
+            return (0, self.ext, 0)  # Unknown extension format — lowest priority
+
 
 Version.UNKNOWN = Version(0, 0)
 
@@ -176,6 +232,23 @@ Version.UNKNOWN = Version(0, 0)
 class Package:
     """Information on any Taipy package and sub-package."""
 
+    # Base names of the sub packages taipy-*
+    # They also are the names of the directory where their code belongs, under the 'taipy' directory,
+    # in the root of the Taipy repository.
+    # Order is important: package that are dependent of others must appear first.
+    NAMES = ["common", "core", "gui", "rest", "templates"]
+
+    _packages = {}
+
+    def __new__(cls, name: str) -> "Package":
+        if name.startswith("taipy-"):
+            name = name[6:]
+        if name in cls._packages:
+            return cls._packages[name]
+        package = super().__new__(cls)
+        cls._packages[name] = package
+        return package
+
     def __init__(self, package: str) -> None:
         self._name = package
         if package == "taipy":
@@ -186,8 +259,20 @@ class Package:
             else:
                 self._name = f"taipy-{package}"
                 self._short = package
-            if self._short not in PACKAGES:
-                raise ValueError(f"Invalid package name {package}.")
+            if self._short not in Package.NAMES:
+                raise ValueError(f"Invalid package name '{package}'.")
+
+    @classmethod
+    def names(cls, add_taipy=False) -> list[str]:
+        return cls.NAMES + (["taipy"] if add_taipy else [])
+
+    @staticmethod
+    def check_argument(value: str) -> str:
+        """Checks package parameter in an argparse context."""
+        n_value = value.lower()
+        if n_value in Package.names(True) or value == "all":
+            return n_value
+        raise argparse.ArgumentTypeError(f"'{value}' is not a valid Taipy package name.")
 
     @property
     def name(self) -> str:
@@ -203,6 +288,21 @@ class Package:
     def package_dir(self) -> str:
         return "taipy" if self._name == "taipy" else os.path.join("taipy", self._short)
 
+    def load_version(self) -> Version:
+        """
+        Returns the Version defined in this package's version.json content.
+        """
+        with open(Path(self.package_dir) / "version.json") as version_file:
+            data = json.load(version_file)
+            return Version(**data)
+
+    def save_version(self, version: Version) -> None:
+        """
+        Saves the Version to this package's version.json file.
+        """
+        with open(os.path.join(Path(self.package_dir), "version.json"), "w") as version_file:
+            json.dump(version.to_dict(), version_file)
+
     def __str__(self) -> str:
         """Returns a string representation of this package."""
         return self.name
@@ -219,54 +319,71 @@ class Package:
 
 
 # --------------------------------------------------------------------------------------------------
-def retrieve_github_path() -> t.Optional[str]:
-    # Retrieve current Git branch remote URL
-    def run(*args) -> str:
-        return subprocess.run(args, stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
-
-    branch_name = run("git", "branch", "--show-current")
-    remote_name = run("git", "config", f"branch.{branch_name}.remote")
-    url = run("git", "remote", "get-url", remote_name)
-    if match := re.fullmatch(r"git@github.com:(.*)\.git", url):
-        return match[1]
-    if match := re.fullmatch(r"https://github.com/(.*)$", url):
-        return match[1]
-    print("ERROR - Could not retrieve GibHub branch path")  # noqa: T201
-    return None
+def run_command(*args) -> str:
+    return subprocess.run(args, stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
 
 
 # --------------------------------------------------------------------------------------------------
-def fetch_github_releases(gh_path: t.Optional[str] = None) -> dict[Package, list[Version]]:
+class Git:
+    @staticmethod
+    def get_current_branch() -> str:
+        return run_command("git", "branch", "--show-current")
+
+    @staticmethod
+    def get_github_path() -> t.Optional[str]:
+        """Retrieve current Git path (<owner>/<repo>)."""
+        branch_name = Git.get_current_branch()
+        remote_name = run_command("git", "config", f"branch.{branch_name}.remote")
+        url = run_command("git", "remote", "get-url", remote_name)
+        if match := re.fullmatch(r"(?:git@github\.com:|https://github\.com/)(.*)\.git", url):
+            return match[1]
+        print("ERROR - Could not retrieve GibHub branch path")  # noqa: T201
+        return None
+
+
+# --------------------------------------------------------------------------------------------------
+class Release(t.TypedDict):
+    version: Version
+    id: str
+    tag: str
+    published_at: str
+
+
+def fetch_github_releases(gh_path: t.Optional[str] = None) -> dict[Package, list[Release]]:
     # Retrieve all available releases (potentially paginating results) for all packages.
-    # Returns a dictionary of package_short_name-Value pairs.
-    # Note for reviewers: using a Package as the dictionary is cumbersome in the rest of the
-    # code.
-    all_releases: dict[str, list[Version]] = {}
+    # Returns a dictionary of package_short_name/list-of-releases pairs.
+    # A 'release' is a dictionary where "version" if the package version, "id" is the release id and
+    # "tag" is the release tag name.
+    headers = {"Accept": "application/vnd.github+json"}
+    all_releases: dict[str, list[Release]] = {}
     if gh_path is None:
-        gh_path = retrieve_github_path()
+        gh_path = Git.get_github_path()
         if gh_path is None:
             raise ValueError("Couldn't figure out GitHub branch path.")
     url = f"https://api.github.com/repos/{gh_path}/releases"
     page = 1
     # Read all release versions and store them in a package_name - list[Version] dictionary
     while url:
-        response = requests.get(url, params={"per_page": 50, "page": page})
+        response = requests.get(url, params={"per_page": 50, "page": page}, headers=headers)
         response.raise_for_status()  # Raise error for bad responses
         for release in response.json():
-            tag_name = release["tag_name"]
-            pkg_ver, pkg = tag_name.split("-") if "-" in tag_name else (tag_name, "taipy")
+            release_id = release["id"]
+            tag = release["tag_name"]
+            published_at = release["published_at"]
+            pkg_ver, pkg = tag.split("-") if "-" in tag else (tag, "taipy")
             # Drop legacy packages (config...)
-            if pkg != "taipy" and pkg not in PACKAGES:
+            if pkg != "taipy" and pkg not in Package.NAMES:
                 continue
 
             # Exception for legacy version: v1.0.0 -> 1.0.0
             if pkg_ver == "v1.0.0":
                 pkg_ver = pkg_ver[1:]
             version = Version.from_string(pkg_ver)
-            if versions := all_releases.get(pkg):
-                versions.append(version)
+            new_release: Release = {"version": version, "id": release_id, "tag": tag, "published_at": published_at}
+            if releases := all_releases.get(pkg):
+                releases.append(new_release)
             else:
-                all_releases[pkg] = [version]
+                all_releases[pkg] = [new_release]
 
         # Check for pagination in the `Link` header
         link_header = response.headers.get("Link", "")
@@ -276,19 +393,29 @@ def fetch_github_releases(gh_path: t.Optional[str] = None) -> dict[Package, list
         else:
             url = None  # No more pages
 
+    # Sort all releases for all packages by publishing date (most recent first)
+    for p in all_releases.keys():
+        all_releases[p].sort(
+            key=lambda r: datetime.fromisoformat(r["published_at"].replace("Z", "+00:00")), reverse=True
+        )
     # Build and return the dictionary using Package instances
     return {Package(p): v for p, v in all_releases.items()}
 
 
 # --------------------------------------------------------------------------------------------------
 def fetch_latest_github_taipy_releases(
-    all_releases: t.Optional[dict[Package, list[Version]]] = None, gh_path: t.Optional[str] = None
+    all_releases: t.Optional[dict[Package, list[Release]]] = None, gh_path: t.Optional[str] = None
 ) -> Version:
     # Retrieve all available releases if necessary
     if all_releases is None:
         all_releases = fetch_github_releases(gh_path)
     # Find the latest 'taipy' version that has no extension
     latest_taipy_version = Version.UNKNOWN
-    if versions := all_releases.get(Package("taipy")):
-        latest_taipy_version = max(filter(lambda v: v.ext is None, versions))
+    releases = all_releases.get(Package("taipy"))
+    if releases := all_releases.get(Package("taipy")):
+        # Retrieve all non-dev releases
+        versions = [release["version"] for release in releases if release["version"].ext is None]
+        # Find the latest
+        if versions:
+            latest_taipy_version = max(versions)
     return latest_taipy_version

+ 98 - 0
tools/release/delete_dev_releases.py

@@ -0,0 +1,98 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed 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.
+# --------------------------------------------------------------------------------------------------
+# Deletes dev releases and tags for a specific version from a GitHub repository.
+# --------------------------------------------------------------------------------------------------
+
+import argparse
+
+import requests
+from common import Git, Version, fetch_github_releases
+
+
+def main(arg_strings=None):
+    parser = argparse.ArgumentParser(
+        description="Deletes Taipy package dev releases and tags from GitHub.",
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    parser.add_argument(
+        "version",
+        action="store",
+        type=Version.check_argument,
+        help="""The version (M.m.p) of the releases to be deleted.
+The indicated version must not have extensions.""",
+    )
+
+    def _check_repository_path(value: str):
+        if len(value.split("/")) != 2:
+            raise argparse.ArgumentTypeError(f"'{value}' is not a valid '<owner>/<repo>' path.")
+        return value
+
+    parser.add_argument(
+        "-r",
+        "--repository_path",
+        type=_check_repository_path,
+        help="""The '<owner>/<repo>' string that identifies the repository where releases are fetched.
+The default is the current repository.""",
+    )
+    parser.add_argument(
+        "-y",
+        "--yes",
+        action="store_true",
+        help="""Do not ask for confirmation of the deletion of the releases and tags.""",
+    )
+    args = parser.parse_args(arg_strings)
+
+    headers = {"Accept": "application/vnd.github+json"}
+    repository_path = args.repository_path if args.repository_path else Git.get_github_path()
+    all_releases = fetch_github_releases(repository_path)
+    found = False
+    if all_releases:
+        for package, releases in all_releases.items():
+            for release in releases:
+                release_version = release["version"]
+                release_id = release["id"]
+                release_tag = release["tag"]
+                if release_version.validate_extension() and args.version.match(release_version):
+                    found = True
+                    confirm = True if args.yes else False
+                    if not args.yes:
+                        print(f"\n➡️ Release: package: {package.name}, version: {release_version}")  # noqa: T201
+                        confirm = (
+                            input("❓ Do you want to delete this release and its tag? (y/N): ").strip().lower() != "y"
+                        )
+                    if confirm:
+                        # Delete release
+                        url = f"https://api.github.com/repos/{repository_path}/releases/{release_id}"
+                        response = requests.delete(url, headers=headers)
+                        if response.status_code == 204:
+                            print(f"✅ Successfully deleted release {release_version} for package '{package.name}'.")  # noqa: T201
+                        else:
+                            print(  # noqa: T201
+                                f"❌ Failed to delete release {release_version} for package '{package.name}':"
+                                + f" {response.status_code} - {response.text}"
+                            )
+                        # Delete tag
+                        url = f"https://api.github.com/repos/{repository_path}/git/refs/tags/{release_tag}'"
+                        response = requests.delete(url, headers=headers)
+                        if response.status_code == 204:
+                            print(f"✅ Successfully deleted tag {release_tag}.")  # noqa: T201
+                        else:
+                            print(f"❌ Failed to delete tag {release_tag}: {response.status_code} - {response.text}")  # noqa: T201
+                    else:
+                        print("ℹ️ Skipped.")  # noqa: T201
+
+    if not found:
+        print(f"No dev releases found for version {args.version}.")  # noqa: T201
+
+
+if __name__ == "__main__":
+    main()

+ 0 - 148
tools/release/fetch_latest_versions.py

@@ -1,148 +0,0 @@
-# Copyright 2021-2024 Avaiga Private Limited
-#
-# Licensed 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.
-# --------------------------------------------------------------------------------------------------
-# Returns the latest released versions for every Taipy package that is compatible with the target
-# version (major and minor numbers match).
-# The target package's version is set to the target version.
-#
-# Invoked from the workflow in build-and-release-single-package.yml.
-#
-# Outputs a line for each package (including  'taipy"):
-#   <package_short_name>_VERSION=<package_version>
-# If a dev release is requested, a similar line is issued indicating the next dev version number:
-#   NEXT_<package_short_name>_VERSION=<package_version>
-# An additional line is added containing the latest release for 'taipy', no matter what the target
-# version is. This version is extracted so that it has no extension:
-#   LATEST_TAIPY_VERSION=<package_version>
-# --------------------------------------------------------------------------------------------------
-
-import sys
-
-import requests
-from common import PACKAGES, Package, Version, fetch_github_releases, fetch_latest_github_taipy_releases
-
-
-def usage() -> None:
-    print(f"Usage: {sys.argv[0]} <package> <version> <dev_version> <deps> [<gh_path>]")  # noqa: T201
-    print("   <package> must be a Taipy package name.")  # noqa: T201
-    print("   <version> is the target version for *package*. It must of the form: <Maj>.<Min>.<Tech>[.dev*].")  # noqa: T201
-    print("   <release_type> must be one of 'dev' or 'production'.")  # noqa: T201
-    print("   <deps> must be 'Pypi' or 'GitHub', indicating where to find Taipy package dependencies.")  # noqa: T201
-    print("   <gh_path>: The path of GitHub repository (owner/repo), used if <deps> is 'GitHub'.")  # noqa: T201
-
-
-def fetch_latest_github_releases(
-    package: Package, version: Version, dev: bool, all_releases: dict[Package, list[Version]]
-) -> dict[Package, Version]:
-    """Find the latest release version for each package, in the GitHub releases.
-
-    All release versions are retrieved from GitHub, and we keep the ones that have a version that
-    is compatible with *version*.
-    "dev" releases are kept only if *dev* is True.
-
-    Arguments:
-        package: The package that we want to force *version* for.
-        version: The incoming version of package *package*.
-        dev: True if we're targeting a dev release, False for a production release.
-        gh_path: The "OWNER/REPO" string at the beginning of the working repository.
-          If not provided, it is computed at runtime from the current Git branch remote URL.
-
-    Return:
-        A dictionary make of [package, version] pairs where the *package* package's version is set
-        to *version*.
-    """
-    # For each package, pick the latest that *version* is compatible with
-    all_package_names = PACKAGES + ["taipy"]
-    releases = {}
-    for pkg_name in all_package_names:
-        a_package = Package(pkg_name)
-        if versions := all_releases.get(a_package):
-            for a_version in versions:
-                if a_version.ext and (not dev or not a_version.validate_extension("dev")):
-                    continue
-                if version.is_compatible(a_version):
-                    releases[pkg_name] = a_version
-                    break
-
-    # Fill in missing versions
-    releases[package.short_name] = version
-    for p in all_package_names:
-        if p not in releases:
-            releases[p] = Version.UNKNOWN
-
-    return {Package(p): v for p, v in releases.items()}
-
-
-def fetch_latest_pypi_releases(package: Package, version: Version, dev: bool) -> dict[Package, Version]:
-    """Find the latest release version for each package, in the Pypi releases.
-
-    All release versions are retrieved from Pypi, and we keep the ones that have a version that
-    is compatible with *version*.
-    "dev" releases are kept only if *dev* is True.
-
-    Return:
-        A dictionary make of [package, version] pairs where the *package* package's version is set
-        to *version*.
-    """
-
-    def retrieve_package_version(sub_pkg: Package, dev: bool) -> Version:
-        """Returns the latest release version for *sub_pkg* on Pypi that is compatible with *version*."""
-        url = f"https://pypi.org/pypi/{sub_pkg.name}/json"
-        response = requests.get(url)
-        resp_json = response.json()
-        # All release versions for the <sub_pkg> package
-        versions = list(resp_json["releases"].keys())
-        if versions:
-            versions.reverse()  # More recent release is last
-            # Find first that <version> would be compatible with
-            for v in versions:
-                check_version = Version.from_string(v)
-                # Drop all version with extension if not dev
-                # Keep 'dev' extensions if dev
-                if check_version.ext and (not dev or not check_version.validate_extension("dev")):
-                    continue
-                if version.is_compatible(check_version):
-                    return check_version
-        return Version.UNKNOWN
-
-    releases = {pkg: retrieve_package_version(pkg, dev) for pkg in [Package(p) for p in PACKAGES]}
-    releases[package] = version
-    return releases
-
-
-if __name__ == "__main__":
-    if len(sys.argv) < 5:
-        usage()
-        raise ValueError("Missing arguments.")
-    package = Package(sys.argv[1])
-    version = Version.from_string(sys.argv[2])
-
-    is_dev_version = sys.argv[3] == "dev"
-    if is_dev_version and (version.ext is None or not version.validate_extension("dev")):
-        raise ValueError("Version extension does not contain 'dev'.")
-
-    pypi_deps = sys.argv[4] == "Pypi" or sys.argv[4] == "true"  # true to keep pre-4.0.3 compatibility
-    gh_path = sys.argv[5] if len(sys.argv) > 5 else None
-
-    # Retrieve all available Github releases for all packages
-    all_releases = fetch_github_releases(gh_path)
-    # Compute the latest versions compatible with *version*
-    versions = (
-        fetch_latest_pypi_releases(package, version, is_dev_version)
-        if pypi_deps
-        else fetch_latest_github_releases(package, version, is_dev_version, all_releases)
-    )
-    # Print them out
-    for p, v in versions.items():
-        print(f"{p.short_name}_VERSION={v}")  # noqa: T201
-
-    # Print out the latest 'taipy' version that has no extension
-    print(f"LATEST_TAIPY_VERSION={fetch_latest_github_taipy_releases(all_releases)}")  # noqa: T201

+ 0 - 101
tools/release/setup_version.py

@@ -1,101 +0,0 @@
-# Copyright 2021-2024 Avaiga Private Limited
-#
-# Licensed 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.
-# --------------------------------------------------------------------------------------------------
-# Checks that build version matches package version.
-# Updates version number for future 'dev' builds.
-#
-# Invoked from the workflow in build-and-release.yml.
-#
-# Outputs a line for each package (all packages if 'all'):
-#   <package_short_name>_VERSION=<package_version>
-# If a dev release is requested, a similar line is issued indicating the next dev version number:
-#   NEXT_<package_short_name>_VERSION=<package_version>
-# --------------------------------------------------------------------------------------------------
-
-import json
-import sys
-from pathlib import Path
-
-from common import PACKAGES, Package, Version, fetch_latest_github_taipy_releases
-
-
-def usage() -> None:
-    print(f"Usage: {sys.argv[0]} <package> <release_type> [<version> <branch>]")  # noqa: T201
-    print("   if <package> is 'ALL' then all packages including taipy are processed.")  # noqa: T201
-    print("   <release_type> must be 'dev' or 'production'")  # noqa: T201
-    print("     If <release_type> is 'production', <version> must be specified as the target")  # noqa: T201
-    print("     version and <branch> must be set to the name of the current branch.")  # noqa: T201
-
-
-def extract_version(package: Package) -> Version:
-    """
-    Returns a Version from a package's version.json content.
-    """
-    with open(Path(package.package_dir) / "version.json") as version_file:
-        data = json.load(version_file)
-        return Version(**data)
-
-
-def __setup_dev_version(package: Package, version: Version) -> None:
-    if not version.validate_extension("dev"):
-        raise ValueError(f"{version=} is not a 'dev' version in package {package}.")
-
-    var_name = f"{package.short_name}_VERSION"
-    print(f"{var_name}={version.full_name}")  # noqa: T201
-
-    # Increment dev version
-    version = version.bump_ext_version()
-    # Save in packages's version.json
-    with open(Path(package.package_dir) / "version.json", "w") as version_file:
-        json.dump(version.to_dict(), version_file)
-    # Return it to the GitHub step
-    print(f"NEXT_{var_name}={version.full_name}")  # noqa: T201
-
-
-def __setup_prod_version(
-    package: Package, version: Version, target_version: str, branch_name: str) -> None:
-    if version.full_name != target_version:
-        raise ValueError(f"Current {version=} does not match {target_version=} for package {package.name}")
-
-    # Production releases can only be performed from a release branch
-    if target_branch_name := f"release/{version.major}.{version.minor}" != branch_name:
-        raise ValueError(
-            f"Branch name mismatch branch={branch_name} does not match target branch name={target_branch_name}"
-        )
-
-    print(f"{package.short_name}_VERSION={version.name}")  # noqa: T201
-
-
-if __name__ == "__main__":
-    if len(sys.argv) < 3:
-        usage()
-        raise ValueError("Missing arguments.")
-
-    packages = [sys.argv[1]] if sys.argv[1].lower() != "all" else PACKAGES + ["taipy"]
-    release_type = sys.argv[2]
-
-    for package_name in packages:
-        package = Package(package_name)
-        version = extract_version(package)
-
-        if release_type == "dev":
-            __setup_dev_version(package, version)
-        elif release_type == "production":
-            if len(sys.argv) < 5:
-                usage()
-                raise ValueError("Missing arguments.")
-            __setup_prod_version(package, version, sys.argv[3], sys.argv[4])
-        else:
-            usage()
-            raise ValueError(f"Invalid <release_type> argument ({release_type}).")
-
-    # Print out the latest 'taipy' version that has no extension
-    print(f"LATEST_TAIPY_VERSION={fetch_latest_github_taipy_releases()}")  # noqa: T201

+ 192 - 0
tools/release/setup_versions.py

@@ -0,0 +1,192 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed 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.
+# --------------------------------------------------------------------------------------------------
+# Checks that build version matches package(s) version.
+#
+# Invoked from the workflow in build-and-release.yml.
+#
+# Outputs a line for each package (all packages if 'all'):
+#   <package_short_name>_VERSION=<release_version>
+#      the release version of the package that gets built.
+#      - if 'dev' release mode, that would be M.m.p.dev<x>
+#        where dev<x> is the first available available release version number that has no release yet.
+#      - if 'production' release mode, that would be M.m.p, as read in the packages's version.json
+#        file.
+# If a 'production' release mode is requested, a similar line is issued indicating the next patch
+# version number:
+#   NEXT_<package_short_name>_VERSION=<next_release_version>
+# --------------------------------------------------------------------------------------------------
+
+import argparse
+import os
+
+from common import Git, Package, Version, fetch_github_releases, fetch_latest_github_taipy_releases
+
+
+def __setup_dev_version(
+    package: Package, version: Version, released_versions: list[Version], target_version: dict[str, Version]
+) -> None:
+    # Find latest dev release for that version
+    ext_index = 0
+    latest_version = (
+        max([v for v in released_versions if v.matches(version) and v.validate_extension()])
+        if released_versions
+        else None
+    )
+    ext, ext_index = ("dev", 0)
+    if latest_version:
+        ext, ext_index = latest_version.split_ext()
+        ext_index += 1
+    target_version[package.short_name] = Version(version.major, version.minor, version.patch, f"{ext}{ext_index}")
+
+
+def __setup_prod_version(
+    package: Package,
+    version: Version,
+    branch_name: str,
+    target_versions: dict[str, Version],
+    next_versions: dict[str, Version],
+) -> None:
+    # Production releases can only be performed from a release branch
+    if (os.environ.get("GITHUB_ACTIONS") == "true") and (
+        target_branch_name := f"release/{version.major}.{version.minor}"
+    ) != branch_name:
+        raise ValueError(f"Current branch '{branch_name}' does not match expected '{target_branch_name}'")
+    target_versions[package.short_name] = version
+    # Compute next patch version
+    next_versions[package.short_name] = Version(version.major, version.minor, version.patch + 1)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Computes the Taipy package versions to be build.", formatter_class=argparse.RawTextHelpFormatter
+    )
+
+    # <package> argument
+    def _check_package(value: str) -> str:
+        n_value = value.lower()
+        if n_value in Package.names(True) or value == "all":
+            return n_value
+        raise argparse.ArgumentTypeError(f"'{value}' is not a valid Taipy package name.")
+
+    parser.add_argument(
+        "package",
+        type=_check_package,
+        action="store",
+        help="""The name of the package to setup the build version for.
+This should be the short name of a Taipy package (common, core...) or 'taipy'.
+If can also be set to 'ALL' then all versions for all packages are computed.
+""",
+    )
+
+    # <version> argument
+    parser.add_argument(
+        "-v",
+        "--version",
+        type=Version.check_argument,
+        required=True,
+        help="""Full name of the target version (M.m.p).
+This version must match the one in the package's 'version.json' file.
+""",
+    )
+    # <release_type> argument
+    parser.add_argument(
+        "-t",
+        "--release_type",
+        choices=["dev", "production"],
+        default="dev",
+        type=str.lower,
+        help="""Type of release to build (default: dev).
+
+If 'dev', the release version is computed from the existing released packages versions
+in the repository:
+- If there is no release with version <version>, the release will have the version set
+  to <version>.dev0.
+- If there is a <version>.dev<n> release, the release will have the version <version>.dev<n+1>.
+- If there is a <version> release (with no 'dev' part), the script fails.
+
+If 'production', the package version is computed from for existing released packages versions
+""",
+    )
+
+    # <repository_name> argument
+    def _check_repository_name(value: str) -> str:
+        if len(value.split("/")) != 2:
+            raise argparse.ArgumentTypeError(f"'{value}' is not a valid '<owner>/<repo>' pair.")
+        return value
+
+    parser.add_argument(
+        "-r",
+        "--repository_name",
+        type=_check_repository_name,
+        help="""The '<owner>/<repo>' string that identifies the repository where releases are fetched.
+The default is the current repository.""",
+    )
+    # <branch_name> argument
+    parser.add_argument(
+        "-b",
+        "--branch_name",
+        help="""The name of the branch to check package versions from."
+If <release_type> is 'production', this branch has to be a release branch ('release/*').
+This value is extracted from the current branch by default.
+        """,
+    )
+    args = parser.parse_args()
+
+    all_releases = fetch_github_releases(args.repository_name)
+    target_versions = {}
+    next_versions = {}
+    for package_name in Package.names(True):
+        package_releases = all_releases.get(Package(package_name))
+        released_versions = [release["version"] for release in package_releases] if package_releases else []
+        if args.release_type == "production":
+            released_versions = list(filter(lambda v: v.ext is None, released_versions))
+        else:
+            released_versions = list(filter(lambda v: v.ext is not None, released_versions))
+        # Matching versions
+        released_versions = [v for v in released_versions if v.matches(args.version, Version.MINOR)]
+        target_version = max(released_versions) if released_versions else None
+        target_versions[package_name] = target_version if target_version else Version.UNKNOWN
+
+    packages: list[str] = [args.package] if args.package != "all" else Package.names(True)
+    branch_name = args.branch_name if args.branch_name else Git.get_current_branch()
+
+    for package_name in packages:
+        package = Package(package_name)
+        version = package.load_version()
+        if version.ext:
+            raise ValueError(f"Package version for '{package.name}' has an extension ({version.full_name}).")
+        if version != args.version:
+            raise ValueError(
+                f"Target version ({args.version.full_name}) does not match version"
+                + f" {version.full_name} in package {package.name}."
+            )
+        package_releases = all_releases.get(package)
+        released_versions = [release["version"] for release in package_releases] if package_releases else []
+        if version in released_versions:
+            raise ValueError(f"{version} is already released for package {package.name}.")
+
+        if args.release_type == "dev":
+            __setup_dev_version(package, version, released_versions, target_versions)
+        else:
+            __setup_prod_version(package, version, branch_name, target_versions, next_versions)
+
+    for p, v in target_versions.items():
+        print(f"{p}_VERSION={v}")  # noqa: T201
+    if next_versions:
+        for p, v in next_versions.items():
+            print(f"NEXT_{p}_VERSION={v}")  # noqa: T201
+    # Print out the latest 'taipy' version that has no extension
+    print(f"LATEST_TAIPY_VERSION={fetch_latest_github_taipy_releases(all_releases)}")  # noqa: T201
+
+
+if __name__ == "__main__":
+    main()

+ 94 - 38
tools/release/update_setup_requirements.py

@@ -1,4 +1,4 @@
-# Copyright 2021-2024 Avaiga Private Limited
+# Copyright 2021-2025 Avaiga Private Limited
 #
 # Licensed 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
@@ -15,30 +15,16 @@
 # Working directory must be [root_dir].
 # --------------------------------------------------------------------------------------------------
 
+import argparse
 import os
 import re
-import sys
 import typing as t
 
-from common import PACKAGES, Version, retrieve_github_path
+from common import Git, Package, Version
 
 BASE_PATH = "./tools/packages"
 
 
-def usage() -> None:
-    packages = "> <".join(f"{p}_ver" for p in PACKAGES)
-    print(  # noqa: T201
-        f"Usage: {sys.argv[0]} <package> <{packages}> <deps> [<gh_path>]"
-    )
-    packages = ", ".join(f"'{p}'" for p in PACKAGES[:-1])
-    packages = f"{packages}, or '{PACKAGES[-1]}'"
-    print(f"   <package> must be one of {packages}.")  # noqa: T201
-    for p in PACKAGES:
-        print(f"   <{p}_ver>: minimal version of the taipy-{p} dependency.")  # noqa: T201
-    print("   <deps> must be 'Pypi' or 'GitHub', indicating where to find Taipy package dependencies.")  # noqa: T201
-    print("   <gh_path>: The path of GitHub repository (owner/repo), used if <deps> is 'GitHub'.")  # noqa: T201
-
-
 def __build_taipy_package_line(line: str, version: Version, use_pypi: bool, gh_path: t.Optional[str]) -> str:
     line = line.strip()
     if use_pypi:
@@ -50,9 +36,9 @@ def __build_taipy_package_line(line: str, version: Version, use_pypi: bool, gh_p
 
 
 def update_setup_requirements(
-    package: str, versions: dict[str, Version], publish_on_py_pi: bool, gh_path: t.Optional[str]
+    package: Package, versions: dict[str, Version], publish_on_py_pi: bool, gh_path: t.Optional[str]
 ) -> None:
-    path = os.path.join(BASE_PATH, "taipy" if package == "taipy" else f"taipy-{package}", "setup.requirements.txt")
+    path = os.path.join(BASE_PATH, package.name, "setup.requirements.txt")
     lines = []
     with open(path, mode="r") as req:
         for line in req:
@@ -73,23 +59,93 @@ def update_setup_requirements(
     print("-" * 32)  # noqa: T201
 
 
+def main():
+    parser = argparse.ArgumentParser(
+        description="Computes the Taipy package versions to be build.", formatter_class=argparse.RawTextHelpFormatter
+    )
+
+    # <package> argument
+    parser.add_argument(
+        "package",
+        type=Package,
+        action="store",
+        help="""The name of the package to setup the build version for.
+This must be the short name of a Taipy package (common, core...) or 'taipy'.
+""",
+    )
+
+    # <common-version> argument
+    parser.add_argument(
+        "common_version",
+        type=Version.check_argument,
+        action="store",
+        help="Full name of the target version (M.m.p) for the taipy-common package.",
+    )
+    # <core-version> argument
+    parser.add_argument(
+        "core_version",
+        type=Version.check_argument,
+        action="store",
+        help="Full name of the target version (M.m.p) for the taipy-core package.",
+    )
+    # <gui-version> argument
+    parser.add_argument(
+        "gui_version",
+        type=Version.check_argument,
+        action="store",
+        help="Full name of the target version (M.m.p) for the taipy-gui package.",
+    )
+    # <rest-version> argument
+    parser.add_argument(
+        "rest_version",
+        type=Version.check_argument,
+        action="store",
+        help="Full name of the target version (M.m.p) for the taipy-rest package.",
+    )
+    # <rest-version> argument
+    parser.add_argument(
+        "templates_version",
+        type=Version.check_argument,
+        action="store",
+        help="Full name of the target version (M.m.p) for the taipy-templates package.",
+    )
+    # <dependencies-location> argument
+    parser.add_argument(
+        "-deps",
+        "-dl",
+        "--dependencies-location",
+        type=str.lower,
+        choices=["github", "pypi"],
+        required=True,
+        help="Where to point dependencies to.",
+    )
+
+    # <repository_name> argument
+    def _check_repository_name(value: str) -> str:
+        if len(value.split("/")) != 2:
+            raise argparse.ArgumentTypeError(f"'{value}' is not a valid '<owner>/<repo>' pair.")
+        return value
+
+    parser.add_argument(
+        "-r",
+        "--repository_name",
+        type=_check_repository_name,
+        help="""The '<owner>/<repo>' string that identifies the repository where releases are fetched.
+The default is the current repository.""",
+    )
+
+    args = parser.parse_args()
+    versions = {
+        "taipy-common": args.common_version,
+        "taipy-core": args.core_version,
+        "taipy-gui": args.gui_version,
+        "taipy-rest": args.rest_version,
+        "taipy-templates": args.templates_version,
+    }
+    publish_on_py_pi = args.dependencies_location == "pypi"
+    repository_name = args.repository_name if args.repository_name else Git.get_github_path()
+    update_setup_requirements(args.package, versions, publish_on_py_pi, repository_name)
+
+
 if __name__ == "__main__":
-    if len(sys.argv) < len(PACKAGES) + 3:
-        usage()
-        raise ValueError("Missing arguments.")
-    package = sys.argv[1]
-    # Store the provided version for each package listed in PACKAGES
-    versions = {f"taipy-{p}": Version.from_string(sys.argv[i]) for i, p in enumerate(PACKAGES, 2)}
-    # Keep compatibility with legacy actions ('true' is equivalent to 'Pypi')
-    pypi_deps = sys.argv[len(PACKAGES) + 2].lower() in ["true", "pypi"]
-    gh_path = None
-    if not pypi_deps:
-        if len(sys.argv) < len(PACKAGES) + 4:
-            gh_path = retrieve_github_path()
-            if gh_path is None:
-                usage()
-                raise ValueError("Couldn't figure out GitHub branch path.")
-        else:
-            gh_path = sys.argv[len(PACKAGES) + 3]
-
-    update_setup_requirements(package, versions, pypi_deps, gh_path)
+    main()