28eb9e19f5
Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
6.8 KiB
Python
259 lines
6.8 KiB
Python
import os
|
|
import re
|
|
import subprocess
|
|
|
|
REF_PATTERN = re.compile(r"^[a-zA-Z0-9._/-]+$")
|
|
|
|
|
|
def is_deploy_enabled():
|
|
return os.getenv("ALLOW_GIT_DEPLOY", "false").lower() in ("1", "true", "yes")
|
|
|
|
|
|
def get_repo_path():
|
|
return os.getenv("GIT_REPO_PATH", "/repo")
|
|
|
|
|
|
def get_git_remote():
|
|
return os.getenv("GIT_REMOTE_URL", "").strip()
|
|
|
|
|
|
def get_container_name():
|
|
return os.getenv("CONTAINER_NAME", os.getenv("HOSTNAME", "photohost-web"))
|
|
|
|
|
|
def _repo_ready():
|
|
repo = get_repo_path()
|
|
return os.path.isdir(repo) and os.path.isdir(os.path.join(repo, ".git"))
|
|
|
|
|
|
def _docker_available():
|
|
try:
|
|
result = subprocess.run(
|
|
["docker", "info"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return False
|
|
|
|
|
|
def _git_base_cmd(repo):
|
|
return ["git", "-c", f"safe.directory={repo}", "-C", repo]
|
|
|
|
|
|
def _run_subprocess(cmd, timeout=120, cwd=None):
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
cwd=cwd,
|
|
)
|
|
output = (result.stderr or result.stdout or "").strip()
|
|
return result.returncode == 0, output
|
|
|
|
|
|
def _run_git_as_root(args, timeout=120):
|
|
repo = get_repo_path()
|
|
container = get_container_name()
|
|
cmd = ["docker", "exec", "-u", "0", container] + _git_base_cmd(repo) + args
|
|
return _run_subprocess(cmd, timeout=timeout)
|
|
|
|
|
|
def _fix_repo_permissions():
|
|
if not _docker_available() or not _repo_ready():
|
|
return False
|
|
|
|
repo = get_repo_path()
|
|
container = get_container_name()
|
|
fix_cmd = [
|
|
"docker",
|
|
"exec",
|
|
"-u",
|
|
"0",
|
|
container,
|
|
"sh",
|
|
"-c",
|
|
f"chown -R appuser:appuser {repo} && chmod -R u+rwX {repo}/.git",
|
|
]
|
|
ok, _ = _run_subprocess(fix_cmd, timeout=60)
|
|
return ok
|
|
|
|
|
|
def run_git(args, timeout=120):
|
|
if not _repo_ready():
|
|
return False, f"Git-репозиторий не найден: {get_repo_path()}"
|
|
|
|
repo = get_repo_path()
|
|
cmd = _git_base_cmd(repo) + args
|
|
ok, output = _run_subprocess(cmd, timeout=timeout)
|
|
if ok:
|
|
return True, output
|
|
|
|
permission_error = "permission denied" in output.lower() and "config" in output.lower()
|
|
if permission_error and _docker_available():
|
|
_fix_repo_permissions()
|
|
ok, output = _run_subprocess(cmd, timeout=timeout)
|
|
if ok:
|
|
return True, output
|
|
ok, output = _run_git_as_root(args, timeout=timeout)
|
|
if ok:
|
|
return True, output
|
|
|
|
return False, output or "Git error"
|
|
|
|
|
|
def run_ls_remote(extra_args=None, timeout=60):
|
|
remote = get_git_remote()
|
|
if not remote:
|
|
return False, "GIT_REMOTE_URL не задан", []
|
|
|
|
cmd = ["git", "ls-remote", remote]
|
|
if extra_args:
|
|
cmd.extend(extra_args)
|
|
|
|
ok, output = _run_subprocess(cmd, timeout=timeout)
|
|
if not ok:
|
|
return False, output or "ls-remote error", []
|
|
return True, "", output.splitlines()
|
|
|
|
|
|
def fetch_remote():
|
|
remote = get_git_remote()
|
|
if not remote:
|
|
return run_git(["fetch", "--all", "--tags", "--prune"], timeout=180)
|
|
|
|
# Fetch by URL only — never run `git remote set-url` or other config writes.
|
|
return run_git(
|
|
[
|
|
"fetch",
|
|
"--tags",
|
|
"--prune",
|
|
remote,
|
|
"+refs/heads/*:refs/heads/*",
|
|
"+refs/tags/*:refs/tags/*",
|
|
],
|
|
timeout=180,
|
|
)
|
|
|
|
|
|
def list_tags():
|
|
ok, err, lines = run_ls_remote(["--tags"])
|
|
if not ok:
|
|
return [], err
|
|
|
|
tags = []
|
|
for line in lines:
|
|
parts = line.split()
|
|
if len(parts) < 2:
|
|
continue
|
|
ref = parts[1]
|
|
if not ref.startswith("refs/tags/"):
|
|
continue
|
|
tag = ref.removeprefix("refs/tags/")
|
|
if tag.endswith("^{}"):
|
|
continue
|
|
tags.append(tag)
|
|
|
|
tags = sorted(set(tags), reverse=True)
|
|
return tags, None
|
|
|
|
|
|
def list_branches():
|
|
ok, err, lines = run_ls_remote(["--heads"])
|
|
if not ok:
|
|
return [], err
|
|
|
|
branches = []
|
|
for line in lines:
|
|
parts = line.split()
|
|
if len(parts) < 2:
|
|
continue
|
|
ref = parts[1]
|
|
if ref.startswith("refs/heads/"):
|
|
branches.append(ref.removeprefix("refs/heads/"))
|
|
|
|
return sorted(set(branches)), None
|
|
|
|
|
|
def get_current_version():
|
|
if not _repo_ready():
|
|
return None, "Репозиторий недоступен"
|
|
|
|
ok, tag = run_git(["describe", "--tags", "--always"])
|
|
if ok and tag:
|
|
return tag, None
|
|
|
|
ok, branch = run_git(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
if ok:
|
|
return branch, None
|
|
return "unknown", None
|
|
|
|
|
|
def checkout_version(ref):
|
|
if not ref or not REF_PATTERN.match(ref):
|
|
return False, "Недопустимое имя версии"
|
|
|
|
ok, msg = fetch_remote()
|
|
if not ok:
|
|
return False, msg
|
|
|
|
ok, msg = run_git(["checkout", ref])
|
|
if not ok:
|
|
return False, msg
|
|
return True, f"Переключено на {ref}"
|
|
|
|
|
|
def deploy_rebuild():
|
|
if not is_deploy_enabled():
|
|
return False, "Обновление через админку отключено (ALLOW_GIT_DEPLOY=false)"
|
|
|
|
repo = get_repo_path()
|
|
compose_file = os.path.join(repo, "docker-compose.yml")
|
|
if not os.path.isfile(compose_file):
|
|
return False, f"Не найден {compose_file}"
|
|
|
|
commands = [
|
|
["docker", "compose", "-f", compose_file, "up", "-d", "--build"],
|
|
["docker-compose", "-f", compose_file, "up", "-d", "--build"],
|
|
]
|
|
|
|
last_error = ""
|
|
for cmd in commands:
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600,
|
|
cwd=repo,
|
|
)
|
|
if result.returncode == 0:
|
|
return True, result.stdout or "Контейнеры пересобраны и запущены"
|
|
last_error = result.stderr or result.stdout
|
|
except FileNotFoundError:
|
|
last_error = f"Команда не найдена: {cmd[0]}"
|
|
except subprocess.TimeoutExpired:
|
|
return False, "Превышено время ожидания пересборки (10 мин)"
|
|
|
|
return False, last_error or "Не удалось выполнить docker compose"
|
|
|
|
|
|
def get_deploy_status():
|
|
current, _ = get_current_version()
|
|
tags, tags_err = list_tags()
|
|
branches, branches_err = list_branches()
|
|
return {
|
|
"enabled": is_deploy_enabled(),
|
|
"repo_path": get_repo_path(),
|
|
"repo_ready": _repo_ready(),
|
|
"remote_url": get_git_remote(),
|
|
"current": current,
|
|
"tags": tags[:30],
|
|
"branches": branches[:30],
|
|
"tags_error": tags_err,
|
|
"branches_error": branches_err,
|
|
}
|