v3.6.16 #764
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: Publish Release | |
| # Action-first html-to-markdown release pipeline. | |
| # | |
| # Orchestrates 11 alef language targets: | |
| # crates · cli · python · node · typescript · wasm · ruby · php · go · c-ffi | |
| # java · csharp · elixir · homebrew | |
| # | |
| # Mirrors kreuzberg's publish.yaml composition: every registry interaction, | |
| # every multi-platform build, and every release-asset upload routes through | |
| # a shared kreuzberg-dev/actions composite. Inline bash is reserved for | |
| # repo-quirky orchestration only (homebrew bottle assembly, Node platform | |
| # package staging, NIF artifact packaging, multi-formula brew flow). | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Release tag to build (e.g., v2.6.0)" | |
| required: false | |
| type: string | |
| ref: | |
| description: "Git ref (branch, tag, or commit) to build; defaults to the tag" | |
| required: false | |
| type: string | |
| targets: | |
| description: "Comma-separated list of release targets, or 'all'" | |
| required: false | |
| type: string | |
| default: "all" | |
| dry_run: | |
| description: "Prepare artifacts without publishing" | |
| required: false | |
| type: boolean | |
| default: false | |
| republish: | |
| description: "Delete and re-create the tag on current HEAD before publishing" | |
| required: false | |
| type: boolean | |
| default: false | |
| force_republish: | |
| description: "Force republish targets whose registry version already exists" | |
| required: false | |
| type: boolean | |
| default: false | |
| release: | |
| types: [published] | |
| repository_dispatch: | |
| types: [publish-release] | |
| permissions: | |
| contents: read | |
| id-token: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' && (github.event.inputs.ref || github.event.inputs.tag)) || github.ref || github.run_id }} | |
| cancel-in-progress: false | |
| jobs: | |
| prepare: | |
| name: Prepare metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.meta.outputs.tag }} | |
| version: ${{ steps.meta.outputs.version }} | |
| ref: ${{ steps.meta.outputs.ref }} | |
| checkout_ref: ${{ steps.meta.outputs.checkout_ref }} | |
| target_sha: ${{ steps.meta.outputs.target_sha }} | |
| matrix_ref: ${{ steps.meta.outputs.matrix_ref }} | |
| is_tag: ${{ steps.meta.outputs.is_tag }} | |
| is_prerelease: ${{ steps.meta.outputs.is_prerelease }} | |
| npm_tag: ${{ steps.meta.outputs.npm_tag }} | |
| dry_run: ${{ steps.meta.outputs.dry_run }} | |
| force_republish: ${{ steps.meta.outputs.force_republish }} | |
| release_targets: ${{ steps.meta.outputs.release_targets }} | |
| release_any: ${{ steps.meta.outputs.release_any }} | |
| release_python: ${{ steps.meta.outputs.release_python }} | |
| release_node: ${{ steps.meta.outputs.release_node }} | |
| release_ruby: ${{ steps.meta.outputs.release_ruby }} | |
| release_cli: ${{ steps.meta.outputs.release_cli }} | |
| release_crates: ${{ steps.meta.outputs.release_crates }} | |
| release_homebrew: ${{ steps.meta.outputs.release_homebrew }} | |
| release_java: ${{ steps.meta.outputs.release_java }} | |
| release_csharp: ${{ steps.meta.outputs.release_csharp }} | |
| release_go: ${{ steps.meta.outputs.release_go }} | |
| release_wasm: ${{ steps.meta.outputs.release_wasm }} | |
| release_php: ${{ steps.meta.outputs.release_php }} | |
| release_elixir: ${{ steps.meta.outputs.release_elixir }} | |
| release_c_ffi: ${{ steps.meta.outputs.release_c_ffi }} | |
| release_kotlin: ${{ steps.meta.outputs.release_kotlin }} | |
| release_swift: ${{ steps.meta.outputs.release_swift }} | |
| release_dart: ${{ steps.meta.outputs.release_dart }} | |
| release_zig: ${{ steps.meta.outputs.release_zig }} | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ (inputs.republish == true && (inputs.ref || github.event.repository.default_branch)) || inputs.ref || inputs.tag || github.ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| token: ${{ steps.app-token.outputs.token }} | |
| persist-credentials: true | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "kreuzberg-dev-publisher[bot]" | |
| git config user.email "291994444+kreuzberg-dev-publisher[bot]@users.noreply.github.com" | |
| - name: Retag for republish | |
| if: ${{ inputs.republish == true || github.event.client_payload.republish == true }} | |
| uses: kreuzberg-dev/actions/retag-for-republish@v1 | |
| with: | |
| tag: ${{ inputs.tag || github.event.client_payload.tag }} | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Resolve release metadata | |
| id: meta | |
| uses: kreuzberg-dev/actions/prepare-release-metadata@v1 | |
| - name: Ensure GitHub Release exists for tag | |
| if: ${{ steps.meta.outputs.is_tag == 'true' && steps.meta.outputs.dry_run != 'true' }} | |
| uses: kreuzberg-dev/actions/publish-github-release@v1 | |
| with: | |
| tag: ${{ steps.meta.outputs.tag }} | |
| target: ${{ steps.meta.outputs.target_sha }} | |
| notes: "Release ${{ steps.meta.outputs.tag }}" | |
| draft: "true" | |
| prerelease: ${{ steps.meta.outputs.is_prerelease }} | |
| token: ${{ steps.app-token.outputs.token }} | |
| validate-versions: | |
| name: Validate language manifest versions | |
| needs: prepare | |
| if: ${{ needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Validate versions | |
| uses: kreuzberg-dev/actions/validate-versions@v1 | |
| with: | |
| version: ${{ needs.prepare.outputs.version }} | |
| # ─── Registry existence checks ──────────────────────────────────────── | |
| check-pypi: | |
| name: Check PyPI for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_python == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: pypi | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-npm: | |
| name: Check npm for existing versions | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_node == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| node_exists: ${{ steps.check.outputs.exists }} | |
| ts_exists: ${{ steps.check.outputs.ts_exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: npm | |
| package: "@kreuzberg/html-to-markdown-node" | |
| version: ${{ needs.prepare.outputs.version }} | |
| extra-packages: | | |
| ts_exists=@kreuzberg/html-to-markdown | |
| check-wasm: | |
| name: Check npm for existing WASM package | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_wasm == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: npm | |
| package: "@kreuzberg/html-to-markdown-wasm" | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-rubygems: | |
| name: Check RubyGems for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_ruby == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: rubygems | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-maven: | |
| name: Check Maven Central for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_java == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: maven | |
| package: "dev.kreuzberg:html-to-markdown" | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-nuget: | |
| name: Check NuGet for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_csharp == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: nuget | |
| package: KreuzbergDev.HtmlToMarkdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-packagist: | |
| name: Check Packagist for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_php == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: packagist | |
| package: kreuzberg-dev/html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-cratesio: | |
| name: Check crates.io for existing versions | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_crates == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| rs_exists: ${{ steps.check.outputs.exists }} | |
| cli_exists: ${{ steps.check.outputs.cli_exists }} | |
| all_exist: ${{ steps.derive.outputs.all_exist }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: cratesio | |
| package: html-to-markdown-rs | |
| version: ${{ needs.prepare.outputs.version }} | |
| extra-packages: | | |
| cli_exists=html-to-markdown-cli | |
| - id: derive | |
| # Inline: aggregate two check-registry outputs into a single boolean | |
| # the publish-crates job can guard on. No shared action equivalent. | |
| run: | | |
| if [[ "${{ steps.check.outputs.exists }}" == "true" && "${{ steps.check.outputs.cli_exists }}" == "true" ]]; then | |
| echo "all_exist=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "all_exist=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| check-hex: | |
| name: Check Hex.pm for existing version | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_elixir == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: hex | |
| package: html_to_markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-homebrew: | |
| name: Check Homebrew tap for formula | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_homebrew == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: homebrew | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| tap-repo: kreuzberg-dev/homebrew-tap | |
| check-cli: | |
| name: Check GitHub Release for CLI assets | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_cli == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: github-release | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| repo: ${{ github.repository }} | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| asset-prefix: cli- | |
| check-go: | |
| name: Check GitHub Release for Go FFI assets | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_go == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: github-release | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| repo: ${{ github.repository }} | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| asset-prefix: html-to-markdown-rs-go- | |
| check-c-ffi: | |
| name: Check GitHub Release for C FFI assets | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_c_ffi == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: github-release | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| repo: ${{ github.repository }} | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| asset-prefix: html-to-markdown-rs-ffi- | |
| check-elixir-release: | |
| name: Check GitHub Release for Elixir NIF assets | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_elixir == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: github-release | |
| package: html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| repo: ${{ github.repository }} | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| asset-prefix: libhtml_to_markdown_nif- | |
| check-maven-kotlin: | |
| name: Check Maven Central for Kotlin Android | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_kotlin == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: maven | |
| package: "dev.kreuzberg:html-to-markdown-kotlin" | |
| version: ${{ needs.prepare.outputs.version }} | |
| check-pub: | |
| name: Check pub.dev for Dart package | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_dart == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: pub | |
| package: html_to_markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| assemble-dart-package: | |
| name: Assemble Dart package artifact | |
| needs: [prepare, check-pub] | |
| if: ${{ needs.prepare.outputs.release_dart == 'true' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && (needs.check-pub.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: dart-package-assembled | |
| path: packages/dart | |
| if-no-files-found: error | |
| retention-days: 7 | |
| # Swift Package Index has no central registry — packages are consumed | |
| # directly from git tags, so the tag push IS the publish event. The | |
| # previous `check-spi-swift` + `publish-swift` jobs only pinged SPI for | |
| # fast re-indexing (SPI auto-discovers within ~1h without a ping), so | |
| # they have been dropped to cut runner cost. The `release_swift` output | |
| # from `prepare` is still emitted but no longer consumed. | |
| # | |
| # Zig is similarly tag-based, but the publish-zig job is retained because | |
| # it appends the tarball URL + SHA-256 to the release notes so consumers | |
| # can copy-paste straight into `build.zig.zon`. | |
| assemble-zig-package: | |
| name: Assemble Zig package with prebuilt FFI libraries | |
| needs: [prepare, check-c-ffi, c-ffi-libraries] | |
| if: ${{ needs.prepare.outputs.release_zig == 'true' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: c-ffi-* | |
| path: c-ffi-artifacts | |
| merge-multiple: true | |
| - name: Stage prebuilt FFI libraries into Zig package | |
| # Inline: alef zig codegen produces packages/zig/ with include/ (C headers | |
| # from the zig backend). This step populates lib/<rid>/ with the prebuilt | |
| # C FFI shared libraries for each platform so the tarball is complete. | |
| # Platform naming follows alef's Go platform convention: linux-x64, | |
| # linux-arm64, darwin-x64, darwin-arm64, windows-x64, windows-arm64. | |
| shell: bash | |
| run: | | |
| mkdir -p packages/zig/lib | |
| # Map alef's platform names to the directories produced by c-ffi-libraries job. | |
| # c-ffi-libraries matrix produces: c-ffi-{platform}/ where platform is | |
| # linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64, windows-arm64. | |
| declare -A platform_map=( | |
| [linux-x64]="linux-x64" | |
| [linux-arm64]="linux-arm64" | |
| [darwin-x64]="darwin-x64" | |
| [darwin-arm64]="darwin-arm64" | |
| [windows-x64]="windows-x64" | |
| [windows-arm64]="windows-arm64" | |
| ) | |
| for alef_rid in "${!platform_map[@]}"; do | |
| artifact_platform="${platform_map[$alef_rid]}" | |
| src_dir="c-ffi-artifacts/${artifact_platform}" | |
| # c-ffi-libraries produces a flat distribution directory per platform | |
| # containing the FFI shared library (.so/.dylib/.dll). Find and copy. | |
| if [[ -d "$src_dir" ]]; then | |
| mkdir -p "packages/zig/lib/${alef_rid}" | |
| # Copy all shared library artifacts (.so, .dylib, .dll) from the distribution | |
| find "$src_dir" -maxdepth 2 -type f \( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \) \ | |
| -exec cp {} "packages/zig/lib/${alef_rid}/" \; | |
| echo "Staged FFI libraries for $alef_rid:" | |
| ls -la "packages/zig/lib/${alef_rid}/" | |
| else | |
| echo "Warning: no C FFI artifact found for $alef_rid at $src_dir" | |
| fi | |
| done | |
| - name: Create Zig package tarball | |
| # Inline: archive packages/zig with the populated lib/ subdirectories. | |
| # alef zig backend has already generated the source code, build files, | |
| # and C headers. This step tarballs the complete package. | |
| shell: bash | |
| run: | | |
| cd packages/zig | |
| tar -czf "../../html-to-markdown-rs-zig-v${{ needs.prepare.outputs.version }}.tar.gz" . | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: zig-package-assembled | |
| path: html-to-markdown-rs-zig-v${{ needs.prepare.outputs.version }}.tar.gz | |
| if-no-files-found: error | |
| retention-days: 7 | |
| check-zig: | |
| name: Check Zig package presence | |
| needs: prepare | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_zig == 'true' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exists: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - id: check | |
| # `registry: github-release` (not `registry: zig`) — alef check-registry's | |
| # zig path only verifies the release tag exists. On a real release that's | |
| # always true, so it short-circuits `publish-zig` to skipped (regression: | |
| # v3.5.1 shipped without the zig tarball asset because of this). Verify | |
| # the actual asset is present instead. | |
| uses: kreuzberg-dev/actions/check-registry@v1 | |
| with: | |
| registry: github-release | |
| package: html-to-markdown-rs-zig-v${{ needs.prepare.outputs.version }}.tar.gz | |
| version: ${{ needs.prepare.outputs.version }} | |
| repo: ${{ github.repository }} | |
| assets: html-to-markdown-rs-zig-v${{ needs.prepare.outputs.version }}.tar.gz | |
| # ─── Build matrix jobs ──────────────────────────────────────────────── | |
| python-wheels: | |
| name: Build Python wheels (${{ matrix.os }}) | |
| needs: [prepare, check-pypi] | |
| if: ${{ needs.prepare.outputs.release_python == 'true' && (needs.check-pypi.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| env: | |
| # Required by cibuildwheel/delocate: the Rust-compiled .so/.dylib targets | |
| # macOS 10.12+ (default in modern toolchains). 10.9 was the legacy default | |
| # which causes delocate to refuse to relocate the wheel. | |
| MACOSX_DEPLOYMENT_TARGET: "10.12" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # windows-11-arm: cibuildwheel does not yet support aarch64-pc-windows-msvc target | |
| os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest, macos-15-intel] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - name: Setup Rust on host (macOS, Windows) | |
| # cibuildwheel's before-build runs `cargo build` which auto-installs | |
| # the toolchain pinned by rust-toolchain.toml via rustup. On macOS-arm64 | |
| # this collides with the runner's pre-installed clippy/rustfmt and | |
| # rustup aborts with "detected conflict: bin/cargo-clippy". Pre-install | |
| # via actions-rust-lang/setup-rust-toolchain so the toolchain is ready | |
| # by the time cibuildwheel runs cargo. No-op for Linux (cibw runs in | |
| # a manylinux container — host rust is invisible there). | |
| if: runner.os != 'Linux' | |
| uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| use-sccache: "false" | |
| - name: Build wheels | |
| uses: kreuzberg-dev/actions/build-python-wheels@v1 | |
| with: | |
| python-version: "3.13" | |
| package-dir: packages/python | |
| # abi3-py310 is enabled in pyo3 features → one wheel per platform covers 3.10+. | |
| # Without this override, cibuildwheel rebuilds for cp310/cp311/cp312/cp313/cp314 = 5x duplicates. | |
| cibw-build: "cp310-*" | |
| cibw-before-build-linux: > | |
| yum install -y openssl-devel && | |
| (test -x /usr/bin/aarch64-linux-gnu-gcc || | |
| ln -sf "$(command -v gcc)" /usr/local/bin/aarch64-linux-gnu-gcc 2>/dev/null || true) && | |
| pip install maturin uv && | |
| source ~/.cargo/env && | |
| python scripts/prepare_wheel.py | |
| cibw-before-build-macos: > | |
| pip install maturin uv && | |
| source ~/.cargo/env && | |
| python scripts/prepare_wheel.py | |
| cibw-before-build-windows: > | |
| pip install maturin uv && | |
| set PATH=%USERPROFILE%\.cargo\bin;%PATH% && | |
| python scripts\prepare_wheel.py | |
| upload-artifact: "false" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: python-wheels-${{ matrix.os }} | |
| path: wheelhouse/*.whl | |
| retention-days: 14 | |
| python-sdist: | |
| name: Build Python sdist | |
| needs: [prepare, check-pypi, publish-crates] | |
| if: ${{ !cancelled() && needs.publish-crates.result != 'failure' && needs.prepare.outputs.release_python == 'true' && (needs.check-pypi.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| - uses: actions/setup-python@v6.2.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Build and bundle CLI binary | |
| # The CLI binary is bundled into the sdist so it can be installed via pipx. | |
| shell: bash | |
| run: | | |
| cargo build --release --locked --package html-to-markdown-cli | |
| mkdir -p packages/python/html_to_markdown/_cli | |
| cp target/release/html-to-markdown packages/python/html_to_markdown/_cli/ 2>/dev/null \ | |
| || cp target/release/html-to-markdown.exe packages/python/html_to_markdown/_cli/ | |
| - uses: kreuzberg-dev/actions/build-python-sdist@v1 | |
| with: | |
| package-dir: packages/python | |
| output-dir: dist | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: python-sdist | |
| path: dist/*.tar.gz | |
| retention-days: 14 | |
| node-typescript-defs: | |
| name: Generate Node TypeScript definitions | |
| needs: [prepare, check-npm] | |
| if: ${{ needs.prepare.outputs.release_node == 'true' && (needs.check-npm.outputs.node_exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| - uses: kreuzberg-dev/actions/setup-node-workspace@v1 | |
| with: | |
| node-version: "24" | |
| - name: Generate TypeScript definitions | |
| # Inline: napi build produces index.js + a fresh index.d.ts as a side | |
| # effect. We keep napi's index.js (its platform-dispatch stub loader) | |
| # but restore alef's committed index.d.ts: alef adds *Update DTO types | |
| # (e.g. ConversionOptionsUpdate, PreprocessingOptionsUpdate) that the | |
| # napi macros deliberately filter out — those types are re-exported by | |
| # packages/typescript/src/index.ts, so dropping them breaks tsc with | |
| # TS2724 at the wrapper publish step. | |
| shell: bash | |
| run: | | |
| pnpm --filter ./crates/html-to-markdown-node exec napi build --release | |
| git checkout HEAD -- crates/html-to-markdown-node/index.d.ts | |
| mkdir -p typescript-defs | |
| cp crates/html-to-markdown-node/index.js crates/html-to-markdown-node/index.d.ts typescript-defs/ | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-typescript-defs | |
| path: typescript-defs/ | |
| retention-days: 14 | |
| node-bindings: | |
| name: Build Node bindings (${{ matrix.target }}) | |
| needs: [prepare, check-npm] | |
| if: ${{ needs.prepare.outputs.release_node == 'true' && (needs.check-npm.outputs.node_exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: macos-latest, target: aarch64-apple-darwin, rust_target: "", use_napi_cross: false } | |
| - { os: macos-15-intel, target: x86_64-apple-darwin, rust_target: "", use_napi_cross: false } | |
| - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, rust_target: "", use_napi_cross: false } | |
| - { | |
| os: ubuntu-latest, | |
| target: aarch64-unknown-linux-gnu, | |
| rust_target: aarch64-unknown-linux-gnu, | |
| use_napi_cross: true, | |
| } | |
| # musl + armv7 deferred — napi-cross artifact path mismatch ("No dist dir found") blocks every cross-compiled binding. | |
| - { os: windows-latest, target: x86_64-pc-windows-msvc, rust_target: "", use_napi_cross: false } | |
| - { | |
| os: windows-latest, | |
| target: aarch64-pc-windows-msvc, | |
| rust_target: aarch64-pc-windows-msvc, | |
| use_napi_cross: false, | |
| } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.rust_target || matrix.target }} | |
| cache-key-prefix: publish-node-${{ matrix.target }} | |
| - uses: kreuzberg-dev/actions/setup-node-workspace@v1 | |
| with: | |
| node-version: "24" | |
| - name: Build native module | |
| uses: kreuzberg-dev/actions/build-node-napi@v1 | |
| with: | |
| crate-dir: crates/html-to-markdown-node | |
| target: ${{ matrix.target }} | |
| use-napi-cross: ${{ matrix.use_napi_cross }} | |
| pack-target-tarball: node-bindings-${{ matrix.target }}.tar.gz | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-bindings-${{ matrix.target }} | |
| path: node-bindings-${{ matrix.target }}.tar.gz | |
| retention-days: 14 | |
| wasm-bindings: | |
| name: Build WASM package | |
| needs: [prepare, check-wasm] | |
| if: ${{ needs.prepare.outputs.release_wasm == 'true' && (needs.check-wasm.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: wasm32-unknown-unknown | |
| cache-key-prefix: publish-wasm | |
| - uses: kreuzberg-dev/actions/setup-node-workspace@v1 | |
| with: | |
| node-version: "24" | |
| - uses: kreuzberg-dev/actions/build-wasm-package@v1 | |
| with: | |
| crate-dir: crates/html-to-markdown-wasm | |
| pack-tarball: "false" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: wasm-bundle | |
| path: | | |
| crates/html-to-markdown-wasm/dist/** | |
| crates/html-to-markdown-wasm/dist-node/** | |
| crates/html-to-markdown-wasm/dist-web/** | |
| crates/html-to-markdown-wasm/pkg/** | |
| crates/html-to-markdown-wasm/package.json | |
| retention-days: 14 | |
| ruby-gem: | |
| name: Build Ruby gem (${{ matrix.label }}) | |
| needs: [prepare, check-rubygems, publish-crates] | |
| if: ${{ !cancelled() && needs.publish-crates.result != 'failure' && needs.prepare.outputs.release_ruby == 'true' && (needs.check-rubygems.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| env: | |
| RB_SYS_CARGO_PROFILE: release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, label: linux } | |
| - { os: ubuntu-24.04-arm, label: linux-aarch64 } | |
| - { os: macos-latest, label: macos-arm64 } | |
| - { os: macos-15-intel, label: macos-x86_64 } | |
| - { os: ubuntu-latest, label: windows-x64, dock_platform: x64-mingw-ucrt } | |
| # windows-11-arm: deferred until rb-sys + Magnus toolchain validated on aarch64-pc-windows-msvc | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - name: Drop cached CLI binaries | |
| # Inline: stale CLI artifacts under packages/ruby/exe/ confuse rake | |
| # build's filtering. One-shot cleanup, no shared action equivalent. | |
| shell: bash | |
| run: rm -rf packages/ruby/exe/* packages/ruby/pkg | |
| - name: Install MSYS2 toolchain (Windows) | |
| if: runner.os == 'Windows' | |
| # Inline: rb-sys requires the MSYS2 toolchain on Windows for clang + | |
| # libffi headers. Magnus also needs ucrt64-prefixed binaries on PATH. | |
| shell: pwsh | |
| run: | | |
| ridk install 1 2 3 | |
| $msys2Path = (ridk version).Split("`n") | Where-Object { $_ -match "msys2:" } | ForEach-Object { ($_ -split ":\s*")[1].Trim() } | |
| if (-not $msys2Path) { $msys2Path = "C:\msys64" } | |
| ridk exec pacman -S --noconfirm --needed mingw-w64-ucrt-x86_64-clang mingw-w64-ucrt-x86_64-libffi | |
| "$msys2Path\ucrt64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| - name: Install Rust (GNU on Windows) | |
| if: runner.os == 'Windows' | |
| # Inline: rb-sys/Magnus only build against the GNU ABI on Windows; the | |
| # default MSVC toolchain produces incompatible artifacts. | |
| shell: pwsh | |
| run: | | |
| rustup install stable-x86_64-pc-windows-gnu | |
| rustup default stable-x86_64-pc-windows-gnu | |
| rustup target add x86_64-pc-windows-gnu | |
| rustc --version | |
| - name: Configure bindgen sysroot (Windows) | |
| if: runner.os == 'Windows' | |
| # Inline: bindgen needs an explicit libclang sysroot pointing at the | |
| # MSYS2 ucrt64 install — discovery fails on the GitHub runner image. | |
| shell: bash | |
| run: | | |
| UCRT="/c/msys64/ucrt64" | |
| echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=${UCRT} -I${UCRT}/include" >>"$GITHUB_ENV" | |
| echo "LIBCLANG_PATH=${UCRT}/bin" >>"$GITHUB_ENV" | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| if: runner.os != 'Windows' | |
| with: | |
| cache-key-prefix: publish-ruby-${{ matrix.label }} | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.3" | |
| bundler: "4.0.3" | |
| bundler-cache: false | |
| - name: Install Ruby dependencies | |
| # Inline: bundle config + install in the package dir; trivially small | |
| # and identical across kreuzcrawl/kreuzberg/html-to-markdown. | |
| shell: bash | |
| working-directory: packages/ruby | |
| run: | | |
| gem install bundler -v 4.0.3 | |
| bundle config set --local path 'vendor/bundle' | |
| bundle install --jobs 4 --retry 3 | |
| - uses: kreuzberg-dev/actions/build-ruby-gem@v1 | |
| if: matrix.dock_platform == '' | |
| with: | |
| package-dir: packages/ruby | |
| - name: Build platform gem | |
| # Inline: each matrix runner builds the gem for its own native platform | |
| # via `rake compile && rake build`. rake-compiler's compile step sets | |
| # the gem's native platform tag from rb_config, so the produced gem is | |
| # already platform-tagged (e.g. kreuzberg-X.Y.Z-x86_64-linux.gem). This | |
| # mirrors the kreuzberg sibling's proven Ruby publish path. For Windows | |
| # we cross-compile inside rb-sys-dock so the GNU toolchain produces a | |
| # x64-mingw-ucrt gem from the linux runner. | |
| shell: bash | |
| run: | | |
| if [ -n "${{ matrix.dock_platform }}" ]; then | |
| # Cross-compile for Windows via Docker using rb-sys-dock. | |
| # The rb-sys-dock container ships its own Ruby toolchain; bundler | |
| # must `install` inside the container before `exec rake` so the | |
| # lockfile materializes against the container's Ruby version | |
| # rather than the host's. | |
| # Pin gem below 0.9.128 to get the 0.9.127 container, then exclude | |
| # Ruby 4.0.2 via RUBY_CC_VERSION — both 0.9.127 and 0.9.128 ship | |
| # Ruby 4.0.2 whose <ruby/defines.h> pulls <sys/select.h>, which | |
| # clang cannot resolve in the mingw cross sysroot. 0.9.127's | |
| # container also ships 3.1.7/3.2.11/3.3.11/3.4.9; pin to those. | |
| # | |
| # Run rb-sys-dock from the WORKSPACE ROOT so the container mounts | |
| # the whole repo — the gem's Cargo.toml has a path-dep | |
| # `html-to-markdown-rs = { path = "../../../../../crates/html-to-markdown" }` | |
| # which resolves outside `packages/ruby/` and would not exist in | |
| # a container mounted at the gem dir. Use `--directory packages/ruby` | |
| # so the container's working dir is still the gem root. | |
| gem install rb_sys -v '< 0.9.128' | |
| # Use `env VAR=val` to plant RUBY_CC_VERSION into the *command's* process | |
| # environment, surviving the container's /etc/profile.d default that | |
| # otherwise re-introduces Ruby 4.0.2 after `export`. | |
| # Wrap the in-container command in `set -euo pipefail` and | |
| # `::group::` markers so an individual step failure surfaces | |
| # its exit code on the runner log instead of disappearing into | |
| # the joined `&&` chain (rb-sys-dock chronic windows-x64 | |
| # failures have been opaque since rc.19 because everything ran | |
| # inside one `bash -c "step1 && step2 && …"`). | |
| rb-sys-dock --directory packages/ruby --platform "${{ matrix.dock_platform }}" -- env RUBY_CC_VERSION=3.4.9:3.3.11:3.2.11:3.1.7 bash -c ' | |
| set -euo pipefail | |
| echo "::group::rustup toolchain install" | |
| rustup toolchain install stable --no-self-update --profile minimal | |
| echo "::endgroup::" | |
| echo "::group::rustup default + target add" | |
| rustup default stable | |
| rustup target add x86_64-pc-windows-gnu | |
| echo "::endgroup::" | |
| echo "::group::bundle install" | |
| bundle install --jobs=4 --retry=3 | |
| echo "::endgroup::" | |
| echo "::group::rake compile" | |
| bundle exec rake compile | |
| echo "::endgroup::" | |
| echo "::group::rake build" | |
| bundle exec rake build | |
| echo "::endgroup::" | |
| ' | |
| else | |
| # Native build on matching architecture | |
| cd packages/ruby | |
| bundle exec rake compile | |
| bundle exec rake build | |
| fi | |
| - name: Drop source gem on non-canonical builders | |
| if: ${{ matrix.label != 'linux' }} | |
| # Inline: only the canonical `linux` builder ships the source gem; others | |
| # would emit byte-different source gems that overwrite each other under | |
| # merge-multiple and produce an invalid .gem at publish time. | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| for f in packages/ruby/pkg/*.gem; do | |
| base="$(basename "$f")" | |
| case "$base" in | |
| *-x86_64-linux.gem|*-aarch64-linux.gem|*-arm64-darwin.gem|*-x86_64-darwin.gem|*-x64-mingw32.gem|*-x64-mingw-ucrt.gem|*-arm64-mingw-ucrt.gem) ;; | |
| *) echo "Removing non-canonical source gem $base"; rm -f "$f" ;; | |
| esac | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: rubygems-${{ matrix.label }} | |
| path: packages/ruby/pkg/*.gem | |
| retention-days: 14 | |
| php-extension: | |
| name: Build PHP extension (php${{ matrix.php }} ${{ matrix.platform.label }}) | |
| needs: [prepare, check-packagist, publish-crates] | |
| if: ${{ !cancelled() && needs.publish-crates.result != 'failure' && needs.prepare.outputs.release_php == 'true' && (needs.check-packagist.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.platform.os }} | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php: ["8.2", "8.3", "8.4", "8.5"] | |
| platform: | |
| - { os: ubuntu-latest, label: linux-x86_64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, label: linux-arm64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, label: macos-arm64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, label: macos-x86_64, target: x86_64-apple-darwin } | |
| - { os: windows-latest, label: windows-x86_64, target: x86_64-pc-windows-msvc } | |
| # windows-arm64: deferred until ext-php-rs cross-compile to aarch64-pc-windows-msvc is validated | |
| exclude: | |
| # PHP 8.5 on macos-arm64: the homebrew tap lacks a php@8.5 arm64 formula, so | |
| # shivammathur/setup-php fails at the setup step ("Could not setup PHP 8.5"). | |
| # Tracked in #333 — re-enable once upstream ships a php@8.5 arm64 formula. | |
| - php: "8.5" | |
| platform: { os: macos-latest, label: macos-arm64, target: aarch64-apple-darwin } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-php@v1 | |
| with: | |
| php-version: ${{ matrix.php }} | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| cache-key-prefix: publish-php-${{ matrix.platform.label }}-php${{ matrix.php }} | |
| - uses: kreuzberg-dev/actions/install-alef@v1 | |
| - uses: kreuzberg-dev/actions/build-php-extension@v1 | |
| with: | |
| crate-name: html-to-markdown-php | |
| lib-name: html_to_markdown_php | |
| php-version: ${{ matrix.php }} | |
| php-ts: nts | |
| - name: Determine Windows compiler | |
| if: runner.os == 'Windows' | |
| id: wincompiler | |
| shell: pwsh | |
| run: | | |
| $compiler = switch ('${{ matrix.php }}') { | |
| '8.2' { 'vs16' } | |
| '8.3' { 'vs16' } | |
| '8.4' { 'vs17' } | |
| '8.5' { 'vs17' } | |
| default { 'vs17' } | |
| } | |
| "compiler=$compiler" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - uses: kreuzberg-dev/actions/package-php-pie@v1 | |
| with: | |
| php-version: ${{ matrix.php }} | |
| php-ts: nts | |
| target: ${{ matrix.platform.target }} | |
| windows-compiler: ${{ steps.wincompiler.outputs.compiler }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| output-dir: dist/php-package | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: php-package-${{ matrix.platform.label }}-php${{ matrix.php }} | |
| path: | | |
| dist/php-package/php_*.tgz | |
| dist/php-package/php_*.tgz.sha256 | |
| dist/php-package/php_*.zip | |
| dist/php-package/php_*.zip.sha256 | |
| retention-days: 14 | |
| cli-binaries: | |
| name: Build CLI binary (${{ matrix.target }}) | |
| needs: [prepare, check-cli] | |
| if: ${{ needs.prepare.outputs.release_cli == 'true' && (needs.check-cli.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-latest, target: x86_64-unknown-linux-musl } | |
| - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, target: x86_64-apple-darwin } | |
| - { os: windows-latest, target: x86_64-pc-windows-msvc } | |
| - { os: windows-11-arm, target: aarch64-pc-windows-msvc } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - name: Install musl tools | |
| if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }} | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-cli-${{ matrix.target }} | |
| - uses: kreuzberg-dev/actions/build-rust-cli@v1 | |
| id: build | |
| with: | |
| package-name: html-to-markdown-cli | |
| binary-name: html-to-markdown | |
| target: ${{ matrix.target }} | |
| - name: Package CLI artifact | |
| # Inline: archive layout (.tar.gz on Unix, .zip on Windows) varies per | |
| # OS in a way no shared action covers; tiny and self-contained. | |
| env: | |
| BINARY_PATH: ${{ steps.build.outputs.binary-path }} | |
| TARGET: ${{ matrix.target }} | |
| shell: bash | |
| run: | | |
| STAGE="cli-${TARGET}" | |
| mkdir -p "$STAGE" | |
| cp LICENSE "$STAGE/" 2>/dev/null || true | |
| cp README.md "$STAGE/" 2>/dev/null || true | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| cp "$BINARY_PATH" "$STAGE/html-to-markdown.exe" | |
| 7z a "${STAGE}.zip" "$STAGE" >/dev/null | |
| else | |
| cp "$BINARY_PATH" "$STAGE/html-to-markdown" | |
| tar -czf "${STAGE}.tar.gz" "$STAGE" | |
| fi | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: cli-${{ matrix.target }} | |
| path: | | |
| cli-${{ matrix.target }}.tar.gz | |
| cli-${{ matrix.target }}.zip | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| go-ffi-libraries: | |
| name: Build Go FFI library (${{ matrix.platform }}) | |
| needs: [prepare, check-go] | |
| if: ${{ needs.prepare.outputs.release_go == 'true' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && (needs.check-go.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, platform: linux-x64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, platform: linux-arm64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, platform: darwin-arm64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, platform: darwin-x64, target: x86_64-apple-darwin } | |
| - { os: windows-latest, platform: windows-x64, target: x86_64-pc-windows-msvc } | |
| - { os: windows-11-arm, platform: windows-arm64, target: aarch64-pc-windows-msvc } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-go-${{ matrix.platform }} | |
| - uses: kreuzberg-dev/actions/install-alef@v1 | |
| - name: Build and package Go FFI library | |
| # Inline: html-to-markdown's FFI build is driven by `alef publish` rather | |
| # than the generic build-go-ffi action — the alef driver knows about | |
| # workspace features, header generation, and the platform-specific | |
| # cgo include paths the package layout expects. | |
| shell: bash | |
| run: | | |
| alef publish build --lang ffi --target ${{ matrix.target }} | |
| alef publish package --lang go --target ${{ matrix.target }} -o dist/go-ffi | |
| # Workaround: alef 0.19.6 packages Go FFI archives with the same | |
| # `-ffi-` infix as the C FFI packager, which collides with the | |
| # `html-to-markdown-rs-ffi-*` asset-prefix probe used by check-go and | |
| # the verify-release-assets pattern list. Rename `-ffi-v` → `-go-v` so | |
| # downstream gating can distinguish Go from C FFI tarballs. Drop this | |
| # block once h2m's alef.toml moves to a release that ships the | |
| # alef-side fix (alef/src/publish/package/go.rs already uses `-go-` | |
| # on main). | |
| shopt -s nullglob | |
| for archive in dist/go-ffi/*-ffi-v*.tar.gz; do | |
| renamed="${archive/-ffi-v/-go-v}" | |
| mv "$archive" "$renamed" | |
| done | |
| shopt -u nullglob | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: go-ffi-${{ matrix.platform }} | |
| path: dist/go-ffi | |
| retention-days: 14 | |
| c-ffi-libraries: | |
| name: Build C FFI distribution (${{ matrix.platform }}) | |
| needs: [prepare, check-c-ffi] | |
| if: ${{ needs.prepare.outputs.release_c_ffi == 'true' && (needs.check-c-ffi.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, platform: linux-x64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, platform: linux-arm64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, platform: darwin-arm64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, platform: darwin-x64, target: x86_64-apple-darwin } | |
| - { os: windows-latest, platform: windows-x64, target: x86_64-pc-windows-msvc } | |
| - { os: windows-11-arm, platform: windows-arm64, target: aarch64-pc-windows-msvc } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-c-ffi-${{ matrix.platform }} | |
| - uses: kreuzberg-dev/actions/install-alef@v1 | |
| - name: Build and package C FFI distribution | |
| # Inline: alef-driven FFI packaging emits the pkgconfig + cmake metadata | |
| # the homebrew formula expects. The generic build-rust-ffi action | |
| # produces only the .so/.dylib/.dll without that scaffolding. | |
| shell: bash | |
| run: | | |
| alef publish build --lang ffi --target ${{ matrix.target }} | |
| alef publish package --lang ffi --target ${{ matrix.target }} -o dist/c-ffi | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: c-ffi-${{ matrix.platform }} | |
| path: dist/c-ffi | |
| retention-days: 14 | |
| swift-artifactbundle: | |
| name: Build Swift Artifact Bundle | |
| needs: [prepare] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.prepare.outputs.release_swift == 'true' }} | |
| runs-on: macos-latest | |
| timeout-minutes: 180 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| cache-key-prefix: publish-swift-artifactbundle | |
| - uses: kreuzberg-dev/actions/build-swift-artifactbundle@v1 | |
| id: build | |
| with: | |
| crate-name: html-to-markdown-rs-swift | |
| artifact-name: HtmlToMarkdown-rs | |
| header-path: target/release/build/html-to-markdown-rs-swift-*/out | |
| output-dir: dist/swift-artifactbundle | |
| build-profile: release | |
| - name: Record checksum | |
| id: checksum | |
| run: | | |
| CHECKSUM=$(cat "${{ steps.build.outputs.bundle-zip }}.sha256" 2>/dev/null || swift package compute-checksum "${{ steps.build.outputs.bundle-zip }}") | |
| echo "checksum=${CHECKSUM}" >> "$GITHUB_OUTPUT" | |
| echo "Artifact bundle checksum: ${CHECKSUM}" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: swift-artifactbundle | |
| path: dist/swift-artifactbundle | |
| retention-days: 14 | |
| - name: Save checksum as artifact | |
| run: | | |
| mkdir -p dist/swift-checksums | |
| echo "${{ steps.checksum.outputs.checksum }}" > dist/swift-checksums/HtmlToMarkdown-rs.artifactbundle.zip.checksum | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: swift-checksum | |
| path: dist/swift-checksums/ | |
| retention-days: 14 | |
| java-natives: | |
| name: Build Java FFI library (${{ matrix.platform }}) | |
| needs: [prepare, check-maven] | |
| if: ${{ needs.prepare.outputs.release_java == 'true' && (needs.check-maven.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, platform: linux-x86_64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, platform: linux-aarch64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, platform: osx-aarch64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, platform: osx-x86_64, target: x86_64-apple-darwin } | |
| - { os: windows-latest, platform: windows-x86_64, target: x86_64-pc-windows-msvc } | |
| - { os: windows-11-arm, platform: windows-aarch64, target: aarch64-pc-windows-msvc } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-java-${{ matrix.platform }} | |
| - uses: kreuzberg-dev/actions/install-alef@v1 | |
| - name: Build and stage FFI library | |
| # Inline: html-to-markdown reuses the C FFI library for Java (Panama), | |
| # so it builds via `alef publish build --lang ffi` and stages the .so/ | |
| # .dylib/.dll into a per-platform directory the publish-maven job | |
| # collects into Maven resources. The build-java-natives shared action | |
| # targets repos with a dedicated jni crate, which we don't have. | |
| shell: bash | |
| run: | | |
| alef publish build --lang ffi --target ${{ matrix.target }} | |
| mkdir -p "dist/java-ffi/${{ matrix.platform }}/native" | |
| for src in "target/${{ matrix.target }}/release" "target/release"; do | |
| [[ -d "$src" ]] || continue | |
| find "$src" -maxdepth 1 -type f \( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \) -name '*html_to_markdown_ffi*' -exec cp {} "dist/java-ffi/${{ matrix.platform }}/native/" \; | |
| done | |
| ls -la "dist/java-ffi/${{ matrix.platform }}/native/" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: java-ffi-${{ matrix.platform }} | |
| path: dist/java-ffi | |
| if-no-files-found: error | |
| retention-days: 14 | |
| csharp-natives: | |
| name: Build C# FFI library (${{ matrix.rid }}) | |
| needs: [prepare, check-nuget] | |
| if: ${{ needs.prepare.outputs.release_csharp == 'true' && (needs.check-nuget.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, rid: linux-x64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, rid: linux-arm64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, rid: osx-arm64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, rid: osx-x64, target: x86_64-apple-darwin } | |
| - { os: windows-latest, rid: win-x64, target: x86_64-pc-windows-msvc } | |
| - { os: windows-11-arm, rid: win-arm64, target: aarch64-pc-windows-msvc } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-csharp-${{ matrix.rid }} | |
| - uses: kreuzberg-dev/actions/install-alef@v1 | |
| - name: Build and stage FFI library | |
| # Inline: html-to-markdown reuses the C FFI library for C# (P/Invoke), | |
| # so it builds via `alef publish build --lang ffi` and stages the .so/ | |
| # .dylib/.dll into runtimes/<rid>/native/ for NuGet packaging. The | |
| # build-csharp-natives shared action targets repos with a dedicated | |
| # csharp interop crate, which we don't have. | |
| shell: bash | |
| run: | | |
| alef publish build --lang ffi --target ${{ matrix.target }} | |
| mkdir -p "dist/csharp-ffi/${{ matrix.rid }}/native" | |
| for src in "target/${{ matrix.target }}/release" "target/release"; do | |
| [[ -d "$src" ]] || continue | |
| find "$src" -maxdepth 1 -type f \( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \) -name '*html_to_markdown_ffi*' -exec cp {} "dist/csharp-ffi/${{ matrix.rid }}/native/" \; | |
| done | |
| ls -la "dist/csharp-ffi/${{ matrix.rid }}/native/" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: csharp-ffi-${{ matrix.rid }} | |
| path: dist/csharp-ffi | |
| if-no-files-found: error | |
| retention-days: 14 | |
| elixir-natives: | |
| name: Build Elixir NIF (${{ matrix.label }}) | |
| needs: [prepare, check-elixir-release, publish-crates] | |
| if: ${{ !cancelled() && needs.publish-crates.result != 'failure' && needs.prepare.outputs.release_elixir == 'true' && (needs.check-elixir-release.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, label: linux-x86_64, target: x86_64-unknown-linux-gnu } | |
| - { os: ubuntu-24.04-arm, label: linux-aarch64, target: aarch64-unknown-linux-gnu } | |
| - { os: macos-latest, label: macos-arm64, target: aarch64-apple-darwin } | |
| - { os: macos-15-intel, label: macos-x86_64, target: x86_64-apple-darwin } | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| uses: kreuzberg-dev/actions/free-disk-space-linux@v1 | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| cache-key-prefix: publish-elixir-${{ matrix.label }} | |
| # Inline build: rewrite the NIF crate's workspace path-deps to registry | |
| # version-deps so the hex source-fallback resolves on a consumer. | |
| - name: Rewrite native dependencies to registry versions | |
| uses: kreuzberg-dev/actions/rewrite-native-deps@v1 | |
| with: | |
| lang: elixir | |
| - name: Build NIF | |
| # Inline: rustler_precompiled expects flat .so/.dylib artifacts named | |
| # libhtml_to_markdown_nif-vVERSION-nif-NIF-TARGET. We build once and | |
| # tarball under both supported NIF API versions (2.16 and 2.17). | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| shell: bash | |
| run: | | |
| cargo build --release --locked --target "${TARGET}" \ | |
| --manifest-path packages/elixir/native/html_to_markdown_nif/Cargo.toml | |
| NIF_DIR="packages/elixir/native/html_to_markdown_nif" | |
| # Source library extension on disk (set by cargo's cdylib output). | |
| if [[ "${RUNNER_OS}" == "macOS" ]]; then | |
| SRC_EXT="dylib" | |
| elif [[ "${RUNNER_OS}" == "Windows" ]]; then | |
| SRC_EXT="dll" | |
| else | |
| SRC_EXT="so" | |
| fi | |
| # Published artifact extension. rustler_precompiled (<= 0.9) hardcodes | |
| # `dll` for windows and `so` for everything else (including macOS) in | |
| # its download URL — see `lib_name_with_ext/2`. If we publish a .dylib | |
| # tarball, consumers 404 on darwin. Normalize to `so` on non-windows. | |
| if [[ "${RUNNER_OS}" == "Windows" ]]; then | |
| ARTIFACT_EXT="dll" | |
| else | |
| ARTIFACT_EXT="so" | |
| fi | |
| LIB_NAME="libhtml_to_markdown_nif.${SRC_EXT}" | |
| LIB_PATH="${NIF_DIR}/target/${TARGET}/release/${LIB_NAME}" | |
| [[ -f "$LIB_PATH" ]] || LIB_PATH="${NIF_DIR}/target/release/${LIB_NAME}" | |
| mkdir -p dist/elixir | |
| for NIF_VERSION in 2.16 2.17; do | |
| ARTIFACT="libhtml_to_markdown_nif-v${VERSION}-nif-${NIF_VERSION}-${TARGET}.${ARTIFACT_EXT}" | |
| cp "$LIB_PATH" "${ARTIFACT}" | |
| tar -czf "dist/elixir/${ARTIFACT}.tar.gz" "${ARTIFACT}" | |
| rm -f "${ARTIFACT}" | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: elixir-${{ matrix.label }} | |
| path: dist/elixir/*.tar.gz | |
| if-no-files-found: error | |
| retention-days: 14 | |
| elixir-package: | |
| name: Build Elixir Hex package | |
| needs: [prepare, check-hex, publish-crates] | |
| # Must wait for `publish-crates`: the shared `build-elixir-hex` action | |
| # rewrites the NIF's `Cargo.toml` path-dep on `html-to-markdown-rs` to a | |
| # registry version-dep and runs `cargo generate-lockfile` against it. If | |
| # publish-crates hasn't published the new version to crates.io yet, the | |
| # lockfile resolution fails with `failed to select a version for the | |
| # requirement \`html-to-markdown-rs = "^X.Y.Z"\``. Mirrors the gate used | |
| # by python-sdist and ruby-gem. | |
| if: ${{ !cancelled() && needs.publish-crates.result != 'failure' && needs.prepare.outputs.release_elixir == 'true' && (needs.check-hex.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| env: | |
| MIX_ENV: dev | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: erlef/setup-beam@v1 | |
| with: | |
| elixir-version: "1.19" | |
| otp-version: "28.1" | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| - uses: kreuzberg-dev/actions/build-elixir-hex@v1 | |
| with: | |
| package-dir: packages/elixir | |
| nif-crate-path: packages/elixir/native/html_to_markdown_nif | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: elixir-hex-package | |
| path: packages/elixir/html_to_markdown-*.tar | |
| retention-days: 14 | |
| cargo-packages: | |
| name: Package Rust crates | |
| needs: [prepare, check-cratesio] | |
| if: ${{ needs.prepare.outputs.release_crates == 'true' && (needs.check-cratesio.outputs.all_exist != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| - name: Package crates | |
| # Inline: cargo package both crates into crate-artifacts/. The CLI | |
| # crate's package step requires html-to-markdown-rs to already be | |
| # registered, so we tolerate failure with --no-verify and fall back | |
| # to publish-time packaging if needed. | |
| env: | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| shell: bash | |
| run: | | |
| cargo package -p html-to-markdown-rs --allow-dirty | |
| cli_status=0 | |
| cargo package -p html-to-markdown-cli --allow-dirty --no-verify || cli_status=$? | |
| mkdir -p crate-artifacts | |
| cp target/package/html-to-markdown-rs-*.crate crate-artifacts/ | |
| if [[ "${cli_status}" -eq 0 ]]; then | |
| cp target/package/html-to-markdown-cli-*.crate crate-artifacts/ | |
| else | |
| echo "::warning::Skipping html-to-markdown-cli crate packaging; html-to-markdown-rs ${RELEASE_VERSION} is not yet on crates.io." | |
| fi | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: cargo-crates | |
| path: crate-artifacts/*.crate | |
| retention-days: 14 | |
| # ─── GitHub Release asset uploads ───────────────────────────────────── | |
| upload-cli-release: | |
| name: Upload CLI binaries to GitHub Release | |
| needs: [prepare, cli-binaries] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.cli-binaries.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: cli-* | |
| path: dist/cli | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/cli | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| cli-*.tar.gz | |
| cli-*.zip | |
| upload-go-release: | |
| name: Upload Go FFI archives to GitHub Release | |
| needs: [prepare, go-ffi-libraries] | |
| if: ${{ needs.prepare.outputs.release_go == 'true' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.go-ffi-libraries.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: go-ffi-* | |
| path: dist/go-ffi | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/go-ffi | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| **/*.tar.gz | |
| **/*.zip | |
| upload-c-ffi-release: | |
| name: Upload C FFI archives to GitHub Release | |
| needs: [prepare, c-ffi-libraries] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.c-ffi-libraries.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: c-ffi-* | |
| path: dist/c-ffi | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/c-ffi | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| **/*.tar.gz | |
| **/*.zip | |
| upload-swift-release: | |
| name: Upload Swift Artifact Bundle to GitHub Release | |
| needs: [prepare, swift-artifactbundle] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.swift-artifactbundle.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: swift-* | |
| path: dist/swift | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/swift | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| **/*.zip | |
| **/*.checksum | |
| upload-elixir-release: | |
| name: Upload Elixir NIF archives to GitHub Release | |
| needs: [prepare, elixir-natives] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.elixir-natives.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: elixir-* | |
| path: dist/elixir | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/elixir | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: libhtml_to_markdown_nif-*.tar.gz | |
| upload-php-pie-release: | |
| name: Upload PHP PIE archives to GitHub Release | |
| needs: [prepare, php-extension] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.php-extension.result != 'cancelled' && needs.php-extension.result != 'skipped' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: php-package-* | |
| path: dist/php-package | |
| merge-multiple: true | |
| # Fail fast if zero PIE archives reach this aggregator — a silently | |
| # empty upload (the old failure mode that orphaned v3.6.11's PIE assets, | |
| # see #333) now hard-errors instead of producing an empty release surface. | |
| if-no-files-found: error | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/php-package | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| php_*.tgz | |
| php_*.tgz.sha256 | |
| php_*.zip | |
| php_*.zip.sha256 | |
| # ─── Publish jobs ───────────────────────────────────────────────────── | |
| publish-crates: | |
| name: Publish crates.io packages | |
| needs: [prepare, validate-versions, cargo-packages, check-cratesio] | |
| if: ${{ needs.prepare.outputs.release_crates == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.cargo-packages.result == 'success' && (needs.check-cratesio.outputs.all_exist != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| cache-key-prefix: publish-crates | |
| - uses: kreuzberg-dev/actions/publish-crates@v1 | |
| with: | |
| # Order matters: html-to-markdown-rs is depended on by html-to-markdown-cli. | |
| crates: html-to-markdown-rs html-to-markdown-cli | |
| version: ${{ needs.prepare.outputs.version }} | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| publish-pypi: | |
| name: Publish to PyPI | |
| needs: [prepare, python-wheels, python-sdist, check-pypi] | |
| if: ${{ needs.prepare.outputs.release_python == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.python-wheels.result == 'success' && needs.python-sdist.result == 'success' && (needs.check-pypi.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: python-wheels-* | |
| path: dist | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: python-sdist | |
| path: dist | |
| - uses: kreuzberg-dev/actions/publish-pypi@v1 | |
| with: | |
| packages-dir: dist | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| publish-rubygems: | |
| name: Publish Ruby gems | |
| needs: [prepare, ruby-gem, check-rubygems] | |
| if: ${{ needs.prepare.outputs.release_ruby == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.ruby-gem.result == 'success' && (needs.check-rubygems.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: rubygems-* | |
| path: dist | |
| merge-multiple: true | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.3" | |
| bundler-cache: false | |
| - name: Update RubyGems | |
| run: gem update --system | |
| shell: bash | |
| - uses: rubygems/configure-rubygems-credentials@v2.0.0 | |
| - uses: kreuzberg-dev/actions/publish-rubygems@v1 | |
| with: | |
| gems-dir: dist | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| publish-node: | |
| name: Publish Node packages | |
| needs: [prepare, node-bindings, node-typescript-defs, check-npm] | |
| if: ${{ needs.prepare.outputs.release_node == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.node-bindings.result == 'success' && needs.node-typescript-defs.result == 'success' && (needs.check-npm.outputs.node_exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: node-bindings-* | |
| path: node-artifacts | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: node-typescript-defs | |
| path: typescript-defs | |
| - uses: kreuzberg-dev/actions/setup-node-workspace@v1 | |
| with: | |
| node-version: "24" | |
| registry-url: "https://registry.npmjs.org/" | |
| - name: Stage platform packages and main package | |
| # Inline: html-to-markdown's per-platform Node artifact layout is bespoke | |
| # (one tarball per target containing pre-staged npm/<platform>/) and the | |
| # main package needs an injected optionalDependencies map listing every | |
| # platform sub-package. Tightly coupled to napi-rs + repo conventions; | |
| # the generic publish-npm action operates on a single package dir. | |
| shell: bash | |
| run: | | |
| rm -rf crates/html-to-markdown-node/npm | |
| mkdir -p crates/html-to-markdown-node/npm | |
| for pkg in node-artifacts/*.tar.gz; do | |
| tar -xzf "${pkg}" -C crates/html-to-markdown-node | |
| done | |
| cp typescript-defs/index.js typescript-defs/index.d.ts crates/html-to-markdown-node/ | |
| # Pack each platform sub-package as a tarball | |
| cd crates/html-to-markdown-node/npm | |
| for dir in */; do | |
| if [ -f "$dir/package.json" ]; then | |
| (cd "$dir" && npm pack && mv ./*.tgz ..) | |
| fi | |
| done | |
| cd ../../.. | |
| # Stamp main package's optionalDependencies with all platform packages | |
| tmp_pkg_json="$(mktemp)" | |
| optional_deps_json='{}' | |
| for platform_pkg_json in crates/html-to-markdown-node/npm/*/package.json; do | |
| name="$(jq -r '.name // empty' "$platform_pkg_json")" | |
| version="$(jq -r '.version // empty' "$platform_pkg_json")" | |
| [[ -z "$name" || -z "$version" ]] && { echo "Invalid: $platform_pkg_json" >&2; exit 1; } | |
| optional_deps_json="$(jq -c --arg n "$name" --arg v "$version" '. + {($n): $v}' <<<"$optional_deps_json")" | |
| done | |
| jq --argjson deps "$optional_deps_json" '.optionalDependencies = $deps' \ | |
| crates/html-to-markdown-node/package.json > "$tmp_pkg_json" | |
| mv "$tmp_pkg_json" crates/html-to-markdown-node/package.json | |
| - name: Publish platform sub-packages | |
| uses: kreuzberg-dev/actions/publish-npm@v1 | |
| with: | |
| packages-dir: crates/html-to-markdown-node/npm | |
| npm-tag: ${{ needs.prepare.outputs.npm_tag }} | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| - name: Wait for npm indexing (linux-x64-gnu) | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| uses: kreuzberg-dev/actions/wait-for-package@v1 | |
| with: | |
| registry: npm | |
| package: "@kreuzberg/html-to-markdown-node-linux-x64-gnu" | |
| version: ${{ needs.prepare.outputs.version }} | |
| max-attempts: "60" | |
| - name: Wait for npm indexing (linux-arm64-gnu) | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| uses: kreuzberg-dev/actions/wait-for-package@v1 | |
| with: | |
| registry: npm | |
| package: "@kreuzberg/html-to-markdown-node-linux-arm64-gnu" | |
| version: ${{ needs.prepare.outputs.version }} | |
| max-attempts: "60" | |
| - name: Publish main @kreuzberg/html-to-markdown-node package | |
| uses: kreuzberg-dev/actions/publish-npm@v1 | |
| with: | |
| package-dir: crates/html-to-markdown-node | |
| npm-tag: ${{ needs.prepare.outputs.npm_tag }} | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| - name: Wait for main Node package indexing | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' && needs.check-npm.outputs.ts_exists != 'true' }} | |
| uses: kreuzberg-dev/actions/wait-for-package@v1 | |
| with: | |
| registry: npm | |
| package: "@kreuzberg/html-to-markdown-node" | |
| version: ${{ needs.prepare.outputs.version }} | |
| max-attempts: "60" | |
| - name: Build TypeScript wrapper package | |
| if: ${{ needs.check-npm.outputs.ts_exists != 'true' }} | |
| # Inline: pnpm tsc + workspace:* rewrite so npm publish (used by the | |
| # publish-npm shared action) ships an installable package. Tightly | |
| # coupled to packages/typescript layout and pnpm workspace conventions. | |
| shell: bash | |
| run: | | |
| (cd packages/typescript && pnpm install --no-frozen-lockfile && pnpm exec tsc --project tsconfig.json) | |
| version="$(node -p "require('./packages/typescript/package.json').version")" | |
| PKG_VERSION="$version" node -e ' | |
| const fs = require("node:fs"); | |
| const path = "packages/typescript/package.json"; | |
| const pkg = JSON.parse(fs.readFileSync(path, "utf8")); | |
| const ver = process.env.PKG_VERSION; | |
| for (const field of ["dependencies", "peerDependencies", "optionalDependencies"]) { | |
| if (pkg[field]) { | |
| for (const [name, spec] of Object.entries(pkg[field])) { | |
| if (typeof spec === "string" && spec.startsWith("workspace:")) { | |
| pkg[field][name] = ver; | |
| } | |
| } | |
| } | |
| } | |
| fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); | |
| ' | |
| - name: Publish TypeScript wrapper package | |
| if: ${{ needs.check-npm.outputs.ts_exists != 'true' }} | |
| uses: kreuzberg-dev/actions/publish-npm@v1 | |
| with: | |
| package-dir: packages/typescript | |
| npm-tag: ${{ needs.prepare.outputs.npm_tag }} | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| publish-wasm: | |
| name: Publish WASM package | |
| needs: [prepare, wasm-bindings, check-wasm] | |
| if: ${{ needs.prepare.outputs.release_wasm == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.wasm-bindings.result == 'success' && (needs.check-wasm.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: wasm-bundle | |
| path: crates/html-to-markdown-wasm | |
| - name: Drop dist .gitignore files | |
| # Inline: wasm-pack writes .gitignore into each dist dir which trips up | |
| # `npm publish`. One-shot cleanup. | |
| shell: bash | |
| run: | | |
| rm -f crates/html-to-markdown-wasm/dist/.gitignore | |
| rm -f crates/html-to-markdown-wasm/dist-node/.gitignore | |
| rm -f crates/html-to-markdown-wasm/dist-web/.gitignore | |
| - uses: kreuzberg-dev/actions/setup-node-workspace@v1 | |
| with: | |
| node-version: "24" | |
| registry-url: "https://registry.npmjs.org/" | |
| - uses: kreuzberg-dev/actions/publish-npm@v1 | |
| with: | |
| package-dir: crates/html-to-markdown-wasm | |
| npm-tag: ${{ needs.prepare.outputs.npm_tag }} | |
| provenance: "true" | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| publish-packagist: | |
| name: Trigger Packagist update | |
| needs: [prepare, upload-php-pie-release, check-packagist] | |
| if: ${{ needs.prepare.outputs.release_php == 'true' && needs.prepare.outputs.is_tag == 'true' && (needs.check-packagist.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: kreuzberg-dev/actions/publish-packagist@v1 | |
| with: | |
| packagist-username: kreuzberg-dev | |
| package-name: kreuzberg-dev/html-to-markdown | |
| version: ${{ needs.prepare.outputs.version }} | |
| repository-url: https://github.com/kreuzberg-dev/html-to-markdown | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| env: | |
| PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }} | |
| publish-maven: | |
| name: Publish Maven (Java) package | |
| needs: [prepare, java-natives, check-maven] | |
| if: ${{ needs.prepare.outputs.release_java == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.java-natives.result == 'success' && (needs.check-maven.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: java-ffi-* | |
| path: java-ffi-artifacts | |
| merge-multiple: true | |
| - name: Stage natives into resources | |
| # Inline: copy classifier-bucketed libs from artifact tree into | |
| # packages/java/src/main/resources/natives/<classifier>/. Tightly | |
| # coupled to the Maven resource layout — no shared action covers it. | |
| shell: bash | |
| run: | | |
| mkdir -p packages/java/src/main/resources/natives | |
| rm -rf packages/java/src/main/resources/natives/* | |
| for dir in java-ffi-artifacts/*/; do | |
| classifier="$(basename "$dir")" | |
| mkdir -p "packages/java/src/main/resources/natives/${classifier}" | |
| cp -r "${dir}native/." "packages/java/src/main/resources/natives/${classifier}/" | |
| done | |
| - uses: actions/setup-java@v5.2.0 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| cache: maven | |
| server-id: ossrh | |
| server-username: MAVEN_USERNAME | |
| server-password: MAVEN_PASSWORD | |
| gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} | |
| gpg-passphrase: MAVEN_GPG_PASSPHRASE | |
| - uses: kreuzberg-dev/actions/setup-maven@v1 | |
| - uses: kreuzberg-dev/actions/configure-maven-gpg@v1 | |
| with: | |
| pom-file: packages/java/pom.xml | |
| - uses: kreuzberg-dev/actions/publish-maven@v1 | |
| with: | |
| pom-file: packages/java/pom.xml | |
| maven-profile: publish | |
| extra-args: -DskipTests | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| env: | |
| MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} | |
| MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| publish-nuget: | |
| name: Publish NuGet package | |
| needs: [prepare, csharp-natives, check-nuget] | |
| if: ${{ needs.prepare.outputs.release_csharp == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.csharp-natives.result == 'success' && (needs.check-nuget.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: csharp-ffi-* | |
| path: dist/csharp-ffi | |
| merge-multiple: true | |
| - name: Stage runtimes/<rid>/native into NuGet project | |
| # Inline: NuGet expects native libs in runtimes/<rid>/native/ adjacent | |
| # to the project. Copy from artifact tree into the C# project. | |
| shell: bash | |
| run: | | |
| mkdir -p packages/csharp/HtmlToMarkdown/runtimes | |
| for dir in dist/csharp-ffi/*/; do | |
| rid="$(basename "$dir")" | |
| mkdir -p "packages/csharp/HtmlToMarkdown/runtimes/${rid}/native" | |
| cp -v "${dir}native/"* "packages/csharp/HtmlToMarkdown/runtimes/${rid}/native/" || true | |
| done | |
| - uses: actions/setup-dotnet@v5.2.0 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Pack NuGet package | |
| # Inline: dotnet pack with the version override against the inner csproj. | |
| shell: bash | |
| run: | | |
| dotnet restore packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj | |
| dotnet pack packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj \ | |
| -c Release \ | |
| -p:Version=${{ needs.prepare.outputs.version }} \ | |
| -o dist/nuget | |
| - uses: kreuzberg-dev/actions/publish-nuget@v1 | |
| with: | |
| packages-dir: dist/nuget | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| env: | |
| NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} | |
| publish-hex: | |
| name: Publish Hex package | |
| needs: [prepare, elixir-package, upload-elixir-release, check-hex] | |
| # Skipped on dry-run: generate-elixir-checksums downloads NIF tarballs from | |
| # the GitHub Release, which doesn't exist on dry-run (upload-release-assets | |
| # only logs in dry-run mode). Real-release runs exercise this path. | |
| if: ${{ needs.prepare.outputs.release_elixir == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.prepare.outputs.dry_run != 'true' && needs.elixir-package.result == 'success' && (needs.check-hex.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| # App token with contents:write so generate-elixir-checksums can | |
| # download NIF assets from the draft release via the authenticated | |
| # gh release download path. The default github.token (contents:read | |
| # at workflow scope) sees only published releases. | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: erlef/setup-beam@v1 | |
| with: | |
| elixir-version: "1.19" | |
| otp-version: "28.1" | |
| - name: Generate NIF checksum file | |
| uses: kreuzberg-dev/actions/generate-elixir-checksums@v1 | |
| with: | |
| github-repo: ${{ github.repository }} | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| lib-name: html_to_markdown_nif | |
| nif-versions: "2.16,2.17" | |
| targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,aarch64-apple-darwin,x86_64-apple-darwin | |
| output-path: packages/elixir/checksum-Elixir.HtmlToMarkdown.Native.exs | |
| token: ${{ steps.app-token.outputs.token }} | |
| - uses: kreuzberg-dev/actions/publish-hex@v1 | |
| with: | |
| package-dir: packages/elixir | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| env: | |
| HEX_API_KEY: ${{ secrets.HEX_API_KEY }} | |
| kotlin-android-natives: | |
| name: Build Kotlin Android natives (${{ matrix.abi }}) | |
| needs: [prepare] | |
| if: ${{ needs.prepare.outputs.release_kotlin == 'true' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| abi: [arm64-v8a, x86_64] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: kreuzberg-dev/actions/setup-rust@v1 | |
| with: | |
| targets: aarch64-linux-android,x86_64-linux-android | |
| - uses: android-actions/setup-android@v4.0.1 | |
| - uses: kreuzberg-dev/actions/build-android-natives@v1 | |
| with: | |
| crate-name: html-to-markdown-ffi | |
| lib-name: html_to_markdown_ffi | |
| abis: ${{ matrix.abi }} | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: kotlin-android-${{ matrix.abi }} | |
| path: target/android-libs/${{ matrix.abi }}/libhtml_to_markdown_ffi.so | |
| retention-days: 7 | |
| publish-kotlin-android: | |
| name: Publish Kotlin Android to Maven Central | |
| needs: [prepare, kotlin-android-natives, check-maven-kotlin] | |
| if: ${{ needs.prepare.outputs.release_kotlin == 'true' && needs.prepare.outputs.is_tag == 'true' && (needs.check-maven-kotlin.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: kotlin-android-* | |
| path: kotlin-android-artifacts | |
| merge-multiple: true | |
| - name: Stage Android natives into Kotlin Android module | |
| shell: bash | |
| run: | | |
| for abi in arm64-v8a x86_64; do | |
| dest="packages/kotlin-android/src/main/jniLibs/${abi}" | |
| mkdir -p "$dest" | |
| src="kotlin-android-artifacts/${abi}/libhtml_to_markdown_ffi.so" | |
| if [[ -f "$src" ]]; then | |
| cp -v "$src" "$dest/" | |
| fi | |
| done | |
| - uses: kreuzberg-dev/actions/publish-maven-gradle@v1 | |
| with: | |
| working-directory: packages/kotlin-android | |
| gradle-task: publishAndReleaseToMavenCentral | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| env: | |
| MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} | |
| MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} | |
| MAVEN_GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| trigger-pubdev: | |
| name: Trigger pub.dev publish workflow | |
| needs: [prepare, check-pub, assemble-dart-package] | |
| # pub.dev OIDC rejects tokens from `release` events. We dispatch a separate | |
| # workflow whose `workflow_dispatch` event produces an accepted token. | |
| if: ${{ needs.prepare.outputs.release_dart == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.prepare.outputs.dry_run != 'true' && (needs.check-pub.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - name: Dispatch publish-pubdev workflow | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| # pub.dev OIDC validation requires refType=tag on the token. When this | |
| # workflow is triggered by `release: published`, github.ref_name is the | |
| # release-creating branch (e.g. main), which yields refType=branch and | |
| # pub.dev rejects with "publishing is only allowed from 'tag' refType". | |
| # Dispatch against the actual tag instead. | |
| gh workflow run publish-pubdev.yaml \ | |
| --ref ${{ needs.prepare.outputs.tag }} \ | |
| -f run_id=${{ github.run_id }} | |
| publish-zig: | |
| name: Publish Zig package metadata | |
| needs: [prepare, check-zig, assemble-zig-package] | |
| if: ${{ needs.prepare.outputs.release_zig == 'true' && needs.prepare.outputs.is_tag == 'true' && (needs.check-zig.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') && needs.assemble-zig-package.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref }} | |
| submodules: recursive | |
| token: ${{ steps.app-token.outputs.token }} | |
| persist-credentials: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: zig-package-assembled | |
| path: . | |
| - uses: kreuzberg-dev/actions/publish-zig@v1 | |
| with: | |
| working-directory: packages/zig | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| # Override the auto-detected package-name (`html_to_markdown_rs` from | |
| # build.zig.zon) to match the alef-emitted test_app URL pattern | |
| # `{crate-name}-zig-v{version}.tar.gz`. Without this, the asset | |
| # uploads as `html_to_markdown_rs-v{version}.tar.gz` while | |
| # test_apps/zig fetches `html-to-markdown-rs-zig-v{version}.tar.gz` | |
| # → 404 + consumer build.zig.zon TODO hash. | |
| package-name: html-to-markdown-rs-zig | |
| update-release-notes: "true" | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| # Pass the App token explicitly so the action sees the draft | |
| # release. The action's step-level env: GH_TOKEN: ${{ inputs.token }} | |
| # overrides any job-level GH_TOKEN, so the env: below would | |
| # otherwise be ignored. | |
| token: ${{ steps.app-token.outputs.token }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| # ─── Promote release out of draft (early) ───────────────────────────── | |
| # | |
| # homebrew-bottles runs `brew install --build-bottle` which fetches the CLI | |
| # source tarball from the formula's URL block (https://github.com/.../releases/ | |
| # download/<tag>/cli-<triple>.tar.gz). Draft releases 404 on the public | |
| # download URL, so the bottle build cannot succeed while the release is still | |
| # draft. release-finalize at the end of the workflow is the canonical | |
| # draft→published promoter, but it gates on homebrew-bottles itself — a | |
| # deadlock. Promote the release here, once the CLI + FFI asset uploads have | |
| # landed, then let release-finalize re-edit draft=false idempotently as the | |
| # canonical home for Go-module tagging + prerelease flag. | |
| promote-release: | |
| name: Publish GitHub Release from draft | |
| needs: | |
| - prepare | |
| - upload-cli-release | |
| - upload-c-ffi-release | |
| - upload-go-release | |
| - upload-swift-release | |
| - upload-elixir-release | |
| - upload-php-pie-release | |
| - upload-r-release | |
| if: | | |
| needs.prepare.outputs.is_tag == 'true' && | |
| needs.prepare.outputs.dry_run != 'true' && | |
| needs.upload-cli-release.result == 'success' && | |
| needs.upload-c-ffi-release.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - name: Flip release out of draft | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: gh release edit "${{ needs.prepare.outputs.tag }}" --draft=false | |
| # ─── Homebrew (formula update + bottles) ────────────────────────────── | |
| # | |
| # Topology (mirrors kreuzberg's; html-to-markdown ships TWO formulas): | |
| # 1. publish-homebrew-formula — update Formula/{html-to-markdown,libhtml-to-markdown}.rb | |
| # (no bottles). Backed by the repo-quirky update-homebrew-formula.sh. | |
| # 2. homebrew-bottles — build per-platform bottle JSON manifests | |
| # 3. publish-homebrew-bottles — merge bottle DSL into formulas and push | |
| publish-homebrew-formula: | |
| name: Update Homebrew formulas | |
| needs: [prepare, check-homebrew, upload-cli-release, upload-c-ffi-release] | |
| if: ${{ needs.prepare.outputs.release_homebrew == 'true' && needs.upload-cli-release.result == 'success' && needs.upload-c-ffi-release.result == 'success' && (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && (needs.check-homebrew.outputs.exists != 'true' || needs.prepare.outputs.force_republish == 'true') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| # tap-token: scoped to homebrew-tap for the cross-repo checkout + push. | |
| - uses: actions/create-github-app-token@v2 | |
| id: tap-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| repositories: homebrew-tap | |
| # source-token: org-wide (no `repositories:`) so the homebrew action's | |
| # `gh release download` against html-to-markdown can see the draft | |
| # release. The tap-token above can't read this repo's releases. | |
| - uses: actions/create-github-app-token@v2 | |
| id: source-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: kreuzberg-dev/homebrew-tap | |
| token: ${{ steps.tap-token.outputs.token }} | |
| path: homebrew-tap | |
| persist-credentials: true | |
| - uses: kreuzberg-dev/actions/publish-homebrew-source-formulas@v1 | |
| with: | |
| tap-dir: ${{ github.workspace }}/homebrew-tap | |
| config-file: scripts/publish/homebrew.json | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| github-repo: ${{ github.repository }} | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| token: ${{ steps.source-token.outputs.token }} | |
| - name: Commit and push to tap | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| working-directory: homebrew-tap | |
| env: | |
| GH_TOKEN: ${{ steps.tap-token.outputs.token }} | |
| # Inline: tap-side commit/push of the regenerated formulas. | |
| run: | | |
| git config user.name "kreuzberg-dev-publisher[bot]" | |
| git config user.email "291994444+kreuzberg-dev-publisher[bot]@users.noreply.github.com" | |
| if git diff --quiet Formula/; then | |
| echo "No formula changes; skipping commit." | |
| exit 0 | |
| fi | |
| git add Formula/html-to-markdown.rb Formula/libhtml-to-markdown.rb | |
| git commit -m "html-to-markdown ${{ needs.prepare.outputs.version }}" | |
| git push origin HEAD | |
| homebrew-bottles: | |
| name: Build Homebrew bottle (${{ matrix.bottle_tag }}) | |
| needs: [prepare, publish-homebrew-formula, promote-release] | |
| # Skipped on dry-run: `brew install --build-bottle` downloads CLI/FFI source | |
| # tarballs from the GitHub Release, which doesn't exist on dry-run (formula | |
| # is rendered locally only; tap isn't pushed). Real-release runs cover this. | |
| # Gates on promote-release so the release is published (not draft) by the | |
| # time `brew install` hits the formula's URL block; v3.6.0 saw all 4 bottles | |
| # 404 because release-finalize was the only draft→public promoter and it | |
| # waited on the bottles themselves. | |
| if: ${{ needs.prepare.outputs.release_homebrew == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.prepare.outputs.dry_run != 'true' && needs.publish-homebrew-formula.result == 'success' && needs.promote-release.result == 'success' }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: macos-latest, bottle_tag: arm64_sonoma, install_brew: false } | |
| - { os: macos-15-intel, bottle_tag: sonoma, install_brew: false } | |
| - { os: ubuntu-latest, bottle_tag: x86_64_linux, install_brew: true } | |
| - { os: ubuntu-24.04-arm, bottle_tag: arm64_linux, install_brew: true } | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| # Pinned to checkout@v5: v6 hits an includeIf credential regression on | |
| # macos-15-intel runners (same workaround liter-llm uses). Other jobs | |
| # stay on @v6; this matrix is the one that includes Intel macOS. | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| - name: Install Homebrew (Linux) | |
| if: ${{ matrix.install_brew }} | |
| uses: kreuzberg-dev/actions/install-homebrew-linux@v1 | |
| - name: Build bottles | |
| uses: kreuzberg-dev/actions/homebrew-build-bottles@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| tap: kreuzberg-dev/tap | |
| formulas: | | |
| html-to-markdown | |
| libhtml-to-markdown | |
| out-dir: ${{ github.workspace }}/bottle-json | |
| github-repo: kreuzberg-dev/html-to-markdown | |
| token: ${{ steps.app-token.outputs.token }} | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: homebrew-bottle-json-${{ matrix.bottle_tag }} | |
| path: ${{ github.workspace }}/bottle-json/*.json | |
| if-no-files-found: error | |
| retention-days: 14 | |
| publish-homebrew-bottles: | |
| name: Merge Homebrew bottle DSL | |
| needs: [prepare, publish-homebrew-formula, homebrew-bottles] | |
| if: ${{ needs.prepare.outputs.release_homebrew == 'true' && needs.prepare.outputs.is_tag == 'true' && needs.publish-homebrew-formula.result == 'success' && needs.homebrew-bottles.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: tap-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| repositories: homebrew-tap | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: kreuzberg-dev/homebrew-tap | |
| token: ${{ steps.tap-token.outputs.token }} | |
| path: homebrew-tap | |
| persist-credentials: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| path: ${{ github.workspace }}/bottle-json | |
| pattern: homebrew-bottle-json-* | |
| merge-multiple: true | |
| - uses: kreuzberg-dev/actions/homebrew-merge-bottles@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| tap-dir: ${{ github.workspace }}/homebrew-tap | |
| json-dir: ${{ github.workspace }}/bottle-json | |
| formulas: | | |
| html-to-markdown | |
| libhtml-to-markdown | |
| github-repo: kreuzberg-dev/html-to-markdown | |
| - name: Commit and push bottle DSL | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| working-directory: homebrew-tap | |
| env: | |
| GH_TOKEN: ${{ steps.tap-token.outputs.token }} | |
| # Inline: tap-side commit/push of the regenerated formula. | |
| run: | | |
| git config user.name "kreuzberg-dev-publisher[bot]" | |
| git config user.email "291994444+kreuzberg-dev-publisher[bot]@users.noreply.github.com" | |
| if git diff --quiet Formula/; then | |
| echo "No bottle DSL changes; skipping commit." | |
| exit 0 | |
| fi | |
| git add Formula/html-to-markdown.rb Formula/libhtml-to-markdown.rb | |
| git commit -m "html-to-markdown ${{ needs.prepare.outputs.version }}: add bottle DSL" | |
| git push origin HEAD | |
| # ─── Verification & finalization ────────────────────────────────────── | |
| verify-assets: | |
| name: Verify release assets | |
| needs: | |
| - prepare | |
| - upload-cli-release | |
| - upload-go-release | |
| - upload-c-ffi-release | |
| - upload-elixir-release | |
| - upload-php-pie-release | |
| if: ${{ always() && needs.prepare.outputs.is_tag == 'true' && needs.prepare.outputs.dry_run != 'true' && !contains(needs.*.result, 'failure') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| # App token with contents:write so the action's /releases listing can | |
| # see the draft release (release-finalize hasn't published it yet). | |
| # The default github.token has contents:read at workflow scope and so | |
| # only sees published releases — that's the source of the chronic | |
| # "release v3.6.0-rc.* not found after 20 attempts" failure. | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: kreuzberg-dev/actions/verify-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| token: ${{ steps.app-token.outputs.token }} | |
| expected-assets: | | |
| # CLI | |
| cli-*.tar.gz | |
| cli-*.zip | |
| # Go FFI | |
| html-to-markdown-rs-go-*.tar.gz | |
| # C FFI | |
| html-to-markdown-rs-ffi-*.tar.gz | |
| # Elixir NIF | |
| libhtml_to_markdown_nif-*.tar.gz | |
| update-swift-package-manifest: | |
| name: Update Swift Package.swift manifest with artifact URL and checksum | |
| needs: [prepare, upload-swift-release] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.upload-swift-release.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| persist-credentials: true | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: swift-checksum | |
| path: swift-checksums | |
| - name: Update Package.swift with version and checksum | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| VERSION="${{ needs.prepare.outputs.version }}" | |
| CHECKSUM=$(cat swift-checksums/*.checksum | tr -d '[:space:]') | |
| # Replace placeholders in Package.swift | |
| sed -i "s/__ALEF_SWIFT_VERSION__/${VERSION}/g" Package.swift | |
| sed -i "s/__ALEF_SWIFT_CHECKSUM__/${CHECKSUM}/g" Package.swift | |
| echo "Updated Package.swift:" | |
| head -30 Package.swift | |
| # Commit the substituted Package.swift back to the rc tag. | |
| # The checksum is only known after the artifact bundle is built, but consumers | |
| # (test_apps) fetch the tagged tree and need the substituted manifest. | |
| # Force-update the tag to point at this post-substitution commit. | |
| # | |
| # This workflow is the sole writer for this tag's substituted state, so | |
| # `--force-with-lease` adds no safety and breaks against annotated→lightweight | |
| # tag conversion (the lease's expected SHA is the annotated tag object, but the | |
| # server compares it differently after we replace it with a lightweight tag). | |
| # Use plain `--force` and retry transient network failures. | |
| git config user.name "kreuzberg-dev-publisher[bot]" | |
| git config user.email "291994444+kreuzberg-dev-publisher[bot]@users.noreply.github.com" | |
| git add Package.swift | |
| git commit -m "chore(release): substitute Swift checksum for ${VERSION}" | |
| git tag -f "v${VERSION}" HEAD | |
| for attempt in 1 2 3; do | |
| if git push origin "v${VERSION}" --force; then | |
| break | |
| fi | |
| if [ "$attempt" = 3 ]; then | |
| echo "::error::Failed to push tag v${VERSION} after 3 attempts" | |
| exit 1 | |
| fi | |
| echo "Push attempt ${attempt} failed, retrying in 5s..." | |
| sleep 5 | |
| done | |
| - name: Update release notes with artifact URL | |
| if: ${{ needs.prepare.outputs.dry_run != 'true' }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| CHECKSUM=$(cat swift-checksums/*.checksum | tr -d '[:space:]') | |
| ASSET_URL="https://github.com/kreuzberg-dev/html-to-markdown/releases/download/v${VERSION}/HtmlToMarkdown-rs.artifactbundle.zip" | |
| cat > /tmp/swift-notes.md << EOF | |
| ## Swift Package Manager | |
| Add to your \`Package.swift\`: | |
| \`\`\`swift | |
| .package(url: "https://github.com/kreuzberg-dev/html-to-markdown", from: "${VERSION}") | |
| \`\`\` | |
| The Swift binding is distributed as a pre-built artifact bundle. No Rust toolchain required. | |
| **Artifact**: [HtmlToMarkdown-rs.artifactbundle.zip](${ASSET_URL}) | |
| **Checksum**: \`${CHECKSUM}\` | |
| EOF | |
| gh release edit "v${VERSION}" --notes-file /tmp/swift-notes.md | |
| build-r-package: | |
| name: Build R source tarball | |
| needs: prepare | |
| if: ${{ needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| submodules: recursive | |
| - uses: r-lib/actions/setup-r@v2 | |
| with: | |
| r-version: "release" | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install system deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y python3 | |
| - name: Build R source tarball | |
| # The packages/r/configure script vendors the html-to-markdown-rs core | |
| # crate from the monorepo at build time; running `R CMD build` inside | |
| # the repo therefore produces a self-contained source tarball. | |
| working-directory: packages/r | |
| run: | | |
| R CMD build . | |
| ls -la htmltomarkdown_*.tar.gz | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: r-source-tarball | |
| path: packages/r/htmltomarkdown_*.tar.gz | |
| if-no-files-found: error | |
| retention-days: 7 | |
| upload-r-release: | |
| name: Upload R source tarball to GitHub Release | |
| needs: [prepare, build-r-package] | |
| if: ${{ (needs.prepare.outputs.is_tag == 'true' || needs.prepare.outputs.dry_run == 'true') && needs.build-r-package.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: r-source-tarball | |
| path: dist/r | |
| - uses: kreuzberg-dev/actions/upload-release-assets@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| working-directory: dist/r | |
| dry-run: ${{ needs.prepare.outputs.dry_run }} | |
| assets: | | |
| htmltomarkdown_*.tar.gz | |
| release-finalize: | |
| name: Finalize GitHub Release | |
| needs: | |
| - prepare | |
| - verify-assets | |
| - publish-crates | |
| - publish-pypi | |
| - publish-rubygems | |
| - publish-node | |
| - publish-wasm | |
| - publish-packagist | |
| - publish-maven | |
| - publish-nuget | |
| - publish-hex | |
| - publish-homebrew-bottles | |
| - publish-kotlin-android | |
| - trigger-pubdev | |
| - publish-zig | |
| - upload-cli-release | |
| - upload-go-release | |
| - upload-c-ffi-release | |
| - upload-swift-release | |
| - update-swift-package-manifest | |
| - upload-r-release | |
| if: | | |
| always() && | |
| !cancelled() && | |
| needs.prepare.outputs.is_tag == 'true' && | |
| needs.prepare.outputs.dry_run != 'true' && | |
| !contains(needs.*.result, 'failure') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: kreuzberg-dev/actions/finalize-release@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| is-prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'true' || 'false' }} | |
| go-module-path: packages/go/v3 | |
| token: ${{ steps.app-token.outputs.token }} | |
| announce-discord: | |
| name: Announce release on Discord | |
| needs: | |
| - prepare | |
| - publish-crates | |
| - publish-pypi | |
| - publish-rubygems | |
| - publish-node | |
| - publish-wasm | |
| - publish-packagist | |
| - publish-maven | |
| - publish-nuget | |
| - publish-hex | |
| - publish-homebrew-bottles | |
| - publish-kotlin-android | |
| - trigger-pubdev | |
| - publish-zig | |
| - upload-cli-release | |
| - upload-go-release | |
| - upload-c-ffi-release | |
| - upload-elixir-release | |
| - upload-php-pie-release | |
| - upload-r-release | |
| - release-finalize | |
| if: | | |
| always() && | |
| !cancelled() && | |
| needs.prepare.outputs.is_tag == 'true' && | |
| needs.prepare.outputs.dry_run != 'true' && | |
| needs.prepare.outputs.is_prerelease != 'true' && | |
| !contains(needs.*.result, 'failure') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.BOT_APP_ID }} | |
| private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} | |
| owner: kreuzberg-dev | |
| - uses: kreuzberg-dev/actions/announce-release-discord@v1 | |
| with: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| project-name: html-to-markdown | |
| token: ${{ steps.app-token.outputs.token }} |