Files
4ventures-client-portal/scripts/dokploy_set_superhuman_env.py
4Ventures 982a9e2954 chore: Dokploy helper to merge Superhuman Docs API key
Keeps existing portal env vars and triggers deploy after merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 21:41:58 +02:00

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())