chore(ti/licenciamiento): carga de produccion automatica y autolimitada en deploy_prod
chore(ti/licenciamiento): la carga de produccion pasa de boton manual a paso autolimitado dentro de deploy_prod
CORRIGE UN ERROR DE DISEÑO DEL COMMIT ANTERIOR
833dc435 traslado a produccion el mismo patron que funciono en QA: dos jobs
when: manual que alguien aprieta despues del deploy. Calcar el patron fue el
error — no se ajusto a QUIEN iba a apretar el boton del otro lado.
GitLab exige permiso de MERGE sobre la rama para disparar un job manual. En qa
eso no fue obstaculo porque la rama no esta protegida y el operador es Developer.
En main solo mergea Diego, asi que el
QUE SE HACE EN SU LUGAR
Los dos jobs (licencias_import_prod_dryrun / _apply) y la etapa oneshot
desaparecen. En su lugar, un paso dentro del script de deploy_prod, ubicado
despues de backfill_canonicals.py — o sea despues de docker compose up -d --build, que es lo que garantiza que el backend ya arranco y que create_all
ya creo la tabla destino.
Corre con los permisos del PIPELINE, no con los de una persona. Nadie aprieta nada, nadie tiene que acordarse de nada, y no depende del rol de quien mergee.
LA GUARDA ES LO QUE IMPIDE QUE ESTO DEGENERE EN UN SYNC
El riesgo obvio de colgar la carga del deploy es convertirla en una lectura
recurrente de Google en cada despliegue, que es exactamente lo contrario de lo
que decidio 375ec704 al abandonar la hoja. Por eso la condicion no es "corre
siempre" sino "corre solo si ti_licencias esta vacia":
· Si la tabla YA tiene filas, no se abre la hoja. Ni una peticion a Google. · Si esta vacia, se carga una vez. En el siguiente deploy ya tiene filas y la guarda la salta para siempre.
Es la misma semantica que tenia el boton, sin el boton.
ESPERA ACTIVA POR LA TABLA — NO ES PARANOIA, ES UNA CARRERA REAL
docker compose up -d --build regresa cuando el container ARRANCA, no cuando
FastAPI termino de bootear, y create_all corre durante ese boot. Sin esperar,
el paso podria leer ti_licencias antes de que exista, interpretar el error como
"cero filas" e intentar una carga contra una tabla inexistente. Se resuelve con
un poll de hasta 60s a to_regclass('public.ti_licencias'). Si se agota, no
carga nada y lo dice — y como la guarda sigue viendo la tabla vacia, el
siguiente deploy lo reintenta solo.
EL UNICO PRERREQUISITO QUE EL PIPELINE NO PUEDE RESOLVER
backend/service_account.json tiene que existir en el host de PRD. deploy_prod
lo preserva con git clean -e, pero preservar no es tener: el comentario de ese
mismo job dice "copiar UNA VEZ al server tras este merge", o sea que el archivo
pudo nunca haberse copiado. En QA si estaba y la lectura funciono a la primera.
El paso verifica su existencia ANTES de intentar leer la hoja y, si falta, imprime que archivo copiar y a donde, sin romper el deploy. Copiarlo requiere acceso al servidor; reintentarlo no requiere nada, porque la guarda sigue viendo la tabla vacia y el siguiente deploy vuelve a intentar.
REPORTE ANTES DE ESCRIBIR
El paso corre --dry-run y DESPUES la carga, ambos al log del job. Se pierde el
gate humano entre reporte y escritura, pero ese gate ya cumplio: el mismo script
contra la misma hoja se valido en QA — 1252 altas, 1213 cruces al catalogo, cero
filas pisadas, cero actualizaciones. La evidencia queda igual en el log, solo que
sin nadie esperando en medio.
Cierra con el conteo por origen, para que el log no termine mudo.
ALCANCE
Un solo archivo, .gitlab-ci.yml. Se elimina la etapa oneshot (ya no la usa
nadie). Cero DDL, cero migraciones, cero cambios de modelo, router o frontend.
backend/scripts/import_licencias_sheet.py sigue en el arbol: produccion todavia
lo necesita. Se borra, junto con este bloque, en el commit siguiente a que PRD
quede cargado.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com