Author SHA1 Message Date
devops 9cfc3a3bdd Merge branch 'fix/promote-script-sin-perl-2026-09-20' 2026-09-20 17:46:01 +00:00
devops b06c1f211e fix(promote-to-production): reemplazar perl por awk, no esta en el host ZimaOS 2026-09-20 17:44:52 +00:00
devops 2058f1ca2a Merge branch 'docs/promote-script-obsoleto-2026-09-20' 2026-09-20 17:32:31 +00:00
devops dfaf5c5a7d docs: marcar promote-to-production.sh como obsoleto (ahora automatico) 2026-09-20 17:31:19 +00:00
devopsandClaude Sonnet 5 f99c955751 Merge branch 'fix/tmux-host-remote-control-continue'
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 15:46:10 +00:00
devopsandClaude Sonnet 5 e64afedf31 fix(tmux-host): retomar la ultima conversacion al recrear la sesion
launch_claude_in_session() ahora lanza 'claude --remote-control --continue'
en vez de 'claude --remote-control' a secas. -c/--continue retoma la
conversacion mas reciente en WORKDIR; si no hay ninguna arranca una nueva
sin error. Evita que cada recreacion de la sesion tmux (reinicio del NAS,
crash del contenedor) empiece con una conversacion en blanco.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 15:45:51 +00:00
devopsandClaude Sonnet 5 0ffa1ff15c Merge branch 'fix/tmux-host-remote-control'
Lanza claude --remote-control al recrear la sesion tmux-host para que
quede visible en claude.ai sin intervencion manual.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-20 15:16:58 +00:00
devopsandClaude Sonnet 5 f48860fcd8 fix(tmux-host): lanzar claude con --remote-control al recrear la sesion
Sin la flag, la sesion de tmux-host nunca queda visible como
controlable remotamente en claude.ai: remoteEnabled quedaba en false
porque nadie la pedia. Confirmado en vivo que --remote-control si
empareja (aparecio como sesion activa "zimaos-quizzical") mientras el
proceso se mantiene vivo.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-20 15:16:00 +00:00
devopsandClaude Sonnet 5 955eec6e75 fix(tmux-host): lanzar claude al recrear la sesion + backoff de arranque
check_tmux_session() recreaba la sesion tmux "lab" tras un reinicio del
NAS pero nunca lanzaba `claude` adentro -- quedaba un shell vacio hasta
que alguien entraba a mano. Confirmado en vivo tras un corte de luz real
(2026-09-19): la sesion SI se recreaba sola, pero Claude Code no.

Se agrega wait_for_workdir() con backoff (hasta 120s por defecto) antes
de tocar el log o crear la sesion, para el caso de un reinicio completo
del host donde /media/HDD-Storage puede tardar en montarse mas de lo
que tarda Docker en arrancar este contenedor -- antes eso fallaba en
silencio y ese arranque no dejaba rastro en el log.

Probado en un contenedor descartable aparte (tmux-host-test): stop+start
desde cero recreo la sesion con claude corriendo, sin intervencion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-19 20:15:37 +00:00
devopsandClaude Sonnet 5 9d04c73b20 feat: bumpear APP_VERSION al promover a producción
Complementa el cambio en apps-registry (footer lee process.env.APP_VERSION
para mostrar la versión real desplegada) -- sin esto, promover a
Producción actualizaría la imagen pero dejaría el footer mostrando la
versión vieja.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
2026-09-14 04:04:17 +00:00
devopsandClaude Sonnet 5 3640cd5754 feat: script de promoción manual de Staging a Producción
Contraparte del cambio en apps-registry/.gitea/workflows/build.yaml:
desde ahora el CI solo actualiza Staging automáticamente en cada push.
Este script lee la versión actual en
workloads/ecommerce-staging/frontend/patches/deployment.yaml y la
aplica a workloads/ecommerce/frontend/patches/deployment.yaml (con
confirmación interactiva), commiteando y pusheando a apps-registry.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
2026-09-14 02:46:06 +00:00
devopsandClaude Sonnet 5 78b97bdd59 feat(tmux-host): Dockerfile con WORKDIR fijo + healthcheck cada 30 min
Reemplaza el docker run inline (alpine sh -c "apk add ...") por una
imagen construida desde scripts/tmux-host/Dockerfile, con WORKDIR en la
raiz del lab para que docker exec y lab-session.sh ya arranquen
posicionados ahi sin cd manual.

