mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-13 18:18:12 +02:00
123 lines
5.1 KiB
Python
Executable File
123 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Publish a SaraSuite release: build the AppImage, create the Gitea release
|
|
v<AppVersion> (from Core.py) with it attached, and copy it to the Shared
|
|
folder. Running SaraSuites then offer to update themselves (SelfUpdate.py).
|
|
|
|
Packaging/Release.py # after bumping AppVersion in Core.py
|
|
Packaging/Release.py --notes "What changed"
|
|
|
|
Everything must be committed and pushed first, so the release's tag points
|
|
at code that's actually on Gitea. The Gitea token comes from git's own
|
|
credential store (the same one `git push` uses), so there's nothing to set up.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
import uuid
|
|
from pathlib import Path
|
|
from urllib.parse import urlsplit
|
|
|
|
RepoDir = Path(__file__).resolve().parent.parent
|
|
AppImage = RepoDir / "dist" / "SaraSuite-x86_64.AppImage"
|
|
SharedDir = Path("/mnt/files/Shared/SaraSuite")
|
|
|
|
|
|
def Git(*Args: str) -> str:
|
|
return subprocess.run(["git", *Args], cwd=RepoDir, capture_output=True, text=True, check=True).stdout.strip()
|
|
|
|
|
|
def Fail(Message: str) -> None:
|
|
sys.exit(f"Release.py: {Message}")
|
|
|
|
|
|
def Token(Host: str) -> str:
|
|
Result = subprocess.run(
|
|
["git", "credential", "fill"], input=f"protocol=https\nhost={Host}\n\n",
|
|
capture_output=True, text=True, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
|
|
)
|
|
Found = re.search(r"^password=(.+)$", Result.stdout, re.MULTILINE)
|
|
if not Found:
|
|
Fail(f"no Gitea token for {Host} in git's credential store; push once so git saves it")
|
|
return Found[1]
|
|
|
|
|
|
def Api(Method: str, Url: str, TokenValue: str, Body: bytes | None = None, ContentType: str = "application/json"):
|
|
Request = urllib.request.Request(Url, data=Body, method=Method, headers={
|
|
"Authorization": f"token {TokenValue}", "Content-Type": ContentType, "Accept": "application/json",
|
|
# Cloudflare's bot check rejects Python's default User-Agent (error 1010).
|
|
"User-Agent": "SaraSuite-Release (git/2)",
|
|
})
|
|
try:
|
|
with urllib.request.urlopen(Request, timeout=300) as Response:
|
|
return json.loads(Response.read() or b"null")
|
|
except urllib.error.HTTPError as Error:
|
|
if Error.code == 404:
|
|
return None
|
|
Fail(f"{Method} {Url} failed: {Error.code} {Error.read().decode(errors='replace')[:300]}")
|
|
|
|
|
|
def Main() -> None:
|
|
Parser = argparse.ArgumentParser(description="Publish a SaraSuite release on Gitea.")
|
|
Parser.add_argument("--notes", default="", help="release notes (default: the commits since the last release)")
|
|
Args = Parser.parse_args()
|
|
|
|
Version = re.search(r'^AppVersion = "([^"]+)"', (RepoDir / "Core.py").read_text(), re.MULTILINE)[1]
|
|
Tag = f"v{Version}"
|
|
|
|
# origin is e.g. https://git.srhtnin.garden/sarah/SaraSuite.git
|
|
Origin = urlsplit(Git("remote", "get-url", "origin"))
|
|
Owner, Repo = Origin.path.strip("/").removesuffix(".git").split("/")[-2:]
|
|
ApiBase = f"{Origin.scheme}://{Origin.netloc}/api/v1/repos/{Owner}/{Repo}"
|
|
|
|
if Git("status", "--porcelain"):
|
|
Fail("there are uncommitted changes; commit (and push) first")
|
|
Branch = Git("rev-parse", "--abbrev-ref", "HEAD")
|
|
Git("fetch", "--quiet", "--tags", "origin")
|
|
Head = Git("rev-parse", "HEAD")
|
|
if Head != Git("rev-parse", f"origin/{Branch}"):
|
|
Fail(f"{Branch} isn't pushed (or is behind origin); push first")
|
|
TokenValue = Token(Origin.netloc)
|
|
if Api("GET", f"{ApiBase}/releases/tags/{Tag}", TokenValue) is not None:
|
|
Fail(f"{Tag} is already released; bump AppVersion in Core.py first")
|
|
|
|
Notes = Args.notes
|
|
if not Notes:
|
|
Previous = Git("tag", "--list", "v*", "--sort=-v:refname").splitlines()
|
|
Range = f"{Previous[0]}..HEAD" if Previous else "HEAD"
|
|
Notes = Git("log", "--format=- %s", Range) or "- First release with self-updates."
|
|
|
|
print(f"== Building SaraSuite {Version}")
|
|
subprocess.run(["Packaging/BuildAppImage.sh"], cwd=RepoDir, check=True,
|
|
env={**os.environ, "PATH": f"{Path.home()}/.local/bin:{os.environ['PATH']}"})
|
|
|
|
print(f"== Creating release {Tag}")
|
|
Created = Api("POST", f"{ApiBase}/releases", TokenValue, json.dumps({
|
|
"tag_name": Tag, "target_commitish": Head, "name": f"SaraSuite {Version}", "body": Notes,
|
|
}).encode())
|
|
|
|
print(f"== Uploading {AppImage.name} ({AppImage.stat().st_size // 1_000_000} MB)")
|
|
Boundary = uuid.uuid4().hex
|
|
Body = (
|
|
f"--{Boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{AppImage.name}\"\r\n"
|
|
f"Content-Type: application/octet-stream\r\n\r\n"
|
|
).encode() + AppImage.read_bytes() + f"\r\n--{Boundary}--\r\n".encode()
|
|
Api("POST", f"{ApiBase}/releases/{Created['id']}/assets?name={AppImage.name}", TokenValue, Body,
|
|
f"multipart/form-data; boundary={Boundary}")
|
|
|
|
if SharedDir.is_dir():
|
|
shutil.copy2(AppImage, SharedDir / AppImage.name)
|
|
print(f"== Copied to {SharedDir}")
|
|
Git("fetch", "--quiet", "--tags", "origin") # the new tag, for the next release's notes
|
|
print(f"== Released {Tag}: {Origin.scheme}://{Origin.netloc}/{Owner}/{Repo}/releases/tag/{Tag}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Main()
|