Skip to content

Restructure PyPI publish so the SBOM never enters dist/ (retire the cleanup step + most of the release guard) #53

Description

Summary

Restructure the PyPI publish job in .github/workflows/release-python.yml so the CycloneDX SBOM (sbom-python / *.cdx.json) never lands in the dist/ upload directory. That makes the "stray SBOM breaks twine" failure structurally impossible, letting us delete the cleanup step and retire most of the CI guard that grew out of PR #50.

Background

The publish job currently downloads artifacts with:

- uses: actions/download-artifact@…  # v8.0.1
  with:
    path: dist
    merge-multiple: true

merge-multiple: true pulls every uploaded artifact into dist/ — the wheels (wheels-*), the sdist, and the sbom-python artifact (ordvec-python.cdx.json). Because pypa/gh-action-pypi-publish uploads everything in dist/ and twine rejects a stray .cdx.json, PR #50 added a cleanup step (find dist -name '*.cdx.json' -delete) before publish, plus a CI guard (tests/release_publish_invariants.sh) asserting that cleanup runs in the right order.

That guard took five review rounds (deletion-proof → publish-scope → multi-line → non-executing-text → block-comments) because it hand-rolls YAML/step parsing in awk. It's correct now, but it's testing around a fragile design rather than removing the failure mode.

Proposed fix (make it structurally impossible)

Download only the wheel + sdist artifacts into dist/, so the SBOM is never present at upload time.

Option A — targeted downloads (most explicit):

- uses: actions/download-artifact@…   # wheels
  with: { path: dist, pattern: 'wheels-*', merge-multiple: true }
- uses: actions/download-artifact@…   # sdist
  with: { path: dist, name: sdist }

The sbom-python artifact is simply not downloaded into dist/ (download it elsewhere if the publish job needs it, or not at all).

Option B — pattern/exclude: keep one download but use a pattern: that matches the wheel/sdist artifacts and excludes sbom-python (verify actions/download-artifact glob/exclude semantics — it uses @actions/glob).

Payoff

  • Deletes the fragile cleanup-ordering invariant entirely (remove the find … -delete step).
  • tests/release_publish_invariants.sh shrinks to part 1 only — assert *.cdx.json is gitignored (git check-ignore), the simple/robust check. The whole publish-job awk (part 2) can go.
  • Removes the maintenance burden that drew 5 review rounds.

Acceptance criteria

  • publish job: dist/ contains only wheels + sdist at upload time (SBOM not present).
  • The Drop the SBOM from the PyPI upload dir (find … -delete) step removed.
  • tests/release_publish_invariants.sh part 2 (download→clean→publish ordering) removed; part 1 (gitignore) retained; the release-guard job in ci.yml kept (now just the gitignore check).
  • SBOM still produced + uploaded as the sbom-python artifact; PEP 740 attestation still covers dist/*.whl + dist/*.tar.gz.
  • Trusted-publishing / publish dry-run path verified unchanged.

Notes

  • Not urgent — follow-up/robustness, not a bug. The PR fix(ci): keep generated SBOMs out of the crate/PyPI publish path (+ CI pin) #50 approach works and the guard is verified across 11 scenarios (A–K); this just removes the need for it.
  • Crate side: the *.cdx.json .gitignore entry (keeps cargo publish from seeing a dirty tree) is already the idiomatic structural fix — keep it.
  • Related but separate: consider actionlint + zizmor CI jobs for generic GitHub Actions hygiene (action pinning, permissions, injection) instead of relying on PR-bot findings.

Refs: PR #50; guard commits 49b97db58a2fac.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgithub_actionsPull requests that update GitHub Actions code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions