diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5474ac9..a271b376 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -221,9 +221,11 @@ jobs: with: python-version: '3.13' - name: Install dependencies - run: python -m pip install --upgrade pip build --user + run: python -m pip install .[wheel] - name: Build a binary wheel and a source tarball run: python3 -m build + - name: Generate SBOM for Python distribution + run: python script/create_sbom.py --py - name: Store the distribution packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 0ba23963..87a3b6ac 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -34,14 +34,21 @@ jobs: with: python-version: '3.x' - name: Install dependencies - run: python -m pip install --upgrade pip build --user + run: python -m pip install .[wheel] - name: Build a binary wheel and a source tarball run: python3 -m build + - name: Generate SBOM for Python distribution + run: python script/create_sbom.py --py --output-dir dist-sbom - name: Store the distribution packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: python-package-distributions path: dist/ + - name: Store the SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-sbom + path: dist-sbom/ publish-to-testpypi: name: Publish Python distribution 📦 to TestPyPI @@ -83,6 +90,7 @@ jobs: url: https://pypi.org/p/dfetch permissions: id-token: write + contents: write steps: - name: Download all the dists @@ -92,3 +100,17 @@ jobs: path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1 + with: + skip-existing: true + - name: Download SBOM + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5 + with: + name: python-sbom + path: dist-sbom/ + - name: Upload SBOM to GitHub Release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v2.5.0 + with: + tag_name: ${{ github.event.release.tag_name }} + files: dist-sbom/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 887a4028..b872a7c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ build = [ "setuptools-scm==10.0.5", # For determining version ] sbom = ["cyclonedx-bom==7.3.0"] +wheel = ["build==1.2.2"] [project.scripts] dfetch = "dfetch.__main__:main" diff --git a/script/create_sbom.py b/script/create_sbom.py index 961d132e..24ca995e 100644 --- a/script/create_sbom.py +++ b/script/create_sbom.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Generate an sbom of the tool.""" +import argparse import contextlib import logging import subprocess # nosec @@ -13,7 +14,6 @@ PROJECT_DIR = Path(__file__).parent.parent.resolve() - DEPS = f"{PROJECT_DIR}[sbom]" PLATFORM_MAPPING = { @@ -46,10 +46,37 @@ def temporary_venv(): yield str(python_bin) +def parse_args() -> argparse.Namespace: + """Parse and return command-line arguments.""" + parser = argparse.ArgumentParser(description="Generate a CycloneDX SBOM for dfetch.") + parser.add_argument( + "--py", + action="store_true", + help="Generate SBOM for the Python distribution instead of the platform binary.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + metavar="DIR", + help="Directory to write the SBOM into (default: dist/ for --py, build/dfetch-package/ otherwise).", + ) + return parser.parse_args() + + +args = parse_args() + +if args.py: + suffix = "py" + output_dir = args.output_dir or PROJECT_DIR / "dist" +else: + suffix = PLATFORM_NAME + output_dir = args.output_dir or PROJECT_DIR / "build" / "dfetch-package" + with temporary_venv() as python: subprocess.check_call([python, "-m", "pip", "install", DEPS]) # nosec - __version__ = ( + version = ( subprocess.run( # nosec [ python, @@ -63,15 +90,10 @@ def temporary_venv(): .strip() ) - OUTPUT_FILE = ( - PROJECT_DIR - / "build" - / "dfetch-package" - / f"dfetch-{__version__}-{PLATFORM_NAME}.cdx.json" - ) - OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True) + output_file = output_dir / f"dfetch-{version}-{suffix}.cdx.json" + output_file.parent.mkdir(parents=True, exist_ok=True) subprocess.check_call( # nosec - [python, "-m", "cyclonedx_py", "environment", "-o", str(OUTPUT_FILE)] + [python, "-m", "cyclonedx_py", "environment", "-o", str(output_file)] ) -logging.info(f"SBOM generated at {OUTPUT_FILE}") +logging.info(f"SBOM generated at {output_file}")