BUH_bot/scripts/publish_gitea_release.py
Игорь Надеин 5155e455d2
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Add Gitea release updates and local Windows release pipeline
2026-09-16 15:03:17 +03:00

114 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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))