name: CI & Wheel Release Pipeline on: push: branches: [ main, master ] tags: [ 'v*' ] pull_request: branches: [ main, master ] workflow_dispatch: permissions: contents: read jobs: test-rust: name: Cargo Test & Lint (Linux x86_64) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: Install cargo-deny uses: taiki-e/install-action@v2 with: tool: cargo-deny - name: Security Advisory & License Check run: cargo deny check - name: Run Tests run: cargo test --workspace - name: Run Clippy run: cargo clippy --workspace --tests -- -D warnings - name: Check Formatting run: cargo fmt --check miri-test: name: Memory Safety (ASan + targeted Miri) runs-on: ubuntu-latest needs: test-rust steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly with: components: miri, rust-src - name: Run memory-safety gate # Fast: ASan on the full core suite + targeted Miri on the only # unsafe module (memory.rs). See scripts/sanitize.sh. run: bash scripts/sanitize.sh build-wheels: name: Build Wheels (${{ matrix.os }} - ${{ matrix.target }}) runs-on: ${{ matrix.os }} needs: miri-test strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu rust-target: x86_64-unknown-linux-gnu zig: true - os: ubuntu-latest target: aarch64-unknown-linux-gnu rust-target: aarch64-unknown-linux-gnu zig: true - os: ubuntu-latest target: x86_64-unknown-linux-musl rust-target: x86_64-unknown-linux-musl zig: true - os: ubuntu-latest target: aarch64-unknown-linux-musl rust-target: aarch64-unknown-linux-musl zig: true - os: macos-latest target: aarch64-apple-darwin rust-target: aarch64-apple-darwin - os: macos-latest target: x86_64-apple-darwin rust-target: x86_64-apple-darwin zig: true - os: windows-latest target: x86_64-pc-windows-msvc rust-target: x86_64-pc-windows-msvc # Free-threaded CPython (3.14t): parallel GIL-free execution. The # limited API (abi3) cannot be free-threaded, so this build passes # --no-default-features (abi3 is the only default feature that must # go; mail/http3/orjson stay) and targets the 3.14t interpreter. - os: ubuntu-latest target: x86_64-unknown-linux-gnu rust-target: x86_64-unknown-linux-gnu python: "3.14t" zig: true features: "--no-default-features --features mail,http3,orjson" steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python || '3.12' }} enable-cache: true cache-dependency-glob: crates/justapi-py/pyproject.toml - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust-target }} - name: Install maturin shell: bash run: | uv tool install maturin echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Install Zig (for cross-compilation) # matrix.zig is a YAML boolean, which GitHub's == does not coerce # against the string 'true' — accept both forms so the step always # runs when zig is needed (the bash build step compares the # interpolated value, which is always the string "true"). # # Download directly from ziglang.org (official tarballs) instead of # mlugg/setup-zig — its mirrors were down (503/502/404) and it is a # Node 20 action (deprecated). Matches the zig version verified # locally (maturin 1.14.1 + zig 0.16.0, 2026-08-07). if: matrix.zig == true || matrix.zig == 'true' shell: bash run: | VER="0.16.0" OS="$(uname -s | tr '[:upper:]' '[:lower:]')" # linux / darwin [ "$OS" = "darwin" ] && OS="macos" HOST_ARCH="$(uname -m)" # host arch of the runner [ "$HOST_ARCH" = "arm64" ] && HOST_ARCH="aarch64" curl -fsSL --retry 5 --retry-all-errors \ "https://ziglang.org/download/${VER}/zig-${HOST_ARCH}-${OS}-${VER}.tar.xz" \ -o /tmp/zig.tar.xz tar -xJf /tmp/zig.tar.xz -C /tmp echo "/tmp/zig-${HOST_ARCH}-${OS}-${VER}" >> "$GITHUB_PATH" - name: Resolve free-threaded interpreter if: matrix.python == '3.14t' shell: bash run: echo "PY314T=$(uv python find 3.14t)" >> "$GITHUB_ENV" - name: Build wheel (maturin + zig cross) shell: bash working-directory: crates/justapi-py run: | INTERP="" if [ -n "${{ matrix.python }}" ]; then INTERP="--interpreter ${{ env.PY314T }}"; fi ZIG="" if [ "${{ matrix.zig }}" = "true" ]; then ZIG="--zig"; fi maturin build --release --out dist $ZIG --target ${{ matrix.target }} \ $INTERP ${{ matrix.features || '' }} - name: Install wheel & test # Only rows where the wheel is installable on the runner itself: # ubuntu x86_64-gnu (glibc matches, incl. the 3.14t row) # macos aarch64 (runner is arm64) # windows x64 (runner is x64) # Skipped: musllinux wheels (can't install on the glibc runner), # linux aarch64, and macos x86_64 (cross-built, arm64 runner). if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-pc-windows-msvc' shell: bash working-directory: crates/justapi-py run: | uv venv --python ${{ matrix.python || '3.12' }} "$RUNNER_TEMP/wtest" # `uv run --directory` resolves the venv python on all platforms # (Scripts/ on Windows, bin/ on Unix). uv pip install --python "$(uv run --directory "$RUNNER_TEMP/wtest" python -c 'import sys; print(sys.executable)')" dist/*.whl uv pip install --python "$(uv run --directory "$RUNNER_TEMP/wtest" python -c 'import sys; print(sys.executable)')" pytest pytest-asyncio requests pydantic jinja2 websockets "$(uv run --directory "$RUNNER_TEMP/wtest" python -c 'import sys; print(sys.executable)')" -m pytest tests/ -v --tb=short - name: Upload Wheels uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ strategy.job-index }} path: crates/justapi-py/dist/*.whl publish: name: Publish to PyPI + GitHub Release runs-on: ubuntu-latest needs: build-wheels if: startsWith(github.ref, 'refs/tags/v') # OIDC trusted publishing — NO token needed. Requires the PyPI project's # trusted publisher to be configured once at pypi.org (see setup note at # the bottom of this file). The `pypi` environment is auto-created by # GitHub on first run. environment: pypi permissions: id-token: write contents: write steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: '3.12' - uses: actions/download-artifact@v4 with: pattern: wheels-* merge-multiple: true path: dist - name: Publish to PyPI (trusted publishing) # `uv publish` defaults to dist/*.whl; `--trusted-publishing always` # uses the OIDC token from the `pypi` environment — no API token. run: uv publish --trusted-publishing always - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl generate_release_notes: true # --- One-time setup (required once, then publishing is fully automated) --- # # 1. At pypi.org → Account → Publishing → "Add a new pending publisher": # • Publisher: GitHub # • Owner: swadhinbiswas # • Repository: JustAPI # • Environment: pypi (must match the job's `environment:`) # • Workflow name: wheels.yml # (Trusted publishing auto-creates the `justapi` project on first upload.) # # 2. Push a tag: `git tag v2.0.8 && git push origin v2.0.8` # # That's it — no MATURIN_PYPI_TOKEN, no secrets. The wheels.yml `publish` job # then builds the 7-platform matrix, uploads to PyPI via OIDC, and creates the # GitHub Release with the wheels attached.