mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-15 02:33:17 +02:00
Self-updates and program list from the repo
This commit is contained in:
1 parent
d9da8438c2
commit
e4272bd606
9 files changed
+416
-15
No files matched your search
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/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",
|
||||
})
|
||||
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()
|
||||
Reference in new issue
Block a user