Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions .github/workflows/deploy-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ on:

permissions:
contents: read
# Las imagenes del Core son PRIVADAS en GHCR. Sin esto el `docker pull` del
# paso `core-up` responde `unauthorized`, que es exactamente como fallo la
# primera prueba local de este cableado.
packages: read

concurrency:
group: deploy-${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -81,6 +85,32 @@ jobs:
- name: Levantar el stack (kind + build + load + Helm + migraciones)
run: bash product/infra/helm/local-test.sh up

# El Core, desde GHCR, en el SHA fijado en product/infra/core-image.sha.
#
# Hasta ahora `core-integration` — el unico robot que asevera las SEIS
# interfaces del par Tracker↔Core — estaba excluido de este pipeline porque
# necesitaba el Core, que no se desplegaba. Se probaba a mano o no se
# probaba; la primera vez que corrio contra un stack real encontro tres
# defectos invisibles para toda otra suite, dos de ellos de producto.
#
# El SHA es fijo y NO `latest`: seguir la punta del Core haria que un
# defecto ajeno, mergeado minutos antes, pusiera rojos los PR de este
# repositorio. El coste aceptado es que alguien tiene que subirlo, en un
# diff y con revisor. Ver product/infra/core-image.sha.
# Login ANTES del pull: los paquetes del Core son privados y el
# `GITHUB_TOKEN` de este repositorio los alcanza porque comparten
# organizacion. Si un dia dejaran de compartirla, esto falla aqui, con el
# nombre del registro, y no como un pod que no arranca.
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Desplegar el Core (imagenes GHCR, SHA fijado)
run: bash product/infra/helm/local-test.sh core-up

- name: Smoke contra el despliegue vivo
env:
SMOKE_OBSERVE_SECONDS: "25"
Expand All @@ -90,11 +120,28 @@ jobs:
# (dev-bypass). Corre aqui, mientras el perfil local esta desplegado y antes del
# redespliegue de produccion, porque los robots actuan por `x-tenant-id` (dev-bypass).
# Ejercitan el funnel SDLC completo, el ciclo ProviderConnection y el aislamiento de
# tenant contra el tracker-api + Postgres reales. `core-integration` se excluye: necesita
# el Core (otro producto), que no se despliega en este pipeline. Este paso MUERDE.
# tenant contra el tracker-api + Postgres reales. Desde 2026-08-04 incluye ademas
# `core-integration`, porque el paso anterior despliega el Core: el robot detecta su
# presencia y entra en la lista solo entonces. Este paso MUERDE.
- name: RoboSoft E2E (agentes deterministicos contra el despliegue local)
run: bash product/infra/helm/local-test.sh robosoft

# El Core se retira EN CUANTO deja de hacer falta.
#
# Es un fixture del paso anterior, no parte del despliegue del Tracker, y
# dejarlo puesto rompe el redespliegue de produccion: ese perfil activa
# `networkPolicy`, que bloquea la salida hacia el namespace del Core, asi
# que el health check `core-capabilities` de la sonda de readiness se queda
# colgado hasta expirar — el pod nunca llega a Ready y helm --wait se
# rinde a los 5 minutos. Sin Core, esa URL simplemente no resuelve y falla
# rapido, que es la condicion bajo la que ese paso se escribio.
#
# Visto de verdad: con el Core puesto, `Error: context deadline exceeded`;
# el mismo paso lleva meses en verde en develop, donde no hay Core.
- name: Retirar el Core (era el fixture del paso anterior)
if: always()
run: bash product/infra/helm/local-test.sh core-down

# GT-464 — hasta aqui el despliegue se probaba SOLO con el perimetro abierto:
# `values-local.yaml` corre en Development con `dev-bypass`, que autentica cualquier
# peticion como administrador con todos los permisos. Lo de abajo lo prueba cerrado,
Expand Down
34 changes: 34 additions & 0 deletions product/infra/core-image.sha
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# The Evolith Core commit this repository's CI deploys and tests against.
#
# ## Why a pinned SHA and not `latest`
#
# The Core publishes all three service images to GHCR on every merge to its
# `main`, tagged with BOTH the commit SHA and `latest`. Following `latest` would
# make this repository's pull requests turn red for a defect in another product,
# merged minutes earlier, by someone who has never seen this pipeline. A pinned
# SHA moves that failure to where it can be read: a deliberate bump, in a diff,
# with a reviewer.
#
# The cost is honest and accepted: this file goes stale on purpose, and somebody
# has to raise it. That is the same discipline the Helm charts already use for
# their own image tags.
#
# ## How to bump it
#
# 1. Pick a Core commit whose images exist:
# gh api orgs/beyondnetcode/packages/container/evolith-core-api/versions \
# --jq '.[0].metadata.container.tags'
# 2. Check the other two carry the SAME tag — they are published together and
# a mismatch means one build failed:
# for p in evolith-core-api evolith-mcp evolith-agent-runtime; do …
# 3. Replace the line below, open a pull request, and let `Deploy check` prove
# the Tracker still talks to that Core before it merges.
#
# ## What breaks if it is wrong
#
# `deploy-check.yml` pulls these three by digest-less tag. A SHA with no
# published image fails the pull loudly, in the pipeline, naming the tag — which
# is the failure mode to want. A SHA that exists but predates a contract change
# fails later, in `core-integration`, naming the interface that moved.

ce85b14d07e3b0684025718039c77840e3e1cfa1
141 changes: 136 additions & 5 deletions product/infra/helm/local-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ CLUSTER="${KIND_CLUSTER:-evolith-tracker}"

API_IMAGE="evolith-tracker-api:$IMAGE_TAG"
WEB_IMAGE="evolith-tracker-web:$IMAGE_TAG"
# El gateway es la puerta que usa `core-integration` para llegar al Core.
GW_IMAGE="evolith-tracker-gateway:$IMAGE_TAG"

kind_create() {
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
Expand All @@ -46,11 +48,67 @@ build() {
docker build -f "$SRC/apps/tracker-api/Dockerfile" -t "$API_IMAGE" "$SRC"
echo "==> Building tracker-web image ($WEB_IMAGE)"
docker build -f "$SRC/apps/tracker-web/Dockerfile" -t "$WEB_IMAGE" "$SRC"
echo "==> Building tracker-gateway image ($GW_IMAGE)"
docker build -f "$SRC/apps/tracker-gateway/Dockerfile" -t "$GW_IMAGE" "$SRC"
}

# ── El Core, dentro de ESTE clúster ──────────────────────────────────────────
#
# Trae las tres imágenes del Core desde GHCR en el SHA fijado, las carga en kind
# y las despliega con un manifiesto mínimo propio. Existe para que
# `core-integration` — el único robot que asevera las seis interfaces del par
# Tracker↔Core — pueda correr en CI en vez de a mano en un portátil.
#
# El SHA es FIJO a propósito: seguir `latest` haría que un defecto del Core,
# mergeado minutos antes por alguien que no ha visto este pipeline, pusiera rojos
# los PR de este repositorio. El razonamiento completo está en
# `product/infra/core-image.sha`.
CORE_NS=evolith-core-local
CORE_SHA_FILE="$ROOT/product/infra/core-image.sha"
CORE_MANIFEST="$ROOT/product/infra/kind/core-stack.local.yaml"

core_sha() {
# Primera línea no vacía que no sea comentario. Un fichero de pin que se lee
# con `cat` acaba arrastrando su propia documentación al tag.
grep -vE '^\s*(#|$)' "$CORE_SHA_FILE" | head -1 | tr -d '[:space:]'
}

core_up() {
local sha; sha="$(core_sha)"
if [ -z "$sha" ]; then
echo "==> ERROR: no hay SHA legible en $CORE_SHA_FILE"; return 1
fi
echo "==> Core fijado en $sha"

local reg="ghcr.io/beyondnetcode"
for img in evolith-core-api evolith-mcp evolith-agent-runtime; do
echo "==> docker pull $reg/$img:$sha"
# Sin `|| true`: un tag que no existe debe MORDER aquí, nombrando el tag,
# y no diez minutos después como un pod que no arranca.
docker pull "$reg/$img:$sha"
kind load docker-image "$reg/$img:$sha" --name "$CLUSTER"
done

echo "==> Desplegando el Core en $CORE_NS"
sed -e "s|IMAGE_CORE_API|$reg/evolith-core-api:$sha|" \
-e "s|IMAGE_CORE_MCP|$reg/evolith-mcp:$sha|" \
-e "s|IMAGE_CORE_AGENT_RUNTIME|$reg/evolith-agent-runtime:$sha|" \
"$CORE_MANIFEST" | kubectl apply -f -

for d in core-api core-mcp core-agent-runtime; do
kubectl -n "$CORE_NS" rollout status "deploy/$d" --timeout=300s
done
kubectl -n "$CORE_NS" get pods
}

core_down() {
kubectl delete namespace "$CORE_NS" --ignore-not-found
}

kind_load() {
echo "==> Loading images into kind cluster '$CLUSTER'"
kind load docker-image "$API_IMAGE" --name "$CLUSTER"
kind load docker-image "$GW_IMAGE" --name "$CLUSTER"
kind load docker-image "$WEB_IMAGE" --name "$CLUSTER"
# Pre-pull the Postgres image into the cluster so it installs even offline.
echo "==> Ensuring postgres:16-alpine is available in the cluster"
Expand All @@ -68,11 +126,24 @@ kind_load() {
# entorno local desechable, y un valor que parezca real invita a copiarlo a uno que no lo es.
ensure_secrets() {
echo "==> Asegurando los secretos locales (CD-15)"
# UNA sola clave local para todo el entorno.
#
# Habia TRES para el mismo despliegue: estos dos secretos con un valor cada
# uno, y `coreApiKey: local-dev-key` por defecto en el chart del gateway. Con
# el Core fuera del clusterr nunca se notaba —nadie llegaba a autenticar— y en
# cuanto el Core entro, los pasos que van POR el gateway pasaron y el que va
# directo al agent-runtime dio 401. Que costo verlo: ese 401 sale al llamante
# como 502 (LV-26), o sea, un problema de credencial disfrazado de problema de
# disponibilidad.
#
# Es una credencial de prueba local y lo dice su nombre; el valor concreto da
# igual mientras sea EL MISMO en los tres sitios.
local key="local-dev-key"
kubectl -n "$NS" create secret generic tracker-core-auth \
--from-literal=EVOLITH_API_KEY=local-only-not-a-real-key-core \
--from-literal=EVOLITH_API_KEY="$key" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$NS" create secret generic tracker-runtime-auth \
--from-literal=AGENT_RUNTIME_API_KEY=local-only-not-a-real-key-runtime \
--from-literal=AGENT_RUNTIME_API_KEY="$key" \
--dry-run=client -o yaml | kubectl apply -f -
}

Expand Down Expand Up @@ -102,9 +173,35 @@ install() {
kubectl -n "$NS" rollout status statefulset/evolith-tracker-postgres --timeout=180s

echo "==> Installing evolith-tracker-api (perfil: $PROFILE; migraciones como Job pre-install)"
# Apuntado al Core que despliega `core-up`, no al del chart del Core.
#
# `values-local.yaml` trae `evolith-runtime-evolith-agent-runtime.evolith-local`
# — los nombres que genera el chart del OTRO repositorio, que aqui no existe.
# Sin esto el tracker-api llama a un host que no resuelve y devuelve 502: el
# mismo 502 que el paso 6 de `core-integration` reportaba, y que parece un
# problema del runtime cuando en realidad es una URL a ninguna parte.
#
# Se fija SIEMPRE, tambien cuando el Core no esta: es configuracion, no una
# dependencia de arranque, y un host inexistente falla igual que el de antes.
helm upgrade --install tracker-api "$HELM/evolith-tracker-api" \
-n "$NS" -f "$(api_values)" \
--set image.tag="$IMAGE_TAG" --wait --timeout 5m
--set image.tag="$IMAGE_TAG" \
--set coreApi.baseUrl=http://core-api.$CORE_NS.svc.cluster.local/api/v1 \
--set agentRuntime.baseUrl=http://core-agent-runtime.$CORE_NS.svc.cluster.local/v1 \
--wait --timeout 5m

# El gateway NO estaba en el install local: `core-integration` lo necesita
# (sus pasos 1-4 van gateway → Core) y por eso el robot quedaba excluido. Aquí
# el Core vive en el MISMO clúster, así que la URL es DNS interno — no hace
# falta el rodeo cross-cluster que sí necesita un portátil con dos clústeres.
echo "==> Installing evolith-tracker-gateway"
helm upgrade --install tracker-gateway "$HELM/evolith-tracker-gateway" \
-n "$NS" -f "$HELM/evolith-tracker-gateway/values-local.yaml" \
--set image.repository=evolith-tracker-gateway \
--set image.pullPolicy=IfNotPresent \
--set image.tag="$IMAGE_TAG" \
--set env.CORE_API_BASE_URL=http://core-api.evolith-core-local.svc.cluster.local/api/v1 \
--set env.CORE_MCP_URL=http://core-mcp.evolith-core-local.svc.cluster.local/

echo "==> Installing evolith-tracker-web"
helm upgrade --install tracker-web "$HELM/evolith-tracker-web" \
Expand Down Expand Up @@ -447,12 +544,44 @@ robosoft() {
return 0
fi
echo "==> RoboSoft E2E (agentes deterministicos) contra el despliegue local (dev-bypass)"

# `core-integration` entra en la lista SOLO si el Core esta desplegado.
#
# Estuvo excluido desde que existe porque necesitaba el Core, otro producto,
# que este pipeline no levantaba. Ahora `core-up` lo trae desde GHCR en un SHA
# fijado, asi que el robot que asevera las SEIS interfaces del par puede correr
# aqui en vez de a mano. La primera vez que se ejecuto contra un stack real
# encontro tres defectos que ninguna otra suite podia ver, dos de ellos de
# producto — uno en cada repositorio.
#
# La condicion se mira, no se asume: si el Core no esta, la lista queda como
# estaba y se DICE, en vez de correr un robot que fallaria por ausencia y
# pareceria un defecto del seam.
local lista="governance-journey,provider-connections,tenant-isolation,gate-enforcement,exception-governance,audit-trail,intake,qa-quality-gate,scorecard,phase-artifact-catalog"
local gw_pf=""
if kubectl -n "$CORE_NS" get deploy core-api >/dev/null 2>&1; then
echo "==> Core presente en $CORE_NS: se incluye core-integration"
lista="$lista,core-integration"
# El robot llega al Core POR el gateway (pasos 1-4), asi que necesita su
# propio port-forward ademas del de tracker-api.
kubectl -n "$NS" port-forward svc/tracker-gateway-evolith-tracker-gateway 3000:80 >/dev/null 2>&1 &
gw_pf=$!
sleep 4
else
echo "==> Core AUSENTE en $CORE_NS: core-integration queda fuera de esta corrida."
echo " Esto NO es una corrida completa del seam; es la lista de siempre."
fi

# Hold a port-forward to the tracker-api for the whole run, then run the robots.
# `_pf` returns the node exit code (0 pass / 1 fail), so this MORDS in CI.
local rc=0
_pf tracker-api-evolith-tracker-api 5100 \
env ROBOSOFT_API_BASE=http://localhost:5100 \
ROBOSOFT_ONLY=governance-journey,provider-connections,tenant-isolation,gate-enforcement,exception-governance,audit-trail,intake,qa-quality-gate,scorecard,phase-artifact-catalog \
node "$ROOT/robosoft/run.mjs"
ROBOSOFT_GATEWAY_BASE=http://localhost:3000 \
ROBOSOFT_ONLY="$lista" \
node "$ROOT/robosoft/run.mjs" || rc=$?
[ -n "$gw_pf" ] && kill "$gw_pf" >/dev/null 2>&1
return $rc
}

# GT-604 — the evidence-deposit loop, end to end: a REAL `evolith evaluate` run
Expand Down Expand Up @@ -495,6 +624,8 @@ case "${1:-up}" in
install) install ;;
smoke) smoke ;;
smoke-prod) smoke_prod ;;
core-up) core_up ;;
core-down) core_down ;;
robosoft) robosoft ;;
robosoft-ingest) robosoft_ingest ;;
verify-failclosed) verify_failclosed ;;
Expand Down
Loading
Loading