From 450280d6dd8da846f80b19a4ad8b4591343fe25e Mon Sep 17 00:00:00 2001 From: spbkgw-beep Date: Fri, 13 Mar 2026 12:45:29 -0700 Subject: [PATCH 1/2] fix: make release workflow re-tag safe --- .github/workflows/release.yml | 59 ++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0f63d1..c87e964 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: run: python -m build - name: Upload release artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: python-package-distributions path: dist/ @@ -58,18 +58,56 @@ jobs: steps: - name: Download release artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: python-package-distributions path: dist/ + - name: Check whether version already exists on PyPI + id: check-pypi-version + env: + PACKAGE_NAME: oacp-cli + PACKAGE_VERSION: ${{ github.ref_name }} + run: | + python - <<'PY' + import json + import os + import urllib.error + import urllib.request + + package = os.environ["PACKAGE_NAME"] + version = os.environ["PACKAGE_VERSION"].removeprefix("v") + url = f"https://pypi.org/pypi/{package}/json" + + try: + with urllib.request.urlopen(url) as response: + releases = json.load(response).get("releases", {}) + except urllib.error.HTTPError as exc: + if exc.code == 404: + releases = {} + else: + raise + + version_exists = version in releases + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: + fh.write(f"version_exists={'true' if version_exists else 'false'}\n") + + if version_exists: + print(f"PyPI already has {package} {version}; skipping publish.") + else: + print(f"PyPI does not have {package} {version}; proceeding with publish.") + PY + - name: Publish to PyPI + if: steps.check-pypi-version.outputs.version_exists != 'true' uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + - name: Skip duplicate PyPI publish + if: steps.check-pypi-version.outputs.version_exists == 'true' + run: echo "PyPI already has ${GITHUB_REF_NAME#v}; skipping upload." + github-release: - needs: - - build - - publish-pypi + needs: build runs-on: ubuntu-latest permissions: contents: write @@ -79,12 +117,17 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download release artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: python-package-distributions path: dist/ - - name: Create GitHub Release + - name: Create or update GitHub Release env: GH_TOKEN: ${{ github.token }} - run: gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes --verify-tag + run: | + if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then + gh release upload "${GITHUB_REF_NAME}" dist/* --clobber + else + gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes --verify-tag + fi From 02d11ab1d40fa08b4c054c93e5db356e90bd2417 Mon Sep 17 00:00:00 2001 From: spbkgw-beep Date: Fri, 13 Mar 2026 12:49:49 -0700 Subject: [PATCH 2/2] test: cover re-tag-safe release workflow --- tests/test_github_workflows.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_github_workflows.py b/tests/test_github_workflows.py index ac5d2cd..2d3d946 100644 --- a/tests/test_github_workflows.py +++ b/tests/test_github_workflows.py @@ -50,13 +50,29 @@ def test_release_workflow_publishes_with_trusted_publishing() -> None: publish_job = workflow["jobs"]["publish-pypi"] assert publish_job["environment"]["name"] == "pypi" assert publish_job["permissions"]["id-token"] == "write" - assert publish_job["steps"][0]["uses"].startswith("actions/download-artifact@") - assert publish_job["steps"][-1]["uses"].startswith("pypa/gh-action-pypi-publish@") + publish_step_names = {step["name"]: step for step in publish_job["steps"]} + assert publish_step_names["Download release artifacts"]["uses"].startswith( + "actions/download-artifact@" + ) + assert publish_step_names["Check whether version already exists on PyPI"]["id"] == ( + "check-pypi-version" + ) + assert publish_step_names["Publish to PyPI"]["if"] == ( + "steps.check-pypi-version.outputs.version_exists != 'true'" + ) + assert publish_step_names["Publish to PyPI"]["uses"].startswith( + "pypa/gh-action-pypi-publish@" + ) + assert publish_step_names["Skip duplicate PyPI publish"]["if"] == ( + "steps.check-pypi-version.outputs.version_exists == 'true'" + ) release_job = workflow["jobs"]["github-release"] - assert set(release_job["needs"]) == {"build", "publish-pypi"} + assert release_job["needs"] == "build" assert release_job["permissions"]["contents"] == "write" release_step_names = {step["name"]: step for step in release_job["steps"]} assert release_step_names["Check out repository"]["uses"].startswith("actions/checkout@") assert release_step_names["Download release artifacts"]["uses"].startswith("actions/download-artifact@") - assert "gh release create" in release_step_names["Create GitHub Release"]["run"] + assert "gh release view" in release_step_names["Create or update GitHub Release"]["run"] + assert "gh release upload" in release_step_names["Create or update GitHub Release"]["run"] + assert "gh release create" in release_step_names["Create or update GitHub Release"]["run"]