From 9e7b92b3659d91e1c15670efbb27dddefc403d36 Mon Sep 17 00:00:00 2001
From: yossi <54272821+Apakottur@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:35:04 +0000
Subject: [PATCH 1/4] Add scripts/release.py to cut releases safely
Interactive helper that dogfoods shpyx to run the git commands. It:
- verifies the working tree is clean and on `main`;
- fetches and fast-forwards to `origin/main`;
- reads the latest published version from PyPI and prompts for the next
one (patch / minor / major);
- offers to delete a pre-existing tag (e.g. from a failed release run)
before recreating it;
- creates and pushes the `v*` tag that triggers the Release workflow.
The shebang uses `uv run`, so `./scripts/release.py` works without a
manually activated virtualenv. Ignore T201 (print) under scripts/.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
README.md | 15 +++----
linters/ruff.toml | 3 ++
scripts/release.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 107 insertions(+), 10 deletions(-)
create mode 100755 scripts/release.py
diff --git a/README.md b/README.md
index faceebb..d48a064 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
-[](https://pypi.org/project/shpyx/)
-[](https://pypi.org/project/shpyx/)
-[](https://pypi.org/project/shpyx/)
+[](https://pypi.python.org/pypi/shpyx)
+[](https://github.com/Apakottur/shpyx/blob/main/LICENSE)
+[](https://pypi.python.org/pypi/shpyx)
**shpyx** is a simple, lightweight and typed library for running shell commands in Python.
@@ -207,12 +207,7 @@ ty check --config-file linters/ty.toml src tests
### Releasing
-The package version is derived from the git tag (via `hatch-vcs`), so there is nothing to bump in
-`pyproject.toml`. To release a new version, push a `v`-prefixed tag from `main`:
+To release a new version, run the interactive script:
```shell
-git tag v0.0.37
-git push origin v0.0.37
+./scripts/release.py
```
-
-This triggers the `Release` workflow, which builds the package, publishes it to PyPI using Trusted
-Publishing (OIDC, no stored token), and creates a GitHub Release with auto-generated notes.
diff --git a/linters/ruff.toml b/linters/ruff.toml
index 8d998f9..9373e79 100644
--- a/linters/ruff.toml
+++ b/linters/ruff.toml
@@ -126,3 +126,6 @@ ban-relative-imports = "all"
"tests/*" = [
"S101", # Use of `assert` detected
]
+"scripts/*" = [
+ "T201", # `print` found (scripts are CLIs that talk to the user via stdout)
+]
diff --git a/scripts/release.py b/scripts/release.py
new file mode 100755
index 0000000..48fdf06
--- /dev/null
+++ b/scripts/release.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env -S uv run python
+"""
+Cut a new shpyx release.
+
+The published version is derived from the git tag (via hatch-vcs), so releasing
+is simply a matter of pushing a `v*` tag to `main`. This script does that safely:
+
+ 1. Verify the working tree is clean and on `main`.
+ 2. Fetch and fast-forward to `origin/main`.
+ 3. Look up the latest version currently on PyPI.
+ 4. Let the user pick the next version (patch / minor / major).
+ 5. If the tag already exists (e.g. a previous release run failed), offer to
+ delete it first.
+ 6. Create the tag and push it, which triggers the `Release` GitHub Action.
+
+Run from the repository root with `./scripts/release.py` (the shebang uses `uv run`,
+so shpyx and its environment are set up automatically).
+"""
+
+import json
+import sys
+import urllib.request
+
+import shpyx
+
+PYPI_URL = "https://pypi.org/pypi/shpyx/json"
+MAIN_BRANCH = "main"
+
+
+def abort(message: str) -> None:
+ """Print an error and exit with a non-zero status."""
+ print(f"\n❌ {message}")
+ sys.exit(1)
+
+
+def confirm(question: str) -> bool:
+ """Ask a yes/no question, defaulting to 'no'."""
+ return input(f"{question} [y/N] ").strip().lower() in ("y", "yes")
+
+
+def main() -> None:
+ # Verify we are on a clean, up-to-date `main` before tagging.
+ branch = shpyx.run("git rev-parse --abbrev-ref HEAD").stdout.strip()
+ if branch != MAIN_BRANCH:
+ abort(f"Must be on the '{MAIN_BRANCH}' branch, but currently on '{branch}'.")
+
+ if shpyx.run("git status --porcelain").stdout.strip():
+ abort("Working tree is not clean. Commit or stash your changes first.")
+
+ print("Fetching from origin...")
+ shpyx.run("git fetch origin --tags --prune", log_output=True)
+ # Fast-forward only: aborts if local `main` has diverged from origin.
+ shpyx.run(f"git pull --ff-only origin {MAIN_BRANCH}", log_output=True)
+
+ # Look up the latest published version on PyPI.
+ with urllib.request.urlopen(PYPI_URL) as response: # noqa: S310 (trusted, hardcoded https URL)
+ version = json.load(response)["info"]["version"]
+ parts = version.split(".")
+ if len(parts) != 3 or not all(part.isdigit() for part in parts):
+ abort(f"Cannot parse PyPI version {version!r} as 'major.minor.patch'.")
+ major, minor, patch = (int(part) for part in parts)
+
+ # Let the user pick the next version.
+ bumps = {
+ "1": ("patch", f"{major}.{minor}.{patch + 1}"),
+ "2": ("minor", f"{major}.{minor + 1}.0"),
+ "3": ("major", f"{major + 1}.0.0"),
+ }
+ print(f"\nLatest version on PyPI: {major}.{minor}.{patch}")
+ print("Select the next version:")
+ for key, (name, next_version) in bumps.items():
+ print(f" {key}) {name:<5} -> {next_version}")
+ while (choice := input("Choice [1/2/3]: ").strip()) not in bumps:
+ print("Invalid choice, please enter 1, 2 or 3.")
+ tag = f"v{bumps[choice][1]}"
+
+ # Handle a pre-existing tag (e.g. from a release run that failed after tagging).
+ local = shpyx.run(f"git tag --list {tag}").stdout.strip()
+ remote = shpyx.run(f"git ls-remote --tags origin {tag}").stdout.strip()
+ if local or remote:
+ print(f"\n⚠️ Tag {tag} already exists (a previous release may have failed).")
+ if not confirm(f"Delete the existing {tag} and recreate it?"):
+ abort("Aborted: tag already exists.")
+ # Local delete may fail if the tag only exists on the remote; ignore that.
+ shpyx.run(f"git tag --delete {tag}", verify_return_code=False)
+ shpyx.run(f"git push --delete origin {tag}", verify_return_code=False, log_output=True)
+
+ if not confirm(f"\nCreate and push tag {tag} to trigger the release?"):
+ abort("Aborted by user.")
+
+ shpyx.run(f"git tag {tag}")
+ shpyx.run(f"git push origin {tag}", log_output=True)
+
+ print(f"\n✅ Pushed {tag}. The Release workflow is now running:")
+ print(" https://github.com/Apakottur/shpyx/actions/workflows/release.yml")
+
+
+if __name__ == "__main__":
+ main()
From cc24e0240f141783f38b460413371e3f55e8d3bd Mon Sep 17 00:00:00 2001
From: yossi <54272821+Apakottur@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:35:10 +0000
Subject: [PATCH 2/4] Add Python version classifiers
The shields.io pyversions badge reads the `Programming Language ::
Python :: 3.x` trove classifiers from PyPI metadata, which were missing,
so the badge was broken. Add classifiers for 3.10-3.14, matching
`requires-python` and the CI matrix.
The badge will recover once the next release publishes this metadata.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
pyproject.toml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index 4501afc..018893e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,12 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
]
[project.urls]
From cf36b2fa530ffeababe945c4ec40bc3fff5722ab Mon Sep 17 00:00:00 2001
From: yossi <54272821+Apakottur@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:44:28 +0000
Subject: [PATCH 3/4] Fixes
---
linters/cspell/words.txt | 1 +
pyproject.toml | 1 +
scripts/release.py | 68 +++++++++++++++-----------------------
uv.lock | 71 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 99 insertions(+), 42 deletions(-)
diff --git a/linters/cspell/words.txt b/linters/cspell/words.txt
index 502735f..4f5818a 100644
--- a/linters/cspell/words.txt
+++ b/linters/cspell/words.txt
@@ -34,3 +34,4 @@ anyio
multibyte
haia
softprops
+httpx
diff --git a/pyproject.toml b/pyproject.toml
index 018893e..5179895 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,6 +4,7 @@ requires = ["hatchling", "hatch-vcs"]
[dependency-groups]
dev = [
+ "httpx==0.28.1",
"mypy==2.2.0",
"pre-commit==4.6.0",
"pytest-cov==7.1.0",
diff --git a/scripts/release.py b/scripts/release.py
index 48fdf06..ecca741 100755
--- a/scripts/release.py
+++ b/scripts/release.py
@@ -1,63 +1,48 @@
-#!/usr/bin/env -S uv run python
+#!/usr/bin/env python
"""
-Cut a new shpyx release.
-
-The published version is derived from the git tag (via hatch-vcs), so releasing
-is simply a matter of pushing a `v*` tag to `main`. This script does that safely:
-
- 1. Verify the working tree is clean and on `main`.
- 2. Fetch and fast-forward to `origin/main`.
- 3. Look up the latest version currently on PyPI.
- 4. Let the user pick the next version (patch / minor / major).
- 5. If the tag already exists (e.g. a previous release run failed), offer to
- delete it first.
- 6. Create the tag and push it, which triggers the `Release` GitHub Action.
-
-Run from the repository root with `./scripts/release.py` (the shebang uses `uv run`,
-so shpyx and its environment are set up automatically).
+Create a new shpyx release.
"""
-import json
import sys
-import urllib.request
+
+import httpx
import shpyx
-PYPI_URL = "https://pypi.org/pypi/shpyx/json"
-MAIN_BRANCH = "main"
+_PYPI_URL = "https://pypi.org/pypi/shpyx/json"
+_MAIN_BRANCH = "main"
-def abort(message: str) -> None:
- """Print an error and exit with a non-zero status."""
+def _abort(message: str) -> None:
print(f"\n❌ {message}")
sys.exit(1)
-def confirm(question: str) -> bool:
- """Ask a yes/no question, defaulting to 'no'."""
- return input(f"{question} [y/N] ").strip().lower() in ("y", "yes")
+def _confirm(question: str) -> None:
+ user_input = input(f"{question} [y/N] ").strip().lower()
+ if user_input != "y":
+ _abort("Aborted by user.")
def main() -> None:
# Verify we are on a clean, up-to-date `main` before tagging.
branch = shpyx.run("git rev-parse --abbrev-ref HEAD").stdout.strip()
- if branch != MAIN_BRANCH:
- abort(f"Must be on the '{MAIN_BRANCH}' branch, but currently on '{branch}'.")
-
+ if branch != _MAIN_BRANCH:
+ _abort(f"Must be on the '{_MAIN_BRANCH}' branch, but currently on '{branch}'.")
if shpyx.run("git status --porcelain").stdout.strip():
- abort("Working tree is not clean. Commit or stash your changes first.")
+ _abort("Working tree is not clean. Commit or stash your changes first.")
+ # Fetch and fast-forward to origin/main.
print("Fetching from origin...")
- shpyx.run("git fetch origin --tags --prune", log_output=True)
- # Fast-forward only: aborts if local `main` has diverged from origin.
- shpyx.run(f"git pull --ff-only origin {MAIN_BRANCH}", log_output=True)
+ shpyx.run("git pull", log_output=True)
# Look up the latest published version on PyPI.
- with urllib.request.urlopen(PYPI_URL) as response: # noqa: S310 (trusted, hardcoded https URL)
- version = json.load(response)["info"]["version"]
+ pypi_response = httpx.get(_PYPI_URL)
+ pypi_response.raise_for_status()
+ version = pypi_response.json()["info"]["version"]
parts = version.split(".")
if len(parts) != 3 or not all(part.isdigit() for part in parts):
- abort(f"Cannot parse PyPI version {version!r} as 'major.minor.patch'.")
+ _abort(f"Cannot parse PyPI version {version!r} as 'major.minor.patch'.")
major, minor, patch = (int(part) for part in parts)
# Let the user pick the next version.
@@ -79,18 +64,17 @@ def main() -> None:
remote = shpyx.run(f"git ls-remote --tags origin {tag}").stdout.strip()
if local or remote:
print(f"\n⚠️ Tag {tag} already exists (a previous release may have failed).")
- if not confirm(f"Delete the existing {tag} and recreate it?"):
- abort("Aborted: tag already exists.")
+ _confirm(f"Delete the existing {tag} and recreate it?")
# Local delete may fail if the tag only exists on the remote; ignore that.
shpyx.run(f"git tag --delete {tag}", verify_return_code=False)
- shpyx.run(f"git push --delete origin {tag}", verify_return_code=False, log_output=True)
-
- if not confirm(f"\nCreate and push tag {tag} to trigger the release?"):
- abort("Aborted by user.")
+ shpyx.run(f"git push --delete origin {tag}", verify_return_code=False)
+ # Create and push the tag.
+ _confirm(f"\nCreate and push tag {tag} to trigger the release?")
shpyx.run(f"git tag {tag}")
- shpyx.run(f"git push origin {tag}", log_output=True)
+ shpyx.run(f"git push origin {tag}")
+ # Print the release URL.
print(f"\n✅ Pushed {tag}. The Release workflow is now running:")
print(" https://github.com/Apakottur/shpyx/actions/workflows/release.yml")
diff --git a/uv.lock b/uv.lock
index af28884..85ec3ad 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,6 +6,20 @@ resolution-markers = [
"python_full_version < '3.15'",
]
+[[package]]
+name = "anyio"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
+]
+
[[package]]
name = "ast-serialize"
version = "0.6.0"
@@ -47,6 +61,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
]
+[[package]]
+name = "certifi"
+version = "2026.6.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
+]
+
[[package]]
name = "cfgv"
version = "3.5.0"
@@ -198,6 +221,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" },
]
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
[[package]]
name = "identify"
version = "2.6.19"
@@ -207,6 +267,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
]
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
[[package]]
name = "iniconfig"
version = "2.3.0"
@@ -594,6 +663,7 @@ source = { editable = "." }
[package.dev-dependencies]
dev = [
+ { name = "httpx" },
{ name = "mypy" },
{ name = "pre-commit" },
{ name = "pytest" },
@@ -607,6 +677,7 @@ dev = [
[package.metadata.requires-dev]
dev = [
+ { name = "httpx", specifier = "==0.28.1" },
{ name = "mypy", specifier = "==2.2.0" },
{ name = "pre-commit", specifier = "==4.6.0" },
{ name = "pytest", specifier = "==9.1.1" },
From 1a887692c12392e86d3452219d654a2567f90c0b Mon Sep 17 00:00:00 2001
From: yossi <54272821+Apakottur@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:48:11 +0000
Subject: [PATCH 4/4] Derive repo slug and package name from the origin remote
Instead of hardcoding "Apakottur/shpyx" and the PyPI package name, parse
the GitHub "owner/repo" slug from `git remote get-url origin` (handles
both SSH and HTTPS URLs). The PyPI lookup URL and the Actions URL are
then built from it.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
linters/ruff.toml | 2 +-
scripts/release.py | 13 ++++++++++---
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/linters/ruff.toml b/linters/ruff.toml
index 9373e79..6a41689 100644
--- a/linters/ruff.toml
+++ b/linters/ruff.toml
@@ -127,5 +127,5 @@ ban-relative-imports = "all"
"S101", # Use of `assert` detected
]
"scripts/*" = [
- "T201", # `print` found (scripts are CLIs that talk to the user via stdout)
+ "T201", # `print` found
]
diff --git a/scripts/release.py b/scripts/release.py
index ecca741..b6f5bad 100755
--- a/scripts/release.py
+++ b/scripts/release.py
@@ -9,7 +9,6 @@
import shpyx
-_PYPI_URL = "https://pypi.org/pypi/shpyx/json"
_MAIN_BRANCH = "main"
@@ -36,8 +35,16 @@ def main() -> None:
print("Fetching from origin...")
shpyx.run("git pull", log_output=True)
+ # Derive the GitHub "owner/repo" slug (and PyPI package name) from the origin remote,
+ # supporting both SSH (git@github.com:owner/repo.git) and HTTPS URLs.
+ remote_url = shpyx.run("git remote get-url origin").stdout.strip()
+ slug = remote_url.removesuffix(".git").split("github.com")[-1].strip(":/")
+ if slug.count("/") != 1:
+ _abort(f"Cannot parse a GitHub 'owner/repo' slug from origin URL {remote_url!r}.")
+ package_name = slug.split("/")[1]
+
# Look up the latest published version on PyPI.
- pypi_response = httpx.get(_PYPI_URL)
+ pypi_response = httpx.get(f"https://pypi.org/pypi/{package_name}/json")
pypi_response.raise_for_status()
version = pypi_response.json()["info"]["version"]
parts = version.split(".")
@@ -76,7 +83,7 @@ def main() -> None:
# Print the release URL.
print(f"\n✅ Pushed {tag}. The Release workflow is now running:")
- print(" https://github.com/Apakottur/shpyx/actions/workflows/release.yml")
+ print(f" https://github.com/{slug}/actions/workflows/release.yml")
if __name__ == "__main__":