165 lines
8.1 KiB
Python
165 lines
8.1 KiB
Python
# Piloto del webhook Jira FAC → plataforma (29-sep-2026). Autorizado por Pedro
|
|
# (vía Johann). Cada escritura en Jira es explícita, una por subcomando:
|
|
#
|
|
# url → lee la URL pública del túnel ngrok local (solo lectura)
|
|
# registrar → POST /rest/webhooks/1.0/webhook (ESCRIBE: 1 webhook de sitio)
|
|
# crear-ticket → POST /rest/api/3/issue (ESCRIBE: 1 ticket de prueba en FAC)
|
|
# esperar CLAVE → espera la detección en la BD local (solo lectura)
|
|
# borrar-ticket CLAVE → DELETE /rest/api/3/issue/CLAVE (ESCRIBE: borra el ticket de prueba)
|
|
# borrar-webhook → DELETE del webhook registrado (reversa total)
|
|
# mover URL_BASE → PUT del webhook a otra URL base (ESCRIBE), p. ej.
|
|
# https://paputec.mx/balam (el receptor vive en /api/jira/webhook)
|
|
# estado → webhooks de sitio actuales (solo lectura)
|
|
#
|
|
# Estado del piloto: webhook de sitio id 1 registrado el 29-sep apuntando al
|
|
# túnel ngrok de Johann; prueba FAC-169 creada, detectada y borrada.
|
|
#
|
|
# El token de Jira y el secreto del webhook nunca se imprimen ni se versionan:
|
|
# token → propuesta/token-jira.txt (gitignoreado) o $JIRA_TOKEN_FILE
|
|
# secreto → $JIRA_WEBHOOK_SECRET_FILE (default: ~/jira-webhook-secret.txt)
|
|
import base64, json, os, subprocess, sys, time, urllib.request, urllib.error
|
|
|
|
SITE = "https://balam-jsm-temp.atlassian.net"
|
|
EMAIL = "pedro.ayala@balamtalentoestrategico.com"
|
|
PEDRO = "712020:0cde8558-7794-468c-9e2c-73dfe63bd57b"
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
HOME = os.path.expanduser("~")
|
|
TOKEN = open(os.environ.get("JIRA_TOKEN_FILE", os.path.join(REPO, "propuesta", "token-jira.txt")), encoding="utf-8").read().strip()
|
|
SECRETO = open(os.environ.get("JIRA_WEBHOOK_SECRET_FILE", os.path.join(HOME, "jira-webhook-secret.txt")), encoding="utf-8").read().strip()
|
|
AUTH = "Basic " + base64.b64encode(f"{EMAIL}:{TOKEN}".encode()).decode()
|
|
ESTADO = os.environ.get("JIRA_WEBHOOK_ESTADO", os.path.join(HOME, "jira-webhook-estado.json"))
|
|
|
|
|
|
def jira(method, path, body=None):
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
req = urllib.request.Request(SITE + path, data=data, method=method, headers={
|
|
"Authorization": AUTH, "Accept": "application/json", "Content-Type": "application/json"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
raw = r.read().decode("utf-8")
|
|
return r.status, (json.loads(raw) if raw else None)
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read().decode("utf-8", "replace")[:500]
|
|
|
|
|
|
def estado(**cambios):
|
|
s = json.load(open(ESTADO, encoding="utf-8")) if os.path.exists(ESTADO) else {}
|
|
s.update(cambios)
|
|
json.dump(s, open(ESTADO, "w", encoding="utf-8"), indent=1)
|
|
return s
|
|
|
|
|
|
def url_publica():
|
|
for puerto in (4040, 4041, 4042):
|
|
try:
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{puerto}/api/tunnels", timeout=3) as r:
|
|
for t in json.loads(r.read().decode())["tunnels"]:
|
|
if t["public_url"].startswith("https://") and "5079" in t["config"]["addr"]:
|
|
return t["public_url"]
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def sql(consulta):
|
|
# Superusuario local del docker-compose: ve la tabla pese a RLS.
|
|
out = subprocess.run(["docker", "exec", "balam-postgres-dev", "psql", "-U", "balam", "-d", "balam",
|
|
"-At", "-F", " | ", "-c", consulta], capture_output=True, text=True, encoding="utf-8")
|
|
return out.stdout.strip() or out.stderr.strip()
|
|
|
|
|
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "estado"
|
|
|
|
if cmd == "url":
|
|
print(url_publica() or "Sin túnel ngrok hacia 5079.")
|
|
|
|
elif cmd == "registrar":
|
|
base = url_publica()
|
|
if not base:
|
|
sys.exit("Sin túnel ngrok hacia 5079: no se registra nada.")
|
|
cuerpo = {
|
|
"name": "Balam plataforma · FAC ticket creado (piloto 29-sep)",
|
|
"url": f"{base}/api/jira/webhook?token={SECRETO}",
|
|
"events": ["jira:issue_created"],
|
|
"filters": {"issue-related-events-section": "project = FAC"},
|
|
"excludeBody": False,
|
|
"secret": SECRETO,
|
|
}
|
|
st, r = jira("POST", "/rest/webhooks/1.0/webhook", cuerpo)
|
|
if st == 400 and "secret" in str(r).lower():
|
|
cuerpo.pop("secret")
|
|
st, r = jira("POST", "/rest/webhooks/1.0/webhook", cuerpo)
|
|
if st in (200, 201) and isinstance(r, dict):
|
|
wid = r.get("self", "").rstrip("/").split("/")[-1]
|
|
estado(webhook_id=wid, webhook_url_base=base, registrado=time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
con_secreto_hmac="secret" in cuerpo)
|
|
print(f"[{st}] webhook registrado · id={wid} · eventos={r.get('events')} · filtro={r.get('filters')} · "
|
|
f"secreto HMAC enviado={'secret' in cuerpo}")
|
|
else:
|
|
print(f"[{st}] {r}")
|
|
|
|
elif cmd == "crear-ticket":
|
|
adf = {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text":
|
|
"Ticket de PRUEBA de la plataforma Balam (29-sep-2026), autorizado por Pedro Ayala, para validar "
|
|
"que la plataforma detecta la creación de tickets por webhook. Se elimina en cuanto se confirma."}]}]}
|
|
st, r = jira("POST", "/rest/api/3/issue", {"fields": {
|
|
"project": {"key": "FAC"}, "issuetype": {"id": "10073"},
|
|
"summary": "[PRUEBA PLATAFORMA BALAM] Detección por webhook — se borra en minutos",
|
|
"description": adf, "assignee": {"accountId": PEDRO}}})
|
|
if st == 201:
|
|
estado(ticket=r["key"], ticket_id=r["id"], creado=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
print(f"[201] ticket de prueba creado: {r['key']} (id {r['id']})")
|
|
else:
|
|
print(f"[{st}] {r}")
|
|
|
|
elif cmd == "esperar":
|
|
clave = sys.argv[2]
|
|
for i in range(60):
|
|
fila = sql(f"""SELECT "Id","IssueKey","Verified","AuthMethod","JiraStatus",
|
|
to_char("JiraTimestampUtc" AT TIME ZONE 'America/Mexico_City','HH24:MI:SS.MS'),
|
|
to_char("ReceivedAtUtc" AT TIME ZONE 'America/Mexico_City','HH24:MI:SS.MS'),
|
|
round(extract(epoch from ("ReceivedAtUtc"-"JiraTimestampUtc"))::numeric,2),
|
|
coalesce("Error",'') FROM "JiraTicketEvents" WHERE "IssueKey"='{clave}'""")
|
|
if fila and "|" in fila:
|
|
print(f"DETECTADO tras ~{i}s de espera: id | llave | verificado | auth | estatus Jira | emitido (MX) | recibido (MX) | retraso s | error")
|
|
print(" " + fila)
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
print(f"Sin detección de {clave} en 60 s.")
|
|
|
|
elif cmd == "borrar-ticket":
|
|
clave = sys.argv[2]
|
|
st, r = jira("DELETE", f"/rest/api/3/issue/{clave}")
|
|
st2, _ = jira("GET", f"/rest/api/3/issue/{clave}")
|
|
estado(ticket_borrado=clave if st == 204 else None, borrado=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
print(f"[{st}] DELETE {clave} · verificación GET → {st2} (404 = ya no existe)")
|
|
|
|
elif cmd == "borrar-webhook":
|
|
wid = estado().get("webhook_id")
|
|
st, r = jira("DELETE", f"/rest/webhooks/1.0/webhook/{wid}")
|
|
print(f"[{st}] DELETE webhook {wid}")
|
|
|
|
elif cmd == "mover":
|
|
# Destino estable (paputec): solo HMAC, sin el secreto en la URL.
|
|
base = sys.argv[2].rstrip("/")
|
|
wid = estado().get("webhook_id") or "1"
|
|
st, r = jira("PUT", f"/rest/webhooks/1.0/webhook/{wid}", {
|
|
"name": "Balam plataforma · FAC ticket creado",
|
|
"url": f"{base}/api/jira/webhook",
|
|
"events": ["jira:issue_created"],
|
|
"filters": {"issue-related-events-section": "project = FAC"},
|
|
"excludeBody": False,
|
|
"secret": SECRETO,
|
|
})
|
|
if st in (200, 201):
|
|
estado(webhook_id=wid, webhook_url_base=base, movido=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
print(f"[{st}] webhook {wid} → {base}/api/jira/webhook" + ("" if st in (200, 201) else f" · {r}"))
|
|
|
|
elif cmd == "estado":
|
|
st, r = jira("GET", "/rest/webhooks/1.0/webhook")
|
|
print(f"[{st}] webhooks de sitio: {len(r) if isinstance(r, list) else r}")
|
|
for w in r if isinstance(r, list) else []:
|
|
print(f" - {w.get('name')} · habilitado={w.get('enabled')} · eventos={w.get('events')} · filtro={w.get('filters')}")
|
|
print("estado local:", json.dumps(estado(), ensure_ascii=False))
|