Files
llamamod/tools/release/publish_gitea.py
T

88 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Creates (or reuses) a Gitea release for a tag and uploads files to it.
publish_gitea.py --tag v0.2.1 --title "LlamaMod 0.2.1" --notes notes.md FILE...
Environment: GITEA_URL (e.g. https://git.example.com), GITEA_REPO (owner/name), GITEA_TOKEN
(a token with write access to the repository). In Gitea Actions these come from the workflow.
Files that already exist on the release with the same name are replaced, so a re-run is safe.
Only the standard library is used.
"""
import argparse
import json
import mimetypes
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
def api(method, url, token, data=None, headers=None):
h = {"Authorization": "token " + token, "Accept": "application/json"}
h.update(headers or {})
req = urllib.request.Request(url, data=data, method=method, headers=h)
try:
with urllib.request.urlopen(req) as r:
body = r.read()
return r.status, json.loads(body) if body else None
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
def multipart(path):
boundary = uuid.uuid4().hex
name = os.path.basename(path)
ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
with open(path, "rb") as f:
content = f.read()
head = ('--%s\r\nContent-Disposition: form-data; name="attachment"; filename="%s"\r\n'
"Content-Type: %s\r\n\r\n" % (boundary, name, ctype)).encode()
tail = ("\r\n--%s--\r\n" % boundary).encode()
return head + content + tail, "multipart/form-data; boundary=" + boundary
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--tag", required=True)
ap.add_argument("--title", required=True)
ap.add_argument("--notes", help="file with the release text (Markdown)")
ap.add_argument("--prerelease", action="store_true")
ap.add_argument("files", nargs="+")
a = ap.parse_args()
base, repo, token = (os.environ.get(k, "") for k in ("GITEA_URL", "GITEA_REPO", "GITEA_TOKEN"))
if not (base and repo and token):
sys.exit("set GITEA_URL, GITEA_REPO and GITEA_TOKEN")
root = "%s/api/v1/repos/%s" % (base.rstrip("/"), repo)
body = open(a.notes, encoding="utf-8").read() if a.notes else ""
st, rel = api("GET", "%s/releases/tags/%s" % (root, urllib.parse.quote(a.tag)), token)
if st == 404:
payload = json.dumps({"tag_name": a.tag, "name": a.title, "body": body,
"draft": False, "prerelease": a.prerelease}).encode()
st, rel = api("POST", root + "/releases", token, payload, {"Content-Type": "application/json"})
if st not in (200, 201):
sys.exit("creating the release failed (%s): %s" % (st, rel))
print("created release", rel["id"], a.tag)
elif st == 200:
print("reusing release", rel["id"], a.tag)
else:
sys.exit("reading the release failed (%s): %s" % (st, rel))
existing = {x["name"]: x["id"] for x in rel.get("assets") or []}
for path in a.files:
name = os.path.basename(path)
if name in existing:
api("DELETE", "%s/releases/%s/assets/%s" % (root, rel["id"], existing[name]), token)
data, ctype = multipart(path)
st, res = api("POST", "%s/releases/%s/assets?name=%s" % (root, rel["id"], urllib.parse.quote(name)),
token, data, {"Content-Type": ctype})
if st not in (200, 201):
sys.exit("uploading %s failed (%s): %s" % (name, st, res))
print("uploaded", name, "(%.1f MB)" % (len(data) / 1e6))
if __name__ == "__main__":
main()