modelaudit release by @mldangelo #588
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release-please | |
| run-name: modelaudit release by @${{ github.actor }} | |
| concurrency: | |
| group: release-please-${{ github.ref }} | |
| cancel-in-progress: false | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| root_version: | |
| description: "Publish an already-versioned root modelaudit release, for example 0.2.39" | |
| required: false | |
| type: string | |
| picklescan_version: | |
| description: "Publish an already-versioned modelaudit-picklescan release, for example 0.1.2" | |
| required: false | |
| type: string | |
| jobs: | |
| release-please: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| outputs: | |
| release_created: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.release_created || steps.release.outputs.release_created }} | |
| tag_name: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.tag_name || steps.release.outputs.tag_name }} | |
| version: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.version || steps.release.outputs.version }} | |
| picklescan_release_created: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.picklescan_release_created || steps.release.outputs['packages/modelaudit-picklescan--release_created'] }} | |
| picklescan_tag_name: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.picklescan_tag_name || steps.release.outputs['packages/modelaudit-picklescan--tag_name'] }} | |
| picklescan_version: ${{ steps.manual.outputs.manual_release == 'true' && steps.manual.outputs.picklescan_version || steps.release.outputs['packages/modelaudit-picklescan--version'] }} | |
| pr_number: ${{ steps.release.outputs.pr && fromJSON(steps.release.outputs.pr).number || '' }} | |
| steps: | |
| - name: Resolve manual release inputs | |
| id: manual | |
| env: | |
| ROOT_VERSION: ${{ github.event.inputs.root_version || '' }} | |
| PICKLESCAN_VERSION: ${{ github.event.inputs.picklescan_version || '' }} | |
| run: | | |
| { | |
| if [[ -n "$ROOT_VERSION" ]]; then | |
| echo "release_created=true" | |
| echo "version=$ROOT_VERSION" | |
| echo "tag_name=v$ROOT_VERSION" | |
| else | |
| echo "release_created=false" | |
| fi | |
| if [[ -n "$PICKLESCAN_VERSION" ]]; then | |
| echo "picklescan_release_created=true" | |
| echo "picklescan_version=$PICKLESCAN_VERSION" | |
| echo "picklescan_tag_name=modelaudit-picklescan-v$PICKLESCAN_VERSION" | |
| else | |
| echo "picklescan_release_created=false" | |
| fi | |
| if [[ -n "$ROOT_VERSION" || -n "$PICKLESCAN_VERSION" ]]; then | |
| echo "manual_release=true" | |
| else | |
| echo "manual_release=false" | |
| fi | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4 | |
| if: steps.manual.outputs.manual_release != 'true' | |
| id: release | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Ensure manual GitHub releases exist | |
| if: steps.manual.outputs.manual_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ROOT_TAG: ${{ steps.manual.outputs.tag_name }} | |
| ROOT_VERSION: ${{ steps.manual.outputs.version }} | |
| PICKLESCAN_TAG: ${{ steps.manual.outputs.picklescan_tag_name }} | |
| PICKLESCAN_VERSION: ${{ steps.manual.outputs.picklescan_version }} | |
| run: | | |
| if [[ -n "$ROOT_VERSION" ]] && ! gh release view "$ROOT_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release create "$ROOT_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$ROOT_TAG" \ | |
| --notes "Manual recovery release for modelaudit $ROOT_VERSION." | |
| fi | |
| if [[ -n "$PICKLESCAN_VERSION" ]] && ! gh release view "$PICKLESCAN_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release create "$PICKLESCAN_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "modelaudit-picklescan: v$PICKLESCAN_VERSION" \ | |
| --notes "Manual recovery release for modelaudit-picklescan $PICKLESCAN_VERSION." | |
| fi | |
| # Format changelogs immediately after release-please creates/updates a PR | |
| # This runs for BOTH new PRs (prs_created) and existing PRs (pr output exists) | |
| - name: Check if PR exists | |
| id: check-pr | |
| run: | | |
| PR_OUTPUT='${{ steps.release.outputs.pr }}' | |
| if [ -n "$PR_OUTPUT" ] && [ "$PR_OUTPUT" != "null" ]; then | |
| echo "has_pr=true" >> "$GITHUB_OUTPUT" | |
| echo "pr_branch=$(echo '${{ steps.release.outputs.pr }}' | jq -r '.headBranchName')" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_pr=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| with: | |
| ref: ${{ steps.check-pr.outputs.pr_branch }} | |
| # Shallow clone for speed - we only need to format and push | |
| fetch-depth: 1 | |
| - name: Install uv | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| - name: Install Rust toolchain for standalone lock refresh | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| run: | | |
| rustup toolchain install stable --profile minimal | |
| rustup default stable | |
| - name: Sync uv.lock with pyproject.toml | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| run: | | |
| uv lock | |
| if git diff --quiet uv.lock; then | |
| echo "✓ uv.lock already in sync" | |
| else | |
| echo "→ uv.lock updated to match pyproject.toml version bump" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add uv.lock | |
| git commit -m "chore: sync uv.lock with pyproject.toml version bump" | |
| git push | |
| echo "✓ uv.lock committed and pushed" | |
| fi | |
| - name: Sync standalone package lock with pyproject.toml | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| working-directory: packages/modelaudit-picklescan | |
| run: | | |
| uv lock | |
| if git diff --quiet uv.lock; then | |
| echo "✓ packages/modelaudit-picklescan/uv.lock already in sync" | |
| else | |
| echo "→ packages/modelaudit-picklescan/uv.lock updated to match pyproject.toml" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add uv.lock | |
| git commit -m "chore: sync standalone package lock" | |
| git push | |
| echo "✓ packages/modelaudit-picklescan/uv.lock committed and pushed" | |
| fi | |
| - name: Setup Node.js | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 | |
| with: | |
| node-version: "24" | |
| - name: Install Node dependencies | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| run: npm ci --ignore-scripts | |
| - name: Format changelogs with Prettier | |
| if: steps.check-pr.outputs.has_pr == 'true' | |
| run: | | |
| echo "Formatting changelogs on branch: ${{ steps.check-pr.outputs.pr_branch }}" | |
| npx prettier --write CHANGELOG.md packages/modelaudit-picklescan/CHANGELOG.md | |
| if git diff --quiet CHANGELOG.md packages/modelaudit-picklescan/CHANGELOG.md; then | |
| echo "✓ No formatting changes needed" | |
| else | |
| echo "→ Formatting changes detected, committing..." | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add CHANGELOG.md packages/modelaudit-picklescan/CHANGELOG.md | |
| git commit -m "chore: format changelogs with prettier" | |
| git push | |
| echo "✓ Formatting committed and pushed" | |
| fi | |
| build: | |
| if: needs.release-please.outputs.release_created == 'true' | |
| runs-on: ubuntu-latest | |
| needs: release-please | |
| permissions: | |
| contents: read | |
| outputs: | |
| artifact-name: ${{ steps.upload.outputs.artifact-id }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| enable-cache: true | |
| - name: Install Rust toolchain | |
| run: | | |
| rustup toolchain install stable --profile minimal | |
| rustup default stable | |
| - name: Sync dependencies | |
| run: uv sync --extra all-ci | |
| - name: Lint root package with Ruff | |
| run: uv run ruff check modelaudit/ tests/ | |
| - name: Check root package formatting with Ruff | |
| run: uv run ruff format --check modelaudit/ tests/ | |
| - name: Type check root package with mypy | |
| run: uv run mypy modelaudit/ tests/ | |
| - name: Run root package tests | |
| run: | | |
| uv run pytest tests -n auto -m "not slow and not integration and not performance" --tb=short | |
| - name: Build package | |
| run: uv build | |
| - name: Build standalone pickle package for smoke installs | |
| run: uv build packages/modelaudit-picklescan --out-dir /tmp/modelaudit-picklescan-dist | |
| - name: Verify distribution artifacts and version consistency | |
| run: | | |
| set -euo pipefail | |
| EXPECTED_VERSION="${{ needs.release-please.outputs.version }}" | |
| shopt -s nullglob | |
| wheels=(dist/*.whl) | |
| sdists=(dist/*.tar.gz) | |
| echo "Built artifacts:" | |
| ls -la dist/ | |
| if [[ ${#wheels[@]} -ne 1 ]]; then | |
| echo "ERROR: Expected exactly 1 wheel artifact, found ${#wheels[@]}" | |
| exit 1 | |
| fi | |
| if [[ ${#sdists[@]} -ne 1 ]]; then | |
| echo "ERROR: Expected exactly 1 sdist artifact, found ${#sdists[@]}" | |
| exit 1 | |
| fi | |
| WHEEL_PATH="${wheels[0]}" | |
| SDIST_PATH="${sdists[0]}" | |
| WHEEL_FILE="$(basename "${WHEEL_PATH}")" | |
| SDIST_FILE="$(basename "${SDIST_PATH}")" | |
| # Extract versions from package metadata, not filenames. | |
| WHEEL_VERSION="$( | |
| python - "${WHEEL_PATH}" <<'PY' | |
| import sys | |
| import zipfile | |
| wheel_path = sys.argv[1] | |
| with zipfile.ZipFile(wheel_path) as zf: | |
| metadata_files = [name for name in zf.namelist() if name.endswith(".dist-info/METADATA")] | |
| if len(metadata_files) != 1: | |
| raise SystemExit(f"Expected exactly one wheel METADATA file, found {len(metadata_files)}") | |
| metadata_text = zf.read(metadata_files[0]).decode("utf-8", errors="replace") | |
| for line in metadata_text.splitlines(): | |
| if line.startswith("Version: "): | |
| print(line.split(":", 1)[1].strip()) | |
| break | |
| else: | |
| raise SystemExit("Could not find Version header in wheel METADATA") | |
| PY | |
| )" | |
| SDIST_VERSION="$( | |
| python - "${SDIST_PATH}" <<'PY' | |
| import sys | |
| import tarfile | |
| sdist_path = sys.argv[1] | |
| with tarfile.open(sdist_path, "r:gz") as tf: | |
| pkg_info_files = [m for m in tf.getmembers() if m.name.endswith("/PKG-INFO")] | |
| if len(pkg_info_files) != 1: | |
| raise SystemExit(f"Expected exactly one PKG-INFO file in sdist, found {len(pkg_info_files)}") | |
| file_obj = tf.extractfile(pkg_info_files[0]) | |
| if file_obj is None: | |
| raise SystemExit("Failed to read PKG-INFO from sdist") | |
| pkg_info_text = file_obj.read().decode("utf-8", errors="replace") | |
| for line in pkg_info_text.splitlines(): | |
| if line.startswith("Version: "): | |
| print(line.split(":", 1)[1].strip()) | |
| break | |
| else: | |
| raise SystemExit("Could not find Version header in sdist PKG-INFO") | |
| PY | |
| )" | |
| if [[ "$WHEEL_VERSION" != "$EXPECTED_VERSION" || "$SDIST_VERSION" != "$EXPECTED_VERSION" ]]; then | |
| echo "ERROR: Artifact versions do not match expected release version" | |
| echo "Expected version: $EXPECTED_VERSION" | |
| echo "Wheel version: $WHEEL_VERSION ($WHEEL_FILE)" | |
| echo "sdist version: $SDIST_VERSION ($SDIST_FILE)" | |
| exit 1 | |
| fi | |
| echo "Artifact validation passed for version ${EXPECTED_VERSION}" | |
| - name: Validate package metadata | |
| run: uvx twine check dist/* | |
| - name: Smoke test wheel install in clean environment | |
| run: | | |
| set -euo pipefail | |
| EXPECTED_VERSION="${{ needs.release-please.outputs.version }}" | |
| shopt -s nullglob | |
| wheel_artifacts=(dist/modelaudit-*.whl) | |
| if [[ ${#wheel_artifacts[@]} -ne 1 ]]; then | |
| echo "ERROR: Expected exactly 1 wheel artifact for install smoke test, found ${#wheel_artifacts[@]}" | |
| ls -la dist/ | |
| exit 1 | |
| fi | |
| WHEEL_ARTIFACT="${wheel_artifacts[0]}" | |
| uv venv /tmp/modelaudit-wheel-smoke | |
| uv pip install \ | |
| --python /tmp/modelaudit-wheel-smoke/bin/python \ | |
| --find-links /tmp/modelaudit-picklescan-dist \ | |
| "${WHEEL_ARTIFACT}" | |
| INSTALLED_VERSION="$( | |
| /tmp/modelaudit-wheel-smoke/bin/python -c "import importlib.metadata as m; print(m.version('modelaudit'))" | |
| )" | |
| if [[ "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then | |
| echo "ERROR: Wheel install version mismatch: expected $EXPECTED_VERSION, got $INSTALLED_VERSION" | |
| exit 1 | |
| fi | |
| # Validate required project URLs in installed metadata. | |
| /tmp/modelaudit-wheel-smoke/bin/python - <<'PY' | |
| import importlib.metadata as md | |
| # Keep these expected URLs in sync with [project.urls] in pyproject.toml. | |
| required = { | |
| "Bug Tracker": "https://github.com/promptfoo/modelaudit/issues", | |
| "Changelog": "https://github.com/promptfoo/modelaudit/blob/main/CHANGELOG.md", | |
| } | |
| project_urls = md.metadata("modelaudit").get_all("Project-URL") or [] | |
| parsed = {} | |
| for entry in project_urls: | |
| if "," in entry: | |
| label, url = entry.split(",", 1) | |
| parsed[label.strip()] = url.strip() | |
| missing = [label for label in required if label not in parsed] | |
| mismatched = [label for label, expected in required.items() if parsed.get(label) != expected] | |
| if missing or mismatched: | |
| raise SystemExit( | |
| f"Project URL metadata invalid. missing={missing}, mismatched={mismatched}, parsed={parsed}" | |
| ) | |
| print("Project URL metadata validated.") | |
| PY | |
| /tmp/modelaudit-wheel-smoke/bin/modelaudit --version | |
| /tmp/modelaudit-wheel-smoke/bin/python - <<'PY' | |
| import modelaudit_picklescan | |
| report = modelaudit_picklescan.scan_bytes(b"\x80\x04}q\x00.") | |
| assert report.status.value == "complete", report | |
| print("modelaudit_picklescan import and scan smoke test passed.") | |
| PY | |
| # Basic CLI smoke run from the installed wheel. | |
| /tmp/modelaudit-wheel-smoke/bin/python - <<'PY' | |
| import pathlib | |
| import pickle | |
| import subprocess | |
| import tempfile | |
| with tempfile.TemporaryDirectory(prefix="modelaudit-wheel-smoke-") as tmpdir: | |
| test_dir = pathlib.Path(tmpdir) | |
| test_file = test_dir / "smoke.pkl" | |
| with test_file.open("wb") as f: | |
| pickle.dump({"smoke": True}, f) | |
| completed = subprocess.run( | |
| ["/tmp/modelaudit-wheel-smoke/bin/modelaudit", str(test_file), "--format", "json"], | |
| capture_output=True, | |
| text=True, | |
| check=False, | |
| ) | |
| print(completed.stdout) | |
| if completed.returncode not in (0, 1): | |
| raise SystemExit(f"Unexpected modelaudit exit code during wheel smoke test: {completed.returncode}") | |
| PY | |
| - name: Smoke test sdist install in clean environment | |
| run: | | |
| set -euo pipefail | |
| EXPECTED_VERSION="${{ needs.release-please.outputs.version }}" | |
| shopt -s nullglob | |
| sdist_artifacts=(dist/modelaudit-*.tar.gz) | |
| if [[ ${#sdist_artifacts[@]} -ne 1 ]]; then | |
| echo "ERROR: Expected exactly 1 sdist artifact for install smoke test, found ${#sdist_artifacts[@]}" | |
| ls -la dist/ | |
| exit 1 | |
| fi | |
| SDIST_ARTIFACT="${sdist_artifacts[0]}" | |
| uv venv /tmp/modelaudit-sdist-smoke | |
| uv pip install \ | |
| --python /tmp/modelaudit-sdist-smoke/bin/python \ | |
| --find-links /tmp/modelaudit-picklescan-dist \ | |
| "${SDIST_ARTIFACT}" | |
| INSTALLED_VERSION="$( | |
| /tmp/modelaudit-sdist-smoke/bin/python -c "import importlib.metadata as m; print(m.version('modelaudit'))" | |
| )" | |
| if [[ "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then | |
| echo "ERROR: sdist install version mismatch: expected $EXPECTED_VERSION, got $INSTALLED_VERSION" | |
| exit 1 | |
| fi | |
| /tmp/modelaudit-sdist-smoke/bin/modelaudit --version | |
| /tmp/modelaudit-sdist-smoke/bin/python - <<'PY' | |
| import modelaudit_picklescan | |
| report = modelaudit_picklescan.scan_bytes(b"\x80\x04}q\x00.") | |
| assert report.status.value == "complete", report | |
| print("modelaudit_picklescan import and scan smoke test passed.") | |
| PY | |
| # Basic CLI smoke run from the installed sdist. | |
| /tmp/modelaudit-sdist-smoke/bin/python - <<'PY' | |
| import pathlib | |
| import pickle | |
| import subprocess | |
| import tempfile | |
| with tempfile.TemporaryDirectory(prefix="modelaudit-sdist-smoke-") as tmpdir: | |
| test_dir = pathlib.Path(tmpdir) | |
| test_file = test_dir / "smoke.pkl" | |
| with test_file.open("wb") as f: | |
| pickle.dump({"smoke": True}, f) | |
| completed = subprocess.run( | |
| ["/tmp/modelaudit-sdist-smoke/bin/modelaudit", str(test_file), "--format", "json"], | |
| capture_output=True, | |
| text=True, | |
| check=False, | |
| ) | |
| print(completed.stdout) | |
| if completed.returncode not in (0, 1): | |
| raise SystemExit(f"Unexpected modelaudit exit code during sdist smoke test: {completed.returncode}") | |
| PY | |
| - name: Upload build artifacts | |
| id: upload | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| build-picklescan-package: | |
| if: needs.release-please.outputs.picklescan_release_created == 'true' | |
| name: Build standalone pickle package (${{ matrix.artifact-suffix }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: release-please | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| artifact-suffix: linux | |
| build-sdist: "true" | |
| - os: macos-14 | |
| artifact-suffix: macos-arm64 | |
| build-sdist: "false" | |
| - os: macos-15-intel | |
| artifact-suffix: macos-x86_64 | |
| build-sdist: "false" | |
| - os: ubuntu-24.04-arm | |
| artifact-suffix: linux-aarch64 | |
| build-sdist: "false" | |
| - os: windows-latest | |
| artifact-suffix: windows | |
| build-sdist: "false" | |
| defaults: | |
| run: | |
| shell: bash | |
| working-directory: packages/modelaudit-picklescan | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| enable-cache: true | |
| - name: Pin Python version | |
| run: | | |
| uv python pin 3.12 | |
| - name: Install Rust toolchain | |
| run: | | |
| rustup toolchain install stable --profile minimal --component rustfmt --component clippy | |
| rustup default stable | |
| - name: Cache Cargo dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/modelaudit-picklescan/target | |
| key: ${{ runner.os }}-cargo-picklescan-${{ hashFiles('packages/modelaudit-picklescan/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-picklescan- | |
| - name: Check standalone package lock is in sync | |
| run: | | |
| uv lock --check | |
| - name: Check Rust scanner formatting | |
| run: cargo fmt --manifest-path Cargo.toml -- --check | |
| - name: Check Rust scanner crate | |
| run: cargo check --manifest-path Cargo.toml | |
| - name: Lint Rust scanner crate | |
| run: cargo clippy --manifest-path Cargo.toml --all-targets -- -D warnings | |
| - name: Test Rust scanner crate | |
| run: | | |
| cargo test --manifest-path Cargo.toml | |
| - name: Lint standalone package with Ruff | |
| run: uv run --with ruff ruff check src tests | |
| - name: Check standalone package formatting with Ruff | |
| run: uv run --with ruff ruff format --check src tests | |
| - name: Type check standalone package with mypy | |
| run: uv run --with mypy mypy src tests | |
| - name: Run standalone package tests | |
| run: uv run --with pytest --with pytest-xdist pytest -n auto tests --tb=short | |
| - name: Build standalone package sdist | |
| if: matrix.build-sdist == 'true' | |
| run: | | |
| uv build --sdist --out-dir dist | |
| - name: Build standalone package manylinux wheel | |
| if: runner.os == 'Linux' | |
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1 | |
| with: | |
| command: build | |
| args: --release --out dist | |
| manylinux: "2_28" | |
| working-directory: packages/modelaudit-picklescan | |
| - name: Build standalone package wheel | |
| if: runner.os != 'Linux' | |
| run: | | |
| uv build --wheel --out-dir dist | |
| - name: Validate standalone package metadata | |
| run: uvx twine check dist/* | |
| - name: Verify standalone artifact version consistency | |
| run: | | |
| set -euo pipefail | |
| EXPECTED_VERSION="${{ needs.release-please.outputs.picklescan_version }}" | |
| shopt -s nullglob | |
| artifacts=(dist/modelaudit_picklescan-*.whl dist/modelaudit_picklescan-*.tar.gz) | |
| if [[ ${#artifacts[@]} -eq 0 ]]; then | |
| echo "ERROR: Expected at least one modelaudit_picklescan artifact" | |
| ls -la dist/ | |
| exit 1 | |
| fi | |
| for artifact in "${artifacts[@]}"; do | |
| ARTIFACT_VERSION="$( | |
| python - "${artifact}" <<'PY' | |
| import sys | |
| import tarfile | |
| import zipfile | |
| from pathlib import Path | |
| artifact_path = Path(sys.argv[1]) | |
| if artifact_path.suffix == ".whl": | |
| with zipfile.ZipFile(artifact_path) as zf: | |
| metadata_files = [name for name in zf.namelist() if name.endswith(".dist-info/METADATA")] | |
| if len(metadata_files) != 1: | |
| raise SystemExit(f"Expected one wheel METADATA file, found {len(metadata_files)}") | |
| metadata_text = zf.read(metadata_files[0]).decode("utf-8", errors="replace") | |
| elif artifact_path.name.endswith(".tar.gz"): | |
| with tarfile.open(artifact_path, "r:gz") as tf: | |
| pkg_info_files = [member for member in tf.getmembers() if member.name.endswith("/PKG-INFO")] | |
| if len(pkg_info_files) != 1: | |
| raise SystemExit(f"Expected one PKG-INFO file, found {len(pkg_info_files)}") | |
| file_obj = tf.extractfile(pkg_info_files[0]) | |
| if file_obj is None: | |
| raise SystemExit("Failed to read PKG-INFO from sdist") | |
| metadata_text = file_obj.read().decode("utf-8", errors="replace") | |
| else: | |
| raise SystemExit(f"Unsupported artifact type: {artifact_path}") | |
| for line in metadata_text.splitlines(): | |
| if line.startswith("Version: "): | |
| print(line.split(":", 1)[1].strip()) | |
| break | |
| else: | |
| raise SystemExit("Could not find Version header") | |
| PY | |
| )" | |
| if [[ "$ARTIFACT_VERSION" != "$EXPECTED_VERSION" ]]; then | |
| echo "ERROR: Artifact version mismatch for ${artifact}" | |
| echo "Expected version: $EXPECTED_VERSION" | |
| echo "Artifact version: $ARTIFACT_VERSION" | |
| exit 1 | |
| fi | |
| done | |
| echo "Standalone artifact validation passed for version ${EXPECTED_VERSION}" | |
| - name: Smoke test standalone package wheel install | |
| run: | | |
| set -euo pipefail | |
| uv venv /tmp/modelaudit-picklescan-wheel-smoke | |
| if [[ -x /tmp/modelaudit-picklescan-wheel-smoke/bin/python ]]; then | |
| SMOKE_PYTHON=/tmp/modelaudit-picklescan-wheel-smoke/bin/python | |
| else | |
| SMOKE_PYTHON=/tmp/modelaudit-picklescan-wheel-smoke/Scripts/python.exe | |
| fi | |
| shopt -s nullglob | |
| picklescan_wheels=(dist/modelaudit_picklescan-*.whl) | |
| if [[ ${#picklescan_wheels[@]} -ne 1 ]]; then | |
| echo "ERROR: Expected exactly 1 modelaudit_picklescan wheel artifact, found ${#picklescan_wheels[@]}" | |
| ls -la dist/ | |
| exit 1 | |
| fi | |
| uv pip install --python "$SMOKE_PYTHON" "${picklescan_wheels[0]}" | |
| smoke_dir="$(mktemp -d)" | |
| ( | |
| cd "$smoke_dir" | |
| PYTHONPATH='' "$SMOKE_PYTHON" -I - <<'PY' | |
| import importlib.util | |
| import modelaudit_picklescan | |
| assert importlib.util.find_spec("modelaudit") is None | |
| assert importlib.util.find_spec("modelaudit_picklescan._rust") is not None | |
| report = modelaudit_picklescan.scan_bytes(b"\x80\x04}q\x00.") | |
| assert report.status.value == "complete", report | |
| assert report.verdict.value == "clean", report | |
| assert not any(notice.code == "engine_fallback" for notice in report.notices) | |
| print("standalone modelaudit_picklescan wheel loaded without modelaudit") | |
| PY | |
| ) | |
| - name: Upload standalone package artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: modelaudit-picklescan-dist-${{ matrix.artifact-suffix }} | |
| path: packages/modelaudit-picklescan/dist/ | |
| publish-pypi: | |
| if: needs.release-please.outputs.release_created == 'true' | |
| needs: [build, release-please] | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/modelaudit/ | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 | |
| with: | |
| print-hash: true | |
| attestations: true | |
| publish-picklescan-pypi: | |
| if: needs.release-please.outputs.picklescan_release_created == 'true' | |
| needs: [build-picklescan-package, release-please] | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/modelaudit-picklescan/ | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Download standalone pickle package artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: modelaudit-picklescan-dist-* | |
| path: dist/ | |
| merge-multiple: true | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 | |
| with: | |
| print-hash: true | |
| attestations: true | |
| provenance: | |
| if: needs.release-please.outputs.release_created == 'true' | |
| needs: [build, publish-pypi, release-please] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| sparse-checkout: | | |
| pyproject.toml | |
| uv.lock | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| enable-cache: true | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Generate artifact attestations | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4 | |
| with: | |
| subject-path: "dist/*" | |
| - name: Generate SBOM | |
| run: | | |
| uv export \ | |
| --frozen \ | |
| --no-dev \ | |
| --preview-features sbom-export \ | |
| --format cyclonedx1.5 \ | |
| --output-file dist/modelaudit-${{ needs.release-please.outputs.version }}.cdx.json \ | |
| > /dev/null | |
| echo "SBOM generated:" | |
| ls -la dist/*.cdx.json | |
| # Upload wheel/sdist and SBOM to the GitHub Release as an alternative | |
| # download location alongside PyPI. --clobber ensures idempotency on re-runs. | |
| - name: Upload build artifacts to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release upload "${{ needs.release-please.outputs.tag_name }}" dist/* \ | |
| --repo "${{ github.repository }}" \ | |
| --clobber | |
| picklescan-provenance: | |
| if: needs.release-please.outputs.picklescan_release_created == 'true' | |
| needs: [build-picklescan-package, publish-picklescan-pypi, release-please] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| sparse-checkout: | | |
| packages/modelaudit-picklescan/pyproject.toml | |
| packages/modelaudit-picklescan/uv.lock | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| enable-cache: true | |
| - name: Download standalone pickle package artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: modelaudit-picklescan-dist-* | |
| path: dist/ | |
| merge-multiple: true | |
| - name: Generate artifact attestations | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4 | |
| with: | |
| subject-path: "dist/*" | |
| - name: Generate standalone package SBOM | |
| working-directory: packages/modelaudit-picklescan | |
| run: | | |
| uv export \ | |
| --frozen \ | |
| --no-dev \ | |
| --preview-features sbom-export \ | |
| --format cyclonedx1.5 \ | |
| --output-file ../../dist/modelaudit-picklescan-${{ needs.release-please.outputs.picklescan_version }}.cdx.json \ | |
| > /dev/null | |
| echo "Standalone package SBOM generated:" | |
| ls -la ../../dist/*.cdx.json | |
| - name: Upload standalone artifacts to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release upload "${{ needs.release-please.outputs.picklescan_tag_name }}" dist/* \ | |
| --repo "${{ github.repository }}" \ | |
| --clobber |