Release #1
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*.*.*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to release (e.g., v0.1.1)' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # Validate tag matches version | |
| validate: | |
| name: Validate Release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Get version from Cargo.toml | |
| id: version | |
| run: | | |
| VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Detected version: $VERSION" | |
| - name: Validate tag matches version | |
| run: | | |
| TAG="${{ github.ref_name }}" | |
| VERSION="${{ steps.version.outputs.version }}" | |
| EXPECTED_TAG="v$VERSION" | |
| if [ "$TAG" != "$EXPECTED_TAG" ]; then | |
| echo "Error: Tag '$TAG' does not match Cargo.toml version '$VERSION'" | |
| echo "Expected tag: $EXPECTED_TAG" | |
| exit 1 | |
| fi | |
| - name: Validate version consistency | |
| run: | | |
| CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| PY_VERSION=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| if [ "$CARGO_VERSION" != "$PY_VERSION" ]; then | |
| echo "Error: Cargo.toml ($CARGO_VERSION) and pyproject.toml ($PY_VERSION) versions don't match" | |
| exit 1 | |
| fi | |
| echo "✓ Cargo.toml and pyproject.toml versions match: $CARGO_VERSION" | |
| - name: Verify release is from main branch | |
| run: | | |
| # Fetch main branch | |
| git fetch origin main | |
| # Get the commit SHA the tag points to | |
| TAG_SHA=$(git rev-parse HEAD) | |
| # Check if this commit is reachable from main | |
| if ! git merge-base --is-ancestor $TAG_SHA origin/main; then | |
| echo "Error: Release tags must be created from commits on the main branch" | |
| echo "Tag SHA: $TAG_SHA" | |
| echo "Please merge your changes to main first, then create the release tag" | |
| exit 1 | |
| fi | |
| echo "✓ Verified: Tag points to a commit on main branch" | |
| # Build binaries for all platforms | |
| build-binaries: | |
| name: Build ${{ matrix.target }} | |
| needs: validate | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux x86_64 (glibc) | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| artifact_name: pdf_oxide-linux-x86_64 | |
| use_cross: false | |
| # Linux x86_64 (musl - static) | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| artifact_name: pdf_oxide-linux-x86_64-musl | |
| use_cross: false | |
| # Linux ARM64 | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| artifact_name: pdf_oxide-linux-aarch64 | |
| use_cross: true | |
| # macOS x86_64 (Intel) | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| artifact_name: pdf_oxide-macos-x86_64 | |
| use_cross: false | |
| # macOS ARM64 (Apple Silicon) | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| artifact_name: pdf_oxide-macos-aarch64 | |
| use_cross: false | |
| # Windows x86_64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| artifact_name: pdf_oxide-windows-x86_64 | |
| use_cross: false | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Cache cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cargo/registry | |
| key: ${{ runner.os }}-${{ matrix.target }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | |
| - name: Cache cargo index | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cargo/git | |
| key: ${{ runner.os }}-${{ matrix.target }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} | |
| - name: Install musl tools (Linux musl) | |
| if: matrix.target == 'x86_64-unknown-linux-musl' | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - name: Install cross-compilation tools (Linux ARM64) | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu | |
| - name: Build binaries | |
| shell: bash | |
| env: | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'aarch64-linux-gnu-gcc' || '' }} | |
| run: | | |
| cargo build --release -p pdf_oxide_cli --target ${{ matrix.target }} | |
| cargo build --release -p pdf_oxide_mcp --target ${{ matrix.target }} | |
| - name: Build .deb package (Linux x86_64 glibc) | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' | |
| run: | | |
| cargo install cargo-deb | |
| cargo deb -p pdf_oxide_cli --no-build --target x86_64-unknown-linux-gnu | |
| - name: Upload .deb artifact | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: debian-package | |
| path: target/x86_64-unknown-linux-gnu/debian/*.deb | |
| retention-days: 7 | |
| - name: Create archive (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| mkdir -p dist | |
| cd target/${{ matrix.target }}/release | |
| tar czf ../../../dist/${{ matrix.artifact_name }}-${VERSION}.tar.gz pdf-oxide pdf-oxide-mcp | |
| - name: Create archive (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $VERSION = "${{ needs.validate.outputs.version }}" | |
| New-Item -ItemType Directory -Force -Path dist | |
| Set-Location target/${{ matrix.target }}/release | |
| Compress-Archive -Path @("pdf-oxide.exe", "pdf-oxide-mcp.exe") -DestinationPath ../../../dist/${{ matrix.artifact_name }}-${VERSION}.zip | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: dist/* | |
| retention-days: 7 | |
| # Build WASM package | |
| build-wasm: | |
| name: Build WASM | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cargo/registry | |
| key: ${{ runner.os }}-wasm-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | |
| - name: Build WASM binary | |
| run: cargo build --lib --target wasm32-unknown-unknown --features wasm --release | |
| - name: Install matching wasm-bindgen-cli | |
| run: | | |
| WASM_BINDGEN_VERSION=$(grep -A1 'name = "wasm-bindgen"' Cargo.lock | grep version | head -1 | sed 's/.*"\(.*\)"/\1/') | |
| echo "Installing wasm-bindgen-cli@${WASM_BINDGEN_VERSION}" | |
| cargo install wasm-bindgen-cli --version "$WASM_BINDGEN_VERSION" | |
| - name: Generate bindings | |
| run: | | |
| wasm-bindgen --target nodejs --out-dir wasm-out/ \ | |
| target/wasm32-unknown-unknown/release/pdf_oxide.wasm | |
| - name: Copy package files | |
| run: | | |
| cp wasm-pkg/package.json wasm-out/ | |
| cp wasm-pkg/README.md wasm-out/ | |
| - name: Upload WASM artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wasm-package | |
| path: wasm-out/ | |
| retention-days: 7 | |
| # Build Python wheels | |
| build-python-wheels: | |
| name: Build Python wheels (${{ matrix.target }}) | |
| needs: validate | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| artifact_name: wheels-linux-x86_64 | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| artifact_name: wheels-macos-x86_64 | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| artifact_name: wheels-macos-aarch64 | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| artifact_name: wheels-windows-x86_64 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install maturin | |
| run: uv pip install --system maturin | |
| # Generate .pyi stubs automatically using mypy stubgen | |
| - name: Generate type stubs | |
| run: | | |
| uv run pip install mypy maturin | |
| uv run maturin develop --features python | |
| uv run stubgen -p pdf_oxide -o python | |
| - name: Build wheels | |
| run: maturin build --release --features python --target ${{ matrix.target }} --out dist | |
| - name: Upload wheels | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: dist/*.whl | |
| retention-days: 7 | |
| # Create GitHub Release | |
| create-release: | |
| name: Create GitHub Release | |
| needs: [validate, build-binaries, build-python-wheels, build-wasm] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| - name: Organize release files | |
| run: | | |
| mkdir -p release-files | |
| find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.whl" -o -name "*.deb" \) -exec cp {} release-files/ \; | |
| echo "=== Release files ===" | |
| ls -lh release-files/ | |
| - name: Extract release notes from CHANGELOG | |
| run: | | |
| chmod +x .github/scripts/extract-release-notes.sh | |
| .github/scripts/extract-release-notes.sh "${{ needs.validate.outputs.version }}" | |
| - name: Read release title | |
| id: title | |
| run: echo "title=$(cat release-title.txt)" >> $GITHUB_OUTPUT | |
| - name: Create release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| name: ${{ steps.title.outputs.title }} | |
| body_path: release-notes.md | |
| files: release-files/* | |
| draft: false | |
| prerelease: ${{ contains(needs.validate.outputs.version, '-') }} | |
| fail_on_unmatched_files: false | |
| # Publish to crates.io (optional) | |
| publish-crates: | |
| name: Publish to crates.io | |
| needs: [validate, create-release] | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(needs.validate.outputs.version, '-') }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Verify crates.io readiness | |
| run: cargo publish -p pdf_oxide --dry-run | |
| - name: Publish to crates.io | |
| run: cargo publish -p pdf_oxide | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} | |
| - name: Publish CLI to crates.io | |
| run: cargo publish -p pdf_oxide_cli | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} | |
| - name: Publish MCP server to crates.io | |
| run: cargo publish -p pdf_oxide_mcp | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} | |
| # Publish to PyPI (optional) | |
| publish-pypi: | |
| name: Publish to PyPI | |
| needs: [validate, build-python-wheels, create-release] | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(needs.validate.outputs.version, '-') }} | |
| steps: | |
| - name: Download wheel artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: wheels | |
| pattern: wheels-* | |
| merge-multiple: true | |
| - name: List wheels | |
| run: | | |
| echo "=== Wheels to publish ===" | |
| ls -lh wheels/ | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| packages-dir: wheels/ | |
| skip-existing: true | |
| # Publish to npm | |
| publish-npm: | |
| name: Publish to npm | |
| needs: [validate, build-wasm, create-release] | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(needs.validate.outputs.version, '-') }} | |
| steps: | |
| - name: Download WASM artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: wasm-package | |
| path: wasm-out | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: List package contents | |
| run: | | |
| echo "=== WASM package contents ===" | |
| ls -lh wasm-out/ | |
| - name: Publish to npm | |
| working-directory: wasm-out | |
| run: npm publish | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # Publish Homebrew formula and Scoop manifest | |
| publish-packaging: | |
| name: Publish Homebrew & Scoop | |
| needs: [validate, create-release] | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(needs.validate.outputs.version, '-') }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download release archives | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| mkdir -p release-archives | |
| cd release-archives | |
| gh release download "v${VERSION}" \ | |
| --pattern "pdf_oxide-macos-aarch64-${VERSION}.tar.gz" \ | |
| --pattern "pdf_oxide-macos-x86_64-${VERSION}.tar.gz" \ | |
| --pattern "pdf_oxide-linux-x86_64-musl-${VERSION}.tar.gz" \ | |
| --pattern "pdf_oxide-windows-x86_64-${VERSION}.zip" | |
| - name: Compute SHA256 hashes | |
| id: hashes | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| cd release-archives | |
| echo "macos_arm=$(sha256sum pdf_oxide-macos-aarch64-${VERSION}.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT | |
| echo "macos_x86=$(sha256sum pdf_oxide-macos-x86_64-${VERSION}.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT | |
| echo "linux_x86=$(sha256sum pdf_oxide-linux-x86_64-musl-${VERSION}.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT | |
| echo "windows=$(sha256sum pdf_oxide-windows-x86_64-${VERSION}.zip | cut -d' ' -f1)" >> $GITHUB_OUTPUT | |
| - name: Generate Homebrew formula | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| sed \ | |
| -e "s/{{VERSION}}/${VERSION}/g" \ | |
| -e "s/{{SHA256_MACOS_ARM}}/${{ steps.hashes.outputs.macos_arm }}/g" \ | |
| -e "s/{{SHA256_MACOS_X86}}/${{ steps.hashes.outputs.macos_x86 }}/g" \ | |
| -e "s/{{SHA256_LINUX_X86}}/${{ steps.hashes.outputs.linux_x86 }}/g" \ | |
| .github/templates/pdf-oxide.rb > pdf-oxide.rb | |
| echo "=== Generated Homebrew formula ===" | |
| cat pdf-oxide.rb | |
| - name: Generate Scoop manifest | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| sed \ | |
| -e "s/{{VERSION}}/${VERSION}/g" \ | |
| -e "s/{{SHA256_WINDOWS}}/${{ steps.hashes.outputs.windows }}/g" \ | |
| .github/templates/pdf-oxide.json > pdf-oxide.json | |
| echo "=== Generated Scoop manifest ===" | |
| cat pdf-oxide.json | |
| - name: Upload packaging artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: packaging-manifests | |
| path: | | |
| pdf-oxide.rb | |
| pdf-oxide.json | |
| retention-days: 90 | |
| - name: Push to Homebrew tap | |
| env: | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/yfedoseev/homebrew-tap.git" tap-repo | |
| cp pdf-oxide.rb tap-repo/Formula/pdf-oxide.rb | |
| cd tap-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/pdf-oxide.rb | |
| git commit -m "Update pdf-oxide to ${{ needs.validate.outputs.version }}" | |
| git push | |
| - name: Push Scoop manifest | |
| env: | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/yfedoseev/scoop-pdf-oxide.git" scoop-repo | |
| cp pdf-oxide.json scoop-repo/pdf-oxide.json | |
| cd scoop-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add pdf-oxide.json | |
| git commit -m "Update pdf-oxide to ${{ needs.validate.outputs.version }}" | |
| git push |