Release 0.1.2 #8
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
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| verify-tag: | |
| name: Validate tag and version bump | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.extract-version.outputs.version }} | |
| steps: | |
| - name: Checkout tags | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Validate tag format | |
| shell: bash | |
| run: | | |
| if [[ ! "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Tag ${GITHUB_REF_NAME} does not match required format vMAJOR.MINOR.PATCH" | |
| exit 1 | |
| fi | |
| - name: Extract version from tag | |
| id: extract-version | |
| shell: bash | |
| run: | | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved release version: ${VERSION}" | |
| - name: Verify Cargo.toml versions match tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| for MANIFEST in crates/openpit/Cargo.toml crates/pit-ffi/Cargo.toml bindings/python/Cargo.toml; do | |
| CARGO_VERSION="$(grep -m1 '^version\s*=' "${MANIFEST}" | sed 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/')" | |
| if [[ "${CARGO_VERSION}" != "${VERSION}" ]]; then | |
| echo "::error::Tag version ${VERSION} does not match ${MANIFEST} version ${CARGO_VERSION}" | |
| exit 1 | |
| fi | |
| echo "${MANIFEST}: ${CARGO_VERSION} OK" | |
| done | |
| - name: Validate monotonic version bump | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| CURRENT_TAG="${GITHUB_REF_NAME}" | |
| CURRENT_VERSION="${CURRENT_TAG#v}" | |
| PREVIOUS_TAG="$( | |
| git tag -l 'v*' \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | grep -vx "${CURRENT_TAG}" \ | |
| | sort -V \ | |
| | tail -n 1 || true | |
| )" | |
| if [[ -z "${PREVIOUS_TAG}" ]]; then | |
| echo "No previous release tag found, skipping monotonic check" | |
| exit 0 | |
| fi | |
| PREVIOUS_VERSION="${PREVIOUS_TAG#v}" | |
| IFS='.' read -r CUR_MAJOR CUR_MINOR CUR_PATCH <<< "${CURRENT_VERSION}" | |
| IFS='.' read -r PREV_MAJOR PREV_MINOR PREV_PATCH <<< "${PREVIOUS_VERSION}" | |
| if (( CUR_MAJOR < PREV_MAJOR )); then | |
| echo "::error::Major version decreased: ${CURRENT_VERSION} < ${PREVIOUS_VERSION}" | |
| exit 1 | |
| fi | |
| if (( CUR_MAJOR > PREV_MAJOR )); then | |
| if (( CUR_MINOR != 0 || CUR_PATCH != 0 )); then | |
| echo "::error::Major version bump requires minor and patch to be 0 (got ${CURRENT_VERSION}, previous ${PREVIOUS_VERSION})" | |
| exit 1 | |
| fi | |
| echo "Valid major version bump: ${PREVIOUS_VERSION} -> ${CURRENT_VERSION}" | |
| exit 0 | |
| fi | |
| # major equal | |
| if (( CUR_MINOR < PREV_MINOR )); then | |
| echo "::error::Minor version decreased: ${CURRENT_VERSION} < ${PREVIOUS_VERSION}" | |
| exit 1 | |
| fi | |
| if (( CUR_MINOR > PREV_MINOR )); then | |
| if (( CUR_PATCH != 0 )); then | |
| echo "::error::Minor version bump requires patch to be 0 (got ${CURRENT_VERSION}, previous ${PREVIOUS_VERSION})" | |
| exit 1 | |
| fi | |
| echo "Valid minor version bump: ${PREVIOUS_VERSION} -> ${CURRENT_VERSION}" | |
| exit 0 | |
| fi | |
| # major and minor equal | |
| if (( CUR_PATCH <= PREV_PATCH )); then | |
| echo "::error::Patch version must increase: ${CURRENT_VERSION} <= ${PREVIOUS_VERSION}" | |
| exit 1 | |
| fi | |
| echo "Valid patch version bump: ${PREVIOUS_VERSION} -> ${CURRENT_VERSION}" | |
| verify: | |
| name: Verify (Rust + Python) | |
| needs: verify-tag | |
| uses: ./.github/workflows/verify.yml | |
| publish-rust: | |
| name: Publish Rust crate | |
| needs: | |
| - verify | |
| - verify-tag | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Ensure crates.io version is not published | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.verify-tag.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${{ secrets.CRATES_IO_API_TOKEN }}" ]]; then | |
| echo "Skipping crates.io version check as run in dry-run mode." | |
| exit 0 | |
| fi | |
| URL="https://crates.io/api/v1/crates/openpit/${VERSION}" | |
| HTTP_CODE="$(curl -H "User-Agent: openpit-release-ci" --retry 3 --retry-delay 2 -sS -o /tmp/openpit-crates-version.json -w '%{http_code}' "${URL}")" | |
| case "${HTTP_CODE}" in | |
| 200) | |
| echo "::error::openpit version ${VERSION} already exists on crates.io" | |
| exit 1 | |
| ;; | |
| 404) | |
| echo "Version ${VERSION} is not published on crates.io yet" | |
| ;; | |
| *) | |
| echo "::error::Failed to query crates.io (HTTP ${HTTP_CODE})" | |
| cat /tmp/openpit-crates-version.json || true | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Publish crate | |
| shell: bash | |
| env: | |
| CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${CRATES_IO_TOKEN}" ]]; then | |
| cargo publish -p openpit --locked --token "${CRATES_IO_TOKEN}" | |
| else | |
| echo "CRATES_IO_API_TOKEN is not configured, skipping publish" | |
| fi | |
| publish-wheels: | |
| name: Publish wheels (${{ matrix.name }}) | |
| needs: | |
| - publish-rust | |
| - verify-tag | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: linux-x86_64-manylinux2014 | |
| os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| manylinux: manylinux2014 | |
| - name: linux-aarch64-manylinux2014 | |
| os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| manylinux: manylinux2014 | |
| - name: linux-x86_64-musllinux_1_2 | |
| os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| manylinux: musllinux_1_2 | |
| - name: windows-x86_64 | |
| os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| - name: macos-x86_64 | |
| os: macos-13 | |
| target: x86_64-apple-darwin | |
| - name: macos-aarch64 | |
| os: macos-14 | |
| target: aarch64-apple-darwin | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install Python test dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -r requirements.txt | |
| python -m pip install twine | |
| - name: Build Linux wheel | |
| if: startsWith(matrix.os, 'ubuntu') | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| working-directory: bindings/python | |
| command: build | |
| target: ${{ matrix.target }} | |
| manylinux: ${{ matrix.manylinux }} | |
| container: off | |
| args: --release --strip --locked --zig -i python3.11 --out dist | |
| - name: Build macOS/Windows wheel | |
| if: ${{ !startsWith(matrix.os, 'ubuntu') }} | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| working-directory: bindings/python | |
| command: build | |
| target: ${{ matrix.target }} | |
| args: --release --strip --locked -i python --out dist | |
| - name: Resolve built wheel path | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.verify-tag.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| CANDIDATE_DIRS=( | |
| "bindings/python/dist" | |
| "dist" | |
| "bindings/python/target/wheels" | |
| "target/wheels" | |
| ) | |
| WHEELS=() | |
| for DIR in "${CANDIDATE_DIRS[@]}"; do | |
| if [[ -d "${DIR}" ]]; then | |
| while IFS= read -r FILE; do | |
| WHEELS+=("${FILE}") | |
| done < <(find "${DIR}" -maxdepth 1 -type f -name "openpit-*.whl" | sort) | |
| fi | |
| done | |
| if [[ ${#WHEELS[@]} -eq 0 ]]; then | |
| echo "::error::No wheel files found in expected directories" | |
| exit 1 | |
| fi | |
| VERSION_MATCH=() | |
| for FILE in "${WHEELS[@]}"; do | |
| BASE="$(basename "${FILE}")" | |
| if [[ "${BASE}" == openpit-"${VERSION}"*".whl" ]]; then | |
| VERSION_MATCH+=("${FILE}") | |
| fi | |
| done | |
| if [[ ${#VERSION_MATCH[@]} -eq 1 ]]; then | |
| WHEEL="${VERSION_MATCH[0]}" | |
| elif [[ ${#VERSION_MATCH[@]} -gt 1 ]]; then | |
| echo "::error::Expected exactly one wheel for version ${VERSION}, found ${#VERSION_MATCH[@]}" | |
| printf '%s\n' "${VERSION_MATCH[@]}" | |
| exit 1 | |
| elif [[ ${#WHEELS[@]} -eq 1 ]]; then | |
| WHEEL="${WHEELS[0]}" | |
| else | |
| echo "::error::Could not uniquely resolve wheel path" | |
| printf '%s\n' "${WHEELS[@]}" | |
| exit 1 | |
| fi | |
| echo "WHEEL_PATH=${WHEEL}" >> "$GITHUB_ENV" | |
| - name: Install built wheel | |
| if: matrix.name == 'linux-x86_64-manylinux2014' || !startsWith(matrix.os, 'ubuntu') | |
| run: python -m pip install --force-reinstall --no-deps "${{ env.WHEEL_PATH }}" | |
| - name: Verify wheel import and version | |
| if: matrix.name == 'linux-x86_64-manylinux2014' || !startsWith(matrix.os, 'ubuntu') | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.verify-tag.outputs.version }} | |
| run: | | |
| python -c "import openpit" | |
| python -c "import importlib.metadata as m; v=m.version('openpit'); print(v); assert v=='${VERSION}', f'expected {VERSION}, got {v}'" | |
| - name: Run wheel tests | |
| if: matrix.name == 'linux-x86_64-manylinux2014' || !startsWith(matrix.os, 'ubuntu') | |
| run: python -m pytest bindings/python/tests | |
| - name: Publish wheel | |
| shell: bash | |
| env: | |
| PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${PYPI_TOKEN}" ]]; then | |
| TWINE_USERNAME=__token__ TWINE_PASSWORD="${PYPI_TOKEN}" \ | |
| twine upload --skip-existing --non-interactive "${WHEEL_PATH}" | |
| else | |
| echo "PYPI_API_TOKEN is not configured, running twine check only" | |
| twine check "${WHEEL_PATH}" | |
| fi | |
| publish-sdist: | |
| name: Publish source distribution | |
| needs: | |
| - publish-wheels | |
| - verify-tag | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install requirements | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -r requirements.txt | |
| python -m pip install twine | |
| - name: Build sdist | |
| run: maturin sdist --manifest-path bindings/python/Cargo.toml --out dist | |
| - name: Publish sdist | |
| shell: bash | |
| env: | |
| PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${PYPI_TOKEN}" ]]; then | |
| TWINE_USERNAME=__token__ TWINE_PASSWORD="${PYPI_TOKEN}" \ | |
| twine upload --skip-existing --non-interactive dist/* | |
| else | |
| echo "PYPI_API_TOKEN is not configured, running twine check only" | |
| twine check dist/* | |
| fi | |
| publish-github-release: | |
| name: Publish GitHub release metadata | |
| needs: | |
| - publish-sdist | |
| - verify-tag | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Create GitHub Release without assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BODY_FILE="$(mktemp)" | |
| cat <<EOF > "${BODY_FILE}" | |
| Install from package registries: | |
| - crates.io: https://crates.io/crates/openpit | |
| - PyPI: https://pypi.org/project/openpit/${{ needs.verify-tag.outputs.version }}/ | |
| This release intentionally does not include attached build artifacts. | |
| EOF | |
| GH_TOKEN="${{ github.token }}" gh release create "${{ github.ref_name }}" \ | |
| --title "${{ github.ref_name }}" \ | |
| --generate-notes \ | |
| --notes-file "${BODY_FILE}" \ | |
| --latest |