From 978e8f2e7cd44aed4d05a80ae9a89a7da8915ff7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 21:21:07 +0000 Subject: [PATCH 1/3] ci: generate and publish Python SBOM (.cdx.json) alongside wheel/tar.gz - In build.yml build-whl: install built wheel + cyclonedx-bom, run cyclonedx-py environment to produce dfetch-VERSION-py.cdx.json in dist/; the existing release job uploads dist/* so the SBOM is automatically attached to the GitHub release draft. - In python-publish.yml build: same generation but SBOM goes into dist-sbom/ (separate from dist/) to keep PyPI upload clean; stored as a python-sbom artifact. - In python-publish.yml deploy: downloads the python-sbom artifact and attaches it to the published GitHub release via softprops/action-gh-release; adds contents: write permission. https://claude.ai/code/session_01VPK44jamDeLp4Tcy916XDT --- .github/workflows/build.yml | 6 ++++++ .github/workflows/python-publish.yml | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5474ac9..6a9b4f8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -224,6 +224,12 @@ jobs: run: python -m pip install --upgrade pip build --user - name: Build a binary wheel and a source tarball run: python3 -m build + - name: Install built package and SBOM generation tool + run: pip install dist/*.whl "cyclonedx-bom==7.3.0" + - name: Generate SBOM for Python distribution + run: | + VERSION=$(python -c "from importlib.metadata import version; print(version('dfetch'))") + python -m cyclonedx_py environment -o "dist/dfetch-${VERSION}-py.cdx.json" - 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..89729da7 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -37,11 +37,23 @@ jobs: run: python -m pip install --upgrade pip build --user - name: Build a binary wheel and a source tarball run: python3 -m build + - name: Install built package and SBOM generation tool + run: pip install dist/*.whl "cyclonedx-bom==7.3.0" + - name: Generate SBOM for Python distribution + run: | + VERSION=$(python -c "from importlib.metadata import version; print(version('dfetch'))") + mkdir -p dist-sbom + python -m cyclonedx_py environment -o "dist-sbom/dfetch-${VERSION}-py.cdx.json" - 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 +95,7 @@ jobs: url: https://pypi.org/p/dfetch permissions: id-token: write + contents: write steps: - name: Download all the dists @@ -92,3 +105,15 @@ jobs: path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1 + - 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 }} From f7c9aefbe26cddbe82bf817eafdd051e18d5f9a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 05:15:14 +0000 Subject: [PATCH 2/3] ci: consolidate Python SBOM generation into create_sbom.py --py Extend script/create_sbom.py with a --py flag and an optional --output-dir argument so the Python distribution SBOM follows the same pattern as the binary one (pip install .[extra] then the script). - create_sbom.py: add parse_args(); --py uses suffix "py" and defaults output to dist/; --output-dir overrides the directory for both modes - pyproject.toml: add wheel = ["build==1.2.2"] optional dependency so the build frontend is pinned alongside the rest of the toolchain - build.yml build-whl: replace pip install + inline cyclonedx invocation with pip install .[wheel] + python script/create_sbom.py --py - python-publish.yml build: same, passing --output-dir dist-sbom to keep the SBOM out of dist/ so PyPI upload stays clean https://claude.ai/code/session_01VPK44jamDeLp4Tcy916XDT --- .github/workflows/build.yml | 8 ++--- .github/workflows/python-publish.yml | 9 ++---- pyproject.toml | 1 + script/create_sbom.py | 44 +++++++++++++++++++++------- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a9b4f8d..a271b376 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -221,15 +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: Install built package and SBOM generation tool - run: pip install dist/*.whl "cyclonedx-bom==7.3.0" - name: Generate SBOM for Python distribution - run: | - VERSION=$(python -c "from importlib.metadata import version; print(version('dfetch'))") - python -m cyclonedx_py environment -o "dist/dfetch-${VERSION}-py.cdx.json" + 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 89729da7..392a8851 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -34,16 +34,11 @@ 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: Install built package and SBOM generation tool - run: pip install dist/*.whl "cyclonedx-bom==7.3.0" - name: Generate SBOM for Python distribution - run: | - VERSION=$(python -c "from importlib.metadata import version; print(version('dfetch'))") - mkdir -p dist-sbom - python -m cyclonedx_py environment -o "dist-sbom/dfetch-${VERSION}-py.cdx.json" + 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: 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}") From b1c6eaec680bce30875e9b8b9f2c4557de6eea94 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 05:25:09 +0000 Subject: [PATCH 3/3] ci: make PyPI publish step idempotent on re-runs Add skip-existing: true to the production PyPI publish step in the deploy job, matching the existing testpypi job. Without this, if the SBOM download/upload steps after the publish step fail, a re-run would be blocked because PyPI rejects re-uploading an already-published version. https://claude.ai/code/session_01VPK44jamDeLp4Tcy916XDT --- .github/workflows/python-publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 392a8851..87a3b6ac 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -100,6 +100,8 @@ 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: