Keeps existing portal env vars and triggers deploy after merge. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
6.7 KiB
Python
178 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Merge SUPERHUMAN_DOCS_API_KEY into client-portal Dokploy env, then Deploy."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = Path("/Users/4ventures/4ventures-client-portal")
|
|
BPS = Path("/Users/4ventures/Projects/bps-facturacion")
|
|
BASE = "https://dokploy.4ventures.es"
|
|
APP_ID = "NZS797t9Rnq9Pj4mHiii3"
|
|
APP = f"{BASE}/dashboard/project/Z_4M-uDOH1_--itGSTh4l/environment/bGja70l_a7HQ1dxyXqSuf/services/application/{APP_ID}"
|
|
HEALTH = "https://app.4ventures.es/api/health"
|
|
|
|
|
|
def cred(key: str) -> str:
|
|
import os
|
|
|
|
v = os.environ.get(key, "").strip()
|
|
if v:
|
|
return v
|
|
p = BPS / ".env"
|
|
if not p.is_file():
|
|
return ""
|
|
for line in p.read_text(encoding="utf-8").splitlines():
|
|
if line.startswith(f"{key}="):
|
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
return ""
|
|
|
|
|
|
def local_key() -> str:
|
|
for line in (ROOT / ".env.local").read_text(encoding="utf-8").splitlines():
|
|
if line.startswith("SUPERHUMAN_DOCS_API_KEY="):
|
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
p = ROOT / ".ops" / "superhuman-docs-api-key.txt"
|
|
if p.is_file():
|
|
return p.read_text(encoding="utf-8").strip()
|
|
return ""
|
|
|
|
|
|
def parse_env(text: str) -> dict[str, str]:
|
|
out: dict[str, str] = {}
|
|
for line in (text or "").splitlines():
|
|
if not line.strip() or line.strip().startswith("#") or "=" not in line:
|
|
continue
|
|
k, v = line.split("=", 1)
|
|
out[k.strip()] = v.strip()
|
|
return out
|
|
|
|
|
|
def dump_env(vals: dict[str, str]) -> str:
|
|
return "\n".join(f"{k}={v}" for k, v in vals.items()) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
sh_key = local_key()
|
|
if not sh_key:
|
|
print("missing SUPERHUMAN_DOCS_API_KEY locally", flush=True)
|
|
return 2
|
|
email = cred("DOKPLOY_EMAIL") or "marketing@4ventures.es"
|
|
password = cred("DOKPLOY_PASSWORD")
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
page = browser.new_page(viewport={"width": 1400, "height": 1000})
|
|
page.goto(BASE + "/", wait_until="domcontentloaded", timeout=90000)
|
|
page.wait_for_timeout(600)
|
|
if "/dashboard" not in page.url:
|
|
page.locator('input[type="email"], input[name="email"]').first.fill(email)
|
|
page.locator('input[type="password"]').first.fill(password)
|
|
page.locator('button[type="submit"]').first.click()
|
|
page.wait_for_url(re.compile(r"/dashboard"), timeout=90000)
|
|
print("login ok", flush=True)
|
|
|
|
page.goto(APP, wait_until="networkidle", timeout=120000)
|
|
page.wait_for_timeout(800)
|
|
|
|
result = page.evaluate(
|
|
"""async ({ appId, shKey }) => {
|
|
const oneUrl = '/api/trpc/application.one?batch=1&input=' + encodeURIComponent(JSON.stringify({ '0': { json: { applicationId: appId } } }));
|
|
const one = await fetch(oneUrl, { credentials: 'include' });
|
|
const oneText = await one.text();
|
|
if (!one.ok) return { ok: false, step: 'one', status: one.status, body: oneText.slice(0, 400) };
|
|
let envText = '';
|
|
try {
|
|
const j = JSON.parse(oneText);
|
|
const data = j[0]?.result?.data?.json || j[0]?.result?.data || j;
|
|
envText = data?.env || data?.environment || '';
|
|
if (typeof envText !== 'string') envText = '';
|
|
} catch (e) {
|
|
return { ok: false, step: 'parse', error: String(e), body: oneText.slice(0, 400) };
|
|
}
|
|
const lines = envText.split('\\n').filter(Boolean);
|
|
const map = {};
|
|
for (const line of lines) {
|
|
const i = line.indexOf('=');
|
|
if (i < 0) continue;
|
|
map[line.slice(0, i)] = line.slice(i + 1);
|
|
}
|
|
map['SUPERHUMAN_DOCS_API_KEY'] = shKey;
|
|
const merged = Object.keys(map).map(k => k + '=' + map[k]).join('\\n') + '\\n';
|
|
const keys = Object.keys(map);
|
|
|
|
const tries = [];
|
|
const payloads = [
|
|
{ path: '/api/application.update', body: { applicationId: appId, env: merged } },
|
|
{ path: '/api/trpc/application.update?batch=1', body: { '0': { json: { applicationId: appId, env: merged } } } },
|
|
{ path: '/api/trpc/application.saveEnvironment?batch=1', body: { '0': { json: { applicationId: appId, env: merged } } } },
|
|
];
|
|
for (const t of payloads) {
|
|
const r = await fetch(t.path, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(t.body),
|
|
});
|
|
const text = await r.text();
|
|
tries.push({ path: t.path, status: r.status, body: text.slice(0, 180) });
|
|
if (r.ok) return { ok: true, keys, tries, hadKey: keys.includes('SUPERHUMAN_DOCS_API_KEY') };
|
|
}
|
|
return { ok: false, keys, tries, envLen: envText.length };
|
|
}""",
|
|
{"appId": APP_ID, "shKey": sh_key},
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"ok": result.get("ok"),
|
|
"hadKey": result.get("hadKey"),
|
|
"keyCount": len(result.get("keys") or []),
|
|
"tries": result.get("tries"),
|
|
"step": result.get("step"),
|
|
"status": result.get("status"),
|
|
},
|
|
indent=2,
|
|
)[:2000],
|
|
flush=True,
|
|
)
|
|
if not result.get("ok"):
|
|
browser.close()
|
|
return 6
|
|
|
|
# Deploy
|
|
page.goto(APP, wait_until="domcontentloaded", timeout=90000)
|
|
page.wait_for_timeout(700)
|
|
page.get_by_role("tab", name=re.compile(r"^General$", re.I)).first.click()
|
|
page.wait_for_timeout(500)
|
|
page.get_by_role("button", name=re.compile(r"^Deploy$", re.I)).first.click()
|
|
page.wait_for_timeout(600)
|
|
conf = page.get_by_role("button", name=re.compile(r"^Confirm$", re.I))
|
|
if conf.count():
|
|
conf.first.click()
|
|
print("DEPLOY_CLICKED", flush=True)
|
|
|
|
for i in range(24):
|
|
time.sleep(10)
|
|
try:
|
|
import urllib.request
|
|
|
|
code = urllib.request.urlopen(HEALTH, timeout=12).getcode()
|
|
except Exception as e:
|
|
code = str(e)
|
|
print(f"health_try{i}", code, flush=True)
|
|
if code == 200:
|
|
break
|
|
|
|
browser.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|