diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2390d8c809e..456596841a9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,12 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "monthly" + interval: "weekly" groups: github-actions: patterns: - "*" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..9506eeb3304 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Publish Python 🐍 distribution 📦 to PyPI + +on: + push: + tags: + - "*" + +jobs: + build: + name: Build distribution 📦 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - name: Build a binary wheel and a source tarball + run: ./build-project.py + - name: Store the distribution packages + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: >- + Publish Python 🐍 distribution 📦 to PyPI + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/pip/${{ github.ref_name }} + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # release/v1 diff --git a/MANIFEST.in b/MANIFEST.in index 6f4197565d3..cb8e14df96b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,10 @@ include README.rst include SECURITY.md include pyproject.toml +include build-requirements.in +include build-requirements.txt +include build-project.py + include src/pip/_vendor/README.rst include src/pip/_vendor/vendor.txt recursive-include src/pip/_vendor *LICENSE* diff --git a/build-project.py b/build-project.py new file mode 100755 index 00000000000..78e183da08a --- /dev/null +++ b/build-project.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Build pip using pinned build requirements.""" + +import subprocess +import tempfile +import venv +from os import PathLike +from pathlib import Path +from types import SimpleNamespace + + +class EnvBuilder(venv.EnvBuilder): + """A subclass of venv.EnvBuilder that exposes the python executable command.""" + + def ensure_directories( + self, env_dir: str | bytes | PathLike[str] | PathLike[bytes] + ) -> SimpleNamespace: + context = super().ensure_directories(env_dir) + self.env_exec_cmd = context.env_exec_cmd + return context + + +def get_git_head_timestamp() -> str: + return subprocess.run( + [ + "git", + "log", + "-1", + "--pretty=format:%ct", + ], + text=True, + stdout=subprocess.PIPE, + ).stdout.strip() + + +def main() -> None: + with tempfile.TemporaryDirectory() as build_env: + env_builder = EnvBuilder(with_pip=True) + env_builder.create(build_env) + subprocess.run( + [ + env_builder.env_exec_cmd, + "-Im", + "pip", + "install", + "--no-deps", + "--only-binary=:all:", + "--require-hashes", + "-r", + Path(__file__).parent / "build-requirements.txt", + ], + check=True, + ) + subprocess.run( + [ + env_builder.env_exec_cmd, + "-Im", + "build", + "--no-isolation", + ], + check=True, + env={"SOURCE_DATE_EPOCH": get_git_head_timestamp()}, + ) + + +if __name__ == "__main__": + main() diff --git a/build-requirements.in b/build-requirements.in new file mode 100644 index 00000000000..4bc215a28d0 --- /dev/null +++ b/build-requirements.in @@ -0,0 +1,2 @@ +build +setuptools diff --git a/build-requirements.txt b/build-requirements.txt new file mode 100644 index 00000000000..ad876c4ada0 --- /dev/null +++ b/build-requirements.txt @@ -0,0 +1,24 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --allow-unsafe --generate-hashes build-requirements.in +# +build==1.2.2.post1 \ + --hash=sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5 \ + --hash=sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7 + # via -r build-requirements.in +packaging==24.2 \ + --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ + --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via build +pyproject-hooks==1.2.0 \ + --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ + --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + # via build + +# The following packages are considered to be unsafe in a requirements file: +setuptools==75.8.0 \ + --hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \ + --hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3 + # via -r build-requirements.in diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 5bf0d278b71..77ee9b5d46b 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -146,11 +146,8 @@ Creating a new release This will update the relevant files and tag the correct commit. #. Submit the ``release/YY.N`` branch as a pull request and ensure CI passes. Merge the changes back into ``main`` and pull them back locally. -#. Build the release artifacts using ``nox -s build-release -- YY.N``. - This will checkout the tag, generate the distribution files to be - uploaded and checkout the main branch again. -#. Upload the release to PyPI using ``nox -s upload-release -- YY.N``. -#. Push the tag created by ``prepare-release``. +#. Push the tag created by ``prepare-release``. This will trigger the release + workflow on GitHub and publish to PyPI. #. Regenerate the ``get-pip.py`` script in the `get-pip repository`_ (as documented there) and commit the results. #. Submit a Pull Request to `CPython`_ adding the new version of pip diff --git a/news/13048.process.rst b/news/13048.process.rst new file mode 100644 index 00000000000..b4d18461b7a --- /dev/null +++ b/news/13048.process.rst @@ -0,0 +1,3 @@ +Started releasing to PyPI from a GitHub Actions CI/CD workflow that implements trusted publishing and bundles :pep:`740` digital attestations. + +In addition to being signed, the released distribution packages are now reproducible through the commit timestamp. diff --git a/noxfile.py b/noxfile.py index 6e6e144bccb..70e24a01142 100644 --- a/noxfile.py +++ b/noxfile.py @@ -339,7 +339,7 @@ def build_release(session: nox.Session) -> None: ) session.log("# Install dependencies") - session.install("build", "twine") + session.install("twine") with release.isolated_temporary_checkout(session, version) as build_dir: session.log( @@ -375,11 +375,11 @@ def build_dists(session: nox.Session) -> List[str]: ) session.log("# Build distributions") - session.run("python", "-m", "build", silent=True) + session.run("python", "build-project.py", silent=True) produced_dists = glob.glob("dist/*") session.log(f"# Verify distributions: {', '.join(produced_dists)}") - session.run("twine", "check", *produced_dists, silent=True) + session.run("twine", "check", "--strict", *produced_dists, silent=True) return produced_dists