name: 'SDK: Package Checks' # Rehearses the PyPI release on every packaging change and once a week, from the # consumer's side. Two incidents this guards against: # # - 5.38.0 shipped an unsatisfiable Requires-Dist (cryptography==50.0.0 while # alibabacloud-tea-openapi and pyopenssl cap it below 49). A [tool.uv] override hid # the conflict inside the repo; pip could not install the wheel and silently # resolved `pip install prowler` to 5.37.1 for a week. # - 5.39.0 never published: an unpinned build backend started emitting core metadata # 2.5 and the twine bundled in the publish action rejected it. # # Both were only detectable at release time because nothing built and installed the # artifact earlier. The weekly run also catches releases yanked from PyPI after we # pinned them (zstd 1.5.7.3, "buggy - not thread safe", sat in uv.lock for months). on: push: branches: - 'master' - 'v5.*' pull_request: branches: - 'master' - 'v5.*' schedule: # Monday 06:00 UTC. Yanks and upstream releases happen without a commit here. - cron: '0 6 * * 1' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} env: # Must equal the twine bundled in the pypa/gh-action-pypi-publish pin used by # sdk-pypi-release.yml (requirements/runtime.txt in that repo at the pinned tag). # A metadata check that passes here must pass there. TWINE_VERSION: '7.0.0' jobs: changes: if: github.repository == 'prowler-cloud/prowler' runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read outputs: # Scheduled and manual runs always execute; pushes and PRs only when a packaging # input changed. Jobs skipped this way still report success to branch protection. run: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.filter.outputs.any_changed == 'true' }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block allowed-endpoints: > github.com:443 api.github.com:443 - name: Checkout repository if: github.event_name == 'push' || github.event_name == 'pull_request' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Detect packaging changes if: github.event_name == 'push' || github.event_name == 'pull_request' id: filter uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: files: | pyproject.toml uv.lock README.md util/replicate_pypi_package.py util/check_yanked_pins.py api/pyproject.toml api/uv.lock mcp_server/pyproject.toml mcp_server/uv.lock .github/workflows/sdk-package-checks.yml .github/workflows/sdk-pypi-release.yml .github/actions/setup-python-uv/** install-from-wheel: needs: changes if: needs.changes.outputs.run == 'true' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read strategy: fail-fast: false matrix: python-version: - '3.10' - '3.11' - '3.12' - '3.13' - '3.14' package: - 'prowler' include: # prowler-cloud is the same tree renamed by util/replicate_pypi_package.py; # one Python is enough to prove the rename and its build still work. - python-version: '3.12' package: 'prowler-cloud' steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block allowed-endpoints: > github.com:443 api.github.com:443 release-assets.githubusercontent.com:443 results-receiver.actions.githubusercontent.com:443 *.blob.core.windows.net:443 pypi.org:443 files.pythonhosted.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Python with uv uses: ./.github/actions/setup-python-uv with: python-version: ${{ matrix.python-version }} install-dependencies: 'false' - name: Rename package to prowler-cloud if: matrix.package == 'prowler-cloud' run: | pip install --no-cache-dir toml python util/replicate_pypi_package.py - name: Build sdist and wheel run: uv build - name: Check metadata with the release workflow's twine run: uvx --from "twine==${TWINE_VERSION}" twine check --strict dist/* - name: Upload Prowler wheel for portability checks if: matrix.python-version == '3.14' && matrix.package == 'prowler' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: prowler-python-3.14-wheel path: dist/prowler-*.whl if-no-files-found: error retention-days: 1 - name: Install the wheel with pip into a clean virtualenv # Plain pip, --isolated, from outside the repo: consumers never see [tool.uv] # override-dependencies or constraint-dependencies, so neither does this step. run: | python -m venv "${RUNNER_TEMP}/consumer" "${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip cd "${RUNNER_TEMP}" "${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir "${GITHUB_WORKSPACE}"/dist/*.whl - name: Smoke test the installed CLI run: | cd "${RUNNER_TEMP}" "${RUNNER_TEMP}/consumer/bin/prowler" --version # Loads every AWS check module from the installed wheel: catches files missing # from the package. grep fails the step if the summary line never appears. "${RUNNER_TEMP}/consumer/bin/prowler" aws --list-checks | grep 'available checks' python-3-14-binary-wheel-portability: needs: install-from-wheel runs-on: ${{ matrix.runner }} timeout-minutes: 10 permissions: actions: read contents: read strategy: fail-fast: false matrix: runner: - ubuntu-latest - macos-15-intel - macos-15 - windows-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: # Block mode is unavailable on GitHub-hosted macOS and Windows runners. egress-policy: audit - name: Set up Python 3.14 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.14' - name: Download built Prowler wheel uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: prowler-python-3.14-wheel path: dist - name: Extract NumPy and pandas requirements from wheel metadata shell: python run: | from email.parser import BytesParser from pathlib import Path import re from zipfile import ZipFile wheel = next(Path("dist").glob("prowler-*.whl")) with ZipFile(wheel) as archive: metadata_name = next( name for name in archive.namelist() if name.endswith(".dist-info/METADATA") ) metadata = BytesParser().parsebytes(archive.read(metadata_name)) requirements = [ requirement for requirement in metadata.get_all("Requires-Dist", []) if re.match(r"^(numpy|pandas)(?:\W|$)", requirement, re.IGNORECASE) ] requirement_names = { re.match(r"^(numpy|pandas)", requirement, re.IGNORECASE).group(1).lower() for requirement in requirements } if requirement_names != {"numpy", "pandas"}: raise SystemExit( f"Expected NumPy and pandas requirements, found: {requirements}" ) Path("binary-wheel-requirements.txt").write_text( "\n".join(requirements) + "\n", encoding="utf-8" ) print("Wheel requirements:", *requirements, sep="\n ") # huaweicloudsdkcore hard-pins pymongo 4.15.1, which has no cp314 wheel; # its optional C extensions fall back to a pure-Python build without a compiler. - name: Require binary distributions for marked NumPy and pandas versions run: >- python -m pip download --only-binary=:all: --dest binary-wheels --requirement binary-wheel-requirements.txt pinned-releases-not-yanked: needs: changes if: needs.changes.outputs.run == 'true' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block allowed-endpoints: > github.com:443 api.github.com:443 release-assets.githubusercontent.com:443 pypi.org:443 files.pythonhosted.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.12' - name: Check every pinned and locked release against PyPI run: python util/check_yanked_pins.py . api mcp_server