Skip to content

Release 0.1.2

Release 0.1.2 #8

Workflow file for this run

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