Skip to content

release: prepare v0.6.1 (#564) #29

release: prepare v0.6.1 (#564)

release: prepare v0.6.1 (#564) #29

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*.*.*'
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
permissions:
contents: write
id-token: write # Required for trusted publishing (PyPI OIDC, npm provenance)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# Validate tag and extract version
validate:
name: Validate Release Tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
- name: Extract version from tag
id: version
run: |
TAG="${{ github.ref_name }}"
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Releasing version: $VERSION"
- name: Verify version matches Cargo.toml
run: |
CARGO_VERSION=$(grep -m 1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
TAG_VERSION="${{ steps.version.outputs.version }}"
if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Cargo.toml version ($CARGO_VERSION) does not match tag version ($TAG_VERSION)"
exit 1
fi
echo "Version validation successful: $TAG_VERSION"
# Build and publish Rust crates to crates.io using Trusted Publishing (OIDC)
# Setup: https://crates.io/docs/trusted-publishing
publish-crates:
name: Publish to crates.io
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
id-token: write # Required for trusted publishing
steps:
- uses: actions/checkout@v7
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "release"
- name: Verify packages can be built
run: |
cargo build -p exarch-core --release --all-features
cargo build -p exarch-cli --release --all-features
- name: Run tests
run: |
cargo test -p exarch-core --all-features
cargo test -p exarch-cli --all-features
- name: Authenticate with crates.io (Trusted Publishing)
id: crates-io-auth
uses: rust-lang/crates-io-auth-action@v1
- name: Publish crates to crates.io
uses: katyo/publish-crates@v2
with:
registry-token: ${{ steps.crates-io-auth.outputs.token }}
ignore-unpublished-changes: true
publish-delay: 10000
# Build Python wheels for multiple platforms
build-python-wheels:
name: Build Python Wheels (${{ matrix.platform.name }})
needs: validate
runs-on: ${{ matrix.platform.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
platform:
# Linux x86_64
- name: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
maturin-args: --target x86_64-unknown-linux-gnu
# Linux aarch64
- name: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
maturin-args: --target aarch64-unknown-linux-gnu
# Linux x86_64 (musl)
- name: linux-musl-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
manylinux: musllinux_1_2
# Linux aarch64 (musl)
- name: linux-musl-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-musl
manylinux: musllinux_1_2
# macOS x86_64
- name: macos-x86_64
os: macos-15-intel
target: x86_64-apple-darwin
maturin-args: --target x86_64-apple-darwin
# macOS aarch64 (Apple Silicon)
- name: macos-aarch64
os: macos-latest
target: aarch64-apple-darwin
maturin-args: --target aarch64-apple-darwin
# Windows x86_64
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
maturin-args: --target x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.10'
- name: Install maturin
run: pip install maturin
- name: Setup cross-compilation (Linux aarch64)
if: matrix.platform.name == 'linux-aarch64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "python-${{ matrix.platform.name }}"
- name: Build wheels (musl)
if: matrix.platform.manylinux
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: ${{ matrix.platform.manylinux }}
args: --release --strip --out wheels -i python3.10
working-directory: crates/exarch-python
- name: Build wheels
if: ${{ !matrix.platform.manylinux }}
working-directory: crates/exarch-python
run: maturin build --release ${{ matrix.platform.maturin-args }} --strip --out wheels -i python3.10
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-${{ matrix.platform.name }}
path: crates/exarch-python/wheels/*.whl
retention-days: 7
# Build exarch-cli binaries for multiple platforms
build-cli-binaries:
name: Build CLI Binary (${{ matrix.platform.name }})
needs: validate
runs-on: ${{ matrix.platform.os }}
timeout-minutes: 45
permissions:
contents: read
strategy:
fail-fast: false
matrix:
platform:
# Linux x86_64
- name: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: tar.gz
# Linux aarch64
- name: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
archive: tar.gz
# macOS x86_64
- name: macos-x86_64
os: macos-15-intel
target: x86_64-apple-darwin
archive: tar.gz
# macOS aarch64 (Apple Silicon)
- name: macos-aarch64
os: macos-latest
target: aarch64-apple-darwin
archive: tar.gz
# Windows x86_64
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
archive: zip
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Setup cross-compilation (Linux aarch64)
if: matrix.platform.name == 'linux-aarch64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "cli-${{ matrix.platform.name }}"
- name: Build exarch-cli
run: cargo build -p exarch-cli --release --target ${{ matrix.platform.target }}
- name: Package archive (Unix)
if: matrix.platform.archive == 'tar.gz'
run: |
VERSION="${{ needs.validate.outputs.version }}"
NAME="exarch-${VERSION}-${{ matrix.platform.target }}"
STAGE="$(mktemp -d)/${NAME}"
mkdir -p "$STAGE"
cp "target/${{ matrix.platform.target }}/release/exarch" "$STAGE/"
cp LICENSE-MIT LICENSE-APACHE README.md "$STAGE/"
tar -C "$(dirname "$STAGE")" -czf "${NAME}.tar.gz" "${NAME}"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
else
shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
fi
- name: Package archive (Windows)
if: matrix.platform.archive == 'zip'
shell: pwsh
run: |
$Version = "${{ needs.validate.outputs.version }}"
$Name = "exarch-$Version-${{ matrix.platform.target }}"
$Stage = Join-Path $Env:RUNNER_TEMP $Name
New-Item -ItemType Directory -Path $Stage | Out-Null
Copy-Item "target/${{ matrix.platform.target }}/release/exarch.exe" -Destination $Stage
Copy-Item LICENSE-MIT, LICENSE-APACHE, README.md -Destination $Stage
Compress-Archive -Path $Stage -DestinationPath "$Name.zip"
(Get-FileHash "$Name.zip" -Algorithm SHA256).Hash.ToLower() + " $Name.zip" | Out-File -Encoding ascii "$Name.zip.sha256"
- name: Upload CLI archive
uses: actions/upload-artifact@v7
with:
name: cli-${{ matrix.platform.name }}
path: |
exarch-*.tar.gz
exarch-*.tar.gz.sha256
exarch-*.zip
exarch-*.zip.sha256
retention-days: 7
# Publish Python wheels to PyPI using Trusted Publishing (OIDC)
# Setup: https://docs.pypi.org/trusted-publishers/adding-a-publisher/
publish-python:
name: Publish to PyPI
needs: [validate, build-python-wheels]
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: pypi
url: https://pypi.org/project/exarch/
permissions:
id-token: write # Required for trusted publishing
steps:
- name: Download all wheels
uses: actions/download-artifact@v8
with:
path: dist
pattern: wheels-*
merge-multiple: true
- name: List wheels
run: ls -lh dist/
- name: Publish to PyPI (Trusted Publishing)
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true
verbose: true
# Build Node.js native addons for multiple platforms
build-node-bindings:
name: Build Node.js Bindings (${{ matrix.platform.name }})
needs: validate
runs-on: ${{ matrix.platform.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
platform:
# Linux x86_64
- name: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
# Linux aarch64
- name: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# macOS x86_64
- name: macos-x86_64
os: macos-15-intel
target: x86_64-apple-darwin
# macOS aarch64 (Apple Silicon)
- name: macos-aarch64
os: macos-latest
target: aarch64-apple-darwin
# Windows x86_64
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- uses: pnpm/action-setup@v6
with:
version: 10
- name: Setup cross-compilation (Linux aarch64)
if: matrix.platform.name == 'linux-aarch64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "node-${{ matrix.platform.name }}"
- name: Install dependencies
working-directory: crates/exarch-node
run: pnpm install --no-frozen-lockfile
- name: Build native addon
working-directory: crates/exarch-node
run: pnpm run build --target ${{ matrix.platform.target }}
- name: Upload bindings
uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.platform.name }}
path: |
crates/exarch-node/*.node
crates/exarch-node/index.js
crates/exarch-node/index.d.ts
retention-days: 7
# Publish Node.js package to npm using Trusted Publishing (OIDC)
# Setup: https://docs.npmjs.com/trusted-publishers/
# No NPM_TOKEN needed - uses OIDC authentication
publish-node:
name: Publish to npm
needs: [validate, build-node-bindings]
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: npm
url: https://www.npmjs.com/package/exarch-rs
permissions:
contents: read
id-token: write # Required for trusted publishing (OIDC)
steps:
- uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v6
with:
version: 10
- name: Download all bindings
uses: actions/download-artifact@v8
with:
path: bindings
pattern: bindings-*
- name: Prepare npm package
working-directory: crates/exarch-node
run: |
mkdir -p native
for platform_dir in ../../bindings/bindings-*; do
if [ -d "$platform_dir" ]; then
cp -v "$platform_dir"/*.node native/ || true
fi
done
ls -lh native/
- name: Install dependencies
working-directory: crates/exarch-node
run: pnpm install --no-frozen-lockfile
- name: Publish to npm (Trusted Publishing)
working-directory: crates/exarch-node
run: pnpm publish --access public --no-git-checks --provenance --ignore-scripts
# Create GitHub Release with artifacts
github-release:
name: Create GitHub Release
needs: [validate, publish-crates, publish-python, publish-node, build-cli-binaries]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download Python wheels
uses: actions/download-artifact@v8
with:
path: release-artifacts/python
pattern: wheels-*
merge-multiple: true
- name: Download Node.js bindings
uses: actions/download-artifact@v8
with:
path: release-artifacts/node
pattern: bindings-*
- name: Download CLI binaries
uses: actions/download-artifact@v8
with:
path: release-artifacts/cli
pattern: cli-*
merge-multiple: true
- name: Generate changelog
id: changelog
run: |
VERSION="${{ needs.validate.outputs.version }}"
# Get the previous tag
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "First release - no previous tag found"
CHANGELOG="Initial release v$VERSION"
else
echo "Generating changelog from $PREV_TAG to v$VERSION"
CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges)
fi
# Save to file for multiline handling
echo "$CHANGELOG" > changelog.txt
cat >> changelog.txt << EOF
## Installation
### Rust
\`\`\`toml
[dependencies]
exarch-core = "$VERSION"
\`\`\`
### Python
\`\`\`bash
pip install exarch==$VERSION
\`\`\`
### Node.js
\`\`\`bash
npm install exarch-rs@$VERSION
\`\`\`
### Rust CLI
\`\`\`bash
cargo install exarch-cli
\`\`\`
Or download a prebuilt \`exarch-$VERSION-<target>\` archive from this release's assets
(or run \`curl -fsSL https://raw.githubusercontent.com/bug-ops/exarch/main/scripts/install.sh | sh\`),
both of which skip the Rust toolchain requirement.
## Artifacts
- Python wheels for Linux (x86_64, aarch64), Linux musl (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64)
- Node.js native addons for Linux (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64)
- CLI binaries for Linux (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64) as
\`exarch-$VERSION-<target>.tar.gz\` / \`.zip\` with \`.sha256\` checksums, plus \`install.sh\`
- Published to [crates.io](https://crates.io/crates/exarch-core), [PyPI](https://pypi.org/project/exarch), and [npm](https://www.npmjs.com/package/exarch-rs)
EOF
- name: Create Release
uses: softprops/action-gh-release@v3
with:
name: Release v${{ needs.validate.outputs.version }}
body_path: changelog.txt
draft: false
prerelease: false
files: |
release-artifacts/python/*.whl
release-artifacts/node/**/*.node
release-artifacts/cli/*.tar.gz
release-artifacts/cli/*.tar.gz.sha256
release-artifacts/cli/*.zip
release-artifacts/cli/*.zip.sha256
scripts/install.sh
token: ${{ secrets.GITHUB_TOKEN }}
# Final success check
release-success:
name: Release Success
needs: [publish-crates, publish-python, publish-node, github-release]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check all release jobs
run: |
REQUIRED_JOBS=(
"${{ needs.publish-crates.result }}"
"${{ needs.publish-python.result }}"
"${{ needs.publish-node.result }}"
"${{ needs.github-release.result }}"
)
for result in "${REQUIRED_JOBS[@]}"; do
if [[ "$result" != "success" ]]; then
echo "Release job failed: $result"
exit 1
fi
done
echo "All release jobs completed successfully!"