name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
actionlint:
name: actionlint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Check GitHub Actions workflows
uses: docker://rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 with:
args: -color
rust:
name: ${{ matrix.os }} / ${{ matrix.toolchain }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable, beta, nightly]
env:
RUSTFLAGS: "-D warnings"
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install Rust (${{ matrix.toolchain }})
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: ${{ matrix.toolchain }}
components: rustfmt, clippy
- name: Set up Python for integration tests
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with:
python-version: '3.12'
- name: Install uv
run: python -m pip install uv
- name: Cache cargo registry and build outputs
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.toolchain }}-
- name: rustfmt (check only)
run: cargo fmt --all -- --check
- name: Clippy (deny warnings)
run: cargo clippy --workspace --all-features --all-targets --locked -- -D warnings
- name: Build Python extension for Rust integration tests (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
if [[ "$RUNNER_OS" == "macOS" ]]; then
cargo rustc -p jsoncompat_py --lib --locked -- -C link-arg=-undefined -C link-arg=dynamic_lookup
else
cargo build -p jsoncompat_py --locked
fi
- name: Build Python extension for Rust integration tests (Windows)
if: runner.os == 'Windows'
run: cargo build -p jsoncompat_py --locked
- name: cargo test
run: cargo test --workspace --exclude jsoncompat_py --all-features --locked
- name: Smoke-check benchmarks
if: matrix.os == 'ubuntu-latest' && matrix.toolchain == 'stable'
run: cargo bench --workspace --exclude jsoncompat_py --all-features --bench '*' --locked -- --test
- name: Set up free-threaded Python
if: matrix.os == 'ubuntu-latest' && matrix.toolchain == 'stable'
id: free-threaded-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with:
python-version: '3.14t'
- name: Test generated models without the GIL
if: matrix.os == 'ubuntu-latest' && matrix.toolchain == 'stable'
env:
PYO3_PYTHON: ${{ steps.free-threaded-python.outputs.python-path }}
UV_PYTHON: ${{ steps.free-threaded-python.outputs.python-path }}
UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/jsoncompat-free-threaded
PYTHON_GIL: '0'
run: |
uv sync --project pybindings --all-extras --locked --no-install-project
cargo build -p jsoncompat_py --locked
UV_NO_SYNC=1 cargo test --locked --test python_dataclasses_prepared --test python_dataclasses_differential --test python_dataclasses_runtime --test codegen_cli
rust-packages:
name: Package and verify Rust release crates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install Rust (stable)
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: stable
- name: Build every published archive
run: cargo package --workspace --exclude jsoncompat_py --exclude jsoncompat_wasm --locked --target-dir "$(mktemp -d)"
python-linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: aarch64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i '3.12 3.13 3.14 3.14t 3.15 3.15t'
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: auto
working-directory: pybindings
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: wheels-linux-${{ matrix.platform.target }}
path: pybindings/dist
if-no-files-found: error
python-musllinux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: aarch64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i '3.12 3.13 3.14 3.14t 3.15 3.15t'
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: musllinux_1_2
working-directory: pybindings
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: pybindings/dist
if-no-files-found: error
python-windows:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install Python 3.15 and its Windows import library
id: python315
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with:
python-version: '3.15'
architecture: ${{ matrix.platform.target }}
allow-prereleases: true
- name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i '3.12 3.13 3.14 3.14t' '${{ steps.python315.outputs.python-path }}'
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
working-directory: pybindings
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: wheels-windows-${{ matrix.platform.target }}
path: pybindings/dist
if-no-files-found: error
python-macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: macos-15-intel
target: x86_64
- runner: macos-latest
target: aarch64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i '3.12 3.13 3.14 3.14t 3.15 3.15t'
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
working-directory: pybindings
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: wheels-macos-${{ matrix.platform.target }}
path: pybindings/dist
if-no-files-found: error
python-sdist:
name: Build and test Python sdist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with:
python-version: '3.12'
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: stable
- name: Build source distribution
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
command: sdist
args: --out dist
manylinux: off
working-directory: pybindings
- name: Install and test source distribution
run: python pybindings/tests/check_sdist.py pybindings/dist/*.tar.gz
- name: Upload source distribution
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: sdist
path: pybindings/dist/*.tar.gz
if-no-files-found: error
python-release:
name: Release
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
needs: [actionlint, rust, rust-packages, python-linux, python-musllinux, python-windows, python-macos, python-sdist]
permissions:
id-token: write
contents: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - name: Generate artifact attestation
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 with:
subject-path: |
wheels-*/*
sdist/*.tar.gz
- name: Publish to PyPI
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad with:
command: upload
args: --non-interactive --skip-existing wheels-*/* sdist/*.tar.gz
wasm:
name: wasm-pack build
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: stable
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- name: Cache cargo registry and build outputs
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: wasm-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Build WebAssembly packages
run: |
set -euo pipefail
for target in bundler nodejs web; do
echo "--- Building for $target target ---"
wasm-pack build wasm --release --target "$target" --out-dir "pkg-$target"
done
- name: Exercise structured WebAssembly verdicts
run: node wasm/tests/verdicts.mjs
- name: Prepare distribution directory
run: |
mkdir -p dist/js
for target in bundler nodejs web; do
mkdir -p "dist/js/$target"
cp -r "wasm/pkg-$target"/* "dist/js/$target/"
cp wasm/README.md "dist/js/$target/README.md"
done
- name: Upload wasm artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: js-packages
path: dist/js
wasm-release:
name: Release wasm package
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
needs: [actionlint, rust, rust-packages, wasm]
permissions:
contents: write
attestations: write
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- name: Generate artifact attestation
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 with:
subject-path: 'js-packages/**/*'
- name: Tree
run: |
tree js-packages
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Publish to npm (tag builds only)
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
set -euo pipefail
echo "Publishing WebAssembly web package …"
# for now, only publish the web package
pushd js-packages/web
# rename the package to jsoncompat from jsoncompat_wasm
perl -0777 -pi -e 's/"name"\s*:\s*"jsoncompat_wasm"/"name": "jsoncompat"/' package.json
npm publish . --access public --provenance
popd
cli:
name: Build CLI binary
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
ext: ""
asset_name: jsoncompat-linux-x86_64
- os: macos-15-intel ext: ""
asset_name: jsoncompat-macos-x86_64
- os: macos-latest ext: ""
asset_name: jsoncompat-macos-aarch64
- os: windows-latest
ext: ".exe"
asset_name: jsoncompat-windows-x86_64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: stable
- name: Cache cargo registry and build outputs
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cli-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Build CLI
run: cargo build --locked --release --bin jsoncompat
- name: Run CLI demo
if: ${{ matrix.os == 'ubuntu-latest' }}
run: ./target/release/jsoncompat demo --noninteractive
- name: Prepare artifact
shell: bash
run: |
mkdir -p dist
cp target/release/jsoncompat${{ matrix.ext }} dist/${{ matrix.asset_name }}
- name: Upload CLI artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: cli-${{ matrix.os }}
path: dist/${{ matrix.asset_name }}
cli-release:
name: Release CLI binaries
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
needs: [actionlint, rust, rust-packages, cli]
permissions:
contents: write
attestations: write
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- name: Generate artifact attestation
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 with:
subject-path: 'cli-*/**'
- name: Upload binaries to GitHub Release
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s nullglob
tag="${GITHUB_REF_NAME}"
assets=(cli-*/*)
if (( ${#assets[@]} == 0 )); then
echo "No release assets matched cli-*/*"
exit 1
fi
repo="${GITHUB_REPOSITORY}"
if gh release view "$tag" --repo "$repo" >/dev/null 2>&1; then
gh release upload "$tag" "${assets[@]}" --repo "$repo" --clobber
else
gh release create "$tag" "${assets[@]}" --repo "$repo" --verify-tag --title "$tag" --notes ""
fi
python-performance:
name: Generated model performance report
runs-on: ubuntu-latest
permissions:
contents: read
env:
PYTHONPATH: pybindings
PYTHONHASHSEED: '0'
JSONCOMPAT_NATIVE_PROFILE: release
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 with:
toolchain: stable
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with:
python-version: '3.12'
- name: Install benchmark dependencies
run: |
python -m pip install uv
uv sync --project pybindings --all-extras --group benchmark --locked --no-install-project
- name: Build release runtime and CLI
run: cargo build --release -p jsoncompat -p jsoncompat_py --locked
- name: Measure validated round trips and unique package startup
run: |
uv run --no-sync --project pybindings python pybindings/bench_dataclasses_codegen.py --iterations 2000 --tree-iterations 50 --repeats 5 --startup-repeats 3
uv run --no-sync --project pybindings python pybindings/bench_dataclasses_codegen_large.py --repeats 3 --startup-repeats 2
uv run --no-sync --project pybindings python pybindings/bench_dataclasses_imports.py --classes 256 --memory-mib 1024
- name: Upload timing samples and provenance
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: python-performance
path: |
target/python-codegen/benchmark.json
target/python-codegen/large/benchmark.json
target/python-codegen/unique-imports/report.json