diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 00000000..6dde0174 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,60 @@ +name: Publish to PyPI + +on: + workflow_dispatch: + inputs: + run_id: + description: The run of wheel-builder to use for finding artifacts. + required: true + environment: + description: Which PyPI environment to upload to + required: true + type: choice + options: ["testpypi", "pypi"] + workflow_run: + workflows: ["Wheel Builder"] + types: [completed] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + # We're not actually verifying that the triggering push event was for a + # tag, because github doesn't expose enough information to do so. + # wheel-builder.yml currently only has push events for tags. + if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success') + permissions: + id-token: write + attestations: write + steps: + - run: echo "$EVENT_CONTEXT" + env: + EVENT_CONTEXT: ${{ toJson(github.event) }} + + - run: | + echo "PYPI_URL=https://upload.pypi.org/legacy/" >> $GITHUB_ENV + if: github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'pypi') + - run: | + echo "PYPI_URL=https://test.pypi.org/legacy/" >> $GITHUB_ENV + if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'testpypi' + + - uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 + with: + path: tmpdist/ + run_id: ${{ github.event.inputs.run_id || github.event.workflow_run.id }} + - run: mkdir dist/ + - run: | + find tmpdist/ -type f -name 'PyNaCl*' -exec mv {} dist/ \; + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + repository-url: ${{ env.PYPI_URL }} + skip-existing: true + # Do not perform attestation for things for TestPyPI. This is + # because there's nothing that would prevent a malicious PyPI from + # serving a signed TestPyPI asset in place of a release intended for + # PyPI. + attestations: ${{ env.PYPI_URL == 'https://upload.pypi.org/legacy/' }} diff --git a/.github/workflows/wheel-builder.yml b/.github/workflows/wheel-builder.yml index 10819790..73859a2d 100644 --- a/.github/workflows/wheel-builder.yml +++ b/.github/workflows/wheel-builder.yml @@ -18,6 +18,26 @@ on: - setup.py jobs: + sdist: + runs-on: ubuntu-latest + name: sdist + steps: + - uses: actions/checkout@v6.0.1 + with: + # The tag to build or the tag received by the tag event + ref: ${{ github.event.inputs.version || github.ref }} + persist-credentials: false + - name: Setup python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: pip install -U pip build + - run: python -m build --sdist + - uses: actions/upload-artifact@v6 + with: + name: pynacl-sdist + path: dist/PyNaCl* + manylinux: runs-on: ${{ matrix.MANYLINUX.RUNNER }} container: diff --git a/release.py b/release.py index 322720d6..a6f8b3f7 100644 --- a/release.py +++ b/release.py @@ -2,138 +2,30 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. +# /// script +# dependencies = [ +# "click", +# ] +# /// -import getpass -import glob -import io -import json -import os import subprocess -import time -import zipfile import click -import requests - def run(*args, **kwargs): print("[running] {}".format(list(args))) subprocess.check_call(list(args), **kwargs) -def wait_for_build_complete_github_actions(session, token, run_url): - while True: - response = session.get( - run_url, - headers={ - "Content-Type": "application/json", - "Authorization": "token {}".format(token), - }, - ) - response.raise_for_status() - if response.json()["conclusion"] is not None: - break - time.sleep(3) - - -def download_artifacts_github_actions(session, token, run_url): - response = session.get( - run_url, - headers={ - "Content-Type": "application/json", - "Authorization": "token {}".format(token), - }, - ) - response.raise_for_status() - - response = session.get( - response.json()["artifacts_url"], - headers={ - "Content-Type": "application/json", - "Authorization": "token {}".format(token), - }, - ) - response.raise_for_status() - paths = [] - for artifact in response.json()["artifacts"]: - response = session.get( - artifact["archive_download_url"], - headers={ - "Content-Type": "application/json", - "Authorization": "token {}".format(token), - }, - ) - with zipfile.ZipFile(io.BytesIO(response.content)) as z: - for name in z.namelist(): - if not name.endswith(".whl"): - continue - p = z.open(name) - out_path = os.path.join( - os.path.dirname(__file__), - "dist", - os.path.basename(name), - ) - with open(out_path, "wb") as f: - f.write(p.read()) - paths.append(out_path) - return paths - - -def build_github_actions_wheels(token, version): - session = requests.Session() - - response = session.post( - "https://api.github.com/repos/pyca/pynacl/actions/workflows/" - "wheel-builder.yml/dispatches", - headers={ - "Content-Type": "application/json", - "Accept": "application/vnd.github.v3+json", - "Authorization": "token {}".format(token), - }, - data=json.dumps({"ref": "master", "inputs": {"version": version}}), - ) - response.raise_for_status() - - # Give it a few seconds for the run to kick off. - time.sleep(5) - response = session.get( - ( - "https://api.github.com/repos/pyca/pynacl/actions/workflows/" - "wheel-builder.yml/runs?event=repository_dispatch" - ), - headers={ - "Content-Type": "application/json", - "Authorization": "token {}".format(token), - }, - ) - response.raise_for_status() - run_url = response.json()["workflow_runs"][0]["url"] - wait_for_build_complete_github_actions(session, token, run_url) - return download_artifacts_github_actions(session, token, run_url) - - @click.command() @click.argument("version") def release(version): """ ``version`` should be a string like '0.4' or '1.0'. """ - github_token = getpass.getpass("Github person access token: ") - run("git", "tag", "-s", version, "-m", "{} release".format(version)) - run("git", "push", "--tags") - - run("python", "setup.py", "sdist") - - sdist = glob.glob("dist/PyNaCl-{}*".format(version)) - - github_actions_wheel_paths = build_github_actions_wheels( - github_token, version - ) - - run("twine", "upload", *github_actions_wheel_paths) - run("twine", "upload", "-s", *sdist) + run("git", "push", "git@github.com:pyca/pynacl.git", version) if __name__ == "__main__":