Skip to content

fix: avoid stack overflows on deeply nested HTML #252

fix: avoid stack overflows on deeply nested HTML

fix: avoid stack overflows on deeply nested HTML #252

Workflow file for this run

name: CI E2E
on:
push:
branches: [main]
paths:
- "crates/**"
- "packages/**"
- "e2e/**"
- "fixtures/**"
- "alef.toml"
- "tools/**"
- "scripts/**"
- "Cargo.toml"
- "Cargo.lock"
- "Taskfile.yaml"
- ".task/**"
- "pyproject.toml"
- "uv.lock"
- "uv.toml"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "package.json"
- "Gemfile"
- "Gemfile.lock"
- "composer.json"
- "composer.lock"
- "go.mod"
- "go.sum"
- "rust-toolchain.toml"
- ".cargo/config.toml"
- ".github/workflows/ci-e2e.yaml"
pull_request:
branches: [main]
paths:
- "crates/**"
- "packages/**"
- "e2e/**"
- "fixtures/**"
- "alef.toml"
- "tools/**"
- "scripts/**"
- "Cargo.toml"
- "Cargo.lock"
- "Taskfile.yaml"
- ".task/**"
- "pyproject.toml"
- "uv.lock"
- "uv.toml"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "package.json"
- "Gemfile"
- "Gemfile.lock"
- "composer.json"
- "composer.lock"
- "go.mod"
- "go.sum"
- "rust-toolchain.toml"
- ".cargo/config.toml"
- ".github/workflows/ci-e2e.yaml"
workflow_dispatch: {}
concurrency:
group: ci-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
BUILD_PROFILE: "ci"
GO_VERSION: "1.26.0"
GO_TOOLCHAIN: "go1.26.0"
GOLANGCI_LINT_VERSION: "latest"
permissions:
contents: read
jobs:
changes:
name: "Detect Changes"
runs-on: ubuntu-24.04-arm
outputs:
core: ${{ steps.filter.outputs.core }}
rust: ${{ steps.filter.outputs.rust }}
ffi: ${{ steps.filter.outputs.ffi }}
python: ${{ steps.filter.outputs.python }}
node: ${{ steps.filter.outputs.node }}
ruby: ${{ steps.filter.outputs.ruby }}
php: ${{ steps.filter.outputs.php }}
go: ${{ steps.filter.outputs.go }}
java: ${{ steps.filter.outputs.java }}
elixir: ${{ steps.filter.outputs.elixir }}
r: ${{ steps.filter.outputs.r }}
wasm: ${{ steps.filter.outputs.wasm }}
csharp: ${{ steps.filter.outputs.csharp }}
kotlin: ${{ steps.filter.outputs.kotlin }}
swift: ${{ steps.filter.outputs.swift }}
dart: ${{ steps.filter.outputs.dart }}
zig: ${{ steps.filter.outputs.zig }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Detect changes
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
core:
- 'crates/html-to-markdown/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.cargo/config.toml'
- 'fixtures/**'
- 'tools/e2e-generator/**'
rust:
- 'crates/html-to-markdown/**'
- 'crates/html-to-markdown-cli/**'
- 'e2e/rust/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rustfmt.toml'
ffi:
- 'crates/html-to-markdown-ffi/**'
- 'crates/html-to-markdown/**'
- 'e2e/c/**'
- 'Cargo.toml'
- 'Cargo.lock'
python:
- 'crates/html-to-markdown-py/**'
- 'packages/python/**'
- 'e2e/python/**'
- 'pyproject.toml'
- 'uv.lock'
- 'uv.toml'
- 'fixtures/**'
node:
- 'crates/html-to-markdown-node/**'
- 'packages/typescript/**'
- 'e2e/node/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'fixtures/**'
ruby:
- 'packages/ruby/**'
- 'e2e/ruby/**'
- 'Gemfile'
- 'Gemfile.lock'
- 'fixtures/**'
php:
- 'crates/html-to-markdown-php/**'
- 'packages/php/**'
- 'packages/php-ext/**'
- 'e2e/php/**'
- 'composer.json'
- 'composer.lock'
- 'fixtures/**'
go:
- 'packages/go/**'
- 'e2e/go/**'
- 'crates/html-to-markdown-ffi/**'
- 'go.mod'
- 'go.sum'
- 'fixtures/**'
java:
- 'packages/java/**'
- 'e2e/java/**'
- 'crates/html-to-markdown-ffi/**'
- 'fixtures/**'
elixir:
- 'packages/elixir/**'
- 'e2e/elixir/**'
- 'crates/html-to-markdown-ffi/**'
- 'fixtures/**'
r:
- 'packages/r/**'
- 'e2e/r/**'
- 'fixtures/**'
wasm:
- 'crates/html-to-markdown-wasm/**'
- 'crates/html-to-markdown-wasm-wasi/**'
- 'e2e/wasm/**'
- 'fixtures/**'
csharp:
- 'packages/csharp/**'
- 'e2e/csharp/**'
- 'crates/html-to-markdown-ffi/**'
- 'fixtures/**'
kotlin:
- 'packages/kotlin-android/**'
- 'crates/html-to-markdown-ffi/**'
- 'fixtures/**'
swift:
- 'packages/swift/**'
- 'e2e/swift_e2e/**'
- 'fixtures/**'
dart:
- 'packages/dart/**'
- 'e2e/dart/**'
- 'fixtures/**'
zig:
- 'packages/zig/**'
- 'e2e/zig/**'
- 'crates/html-to-markdown-ffi/**'
- 'fixtures/**'
build-ffi:
needs: [changes]
name: "Build: FFI (${{ matrix.runner }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.ffi == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- ubuntu-24.04-arm
- macos-latest
- windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Free disk space
if: startsWith(matrix.runner, 'ubuntu')
uses: xberg-io/actions/free-disk-space-linux@v1 # v1
with:
show-initial: "false"
show-final: "true"
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: ffi-${{ matrix.runner }}
use-sccache: false
- name: Build html-to-markdown-ffi (release, Unix)
if: matrix.runner != 'windows-latest'
run: cargo build --release --locked -p html-to-markdown-ffi
shell: bash
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
RUST_BACKTRACE: short
- name: Build html-to-markdown-ffi (debug, Windows)
if: matrix.runner == 'windows-latest'
run: cargo build --locked -p html-to-markdown-ffi
shell: bash
env:
CARGO_TERM_COLOR: always
- name: Verify header exists
shell: bash
run: |
HEADER="crates/html-to-markdown-ffi/include/html_to_markdown.h"
test -f "$HEADER"
echo "Header verified: $HEADER"
- name: Upload FFI artifacts
if: matrix.runner != 'windows-latest'
uses: actions/upload-artifact@v7
with:
name: ffi-${{ matrix.runner }}
path: |
target/release/libhtml_to_markdown_ffi.*
target/release/html_to_markdown_ffi.*
crates/html-to-markdown-ffi/include/html_to_markdown.h
retention-days: 7
if-no-files-found: warn
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-python:
needs: [changes]
name: "Build: Python (${{ matrix.os }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.python == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ["3.10", "3.12", "3.14"]
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
components: rustfmt, clippy, llvm-tools-preview
cache-key-prefix: python-${{ matrix.os }}-${{ matrix.python }}
- name: Install Python Dependencies
uses: nick-fields/retry@v4 # v4
with:
timeout_minutes: 5
max_attempts: 3
retry_wait_seconds: 30
command: |
if [[ "${{ runner.os }}" == "Windows" ]] && [[ -d ".venv" ]]; then
echo "Removing existing .venv directory on Windows"
rm -rf .venv
fi
uv sync --all-extras --no-install-project --no-install-workspace
shell: bash
- name: Build Python Bindings
run: |
uv pip install maturin
cd packages/python && uv run maturin develop --release
shell: bash
- name: Build CLI binary
run: cargo build --release --locked -p html-to-markdown-cli
shell: bash
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-node:
needs: [changes]
name: "Build: Node (${{ matrix.os }}, ${{ matrix.runtime }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.node == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runtime: [node, bun]
exclude:
- os: windows-latest
runtime: bun
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: node-${{ matrix.os }}-${{ matrix.runtime }}
- name: Setup Node.js workspace
if: matrix.runtime == 'node'
uses: xberg-io/actions/setup-node-workspace@v1 # v1
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build NAPI-RS Bindings (Node.js)
if: matrix.runtime == 'node'
uses: xberg-io/actions/build-node-napi@v1 # v1
with:
crate-dir: crates/html-to-markdown-node
- name: Install workspace dependencies (Bun)
if: matrix.runtime == 'bun'
run: bun install
shell: bash
- name: Build NAPI-RS Bindings (Bun)
if: matrix.runtime == 'bun'
working-directory: crates/html-to-markdown-node
run: bun run build
shell: bash
- name: Build TypeScript package (Node.js)
if: matrix.runtime == 'node'
uses: ./.github/actions/build-typescript
- name: Build TypeScript package (Bun)
if: matrix.runtime == 'bun'
working-directory: packages/typescript
run: bun x tsc --project tsconfig.json
shell: bash
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-ruby:
needs: [changes]
name: "Build: Ruby (${{ matrix.os }}, ruby-${{ matrix.ruby }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.ruby == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
ruby: ["3.2", "3.3"]
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: ruby-${{ matrix.os }}-${{ matrix.ruby }}
- name: Setup Ruby (Unix)
if: runner.os != 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler: "4.0.3"
bundler-cache: false
working-directory: packages/ruby
- name: Setup Ruby (Windows)
if: runner.os == 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler: "4.0.3"
bundler-cache: false
working-directory: packages/ruby
windows-toolchain: UCRT64
- name: Build CLI binary
uses: xberg-io/actions/build-rust-cli@v1 # v1
with:
package-name: html-to-markdown-cli
binary-name: html-to-markdown
- name: Build Ruby extension
uses: xberg-io/actions/build-ruby-gem@v1 # v1
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-php:
needs: [changes]
name: "Build: PHP"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.php == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: ctype, dom, json, libxml, mbstring, tokenizer, xml, xmlwriter
tools: composer:2.9.8
coverage: none
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: php
- name: Capture php-config path
run: scripts/ci/php/set-php-config.sh
shell: bash
- name: Install root Composer dependencies
uses: ramsey/composer-install@4.0.0
with:
dependency-versions: locked
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}'
- name: Install PHP package Composer dependencies
uses: ramsey/composer-install@4.0.0
with:
dependency-versions: locked
working-directory: packages/php
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}'
- name: Build PHP extension
id: build-php-extension
uses: xberg-io/actions/build-php-extension@v1 # v1
with:
crate-name: html-to-markdown-php
lib-name: html_to_markdown_php
- name: Upload PHP extension artifact
uses: actions/upload-artifact@v7
with:
name: php-extension-ubuntu
path: ${{ steps.build-php-extension.outputs.extension-path }}
retention-days: 7
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-csharp:
needs: [changes]
name: "Build: C# (${{ matrix.os }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.csharp == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: csharp-${{ matrix.os }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Build FFI library (Unix)
if: runner.os != 'Windows'
run: cargo build --release --locked -p html-to-markdown-ffi
shell: bash
- name: Build FFI library (Windows)
if: runner.os == 'Windows'
run: cargo build --locked -p html-to-markdown-ffi
shell: bash
- name: Restore C# dependencies
run: dotnet restore packages/csharp/
shell: bash
- name: Build C# package
run: dotnet build packages/csharp/ -c Release
shell: bash
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-java:
needs: [changes]
name: "Build: Java (${{ matrix.os }})"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.java == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
java: ["25"]
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: java-${{ matrix.os }}
- name: Test Java Panama FFI bindings
uses: xberg-io/actions/test-java-ffi@v1 # v1
with:
ffi-crate-name: html-to-markdown-ffi
ffi-lib-name: html_to_markdown_ffi
java-version: ${{ matrix.java }}
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
build-wasm:
needs: [changes]
name: "Build: WASM"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.wasm == 'true'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
target: wasm32-unknown-unknown
use-sccache: false
cache-key-prefix: wasm
- name: Ensure wasm target installed
run: scripts/common/ensure-wasm-target.sh
shell: bash
- name: Install wasm-pack
run: scripts/common/install-wasm-pack.sh
shell: bash
- name: Setup Node workspace
uses: xberg-io/actions/setup-node-workspace@v1 # v1
- name: Build WASM (all targets)
uses: xberg-io/actions/build-wasm-package@v1 # v1
with:
crate-dir: crates/html-to-markdown-wasm
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
test-python:
needs: [build-python]
name: "Test: Python (${{ matrix.os }}, py-${{ matrix.python }})"
if: always() && !cancelled() && needs.build-python.result != 'skipped'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ["3.10", "3.12", "3.14"]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Install Python Dependencies
uses: nick-fields/retry@v4 # v4
with:
timeout_minutes: 5
max_attempts: 3
retry_wait_seconds: 30
command: |
if [[ "${{ runner.os }}" == "Windows" ]] && [[ -d ".venv" ]]; then
echo "Removing existing .venv directory on Windows"
rm -rf .venv
fi
uv sync --all-extras --no-install-project --no-install-workspace
shell: bash
- name: Build Python Bindings
run: |
uv pip install maturin
cd packages/python && uv run maturin develop --release
shell: bash
- name: Build CLI binary
run: cargo build --release --locked -p html-to-markdown-cli
shell: bash
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang python
shell: bash
test-node:
needs: [build-node]
name: "Test: Node (${{ matrix.os }}, ${{ matrix.runtime }})"
if: always() && !cancelled() && needs.build-node.result != 'skipped'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runtime: [node, bun]
exclude:
- os: windows-latest
runtime: bun
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Setup Node.js workspace
if: matrix.runtime == 'node'
uses: xberg-io/actions/setup-node-workspace@v1 # v1
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build NAPI-RS Bindings (Node.js)
if: matrix.runtime == 'node'
uses: xberg-io/actions/build-node-napi@v1 # v1
with:
crate-dir: crates/html-to-markdown-node
- name: Install workspace dependencies (Bun)
if: matrix.runtime == 'bun'
run: bun install
shell: bash
- name: Build NAPI-RS Bindings (Bun)
if: matrix.runtime == 'bun'
working-directory: crates/html-to-markdown-node
run: bun run build
shell: bash
- name: Run Rust Tests (Node.js only)
if: matrix.runtime == 'node'
run: task rust:test
shell: bash
- name: Build TypeScript package (Node.js)
if: matrix.runtime == 'node'
uses: ./.github/actions/build-typescript
- name: Build TypeScript package (Bun)
if: matrix.runtime == 'bun'
working-directory: packages/typescript
run: bun x tsc --project tsconfig.json
shell: bash
- name: Install alef
if: matrix.runtime == 'node'
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests (Node.js only)
if: matrix.runtime == 'node'
run: alef test --e2e --lang node
shell: bash
test-ruby:
needs: [build-ruby]
name: "Test: Ruby (${{ matrix.os }}, ruby-${{ matrix.ruby }})"
if: always() && !cancelled() && needs.build-ruby.result != 'skipped'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
ruby: ["3.2", "3.3"]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Setup Ruby (Unix)
if: runner.os != 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler: "4.0.3"
bundler-cache: false
working-directory: packages/ruby
- name: Setup Ruby (Windows)
if: runner.os == 'Windows'
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler: "4.0.3"
bundler-cache: false
working-directory: packages/ruby
windows-toolchain: UCRT64
- name: Build CLI binary
uses: xberg-io/actions/build-rust-cli@v1 # v1
with:
package-name: html-to-markdown-cli
binary-name: html-to-markdown
- name: Build Ruby extension
uses: xberg-io/actions/build-ruby-gem@v1 # v1
- name: Run Rubocop (Ubuntu/ruby-3.3 only)
if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'
run: ./scripts/ci/ruby/run-rubocop.sh
shell: bash
- name: Validate RBS signatures (Ubuntu/ruby-3.3 only)
if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'
run: ./scripts/ci/ruby/run-rbs-validate.sh
shell: bash
- name: Run Steep type checking (Ubuntu/ruby-3.3 only)
if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'
working-directory: packages/ruby
run: ../../scripts/ci/ruby/run-steep.sh
shell: bash
- name: Run Ruby specs (Unix)
if: runner.os != 'Windows'
working-directory: packages/ruby
run: ../../scripts/ci/ruby/run-rspec-unix.sh
shell: bash
- name: Run Ruby specs (Windows)
if: runner.os == 'Windows'
working-directory: packages/ruby
shell: pwsh
run: ../../scripts/ci/ruby/run-rspec-windows.ps1
- name: Install Task
if: runner.os != 'Windows'
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
if: runner.os != 'Windows'
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests (Unix only)
if: runner.os != 'Windows'
run: alef test --e2e --lang ruby
shell: bash
test-php:
needs: [build-php]
name: "Test: PHP"
if: always() && !cancelled() && needs.build-php.result != 'skipped'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: ctype, dom, json, libxml, mbstring, tokenizer, xml, xmlwriter
tools: composer:2.9.8
coverage: none
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Capture php-config path
run: scripts/ci/php/set-php-config.sh
shell: bash
- name: Install root Composer dependencies
uses: ramsey/composer-install@4.0.0
with:
dependency-versions: locked
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}'
- name: Install PHP package Composer dependencies
uses: ramsey/composer-install@4.0.0
with:
dependency-versions: locked
working-directory: packages/php
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}'
- name: Build PHP extension
id: build-php-extension
uses: xberg-io/actions/build-php-extension@v1 # v1
with:
crate-name: html-to-markdown-php
lib-name: html_to_markdown_php
- name: Run PHP static analysis
run: scripts/ci/php/run-phpstan.sh
shell: bash
- name: Run PHP tests
run: scripts/ci/php/run-php-tests.sh
shell: bash
env:
EXTENSION_PATH: ${{ steps.build-php-extension.outputs.extension-path }}
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang php
shell: bash
env:
EXTENSION_PATH: ${{ steps.build-php-extension.outputs.extension-path }}
test-csharp:
needs: [build-csharp, build-ffi, changes]
name: "Test: C# (${{ matrix.os }})"
if: |
always() && !cancelled() && needs.build-csharp.result != 'skipped' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.csharp == 'true' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: csharp-test-${{ matrix.os }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Build FFI library (Unix)
if: runner.os != 'Windows'
run: cargo build --release --locked -p html-to-markdown-ffi
shell: bash
- name: Build FFI library (Windows)
if: runner.os == 'Windows'
run: cargo build --locked -p html-to-markdown-ffi
shell: bash
- name: Restore C# dependencies
run: dotnet restore packages/csharp/
shell: bash
- name: Build C# package
run: dotnet build packages/csharp/ -c Release
shell: bash
- name: Run C# unit tests
run: dotnet test packages/csharp/ -c Release
shell: bash
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests (Unix)
if: runner.os != 'Windows'
run: alef test --e2e --lang csharp
shell: bash
env:
LD_LIBRARY_PATH: ${{ github.workspace }}/target/release:${{ github.workspace }}/target/debug
DYLD_LIBRARY_PATH: ${{ github.workspace }}/target/release:${{ github.workspace }}/target/debug
- name: Run E2E tests (Windows)
if: runner.os == 'Windows'
# Prepend the target dirs to PATH at runtime (for FFI .dll resolution)
# using the live $PATH rather than the `env:` `${{ env.PATH }}`
# expression, which does not include cargo's $GITHUB_PATH additions on
# Windows Git Bash and broke the before-hook with `cargo: command not found`.
run: |
export PATH="${{ github.workspace }}/target/release:${{ github.workspace }}/target/debug:$PATH"
alef test --e2e --lang csharp
shell: bash
- name: Cleanup Rust cache
if: always()
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
test-go:
needs: [build-ffi, changes]
name: "Test: Go"
if: |
always() && !cancelled() && needs.build-ffi.result != 'skipped' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.go == 'true' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
check-latest: true
- name: Build FFI library
run: cargo build --release --locked -p html-to-markdown-ffi
shell: bash
- name: Detect Go modules
id: set-modules
shell: bash
run: scripts/ci/go/detect-go-modules.sh
- name: Install golangci-lint
if: steps.set-modules.outputs.modules != '[]'
env:
GOTOOLCHAIN: ${{ env.GO_TOOLCHAIN }}
run: scripts/ci/go/install-golangci-lint.sh
shell: bash
- name: Run golangci-lint (all modules)
if: steps.set-modules.outputs.modules != '[]'
shell: bash
run: |
for module in $(echo '${{ steps.set-modules.outputs.modules }}' | jq -r '.[]'); do
echo "=== Linting $module ==="
(cd "$module" && "${{ github.workspace }}/scripts/ci/go/run-golangci-lint.sh")
done
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang go
shell: bash
test-java:
needs: [build-java]
name: "Test: Java (${{ matrix.os }})"
if: always() && !cancelled() && needs.build-java.result != 'skipped'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
java: ["25"]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Test Java Panama FFI bindings
uses: xberg-io/actions/test-java-ffi@v1 # v1
with:
ffi-crate-name: html-to-markdown-ffi
ffi-lib-name: html_to_markdown_ffi
java-version: ${{ matrix.java }}
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
if: matrix.os != 'windows-latest'
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests (Linux + macOS)
# macOS coverage exercises the Java native loader on Apple Silicon so
# macOS-specific binding/RID regressions are caught (alef test is not
# supported on the Windows runner here).
if: matrix.os != 'windows-latest'
run: alef test --e2e --lang java
shell: bash
test-elixir:
needs: [build-ffi, changes]
name: "Test: Elixir"
if: |
always() && !cancelled() && needs.build-ffi.result != 'skipped' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.elixir == 'true' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.19"
otp-version: "28.1"
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Install Hex/Rebar
run: scripts/ci/elixir/install-hex-rebar.sh
shell: bash
- name: Install dependencies
working-directory: packages/elixir
run: ../../scripts/ci/elixir/install-deps.sh
shell: bash
- name: Run tests
working-directory: packages/elixir
run: ../../scripts/ci/elixir/run-tests.sh
shell: bash
- name: Credo lint
working-directory: packages/elixir
run: ../../scripts/ci/elixir/run-credo.sh
shell: bash
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang elixir
shell: bash
test-r:
needs: [changes]
name: "Test: R"
if: |
always() && !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.r == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup R
uses: xberg-io/actions/setup-r@v1 # v1
with:
install-deps-script: scripts/ci/r/install-deps.sh
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
- name: Run tests
working-directory: packages/r
run: ../../scripts/ci/r/run-tests.sh
shell: bash
- name: Run lintr
working-directory: packages/r
run: ../../scripts/ci/r/run-lintr.sh
shell: bash
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang r
shell: bash
test-c-ffi:
needs: [build-ffi, changes]
name: "Test: C FFI (${{ matrix.runner }})"
if: |
always() && !cancelled() && needs.build-ffi.result != 'skipped' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- ubuntu-24.04-arm
- macos-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: c-ffi-${{ matrix.runner }}
use-sccache: false
- name: Build html-to-markdown-ffi
shell: bash
run: cargo build --release --locked -p html-to-markdown-ffi
- name: Run C e2e tests
shell: bash
env:
LD_LIBRARY_PATH: ${{ github.workspace }}/target/release
DYLD_LIBRARY_PATH: ${{ github.workspace }}/target/release
run: cd e2e/c && make test
- name: Verify header exists
shell: bash
run: |
HEADER="crates/html-to-markdown-ffi/include/html_to_markdown.h"
test -f "$HEADER"
echo "Header verified: $HEADER"
- name: Verify pkg-config output
shell: bash
run: |
PC_DIR="$(pwd)/target/release/build"
PC_FILE=$(find "$PC_DIR" -name 'html-to-markdown.pc' -path '*/html-to-markdown-ffi-*/out/*' 2>/dev/null | head -1)
if [ -z "$PC_FILE" ]; then
echo "Warning: html-to-markdown.pc not found in build output"
find "$PC_DIR" -name '*.pc' 2>/dev/null || echo "No .pc files found"
else
echo "Found pkg-config file: $PC_FILE"
cat "$PC_FILE"
fi
test-c-ffi-windows:
needs: [build-ffi, changes]
name: "Test: C FFI (windows-latest)"
if: |
always() && !cancelled() && needs.build-ffi.result != 'skipped' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.core == 'true')
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: c-ffi-windows
use-sccache: false
- name: Build html-to-markdown-ffi
shell: bash
run: cargo build --locked -p html-to-markdown-ffi
- name: Verify header generated
shell: bash
run: |
test -f crates/html-to-markdown-ffi/include/html_to_markdown.h
echo "Header verified on Windows."
test-wasm:
needs: [build-wasm]
name: "Test: WASM"
if: always() && !cancelled() && needs.build-wasm.result != 'skipped'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
target: wasm32-unknown-unknown
use-sccache: false
- name: Ensure wasm target installed
run: scripts/common/ensure-wasm-target.sh
shell: bash
- name: Install wasm-pack
run: scripts/common/install-wasm-pack.sh
shell: bash
- name: Setup Node workspace
uses: xberg-io/actions/setup-node-workspace@v1 # v1
- name: Build WASM (all targets)
uses: xberg-io/actions/build-wasm-package@v1 # v1
with:
crate-dir: crates/html-to-markdown-wasm
- name: Test WASM bundle
working-directory: crates/html-to-markdown-wasm
run: ../../scripts/ci/wasm/test-wasm-bundle.sh
shell: bash
- name: Run Rust WASM tests
working-directory: crates/html-to-markdown-wasm
run: ../../scripts/ci/wasm/test-wasm-rust.sh
shell: bash
build-kotlin-android:
needs: [changes]
name: "Build: Kotlin Android"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.kotlin == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
id: checkout
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
targets: aarch64-linux-android,x86_64-linux-android
cache-key-prefix: kotlin-android
- name: Setup Android SDK/NDK
uses: android-actions/setup-android@v4
- name: Build Android native libraries
uses: xberg-io/actions/build-android-natives@v1 # v1
with:
crate-name: html-to-markdown-ffi
lib-name: html_to_markdown_ffi
abis: arm64-v8a,x86_64
- name: Cleanup Rust cache
if: always() && steps.checkout.outcome == 'success'
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
test-swift:
needs: [changes]
name: "Test: Swift"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.swift == 'true'
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: swift
- name: Setup Swift
uses: xberg-io/actions/setup-swift@v1 # v1
with:
version: "6.0"
- name: Setup PHP
# ext-php-rs's build.rs requires a `php` binary on PATH even when the
# PHP binding crate isn't the one being built — cargo's workspace
# resolver evaluates ALL workspace members for unification. Without
# PHP here, `cargo build -p html-to-markdown-rs-swift` fails inside
# ext-php-rs's build script.
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang swift
shell: bash
- name: Cleanup Rust cache
if: always()
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
test-dart:
needs: [changes]
name: "Test: Dart"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.dart == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: dart
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang dart
shell: bash
- name: Cleanup Rust cache
if: always()
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1
test-zig:
needs: [changes]
name: "Test: Zig"
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.core == 'true' ||
needs.changes.outputs.ffi == 'true' ||
needs.changes.outputs.zig == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Task
uses: xberg-io/actions/install-task@v1 # v1
- name: Setup Rust
uses: xberg-io/actions/setup-rust@v1 # v1
with:
cache-key-prefix: zig
- name: Setup Zig
uses: xberg-io/actions/setup-zig@v1 # v1
with:
version: "0.16.0"
- name: Install alef
uses: xberg-io/actions/install-alef@v1 # v1
- name: Run E2E tests
run: alef test --e2e --lang zig
shell: bash
- name: Cleanup Rust cache
if: always()
uses: xberg-io/actions/cleanup-rust-cache@v1 # v1