Skip to content

Commit 13af7e0

Browse files
author
Albert
committed
ci(release): publish releases via GitHub API
1 parent 186368a commit 13af7e0

1 file changed

Lines changed: 75 additions & 20 deletions

File tree

.github/workflows/release.yml

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)