Initial commit
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user