Agrega scripts/tmux-host/healthcheck.sh (via entrypoint.sh, background
del propio contenedor): cada 30 min chequea conectividad real
(curl a gitea.cruzcloud.net) y la existencia de la sesion tmux "lab".
La red solo se loguea+notifica por Telegram (nunca se intenta corregir
desde el contenedor); la sesion tmux si se recrea sola si falta. Log
persistente en /DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log.
Reusa el bot/chat_id de Telegram ya configurado en scripts/lab-agent/.

Validado en un contenedor descartable (tmux-host-test, intervalo de 60s):
confirmado WORKDIR correcto via docker exec, creacion inicial de la
sesion y recreacion automatica tras un kill-session manual.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012xyPak6HWFmBCqXrvFsejL
2026-09-06 17:16:41 +00:00
devops 40cb058fd4 Merge fix/lab-session-tmux-persistente into main
Merge local directo (sin PR de Gitea) por acuerdo del usuario; ver
feedback_git_fix_branches en memoria del proyecto para el porque de
este flujo estandar.
2026-08-27 22:18:47 -05:00
5 changed files with 273 additions and 1 deletions

No files matched your search

+6 -1
View File
@@ -10,6 +10,11 @@ set -Eeuo pipefail
CONTAINER="tmux-host"
SESSION="lab"
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
# Mismo path que el WORKDIR de scripts/tmux-host/Dockerfile -- pasado
# explicito acá (-c) para no depender de que docker exec herede el WORKDIR
# de la imagen.
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
echo "El contenedor '$CONTAINER' no esta corriendo. Verifica con 'docker ps -a'." >&2
@@ -17,4 +22,4 @@ if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
fi
docker exec -it "$CONTAINER" tmux attach -t "$SESSION" || \
docker exec -it "$CONTAINER" tmux new -s "$SESSION"
docker exec -it "$CONTAINER" tmux new -s "$SESSION" -c "$WORKDIR"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# OBSOLETO como camino normal desde 2026-09-20: la promoción de Staging a
# Producción ahora corre sola dentro de .gitea/workflows/build.yaml (step
# "Promover a Producción (CD)"), en el mismo job que ya despliega a
# Staging -- decisión del usuario de eliminar el gate humano entre los
# dos ambientes. En uso normal este script ya no hace nada (Producción
# va a estar al día apenas termine el pipeline).
#
# Se conserva solo como herramienta de excepción/rollback manual --
# ej. forzar Producción a una versión de Staging vieja sin pasar por un
# build nuevo. Mismo sed/perl que el step automático del pipeline, con
# la diferencia de que este sí pide confirmación interactiva.
#
# Asume que este repo (scripts) y apps-registry están clonados como
# hermanos bajo el mismo directorio del lab (mismo layout que
# lab-cruzcloud/{apps-registry,platform-infra,scripts} en esta NAS).
APPS_REGISTRY_DIR="${APPS_REGISTRY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../apps-registry" && pwd)}"
STAGING_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce-staging/frontend/patches/deployment.yaml"
PROD_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce/frontend/patches/deployment.yaml"
IMAGE_NAME="gitea.cruzcloud.net/devops/ecommerce-frontend"
test -d "${APPS_REGISTRY_DIR}/.git" || {
echo "ERROR: no se encontró un checkout de apps-registry en ${APPS_REGISTRY_DIR}."
echo "Definí APPS_REGISTRY_DIR=/ruta/al/checkout si no está junto a este repo."
exit 1
}
STAGING_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${STAGING_MANIFEST}" | head -1 | cut -d: -f2)"
test -n "${STAGING_VERSION}" || {
echo "ERROR: no se pudo leer la versión actual de Staging desde ${STAGING_MANIFEST}."
exit 1
}
CURRENT_PROD_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${PROD_MANIFEST}" | head -1 | cut -d: -f2)"
echo "Staging: ${STAGING_VERSION}"
echo "Producción: ${CURRENT_PROD_VERSION}"
if [[ "${STAGING_VERSION}" == "${CURRENT_PROD_VERSION}" ]]; then
echo "Producción ya está en ${STAGING_VERSION}. Nada que promover."
exit 0
fi
read -rp "¿Promover ${STAGING_VERSION} a Producción? [s/N] " CONFIRM
[[ "${CONFIRM}" =~ ^[sS]$ ]] || { echo "Cancelado."; exit 1; }
cd "${APPS_REGISTRY_DIR}"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${STAGING_VERSION}|g" "${PROD_MANIFEST}"
# Mismo bump que hace build.yaml en Staging -- el footer lee APP_VERSION
# para mostrar la version real desplegada. awk en vez de perl (usado en
# build.yaml, donde sí está disponible porque corre en un runner
# ubuntu-latest): el host real de ZimaOS donde se corre este script a
# mano no trae perl instalado (confirmado en vivo 2026-09-20), a
# diferencia de sed/awk que sí son parte del sistema base.
awk -v ver="${STAGING_VERSION}" '
/- name: APP_VERSION/ { print; getline; sub(/value:.*/, "value: " ver); print; next }
{ print }
' "${PROD_MANIFEST}" > "${PROD_MANIFEST}.tmp" && mv "${PROD_MANIFEST}.tmp" "${PROD_MANIFEST}"
git add "${PROD_MANIFEST}"
if git diff --cached --quiet; then
echo "No hay cambios en el manifiesto de Producción."
exit 0
fi
git commit -m "chore: promote ${STAGING_VERSION} to production"
git push origin HEAD:main
echo "Promovido. Argo CD sincroniza ecommerce-app en unos segundos."
+23
View File
@@ -0,0 +1,23 @@
# Imagen de tmux-host: sesion tmux persistente para Claude Code sobre el
# lab GitOps de cruzcloud. Reemplaza el `docker run ... alpine sh -c "apk
# add ... && npm install ..."` inline documentado antes en
# docs/arquitectura/sesion-tmux-persistente.md — mismo contenido, pero
# construido una vez como imagen en vez de reinstalar todo en cada arranque
# del contenedor.
FROM alpine:3.24
RUN apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm \
&& npm install -g @anthropic-ai/claude-code \
&& git config --global --add safe.directory '*'
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
# docker exec hereda este WORKDIR como cwd por defecto, y ademas
# lab-session.sh y healthcheck.sh lo pasan explicito via `tmux ... -c`
# para no depender de ese comportamiento implicito.
WORKDIR /DATA/AppData/k3d-lab-v3/lab-cruzcloud
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/healthcheck.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# Proceso principal (PID 1) del contenedor tmux-host: lanza el chequeo de
# salud en background y mantiene el contenedor vivo, igual que el
# `tail -f /dev/null` del `docker run` inline original.
set -eu
/usr/local/bin/healthcheck.sh &
exec tail -f /dev/null
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# Chequeo de salud de tmux-host, corre en background dentro del propio
# contenedor (ver entrypoint.sh). Cada HEALTHCHECK_INTERVAL_SEC (30 min
# por defecto):
#
# a) Conectividad de red real (curl a gitea.cruzcloud.net). Si falla,
# SOLO se registra en el log y se notifica por Telegram (si el canal
# del lab-agent ya esta configurado) -- nunca se intenta "arreglar"
# la red del host desde aca, eso puede ser un problema de
# hardware/BIOS fuera del alcance del contenedor.
# b) Existencia de la sesion tmux "lab". Si no existe (crash, o el
# contenedor se acaba de recrear), SI se recrea automaticamente --
# es solo recuperar el propio entorno de trabajo, no tocar
# infraestructura externa. Al recrearla tambien se lanza `claude`
# adentro (ver launch_claude_in_session) -- una sesion recreada sin
# esto es un shell pelado, no "el entorno de trabajo".
#
# Todo evento (chequeo ok, falla, accion tomada) queda en LOG_FILE, que
# vive bajo /DATA (persistente, sobrevive a recrear el contenedor) y no
# solo en el stdout del contenedor.
#
# set -u sin 'e': un check individual que falle no debe matar el loop
# completo -- el siguiente ciclo tiene que seguir corriendo igual.
set -uo pipefail
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
SESSION="lab"
LOG_DIR="/DATA/AppData/k3d-lab-v3/tmux-host/logs"
LOG_FILE="${LOG_DIR}/health.log"
# Reusa el mismo canal de Telegram que ya configuro scripts/lab-agent/ --
# una sola fuente de verdad para el bot token / chat id, en vez de
# duplicar credenciales entre los dos agentes del lab.
LAB_AGENT_ENV="${WORKDIR}/scripts/lab-agent/lab-agent.env"
NETWORK_CHECK_URL="https://gitea.cruzcloud.net/"
INTERVAL_SEC="${HEALTHCHECK_INTERVAL_SEC:-1800}"
# Cuanto esperar, SOLO al arrancar, a que WORKDIR este disponible antes de
# tocarlo. Tras un `docker restart` normal el host y sus mounts ya estaban
# arriba; tras un reinicio COMPLETO del NAS (corte de luz), Docker puede
# arrancar este contenedor antes de que /media/HDD-Storage termine de
# montarse en el host -- y WORKDIR llega a el via el symlink absoluto
# /DATA/AppData -> /media/HDD-Storage/AppData (ver el gotcha del symlink en
# docs-portal/docs/arquitectura/sesion-tmux-persistente.md). Sin esta
# espera, `mkdir -p "$LOG_DIR"` de mas abajo fallaba en silencio (set -u sin
# -e) y ese arranque quedaba SIN NINGUN rastro en el log -- asi se paso por
# alto originalmente el gotcha de "no lanzo claude" reportado 2026-09-19.
STARTUP_MOUNT_WAIT_MAX_SEC="${STARTUP_MOUNT_WAIT_MAX_SEC:-120}"
STARTUP_MOUNT_WAIT_STEP_SEC=5
wait_for_workdir() {
local waited=0
while [[ ! -d "$WORKDIR" ]] && [[ "$waited" -lt "$STARTUP_MOUNT_WAIT_MAX_SEC" ]]; do
sleep "$STARTUP_MOUNT_WAIT_STEP_SEC"
waited=$((waited + STARTUP_MOUNT_WAIT_STEP_SEC))
done
echo "$waited"
}
STARTUP_WAITED_SEC="$(wait_for_workdir)"
mkdir -p "$LOG_DIR" 2>/dev/null || true
log() {
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE" 2>/dev/null || true
}
if [[ ! -d "$WORKDIR" ]]; then
log "ARRANQUE: ERROR -- ${WORKDIR} sigue sin existir tras esperar ${STARTUP_MOUNT_WAIT_MAX_SEC}s. El mount de /DATA o /media/HDD-Storage del host no esta listo; revisar fuera del contenedor."
elif [[ "$STARTUP_WAITED_SEC" -gt 0 ]]; then
log "ARRANQUE: ${WORKDIR} tardo ${STARTUP_WAITED_SEC}s en estar disponible (probable reinicio completo del host reordenando el montaje de /DATA)."
fi
notify_telegram() {
local title="$1" message="$2"
if [[ ! -f "$LAB_AGENT_ENV" ]]; then
log "NOTIFY omitida: no existe ${LAB_AGENT_ENV}"
return 0
fi
local token_file chat_id token resp
token_file="$(grep -E '^TELEGRAM_BOT_TOKEN_FILE=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
chat_id="$(grep -E '^TELEGRAM_CHAT_ID=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
if [[ -z "$token_file" || ! -r "$token_file" || -z "$chat_id" ]]; then
log "NOTIFY omitida: canal de Telegram del lab-agent no esta configurado todavia"
return 0
fi
token="$(<"$token_file")"
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
--data-urlencode "chat_id=${chat_id}" \
--data-urlencode "text=🤖 tmux-host healthcheck
${title}
──────────────
${message}" 2>&1)"
token=""
[[ "$resp" == *'"ok":true'* ]] || log "WARN: fallo al notificar por Telegram: ${resp}"
}
check_network() {
local code
code="$(curl -sL -o /dev/null -w '%{http_code}' --max-time 10 "$NETWORK_CHECK_URL" 2>/dev/null || echo "000")"
if [[ "$code" == "000" ]]; then
log "RED: sin conectividad real (curl ${NETWORK_CHECK_URL} sin respuesta). No se intenta corregir desde el contenedor."
notify_telegram "Sin conectividad" "curl ${NETWORK_CHECK_URL} no respondio desde tmux-host (posible falla de red del host/NAS, ej. hardware/BIOS). El contenedor NO intenta corregir esto, solo lo registra."
else
log "RED: ok (curl ${NETWORK_CHECK_URL} -> HTTP ${code})"
fi
}
launch_claude_in_session() {
# El pane recien creado tarda un instante en tener su shell listo para
# recibir input -- un send-keys inmediato a veces se pierde. 1s alcanza
# sobrado para un shell interactivo local (no depende de red).
sleep 1
# --remote-control: sin esto la sesion nunca queda visible como
# controlable remotamente en claude.ai (confirmado en vivo 2026-09-20 --
# sin la flag, "remoteEnabled" queda en false para siempre porque nadie
# la pide). El nombre de sesion remota usa el hostname del contenedor
# (ZimaOS) como prefijo por defecto.
# --continue (-c): retoma la conversacion mas reciente en WORKDIR en vez
# de arrancar una sesion en blanco cada vez que se recrea el pane (reinicio
# del NAS, crash del contenedor, etc). Si no hay conversacion previa en el
# directorio, el propio CLI arranca una nueva sin error.
if tmux send-keys -t "$SESSION" "claude --remote-control --continue" Enter 2>>"$LOG_FILE"; then
log "TMUX: 'claude --remote-control --continue' lanzado dentro de la sesion recien creada"
else
log "TMUX: ERROR al lanzar 'claude --remote-control --continue' dentro de la sesion recien creada"
fi
}
check_tmux_session() {
if tmux has-session -t "$SESSION" 2>/dev/null; then
log "TMUX: sesion '${SESSION}' ok"
return
fi
if [[ ! -d "$WORKDIR" ]]; then
# No se crea sobre un WORKDIR ausente (quedaria posicionada en /). Se
# reintenta en el proximo ciclo -- ver STARTUP_MOUNT_WAIT_MAX_SEC arriba
# para el caso especifico de arranque en frio del host.
log "TMUX: sesion '${SESSION}' no existe y ${WORKDIR} tampoco -- no se crea esta vuelta, se reintenta en el proximo ciclo (${INTERVAL_SEC}s)."
return
fi
log "TMUX: sesion '${SESSION}' no existe -- recreando en ${WORKDIR}"
if tmux new-session -d -s "$SESSION" -c "$WORKDIR" 2>>"$LOG_FILE"; then
log "TMUX: sesion '${SESSION}' recreada exitosamente"
# Una sesion recreada sin esto es un shell vacio hasta que alguien
# entre a mano y escriba "claude" -- exactamente el gotcha reportado
# tras el reinicio completo del NAS del 2026-09-18/19: la sesion SI se
# recreaba sola (confirmado en el log de ese arranque), pero Claude
# Code nunca arrancaba adentro.
launch_claude_in_session
else
log "TMUX: ERROR al recrear la sesion '${SESSION}'"
fi
}
log "=== healthcheck inicio (intervalo=${INTERVAL_SEC}s) ==="
while true; do
check_network
check_tmux_session
sleep "$INTERVAL_SEC"
done