@@ -192,30 +192,85 @@ jobs:
192192 cat build/release-body.md
193193 echo "----- end preview -----"
194194
195- - name : Publish release via gh CLI (delete-then-create for guaranteed body refresh)
195+ - name : Publish release via GitHub API (delete-then-create for guaranteed body refresh)
196196 # softprops/action-gh-release@v2 silently keeps the body of an already-
197197 # existing release when run in update mode (observed: re-pushed
198- # v1.0.4-tahoe.1 still showed the v1.0.3 body). gh CLI gives us an
199- # explicit delete-recreate cycle so every push of a tag yields the
200- # exact body from our generated markdown file. The tag itself is
201- # preserved (--cleanup-tag=false).
198+ # v1.0.4-tahoe.1 still showed the v1.0.3 body). Use the REST API
199+ # directly so every tag push recreates the release body and asset while
200+ # preserving the tag.
202201 if : startsWith(github.ref, 'refs/tags/')
203202 env :
204203 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
205204 run : |
206205 set -euo pipefail
207- REPO="${{ github.repository }}"
208- TAG="${{ github.ref_name }}"
209- if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
210- echo "Existing release for $TAG found — deleting (tag preserved)…"
211- # NOTE: gh release delete's --cleanup-tag is a pure boolean flag;
212- # passing `--cleanup-tag=false` is a syntax error. Omitting it
213- # keeps the tag, which is what we want.
214- gh release delete "$TAG" --repo "$REPO" --yes
215- fi
216- echo "Creating release $TAG…"
217- gh release create "$TAG" "$DMG_PATH" \
218- --repo "$REPO" \
219- --title "$TAG" \
220- --notes-file build/release-body.md \
221- --verify-tag
206+ python3 <<'PY'
207+ import json
208+ import mimetypes
209+ import os
210+ import sys
211+ import urllib.error
212+ import urllib.parse
213+ import urllib.request
214+
215+ repo = os.environ["GITHUB_REPOSITORY"]
216+ tag = os.environ["GITHUB_REF_NAME"]
217+ token = os.environ["GH_TOKEN"]
218+ dmg_path = os.environ["DMG_PATH"]
219+
220+ with open("build/release-body.md", "r", encoding="utf-8") as handle:
221+ body = handle.read()
222+
223+ api = f"https://api.github.com/repos/{repo}"
224+ headers = {
225+ "Authorization": f"Bearer {token}",
226+ "Accept": "application/vnd.github+json",
227+ "X-GitHub-Api-Version": "2022-11-28",
228+ }
229+
230+ def request(method, url, payload=None, content_type="application/json"):
231+ data = None
232+ request_headers = dict(headers)
233+ if payload is not None:
234+ if isinstance(payload, bytes):
235+ data = payload
236+ else:
237+ data = json.dumps(payload).encode("utf-8")
238+ request_headers["Content-Type"] = content_type
239+
240+ req = urllib.request.Request(url, data=data, headers=request_headers, method=method)
241+ try:
242+ with urllib.request.urlopen(req) as response:
243+ raw = response.read()
244+ if not raw:
245+ return None
246+ return json.loads(raw.decode("utf-8"))
247+ except urllib.error.HTTPError as error:
248+ raw = error.read().decode("utf-8", errors="replace")
249+ if error.code == 404:
250+ return {"_not_found": True}
251+ print(raw, file=sys.stderr)
252+ raise
253+
254+ existing = request("GET", f"{api}/releases/tags/{urllib.parse.quote(tag, safe='')}")
255+ if existing and not existing.get("_not_found"):
256+ print(f"Existing release for {tag} found; deleting release while preserving tag.")
257+ request("DELETE", f"{api}/releases/{existing['id']}")
258+
259+ print(f"Creating release {tag}.")
260+ release = request("POST", f"{api}/releases", {
261+ "tag_name": tag,
262+ "name": tag,
263+ "body": body,
264+ "draft": False,
265+ "prerelease": False,
266+ })
267+
268+ asset_name = os.path.basename(dmg_path)
269+ content_type = mimetypes.guess_type(dmg_path)[0] or "application/octet-stream"
270+ upload_url = release["upload_url"].split("{", 1)[0]
271+ upload_url = f"{upload_url}?name={urllib.parse.quote(asset_name)}"
272+ with open(dmg_path, "rb") as handle:
273+ request("POST", upload_url, handle.read(), content_type)
274+
275+ print(f"Published {tag} with asset {asset_name}.")
276+ PY
0 commit comments