This repository was archived by the owner on Mar 10, 2026. It is now read-only.
Chore: bump version 0.4.2 → 0.5.0 #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This file is part of the jebel-quant/rhiza repository | |
| # (https://github.com/jebel-quant/rhiza). | |
| # | |
| # Release Workflow for Python Packages with Optional Devcontainer Publishing | |
| # | |
| # This workflow implements a secure, maintainable release pipeline with distinct phases: | |
| # | |
| # 📋 Pipeline Phases: | |
| # 1. 🔍 Validate Tag - Check tag format and ensure release doesn't already exist | |
| # 2. 🏗️ Build - Build Python package with Hatch (if [build-system] is defined in pyproject.toml) | |
| # 3. 📦 Generate SBOM - Create Software Bill of Materials (CycloneDX format) | |
| # 4. 📝 Draft Release - Create draft GitHub release with build artifacts and SBOM | |
| # 5. 🚀 Publish to PyPI - Publish package using OIDC or custom feed | |
| # 6. 🐳 Publish Devcontainer - Build and publish devcontainer image (conditional) | |
| # 7. ✅ Finalize Release - Publish the GitHub release with links | |
| # | |
| # 📦 SBOM Generation: | |
| # - Generated using CycloneDX format (industry standard for software supply chain security) | |
| # - Creates both JSON and XML formats for maximum compatibility | |
| # - SBOM attestations are created and stored (public repos only) | |
| # - Attached to GitHub releases for transparency and compliance | |
| # - Skipped if pyproject.toml doesn't exist | |
| # | |
| # 🐳 Devcontainer Publishing: | |
| # - Only occurs when PUBLISH_DEVCONTAINER repository variable is set to "true" | |
| # - Requires .devcontainer directory to exist | |
| # - Uses the release tag (e.g., v1.2.3) as the image tag | |
| # - Image name: {registry}/{owner}/{repository}/devcontainer | |
| # - Registry defaults to ghcr.io (override with DEVCONTAINER_REGISTRY variable) | |
| # - Config file: Always .devcontainer/devcontainer.json | |
| # - Adds devcontainer image link to GitHub release notes | |
| # | |
| # 🚀 PyPI Publishing: | |
| # - Skipped if no dist/ artifacts exist | |
| # - Skipped if pyproject.toml contains "Private :: Do Not Upload" | |
| # - Uses Trusted Publishing (OIDC) for PyPI (no stored credentials) | |
| # - For custom feeds, use PYPI_REPOSITORY_URL and PYPI_TOKEN secrets | |
| # - Adds PyPI/custom feed link to GitHub release notes | |
| # | |
| # 🔐 Security: | |
| # - No PyPI credentials stored; relies on Trusted Publishing via GitHub OIDC | |
| # - For custom feeds, PYPI_TOKEN secret is used with default username __token__ | |
| # - Container registry uses GITHUB_TOKEN for authentication | |
| # - SLSA provenance attestations generated for build artifacts (public repos only) | |
| # - SBOM attestations generated for supply chain transparency (public repos only) | |
| # | |
| # 📄 Requirements: | |
| # - pyproject.toml with top-level version field (for Python packages) | |
| # - Package registered on PyPI as Trusted Publisher (for PyPI publishing) | |
| # - PUBLISH_DEVCONTAINER variable set to "true" (for devcontainer publishing) | |
| # - .devcontainer/devcontainer.json file (for devcontainer publishing) | |
| # | |
| # ✅ To Trigger: | |
| # Create and push a version tag: | |
| # git tag v1.2.3 | |
| # git push origin v1.2.3 | |
| # | |
| # 🎯 For repos without Python packages: | |
| # The workflow gracefully skips Python-related steps if pyproject.toml doesn't exist. | |
| # | |
| # 🎯 For repos without devcontainers: | |
| # The workflow gracefully skips devcontainer steps if PUBLISH_DEVCONTAINER is not | |
| # set to "true" or .devcontainer directory doesn't exist. | |
| name: (RHIZA) RELEASE | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write # Needed to create releases | |
| id-token: write # Needed for OIDC authentication with PyPI | |
| packages: write # Needed to publish devcontainer image | |
| attestations: write # Needed for SLSA provenance attestations (public repos only) | |
| jobs: | |
| tag: | |
| name: Validate Tag | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.set_tag.outputs.tag }} | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set Tag Variable | |
| id: set_tag | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Validate Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.set_tag.outputs.tag }} | |
| run: | | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| DRAFT_STATUS=$(gh release view "$TAG" --json isDraft --jq '.isDraft') | |
| if [ "$DRAFT_STATUS" != "true" ]; then | |
| echo "::error::Release '$TAG' already exists and is not a draft. Please use a new tag." | |
| exit 1 | |
| else | |
| echo "::warning::Release '$TAG' exists as a draft. Will proceed to update it." | |
| fi | |
| fi | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| needs: tag | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7.3.1 | |
| with: | |
| version: "0.10.8" | |
| - name: Verify version matches tag | |
| if: hashFiles('pyproject.toml') != '' | |
| run: | | |
| TAG_VERSION="${{ needs.tag.outputs.tag }}" | |
| TAG_VERSION=${TAG_VERSION#v} | |
| PROJECT_VERSION=$(uv version --short) | |
| # Normalize tag version to PEP 440 format for comparison. | |
| # Tags use semver format (e.g., 0.11.1-beta.1) while uv version --short | |
| # returns PEP 440 normalized format (e.g., 0.11.1b1). | |
| NORMALIZED_TAG=$(uv run --with packaging --no-project python3 -c "from packaging.version import Version; print(Version('$TAG_VERSION'))") | |
| if [[ "$PROJECT_VERSION" != "$NORMALIZED_TAG" ]]; then | |
| echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$NORMALIZED_TAG' (from tag '$TAG_VERSION')" | |
| exit 1 | |
| fi | |
| echo "Version verified: $PROJECT_VERSION matches tag (normalized: $NORMALIZED_TAG)" | |
| - name: Detect buildable Python package | |
| id: buildable | |
| run: | | |
| if [[ -f pyproject.toml ]] && grep -q '^\[build-system\]' pyproject.toml; then | |
| echo "buildable=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "buildable=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build | |
| if: steps.buildable.outputs.buildable == 'true' | |
| run: | | |
| printf "[INFO] Building package...\n" | |
| uv build | |
| - name: Install Python for SBOM generation | |
| if: hashFiles('pyproject.toml') != '' | |
| uses: actions/setup-python@v6.2.0 | |
| with: | |
| python-version-file: .python-version | |
| - name: Sync environment for SBOM generation | |
| if: hashFiles('pyproject.toml') != '' | |
| run: | | |
| export UV_EXTRA_INDEX_URL="${{ secrets.UV_EXTRA_INDEX_URL }}" | |
| uv sync --all-extras --all-groups --frozen | |
| - name: Generate SBOM (CycloneDX) | |
| if: hashFiles('pyproject.toml') != '' | |
| run: | | |
| printf "[INFO] Generating SBOM in CycloneDX format...\n" | |
| # Note: uvx caches the tool environment, so the second call is fast | |
| uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of JSON -o sbom.cdx.json | |
| uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of XML -o sbom.cdx.xml | |
| printf "[INFO] SBOM generation complete\n" | |
| printf "Generated files:\n" | |
| ls -lh sbom.cdx.* | |
| - name: Attest SBOM | |
| # Attest only the JSON format as it's the canonical machine-readable format. | |
| # The XML format is provided for compatibility but doesn't need separate attestation. | |
| if: hashFiles('pyproject.toml') != '' && github.event.repository.private == false | |
| uses: actions/attest-sbom@v4 | |
| with: | |
| subject-path: sbom.cdx.json | |
| sbom-path: sbom.cdx.json | |
| - name: Upload SBOM artifacts | |
| if: hashFiles('pyproject.toml') != '' | |
| uses: actions/upload-artifact@v7.0.0 | |
| with: | |
| name: sbom | |
| path: | | |
| sbom.cdx.json | |
| sbom.cdx.xml | |
| - name: Generate SLSA provenance attestations | |
| if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: dist/* | |
| - name: Upload dist artifact | |
| if: steps.buildable.outputs.buildable == 'true' | |
| uses: actions/upload-artifact@v7.0.0 | |
| with: | |
| name: dist | |
| path: dist | |
| # Don't try to upload artifacts to GitHub Release at all | |
| draft-release: | |
| name: Draft GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [tag, build] | |
| steps: | |
| - name: Download SBOM artifact | |
| # Downloads sbom.cdx.json and sbom.cdx.xml into sbom/ directory | |
| uses: actions/download-artifact@v8.0.0 | |
| with: | |
| name: sbom | |
| path: sbom | |
| continue-on-error: true | |
| - name: Create GitHub Release with artifacts | |
| uses: softprops/action-gh-release@v2.5.0 | |
| with: | |
| tag_name: ${{ needs.tag.outputs.tag }} | |
| name: ${{ needs.tag.outputs.tag }} | |
| generate_release_notes: true | |
| draft: true | |
| files: | | |
| sbom/* | |
| # Decide at step-level whether to publish | |
| pypi: | |
| name: Publish to PyPI | |
| runs-on: ubuntu-latest | |
| environment: release | |
| needs: [tag, build, draft-release] | |
| outputs: | |
| should_publish: ${{ steps.check_dist.outputs.should_publish }} | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download dist artifact | |
| uses: actions/download-artifact@v8.0.0 | |
| with: | |
| name: dist | |
| path: dist | |
| # Continue if dist folder is not found. Don't fail the job. | |
| continue-on-error: true | |
| - name: Check if dist contains artifacts and not marked as private | |
| id: check_dist | |
| run: | | |
| if [[ ! -d dist ]]; then | |
| echo "::warning::No folder dist/. Skipping PyPI publish." | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| else | |
| if grep -R "Private :: Do Not Upload" pyproject.toml; then | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| cat "$GITHUB_OUTPUT" | |
| # this should not take place, as "Private :: Do Not Upload" set in pyproject.toml | |
| # repository-url and password only used for custom feeds, not for PyPI with OIDC | |
| - name: Publish to PyPI | |
| if: ${{ steps.check_dist.outputs.should_publish == 'true' }} | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist/ | |
| skip-existing: true | |
| repository-url: ${{ vars.PYPI_REPOSITORY_URL }} | |
| password: ${{ secrets.PYPI_TOKEN }} | |
| finalise-release: | |
| name: Finalise Release | |
| runs-on: ubuntu-latest | |
| needs: [tag, pypi] | |
| if: needs.pypi.result == 'success' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7.3.1 | |
| with: | |
| version: "0.10.8" | |
| - name: "Sync the virtual environment for ${{ github.repository }}" | |
| shell: bash | |
| run: | | |
| export UV_EXTRA_INDEX_URL="${{ secrets.uv_extra_index_url }}" | |
| # will just use .python-version? | |
| uv sync --all-extras --all-groups --frozen | |
| - name: Set up Python | |
| uses: actions/setup-python@v6.2.0 | |
| - name: Generate PyPI Link | |
| id: pypi_link | |
| if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success' | |
| run: | | |
| PACKAGE_NAME=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") | |
| VERSION="${{ needs.tag.outputs.tag }}" | |
| VERSION=${VERSION#v} | |
| REPO_URL="${{ vars.PYPI_REPOSITORY_URL || '' }}" | |
| if [ -z "$REPO_URL" ]; then | |
| LINK="https://pypi.org/project/$PACKAGE_NAME/$VERSION/" | |
| NAME="PyPI Package" | |
| else | |
| LINK="$REPO_URL" | |
| NAME="Custom Feed Package" | |
| fi | |
| { | |
| echo "message<<EOF" | |
| echo "### $NAME" | |
| echo "" | |
| echo "[$PACKAGE_NAME]($LINK)" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Publish Release | |
| uses: softprops/action-gh-release@v2.5.0 | |
| with: | |
| tag_name: ${{ needs.tag.outputs.tag }} | |
| draft: false | |
| append_body: true | |
| body: | | |
| ${{ steps.pypi_link.outputs.message }} | |