feat: add repo-pull action to re-clone/pull repository and redeploy app
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
4ec759296b
commit
4be4ae4144
2 changed files with 100 additions and 0 deletions
|
|
@ -390,6 +390,7 @@ const api = {
|
||||||
deploy: (n) => api.request(`/apps/${n}/deploy`, "POST"),
|
deploy: (n) => api.request(`/apps/${n}/deploy`, "POST"),
|
||||||
restart: (n) => api.request(`/apps/${n}/restart`, "POST"),
|
restart: (n) => api.request(`/apps/${n}/restart`, "POST"),
|
||||||
stop: (n) => api.request(`/apps/${n}/stop`, "POST"),
|
stop: (n) => api.request(`/apps/${n}/stop`, "POST"),
|
||||||
|
repoPull: (n) => api.request(`/apps/${n}/repo-pull`, "POST"),
|
||||||
remove: (n, keep) => api.request(`/apps/${n}/remove`, "POST", { keepVolumes: keep }),
|
remove: (n, keep) => api.request(`/apps/${n}/remove`, "POST", { keepVolumes: keep }),
|
||||||
saveCompose: (n, c) => api.request(`/apps/${n}/compose`, "POST", { content: c }),
|
saveCompose: (n, c) => api.request(`/apps/${n}/compose`, "POST", { content: c }),
|
||||||
validateCompose: (n) => api.request(`/apps/${n}/validate-compose`, "POST"),
|
validateCompose: (n) => api.request(`/apps/${n}/validate-compose`, "POST"),
|
||||||
|
|
@ -534,6 +535,7 @@ function renderAppCard(app) {
|
||||||
<button class="btn-sm btn-primary" data-action="deploy" data-app="${app.name}">Deploy</button>
|
<button class="btn-sm btn-primary" data-action="deploy" data-app="${app.name}">Deploy</button>
|
||||||
<button class="btn-sm" data-action="restart" data-app="${app.name}">Restart</button>
|
<button class="btn-sm" data-action="restart" data-app="${app.name}">Restart</button>
|
||||||
<button class="btn-sm" data-action="stop" data-app="${app.name}">Stop</button>
|
<button class="btn-sm" data-action="stop" data-app="${app.name}">Stop</button>
|
||||||
|
<button class="btn-sm" data-action="repo-pull" data-app="${app.name}">Git Pull</button>
|
||||||
<button class="btn-sm btn-danger" data-action="remove" data-app="${app.name}">Remove</button>
|
<button class="btn-sm btn-danger" data-action="remove" data-app="${app.name}">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -699,6 +701,12 @@ async function handleAction(action, name, btnElement) {
|
||||||
setStatus(`Stopped ${name}.`);
|
setStatus(`Stopped ${name}.`);
|
||||||
await loadApps();
|
await loadApps();
|
||||||
break;
|
break;
|
||||||
|
case "repo-pull":
|
||||||
|
setStatus(`Pulling repo for ${name}...`);
|
||||||
|
await api.repoPull(name);
|
||||||
|
setStatus(`Repo pulled for ${name}.`);
|
||||||
|
await loadApps();
|
||||||
|
break;
|
||||||
case "remove": {
|
case "remove": {
|
||||||
const keep = confirm("Keep volumes? OK = keep, Cancel = delete everything.");
|
const keep = confirm("Keep volumes? OK = keep, Cancel = delete everything.");
|
||||||
setStatus(`Removing ${name}...`);
|
setStatus(`Removing ${name}...`);
|
||||||
|
|
|
||||||
92
panel-api.py
92
panel-api.py
|
|
@ -738,6 +738,98 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
self._json(200 if result["ok"] else 400, result)
|
self._json(200 if result["ok"] else 400, result)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# POST /apps/<name>/repo-pull — re-clone/pull repo and redeploy
|
||||||
|
if action == "repo-pull":
|
||||||
|
if not is_safe_name(name):
|
||||||
|
self._json(400, {"ok": False, "error": "invalid app name"})
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, err = read_app_info(name)
|
||||||
|
if err is not None or app is None:
|
||||||
|
self._json(404, {"ok": False, "error": "app not found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
repo_url = app.get("APP_REPO_URL", "").strip()
|
||||||
|
branch = app.get("APP_REPO_BRANCH", "main").strip()
|
||||||
|
|
||||||
|
if not repo_url:
|
||||||
|
self._json(400, {"ok": False, "error": "app has no APP_REPO_URL"})
|
||||||
|
return
|
||||||
|
|
||||||
|
git_bin = shutil.which("git")
|
||||||
|
if not git_bin:
|
||||||
|
self._json(400, {"ok": False, "error": "git is not installed"})
|
||||||
|
return
|
||||||
|
|
||||||
|
target_dir = os.path.join(app["APP_STACK_DIR"], "repo")
|
||||||
|
pat = ""
|
||||||
|
|
||||||
|
auth_url = repo_url
|
||||||
|
if pat:
|
||||||
|
auth_url = auth_url.replace("://", f"://{pat}@")
|
||||||
|
if not auth_url.endswith(".git"):
|
||||||
|
auth_url += ".git"
|
||||||
|
|
||||||
|
if os.path.exists(target_dir):
|
||||||
|
# Already cloned — try git pull
|
||||||
|
pull_result = subprocess.run(
|
||||||
|
[git_bin, "-C", target_dir, "pull", "origin", branch],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if pull_result.returncode != 0:
|
||||||
|
# Fall back to re-clone
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
clone_result = subprocess.run(
|
||||||
|
[git_bin, "clone", "--branch", branch, auth_url, target_dir],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if clone_result.returncode != 0:
|
||||||
|
self._json(400, {"ok": False, "error": f"git clone failed: {clone_result.stderr.strip()}"})
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
clone_result = subprocess.run(
|
||||||
|
[git_bin, "clone", "--branch", branch, auth_url, target_dir],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if clone_result.returncode != 0:
|
||||||
|
self._json(400, {"ok": False, "error": f"git clone failed: {clone_result.stderr.strip()}"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find compose file
|
||||||
|
compose_path = None
|
||||||
|
for fname in ["compose.yaml", "docker-compose.yml", "compose.yml", "docker-compose.yaml"]:
|
||||||
|
candidate = os.path.join(target_dir, fname)
|
||||||
|
if os.path.isfile(candidate):
|
||||||
|
compose_path = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if not compose_path:
|
||||||
|
self._json(400, {"ok": False, "error": "compose file not found in repository"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update manifest compose path
|
||||||
|
manifest_path = os.path.join(BASE_DIR, "state", "apps", f"{name}.env")
|
||||||
|
try:
|
||||||
|
with open(manifest_path, "r", encoding="utf-8") as fh:
|
||||||
|
lines = fh.readlines()
|
||||||
|
with open(manifest_path, "w", encoding="utf-8") as fh:
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("APP_COMPOSE_FILE="):
|
||||||
|
fh.write(f'APP_COMPOSE_FILE="{compose_path}"\n')
|
||||||
|
else:
|
||||||
|
fh.write(line)
|
||||||
|
except OSError as exc:
|
||||||
|
self._json(500, {"ok": False, "error": f"failed to update manifest: {exc}"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Redeploy
|
||||||
|
result = run_panelctl(["deploy", name])
|
||||||
|
self._json(200 if result["ok"] else 400, result)
|
||||||
|
except Exception as exc:
|
||||||
|
self._json(500, {"ok": False, "error": f"repo-pull failed: {exc}"})
|
||||||
|
return
|
||||||
|
|
||||||
# POST /apps/<name>/remove
|
# POST /apps/<name>/remove
|
||||||
if action == "remove":
|
if action == "remove":
|
||||||
if not is_safe_name(name):
|
if not is_safe_name(name):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue