Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
114 lines
4.5 KiB
Python
114 lines
4.5 KiB
Python
"""Publish a verified Windows installer as a Gitea release.
|
||
|
||
CI uses GITEA_TOKEN. For a manual local release, pass --use-git-credential.
|
||
"""
|
||
|
||
import argparse
|
||
import hashlib
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from urllib.parse import quote, urlsplit
|
||
|
||
import requests
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
from app_version import APP_VERSION, CHECKSUM_ASSET_NAME, GITEA_REPOSITORY_URL, INSTALLER_ASSET_NAME
|
||
|
||
|
||
API = "https://git.robotail.pro/api/v1/repos/Robotail/BUH_bot"
|
||
|
||
|
||
def session_for_gitea(use_git_credential: bool) -> requests.Session:
|
||
session = requests.Session()
|
||
token = os.environ.get("GITEA_TOKEN")
|
||
if token:
|
||
session.headers["Authorization"] = f"token {token}"
|
||
elif use_git_credential:
|
||
host = urlsplit(GITEA_REPOSITORY_URL).hostname
|
||
result = subprocess.run(
|
||
["git", "credential", "fill"],
|
||
input=f"protocol=https\nhost={host}\n\n",
|
||
text=True, capture_output=True, check=True,
|
||
env={**os.environ, "GCM_INTERACTIVE": "never", "GIT_TERMINAL_PROMPT": "0"},
|
||
)
|
||
credentials = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)
|
||
session.auth = (credentials["username"], credentials["password"])
|
||
else:
|
||
raise RuntimeError("Укажите GITEA_TOKEN или --use-git-credential")
|
||
return session
|
||
|
||
|
||
def request_json(session: requests.Session, method: str, path: str, **kwargs):
|
||
timeout = kwargs.pop("timeout", (10, 120))
|
||
response = session.request(method, API + path, timeout=timeout, **kwargs)
|
||
if response.status_code >= 400:
|
||
raise RuntimeError(f"Gitea API: {method} {path} вернул HTTP {response.status_code}: {response.text[:300]}")
|
||
return response.json()
|
||
|
||
|
||
def publish(tag: str, body: str, use_git_credential: bool = False) -> str:
|
||
if tag != f"v{APP_VERSION}":
|
||
raise ValueError(f"Тег {tag} не совпадает с версией приложения v{APP_VERSION}")
|
||
installer = ROOT / "installer_output" / INSTALLER_ASSET_NAME
|
||
if not installer.is_file() or installer.stat().st_size == 0:
|
||
raise FileNotFoundError(installer)
|
||
digest_state = hashlib.sha256()
|
||
with installer.open("rb") as handle:
|
||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||
digest_state.update(chunk)
|
||
digest = digest_state.hexdigest()
|
||
checksum = installer.with_name(CHECKSUM_ASSET_NAME)
|
||
checksum.write_text(f"{digest} {INSTALLER_ASSET_NAME}\n", encoding="ascii")
|
||
|
||
session = session_for_gitea(use_git_credential)
|
||
response = session.get(API + "/releases/tags/" + quote(tag), timeout=(10, 30))
|
||
if response.status_code == 404:
|
||
release = request_json(session, "POST", "/releases", json={
|
||
"tag_name": tag,
|
||
"target_commitish": "main",
|
||
"name": f"МойСклад — Генератор документов {tag}",
|
||
"body": body,
|
||
"draft": True,
|
||
"prerelease": False,
|
||
})
|
||
else:
|
||
response.raise_for_status()
|
||
release = response.json()
|
||
|
||
release_id = release["id"]
|
||
assets = {asset["name"]: asset for asset in release.get("assets", [])}
|
||
if not release.get("draft") and {INSTALLER_ASSET_NAME, CHECKSUM_ASSET_NAME} <= assets.keys():
|
||
return release["html_url"]
|
||
for path in (installer, checksum):
|
||
existing = assets.get(path.name)
|
||
if existing:
|
||
if existing["size"] != path.stat().st_size:
|
||
raise RuntimeError(f"Файл {path.name} уже опубликован с другим размером")
|
||
continue
|
||
with path.open("rb") as handle:
|
||
request_json(
|
||
session, "POST", f"/releases/{release_id}/assets",
|
||
params={"name": path.name},
|
||
files={"attachment": (path.name, handle, "application/octet-stream")},
|
||
timeout=(10, 300),
|
||
)
|
||
|
||
if release.get("draft"):
|
||
release = request_json(
|
||
session, "PATCH", f"/releases/{release_id}",
|
||
json={"draft": False, "prerelease": False, "body": body},
|
||
)
|
||
return release["html_url"]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("tag")
|
||
parser.add_argument("--body-file", type=Path, required=True)
|
||
parser.add_argument("--use-git-credential", action="store_true")
|
||
args = parser.parse_args()
|
||
print(publish(args.tag, args.body_file.read_text(encoding="utf-8"), args.use_git_credential))
|