Skip to content

Commit 2b1300d

Browse files
Skip PyPI publish when version exists
1 parent cfcfd8a commit 2b1300d

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

.github/workflows/publish.yml

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,45 +152,75 @@ jobs:
152152
echo "version=$package_version" >> "$GITHUB_OUTPUT"
153153
echo "Package: $package_name $package_version"
154154
155-
- name: Ensure version is not already on PyPI
155+
- name: Check whether package version exists on PyPI
156+
id: pypi_version
156157
env:
157158
PACKAGE_NAME: ${{ steps.package.outputs.name }}
158159
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
159160
run: |
160161
python - <<'PY'
161162
import os
163+
import re
162164
import sys
163165
import urllib.error
164166
import urllib.request
165167
166-
package_name = os.environ["PACKAGE_NAME"].replace("_", "-")
168+
def write_output(name, value):
169+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
170+
output.write(f"{name}={value}\n")
171+
172+
def write_summary(message):
173+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
174+
if summary_path:
175+
with open(summary_path, "a", encoding="utf-8") as summary:
176+
summary.write(f"{message}\n")
177+
178+
package_name = re.sub(r"[-_.]+", "-", os.environ["PACKAGE_NAME"]).lower()
167179
package_version = os.environ["PACKAGE_VERSION"]
168180
if not package_name or not package_version:
169181
raise SystemExit("PACKAGE_NAME and PACKAGE_VERSION must not be empty.")
170182
url = f"https://pypi.org/pypi/{package_name}/{package_version}/json"
171183
print(f"Checking {url}")
172184
173185
try:
174-
urllib.request.urlopen(url, timeout=15)
186+
with urllib.request.urlopen(url, timeout=15):
187+
pass
175188
except urllib.error.HTTPError as exc:
176189
if exc.code == 404:
177190
print(f"{package_name} {package_version} is not on PyPI yet.")
191+
write_output("exists", "false")
192+
write_summary(
193+
f"{package_name} {package_version} is not on PyPI yet. Publishing will continue."
194+
)
178195
sys.exit(0)
179196
raise
180197
181-
raise SystemExit(
198+
print(
199+
f"{package_name} {package_version} already exists on PyPI. "
200+
"Skipping publish and GitHub release steps."
201+
)
202+
write_output("exists", "true")
203+
write_summary(
182204
f"{package_name} {package_version} already exists on PyPI. "
183-
"Bump setup.py version before merging to main."
205+
"Publish and GitHub release steps were skipped."
184206
)
185207
PY
186208
209+
- name: Skip publish because package version already exists
210+
if: steps.pypi_version.outputs.exists == 'true'
211+
run: |
212+
echo "${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }} already exists on PyPI."
213+
echo "Publishing and GitHub release creation were skipped."
214+
187215
- name: Download package artifacts
216+
if: steps.pypi_version.outputs.exists != 'true'
188217
uses: actions/download-artifact@v4
189218
with:
190219
name: package-dist
191220
path: dist
192221

193222
- name: Ensure downloaded wheel and source distribution exist
223+
if: steps.pypi_version.outputs.exists != 'true'
194224
run: |
195225
shopt -s nullglob
196226
wheel_files=(dist/*.whl)
@@ -212,9 +242,11 @@ jobs:
212242
printf '%s\n' "${sdist_files[@]}"
213243
214244
- name: Publish package distributions to PyPI
245+
if: steps.pypi_version.outputs.exists != 'true'
215246
uses: pypa/gh-action-pypi-publish@release/v1
216247

217248
- name: Create GitHub release
249+
if: steps.pypi_version.outputs.exists != 'true'
218250
env:
219251
GH_TOKEN: ${{ github.token }}
220252
PACKAGE_NAME: ${{ steps.package.outputs.name }}

tests/test_publish_workflow.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from pathlib import Path
2+
3+
from django.test import SimpleTestCase
4+
5+
6+
WORKFLOW_PATH = (
7+
Path(__file__).resolve().parents[1] / ".github" / "workflows" / "publish.yml"
8+
)
9+
10+
11+
def _step_block(workflow_text, step_name):
12+
marker = f" - name: {step_name}\n"
13+
start = workflow_text.find(marker)
14+
if start == -1:
15+
raise AssertionError(f"Workflow step not found: {step_name}")
16+
17+
next_step = workflow_text.find("\n - name:", start + len(marker))
18+
if next_step == -1:
19+
return workflow_text[start:]
20+
return workflow_text[start:next_step]
21+
22+
23+
class PublishWorkflowTests(SimpleTestCase):
24+
def test_existing_pypi_version_skips_publish_without_failing_job(self):
25+
workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
26+
27+
check_step = _step_block(
28+
workflow_text, "Check whether package version exists on PyPI"
29+
)
30+
self.assertIn("id: pypi_version", check_step)
31+
self.assertIn('write_output("exists", "true")', check_step)
32+
self.assertIn('write_output("exists", "false")', check_step)
33+
self.assertNotIn("Bump setup.py version before merging to main", check_step)
34+
35+
skip_step = _step_block(
36+
workflow_text, "Skip publish because package version already exists"
37+
)
38+
self.assertIn("if: steps.pypi_version.outputs.exists == 'true'", skip_step)
39+
40+
guarded_steps = [
41+
"Download package artifacts",
42+
"Ensure downloaded wheel and source distribution exist",
43+
"Publish package distributions to PyPI",
44+
"Create GitHub release",
45+
]
46+
for step_name in guarded_steps:
47+
with self.subTest(step=step_name):
48+
step = _step_block(workflow_text, step_name)
49+
self.assertIn("if: steps.pypi_version.outputs.exists != 'true'", step)

0 commit comments

Comments
 (0)