From afa456f396ca5efb67329072765d0d153a830469 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 24 Oct 2025 14:53:40 +0200 Subject: [PATCH] ci: Skip existing artifacts when reuploading After a release failed and we want to retry, some assets might already have been uploaded. Skip those instead of attempting to reupload. --- .github/workflows/release.yml | 2 +- scripts/dev/build_release.py | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd663cdce..db6184ba3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: # FIXME consider switching to trusted publishers: # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ - name: Build and upload release - run: "tox -e build-release -- --upload --no-confirm" + run: "tox -e build-release -- --upload --no-confirm ${{ inputs.release_type == 'reupload' && '--reupload' || '' }}" env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }} diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index c153608cb..c5b81f3bb 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -20,6 +20,7 @@ import platform import collections import dataclasses import re +import http from typing import Optional from collections.abc import Iterable @@ -28,6 +29,8 @@ try: except ImportError: pass +import requests + REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) @@ -566,6 +569,7 @@ def github_upload( tag: str, gh_token: str, experimental: bool, + skip_if_exists: bool, ) -> None: """Upload the given artifacts to GitHub. @@ -574,6 +578,7 @@ def github_upload( tag: The name of the release tag gh_token: The GitHub token to use experimental: Upload to the experiments repo + skip_if_exists: Skip uploading artifacts that already exist """ # pylint: disable=broad-exception-raised import github3 @@ -597,6 +602,13 @@ def github_upload( f"No release found for {tag!r} in {repo.full_name}, found: {releases}") for artifact in artifacts: + assets = [ + asset for asset in release.assets() if asset.name == artifact.path.name + ] + if assets and skip_if_exists: + print(f"Artifact {artifact.path.name} already exists, skipping") + continue + while True: print(f"Uploading {artifact.path}") @@ -640,12 +652,27 @@ def github_upload( break -def pypi_upload(artifacts: list[Artifact], experimental: bool) -> None: +def check_pypi_exists(version: str) -> bool: + """Check whether the given version exists on PyPI.""" + response = requests.get(f"https://pypi.org/pypi/qutebrowser/{version}/json") + if response.status_code == http.HTTPStatus.NOT_FOUND: + return False + response.raise_for_status() + return bool(response.json()["urls"]) + + +def pypi_upload( + artifacts: list[Artifact], experimental: bool, skip_if_exists: bool +) -> None: """Upload the given artifacts to PyPI using twine.""" + utils.print_title("Uploading to PyPI...") + if skip_if_exists and check_pypi_exists(qutebrowser.__version__): + print(f"Version {qutebrowser.__version__} already exists on PyPI, skipping") + return + # https://blog.pypi.org/posts/2023-05-23-removing-pgp/ artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature'] - utils.print_title("Uploading to PyPI...") if experimental: run_twine('upload', artifacts, "-r", "testpypi") else: @@ -671,6 +698,8 @@ def main() -> None: nargs='?') parser.add_argument('--upload', action='store_true', required=False, help="Toggle to upload the release to GitHub.") + parser.add_argument('--reupload', action='store_true', required=False, + help="Skip uploading artifacts that already exist.") parser.add_argument('--no-confirm', action='store_true', required=False, help="Skip confirmation before uploading.") parser.add_argument('--skip-packaging', action='store_true', required=False, @@ -730,9 +759,16 @@ def main() -> None: assert gh_token is not None github_upload( - artifacts, version_tag, gh_token=gh_token, experimental=args.experimental) + artifacts, + version_tag, + gh_token=gh_token, + experimental=args.experimental, + skip_if_exists=args.reupload, + ) if upload_to_pypi: - pypi_upload(artifacts, experimental=args.experimental) + pypi_upload( + artifacts, experimental=args.experimental, skip_if_exists=args.reupload + ) else: print() utils.print_title("Artifacts")