88 lines
3.6 KiB
Python
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()
|