Repository navigation
feat(release): build and publish sdist/wheels on GitHub Release #1207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6fb4294
feat(release): build and publish sdist/wheels on GitHub Release
seonghobae 68e93b3
test(release): pin publish workflow trust boundaries
seonghobae 2c95af5
fix(release): harden package publish provenance
seonghobae 4b11bbd
docs(release): record hardened PyPI publishing
seonghobae 7507947
docs(release): terminate changelog fragment
seonghobae 3ad31e7
test(release): keep release assets immutable
seonghobae 0d9d327
fix(release): keep published assets immutable
seonghobae 25007d2
docs(release): record immutable release assets
seonghobae 3647e60
test(release): require explicit package dispatch
seonghobae 671b5de
fix(release): dispatch publish from immutable tag
seonghobae a10d3fa
fix(release): explicitly dispatch package publication
seonghobae 4ba34e9
docs(release): record explicit publish dispatch
seonghobae 29acb85
test(release): require wheels for supported Python
seonghobae 0fe0411
fix(release): build wheels for supported Python
seonghobae 64b06c8
test(release): decouple PyPI recovery from asset upload
seonghobae b51b88c
fix(release): decouple PyPI recovery from asset upload
seonghobae 32030e8
docs(release): record independent PyPI recovery
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| name: Publish Package | ||
|
|
||
| # Builds hash-pinnable sdist + wheel artifacts for a published GitHub Release | ||
| # and publishes them to PyPI. This exists so downstream consumers can depend on | ||
| # fast-mlsirm through a normal versioned package with uv/pip hash pinning. | ||
|
|
||
| on: | ||
| release: | ||
| types: [published] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
|
|
||
| concurrency: | ||
| group: publish-pypi-${{ github.event.release.tag_name }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| verify-release: | ||
| name: verify release provenance | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| ref: ${{ github.event.release.tag_name }} | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Require release tag to match project version | ||
| env: | ||
| RELEASE_TAG: ${{ github.event.release.tag_name }} | ||
| run: | | ||
| python - <<'PY' | ||
| import os | ||
| import tomllib | ||
|
|
||
| with open("pyproject.toml", "rb") as stream: | ||
| project = tomllib.load(stream)["project"] | ||
|
|
||
| expected = f"v{project['version']}" | ||
| actual = os.environ["RELEASE_TAG"] | ||
| if actual != expected: | ||
| raise SystemExit( | ||
| f"release tag does not match project version: expected {expected!r}, got {actual!r}" | ||
| ) | ||
| PY | ||
|
|
||
| sdist: | ||
| name: sdist | ||
| needs: verify-release | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| ref: ${{ github.event.release.tag_name }} | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Build sdist | ||
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b | ||
| with: | ||
| command: sdist | ||
| maturin-version: v1.14.1 | ||
| args: --out dist | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | ||
| with: | ||
| name: dist-sdist | ||
| path: dist | ||
|
|
||
| wheels: | ||
| name: wheels (${{ matrix.target }}) | ||
| needs: verify-release | ||
| runs-on: ${{ matrix.runner }} | ||
| timeout-minutes: 30 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - runner: ubuntu-latest | ||
| target: x86_64-unknown-linux-gnu | ||
| manylinux: "2014" | ||
| - runner: ubuntu-24.04-arm | ||
| target: aarch64-unknown-linux-gnu | ||
| manylinux: "2014" | ||
| - runner: macos-latest | ||
| target: universal2-apple-darwin | ||
| manylinux: "" | ||
| - runner: windows-latest | ||
| target: x86_64-pc-windows-msvc | ||
| manylinux: "" | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| with: | ||
| ref: ${{ github.event.release.tag_name }} | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Build wheel | ||
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b | ||
| with: | ||
| target: ${{ matrix.target }} | ||
| manylinux: ${{ matrix.manylinux || 'auto' }} | ||
| maturin-version: v1.14.1 | ||
| args: --release --out dist | ||
|
github-actions[bot] marked this conversation as resolved.
Resolved
|
||
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | ||
| with: | ||
| name: dist-wheel-${{ matrix.target }} | ||
| path: dist | ||
|
|
||
| release-assets: | ||
| name: attach release assets | ||
| needs: [sdist, wheels] | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c | ||
| with: | ||
| pattern: dist-* | ||
| path: dist | ||
| merge-multiple: true | ||
| - name: List built artifacts | ||
| run: ls -la dist | ||
| - name: Attach release assets | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| RELEASE_TAG: ${{ github.event.release.tag_name }} | ||
| run: gh release upload "$RELEASE_TAG" dist/* --repo "${{ github.repository }}" | ||
|
|
||
| publish-pypi: | ||
| name: publish to PyPI | ||
| needs: [sdist, wheels, release-assets] | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| environment: pypi | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c | ||
| with: | ||
| pattern: dist-* | ||
| path: dist | ||
| merge-multiple: true | ||
| - name: List built artifacts | ||
| run: ls -la dist | ||
| - name: Publish package distributions to PyPI | ||
| uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 | ||
| with: | ||
| password: ${{ secrets.PIPY_TOKEN }} | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
| attestations: false | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Reproducible PyPI release publishing | ||
|
|
||
| ## Added | ||
|
|
||
| - Added release-tag-bound sdist and wheel publication with a project-version provenance check, pinned Maturin and PyPA publisher revisions, and persisted checkout credentials disabled. | ||
| - Isolated GitHub release-asset mutation from PyPI credentials, removed the unpinned runtime Twine installation path, and kept duplicate GitHub release assets and PyPI filenames fail-closed rather than silently replacing an immutable release artifact. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| import re | ||
|
|
||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[1] | ||
| WORKFLOW = REPO_ROOT / ".github" / "workflows" / "publish-pypi.yml" | ||
| PYPI_PUBLISH_SHA = "dc37677b2e1c63e2034f94d8a5b11f265b73ba33" | ||
|
|
||
|
|
||
| def _workflow_text() -> str: | ||
| return WORKFLOW.read_text(encoding="utf-8") | ||
|
|
||
|
|
||
| def _job_block(text: str, name: str) -> str: | ||
| match = re.search( | ||
| rf"(?ms)^ {re.escape(name)}:\n(?P<body>.*?)(?=^ [A-Za-z0-9_-]+:\n|\Z)", | ||
| text, | ||
| ) | ||
| assert match is not None, f"missing {name!r} job" | ||
| return match.group(0) | ||
|
|
||
|
|
||
| def test_release_builds_are_bound_to_the_published_version() -> None: | ||
| text = _workflow_text() | ||
| verify = _job_block(text, "verify-release") | ||
| sdist = _job_block(text, "sdist") | ||
| wheels = _job_block(text, "wheels") | ||
|
|
||
| for job in (verify, sdist, wheels): | ||
| assert "ref: ${{ github.event.release.tag_name }}" in job | ||
| assert "persist-credentials: false" in job | ||
|
|
||
| assert 'RELEASE_TAG: ${{ github.event.release.tag_name }}' in verify | ||
| assert 'tomllib.load' in verify or 'tomllib.loads' in verify | ||
| assert 'f"v{project[\'version\']}"' in verify | ||
| assert text.count("maturin-version: v1.14.1") == 2 | ||
|
|
||
|
|
||
| def test_release_asset_write_is_isolated_from_pypi_credentials() -> None: | ||
| text = _workflow_text() | ||
| assets = _job_block(text, "release-assets") | ||
| publish = _job_block(text, "publish-pypi") | ||
|
|
||
| assert "permissions:\n contents: write" in assets | ||
| assert "GH_TOKEN: ${{ github.token }}" in assets | ||
| assert "RELEASE_TAG: ${{ github.event.release.tag_name }}" in assets | ||
| assert 'gh release upload "$RELEASE_TAG"' in assets | ||
| assert "--clobber" not in assets | ||
| assert "secrets.PIPY_TOKEN" not in assets | ||
|
|
||
| assert "environment: pypi" in publish | ||
| assert "permissions:\n contents: read" in publish | ||
| assert "gh release upload" not in publish | ||
| assert "contents: write" not in publish | ||
|
|
||
|
|
||
| def test_pypi_publish_uses_a_pinned_package_owned_uploader() -> None: | ||
| text = _workflow_text() | ||
| publish = _job_block(text, "publish-pypi") | ||
|
|
||
| assert "python -m pip install" not in text | ||
| assert "python -m twine upload" not in text | ||
| assert "TWINE_USERNAME" not in text | ||
| assert f"uses: pypa/gh-action-pypi-publish@{PYPI_PUBLISH_SHA}" in publish | ||
| assert "password: ${{ secrets.PIPY_TOKEN }}" in publish | ||
| assert "attestations: false" in publish | ||
| assert "skip-existing" not in publish |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.