Initial commit

This commit is contained in:
2026-07-14 12:10:22 +03:00
Unverified
commit 50ea565433
39 changed files with 3611 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
"""Remote auto-updater install (matches install.sh setup_updater)."""
from __future__ import annotations
import getpass
import os
import shlex
import shutil
import subprocess
from pathlib import Path
from deploy.paths import ProjectPaths
from deploy.ssh_auth import SshCredentials, ssh_argv, ssh_common_options
import deploy.ui as ui
DEFAULT_BACKEND_REPO = "https://github.com/fromchat-messenger/backend.git"
DEFAULT_WEB_REPO = "https://github.com/fromchat-messenger/web.git"
DEFAULT_APP_REPO = "https://github.com/fromchat-messenger/app.git"
UPDATER_IMAGE = "fromchat/updater:latest"
def _repo_urls(paths: ProjectPaths) -> tuple[str, str, str]:
backend = os.environ.get("FROMCHAT_BACKEND_REPO", DEFAULT_BACKEND_REPO)
web = os.environ.get("FROMCHAT_WEB_REPO", DEFAULT_WEB_REPO)
app = os.environ.get("FROMCHAT_APP_REPO", DEFAULT_APP_REPO)
if paths.env_file.is_file():
for line in paths.env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
key = key.strip()
val = val.strip().strip('"').strip("'")
if key == "FROMCHAT_BACKEND_REPO" and val:
backend = val
elif key == "FROMCHAT_WEB_REPO" and val:
web = val
elif key == "FROMCHAT_APP_REPO" and val:
app = val
return backend, web, app
def resolve_git_token(explicit: str | None) -> str:
if explicit:
return explicit
for key in ("GIT_TOKEN", "GITHUB_TOKEN", "RELEASES_TOKEN"):
val = os.environ.get(key, "").strip()
if val:
return val
ui.step("Git token for the auto-updater (GitHub or Gitea)")
print(" GitHub: https://github.com/settings/tokens/new?scopes=read:packages,repo")
print(" Gitea: Settings → Applications → Generate New Token")
token = getpass.getpass(" Paste token (input hidden): ").strip()
if not token:
ui.error("Git token is required when updater is selected.")
raise SystemExit(1)
return token
def _updater_compose_source(paths: ProjectPaths) -> Path:
parent = paths.deployment_root.parent
candidates = [
paths.deployment_root.parent / "updater" / "compose.yml",
parent / "updater" / "compose.yml",
]
env_dir = os.environ.get("FROMCHAT_UPDATER_DIR", "")
if env_dir:
candidates.insert(0, Path(env_dir).expanduser() / "compose.yml")
for cand in candidates:
if cand.is_file():
return cand
ui.error(
"updater/compose.yml not found. Set FROMCHAT_UPDATER_DIR or keep ../updater sibling."
)
raise SystemExit(1)
def write_updater_env(
dest: Path,
*,
token: str,
deploy_path_resolved: str,
components: list[str],
paths: ProjectPaths,
) -> None:
backend, web, app = _repo_urls(paths)
components_csv = ",".join(components)
dest.write_text(
"\n".join(
[
f"GITHUB_TOKEN={token}",
f"GIT_TOKEN={token}",
f"BACKEND_REPO={backend}",
f"WEB_REPO={web}",
f"DEPLOYMENT_REPO={app}",
f"COMPOSE_PROJECT_DIR={deploy_path_resolved}",
f"FROMCHAT_COMPONENTS={components_csv}",
"CHECK_INTERVAL_SECONDS=60",
"",
]
),
encoding="utf-8",
)
def setup_updater_remote(
creds: SshCredentials,
deploy_path: str,
deploy_path_resolved: str,
*,
components: list[str],
paths: ProjectPaths,
git_token: str | None,
sudo_password: str,
) -> None:
ui.step("Setting up auto-updater on server")
token = resolve_git_token(git_token)
staging = paths.staging_dir / "updater"
if staging.exists():
shutil.rmtree(staging)
staging.mkdir(parents=True)
compose_src = _updater_compose_source(paths)
shutil.copy2(compose_src, staging / "compose.yml")
write_updater_env(
staging / ".env",
token=token,
deploy_path_resolved=deploy_path_resolved,
components=components,
paths=paths,
)
remote_updater = f"{deploy_path}/updater"
ui.substep("Syncing updater/ to server…")
subprocess.run(
ssh_argv(creds.server, f"mkdir -p {shlex.quote(remote_updater)}"),
check=True,
capture_output=True,
)
rsync = subprocess.run(
[
"rsync",
"-avz",
"-e",
"ssh " + " ".join(shlex.quote(o) for o in ssh_common_options()),
f"{staging}/",
f"{creds.server}:{remote_updater}/",
],
capture_output=True,
text=True,
)
if rsync.returncode != 0:
ui.error("Failed to sync updater directory")
for line in (rsync.stderr or rsync.stdout or "").splitlines():
print(f" {line}")
raise SystemExit(1)
ui.substep("Starting updater service…")
remote_cmd = (
f"cd {shlex.quote(remote_updater)} && "
f"COMPOSE_PROJECT_DIR={shlex.quote(deploy_path_resolved)} "
f"docker compose --env-file .env up -d --wait --timeout 120"
)
if subprocess.run(ssh_argv(creds.server, remote_cmd), capture_output=True).returncode != 0:
ui.error("Failed to start updater on server")
raise SystemExit(1)
ui.success("Updater service started on server")