mirror of
https://github.com/thegatesbrowser/thegates.git
synced 2025-09-04 08:25:50 -04:00
upload builds to the server
This commit is contained in:
parent
6941bfe99a
commit
ad1dd849d5
3 changed files with 152 additions and 18 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -2,4 +2,7 @@
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
# for ipc files (inter process communication)
|
# for ipc files (inter process communication)
|
||||||
app/sandbox
|
app/sandbox
|
||||||
|
|
||||||
|
# for deployment
|
||||||
|
deployment/upload_api.key
|
||||||
|
|
|
@ -9,6 +9,7 @@ import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
@ -26,27 +27,33 @@ def parse_version(project_path: Path) -> str:
|
||||||
raise RuntimeError(f"Failed to parse version from {project_path}")
|
raise RuntimeError(f"Failed to parse version from {project_path}")
|
||||||
|
|
||||||
|
|
||||||
def open_folder_and_url(builds_dir: Path, url: str) -> None:
|
def build_expected_zip_paths(builds_dir: Path, version: str, os_name: str) -> list[Path]:
|
||||||
os_name = platform.system()
|
paths: list[Path] = []
|
||||||
try:
|
if os_name == "Linux":
|
||||||
if os_name == "Linux":
|
paths.append(builds_dir / "Linux" / f"TheGates_Linux_{version}.zip")
|
||||||
# fire-and-forget
|
paths.append(builds_dir / "Windows" / f"TheGates_Windows_{version}.zip")
|
||||||
subprocess.Popen(["xdg-open", str(builds_dir)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
elif os_name == "Darwin":
|
||||||
subprocess.Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
paths.append(builds_dir / f"TheGates_MacOS_{version}.zip")
|
||||||
elif os_name == "Darwin":
|
return paths
|
||||||
subprocess.Popen(["open", str(builds_dir)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
except Exception:
|
def parse_args() -> argparse.Namespace:
|
||||||
# Non-fatal if opening fails
|
parser = argparse.ArgumentParser(description="Export, compress, and upload builds.")
|
||||||
pass
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Overwrite existing compressed files (Linux compressor only).",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
repo_root = script_dir.parent
|
repo_root = script_dir.parent
|
||||||
app_dir = repo_root / "app"
|
app_dir = repo_root / "app"
|
||||||
export_script = repo_root / "deployment" / "export_project.py"
|
export_script = repo_root / "deployment" / "export_project.py"
|
||||||
open_url = "https://devs.thegates.io/files/builds/"
|
uploader = repo_root / "deployment" / "upload_build.py"
|
||||||
|
|
||||||
os_name = platform.system()
|
os_name = platform.system()
|
||||||
print(f"==> Using repo root: {repo_root}")
|
print(f"==> Using repo root: {repo_root}")
|
||||||
|
@ -63,6 +70,8 @@ def main() -> int:
|
||||||
version = parse_version(app_dir / "project.godot")
|
version = parse_version(app_dir / "project.godot")
|
||||||
print(f"==> App version: {version}")
|
print(f"==> App version: {version}")
|
||||||
|
|
||||||
|
uploaded: list[Path] = []
|
||||||
|
|
||||||
if os_name == "Linux":
|
if os_name == "Linux":
|
||||||
builds_dir = Path("/media/common/Projects/thegates-folder/AppBuilds")
|
builds_dir = Path("/media/common/Projects/thegates-folder/AppBuilds")
|
||||||
compress_src = repo_root / "deployment" / "compress_builds_linux.py"
|
compress_src = repo_root / "deployment" / "compress_builds_linux.py"
|
||||||
|
@ -77,9 +86,12 @@ def main() -> int:
|
||||||
shutil.copy2(compress_src, compress_dst)
|
shutil.copy2(compress_src, compress_dst)
|
||||||
|
|
||||||
print(f"==> Compressing Linux/Windows builds with version {version}...")
|
print(f"==> Compressing Linux/Windows builds with version {version}...")
|
||||||
run([sys.executable, str(compress_dst), version], cwd=builds_dir)
|
compress_cmd = [sys.executable, str(compress_dst), version]
|
||||||
|
if args.force:
|
||||||
|
compress_cmd.append("--force")
|
||||||
|
run(compress_cmd, cwd=builds_dir)
|
||||||
|
|
||||||
open_folder_and_url(builds_dir, open_url)
|
uploaded = build_expected_zip_paths(builds_dir, version, os_name)
|
||||||
|
|
||||||
elif os_name == "Darwin":
|
elif os_name == "Darwin":
|
||||||
builds_dir = Path("/Users/nordup/Projects/thegates-folder/AppBuilds")
|
builds_dir = Path("/Users/nordup/Projects/thegates-folder/AppBuilds")
|
||||||
|
@ -91,7 +103,19 @@ def main() -> int:
|
||||||
print(f"==> Compressing macOS build with version {version}...")
|
print(f"==> Compressing macOS build with version {version}...")
|
||||||
run([sys.executable, str(compress_script), version], cwd=builds_dir)
|
run([sys.executable, str(compress_script), version], cwd=builds_dir)
|
||||||
|
|
||||||
open_folder_and_url(builds_dir, open_url)
|
uploaded = build_expected_zip_paths(builds_dir, version, os_name)
|
||||||
|
|
||||||
|
# Upload created zip files via uploader
|
||||||
|
existing = [p for p in uploaded if p.exists()]
|
||||||
|
if not existing:
|
||||||
|
print("No compressed build files found to upload.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("==> Uploading:")
|
||||||
|
for p in existing:
|
||||||
|
print(f" - {p}")
|
||||||
|
|
||||||
|
run([sys.executable, str(uploader), *[str(p) for p in existing]], cwd=repo_root)
|
||||||
|
|
||||||
print("==> Done.")
|
print("==> Done.")
|
||||||
return 0
|
return 0
|
||||||
|
|
107
deployment/upload_build.py
Executable file
107
deployment/upload_build.py
Executable file
|
@ -0,0 +1,107 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import mimetypes
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
from urllib import request, error
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ENDPOINT = "https://app.thegates.io/api/upload_build"
|
||||||
|
|
||||||
|
|
||||||
|
def read_api_key(repo_root: Path) -> str:
|
||||||
|
# Allow override via env. Default to deployment/upload_api.key
|
||||||
|
key_file_env = os.environ.get("TG_UPLOAD_API_KEY_FILE")
|
||||||
|
if key_file_env:
|
||||||
|
key_path = Path(key_file_env).expanduser().resolve()
|
||||||
|
else:
|
||||||
|
key_path = (repo_root / "deployment" / "upload_api.key").resolve()
|
||||||
|
|
||||||
|
if not key_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"API key file not found: {key_path}. Set TG_UPLOAD_API_KEY_FILE or create the file."
|
||||||
|
)
|
||||||
|
|
||||||
|
key = key_path.read_text(encoding="utf-8").strip()
|
||||||
|
if not key:
|
||||||
|
raise ValueError(f"API key file {key_path} is empty")
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def build_multipart_body(field_name: str, file_path: Path, boundary: str) -> tuple[bytes, str]:
|
||||||
|
filename = file_path.name
|
||||||
|
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||||
|
file_bytes = file_path.read_bytes()
|
||||||
|
|
||||||
|
boundary_bytes = boundary.encode("utf-8")
|
||||||
|
crlf = b"\r\n"
|
||||||
|
|
||||||
|
body = []
|
||||||
|
body.append(b"--" + boundary_bytes + crlf)
|
||||||
|
body.append(
|
||||||
|
(
|
||||||
|
f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"' # noqa: E501
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
|
body.append((f"Content-Type: {content_type}").encode("utf-8") + crlf + crlf)
|
||||||
|
body.append(file_bytes + crlf)
|
||||||
|
body.append(b"--" + boundary_bytes + b"--" + crlf)
|
||||||
|
|
||||||
|
body_bytes = b"".join(body)
|
||||||
|
content_type_header = f"multipart/form-data; boundary={boundary}"
|
||||||
|
return body_bytes, content_type_header
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file(endpoint: str, api_key: str, file_path: Path) -> int:
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
|
boundary = f"----TheGatesBoundary{uuid.uuid4().hex}"
|
||||||
|
body, content_type = build_multipart_body("file", file_path, boundary)
|
||||||
|
|
||||||
|
req = request.Request(endpoint, method="POST")
|
||||||
|
req.add_header("Content-Type", content_type)
|
||||||
|
req.add_header("Content-Length", str(len(body)))
|
||||||
|
req.add_header("X-API-Key", api_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with request.urlopen(req, data=body, timeout=300) as resp:
|
||||||
|
status = resp.getcode()
|
||||||
|
print(f"Uploaded {file_path.name}: HTTP {status}")
|
||||||
|
return status
|
||||||
|
except error.HTTPError as e:
|
||||||
|
print(f"Upload failed for {file_path.name}: HTTP {e.code} - {e.read().decode(errors='ignore')}")
|
||||||
|
return e.code
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Upload error for {file_path.name}: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("Usage: upload_build.py <file1> [<file2> ...]")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
repo_root = Path(__file__).resolve().parent.parent
|
||||||
|
endpoint = os.environ.get("TG_UPLOAD_ENDPOINT", DEFAULT_ENDPOINT)
|
||||||
|
api_key = read_api_key(repo_root)
|
||||||
|
|
||||||
|
statuses: list[int] = []
|
||||||
|
for arg in argv[1:]:
|
||||||
|
file_path = Path(arg).expanduser().resolve()
|
||||||
|
statuses.append(upload_file(endpoint, api_key, file_path))
|
||||||
|
|
||||||
|
# Return non-zero if any upload failed (status >= 400 or ==1)
|
||||||
|
for s in statuses:
|
||||||
|
if isinstance(s, int) and (s >= 400 or s == 1):
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
Loading…
Add table
Add a link
Reference in a new issue