From f2098304200e75d90fdd6e10627103be4600ee48 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Tue, 4 Aug 2026 19:57:39 -0500 Subject: [PATCH] =?UTF-8?q?feat(ci):=20deploy=20the=20Core=20from=20GHCR?= =?UTF-8?q?=20so=20the=20Tracker=E2=86=94Core=20seam=20is=20tested=20by=20?= =?UTF-8?q?CI=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ci): deploy the Core from GHCR so the Tracker↔Core seam is tested by CI `core-integration` asserts the SIX interfaces of the pair and had never run in this pipeline: it needs the Core, another product, which nothing here deployed. So the seam was verified by hand or not at all. The first time it ran against a real stack it found three defects invisible to every other suite, two of them product defects — one in each repository. The Core already publishes all three service images to GHCR on every merge to its main, tagged with the commit SHA as well as `latest`. That SHA tag is what makes this safe to wire. PINNED, not `latest`, and the file says why: following the Core's tip would turn this repository's pull requests red for a defect merged minutes earlier by someone who has never seen this pipeline. A pinned SHA moves that failure into a diff with a reviewer. The accepted cost is that `core-image.sha` goes stale on purpose and somebody has to raise it — the same discipline the charts already use for their own tags. The Core is deployed by a MINIMAL manifest of this repository's own, not by vendoring its charts: those live in the other repo, would drift here, and would drag in OPA sidecars, HPAs and IngressRoutes this test neither needs nor can maintain. It is a fixture, and it says so. It pins uid 1001 because pinning 1000 is what made the deployed MCP unable to read its own policy.wasm (evolith_arch32#425). `core-integration` enters the robot list only when the Core is actually present, and when it is absent the run SAYS so instead of quietly shrinking — a suite that silently drops its only cross-product robot reads exactly like one that passed it. The gateway also joins the local install: steps 1-4 of that robot go through it, which is part of why the robot was excluded. Verified on a live kind cluster, not argued: the Core rolls out from the pinned refs, the gateway reaches it by in-cluster DNS, and the full suite runs 11 robots — 242 passed, 0 failed, `core-integration` 13/13. Both branches of the inclusion predicate were exercised. NOT verified here, and stated rather than implied: the `docker pull` itself. The packages are PRIVATE, which the first local attempt proved by answering `unauthorized` — and that failure is what exposed the two things missing from this wiring, now fixed: `packages: read` on the workflow and a GHCR login step before the pull. The local run used the same image refs loaded by hand, so everything downstream of the pull is proven and the pull is exercised the first time this lands. Co-Authored-By: Claude Opus 5 * fix(ci): one local key, not three, for the same deployment CI ran the seam for the first time and failed on exactly one check: `POST /assistant/converse` → 502. The cause is a three-way mismatch that existed before this change and could not be seen until now. The same local deployment carried three different credentials: this script wrote `local-only-not-a-real-key-core` and `local-only-not-a-real-key-runtime`, while the gateway chart defaults to `local-dev-key`. With the Core outside the cluster nobody ever authenticated against it, so nothing disagreed. The moment the Core came in, every step that goes THROUGH the gateway passed — both ends were `local-dev-key` — and the one step that goes straight from tracker-api to the agent-runtime got 401. What made it expensive to read: that 401 reaches the caller as 502 (LV-26), so a credential problem arrives dressed as an availability problem. It cost time on a laptop two days ago and it cost this run. One key everywhere now. It is a local test credential and its name says so; the value does not matter as long as all three agree. Co-Authored-By: Claude Opus 5 * fix(ci): point tracker-api at the Core this pipeline deploys Step 6 of `core-integration` kept returning 502 and the previous commit blamed the wrong thing. Aligning the local keys was correct on its own — three credentials for one deployment is a defect — but it was not this. `values-local.yaml` points the tracker-api at `evolith-runtime-evolith-agent-runtime.evolith-local`: the service names the CORE's Helm chart generates, in the Core's own namespace. This pipeline has neither. It deploys a minimal fixture as `core-agent-runtime` in `evolith-core-local`, so the tracker-api was calling a host that does not resolve — and an unresolvable host surfaces as 502, which reads as "the runtime is down" rather than "the URL points nowhere". I set these by hand on a laptop two days ago and never wired them into `install()`. CI ran the path I had only ever run manually, and that is precisely what it is for. Set unconditionally, including when the Core is absent: it is configuration, not a boot dependency, and a nonexistent host fails exactly as the previous one did. Co-Authored-By: Claude Opus 5 * fix(ci): retire the Core fixture before the production redeploy The robots pass — 242, 0 failed, `core-integration` included — and the step AFTER them broke: the production redeploy timed out with `context deadline exceeded`. Not resource pressure, which is what I assumed first. The log names it: the tracker-api's readiness probe runs a `core-capabilities` health check that calls the Core, and under the PRODUCTION profile `networkPolicy` is enabled, blocking egress to the Core's namespace. The call hangs until it cancels, the pod never reaches Ready, and helm --wait gives up after five minutes. Without a Core the same URL simply does not resolve and fails fast, which is the condition that step was written under and why it has been green on develop for months. So the fixture is retired as soon as the test that needs it is done. It was never part of the Tracker's deployment; it is scaffolding for one step, and leaving it standing changed the conditions of every step after. `if: always()` so a failing robot run still cleans up and the production steps are judged on their own terms. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .github/workflows/deploy-check.yml | 51 ++++++- product/infra/core-image.sha | 34 +++++ product/infra/helm/local-test.sh | 141 +++++++++++++++++++- product/infra/kind/core-stack.local.yaml | 162 +++++++++++++++++++++++ 4 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 product/infra/core-image.sha create mode 100644 product/infra/kind/core-stack.local.yaml diff --git a/.github/workflows/deploy-check.yml b/.github/workflows/deploy-check.yml index 9d5f51d5..9e5193e9 100644 --- a/.github/workflows/deploy-check.yml +++ b/.github/workflows/deploy-check.yml @@ -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 }} @@ -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" @@ -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, diff --git a/product/infra/core-image.sha b/product/infra/core-image.sha new file mode 100644 index 00000000..6adcd5fb --- /dev/null +++ b/product/infra/core-image.sha @@ -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 diff --git a/product/infra/helm/local-test.sh b/product/infra/helm/local-test.sh index ecc13afa..25b0aedf 100755 --- a/product/infra/helm/local-test.sh +++ b/product/infra/helm/local-test.sh @@ -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 @@ -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" @@ -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 - } @@ -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" \ @@ -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 @@ -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 ;; diff --git a/product/infra/kind/core-stack.local.yaml b/product/infra/kind/core-stack.local.yaml new file mode 100644 index 00000000..915de78a --- /dev/null +++ b/product/infra/kind/core-stack.local.yaml @@ -0,0 +1,162 @@ +# The Evolith Core, deployed INTO this repository's kind cluster so the +# Tracker↔Core seam can be exercised by CI. +# +# ## Why a hand-written manifest and not the Core's Helm charts +# +# The charts live in the Core repository and this pipeline has no copy of them. +# Vendoring them would give this repo a second, drifting source of truth for +# somebody else's deployment — and it would bring OPA sidecars, HPAs, PDBs, +# IngressRoutes and NetworkPolicies that this test does not need and cannot +# maintain. What the seam needs is three reachable services. +# +# So this is deliberately minimal and deliberately NOT a deployment reference. +# It is a fixture: the smallest Core that answers the interfaces +# `core-integration` asserts. +# +# ## Images +# +# `IMAGE_TAG` is substituted by `local-test.sh core-up` from +# `product/infra/core-image.sha` — a pinned Core commit, never `latest`. The +# reasoning for pinning is in that file. +# +# ## The API key +# +# One key for all three, matching what the Tracker's own secrets carry. It is a +# local test credential and it says so; the Tracker's `tracker-core-auth` and +# `tracker-runtime-auth` must hold the SAME value or the seam answers 401 — +# which is exactly how this was first found, and the failure surfaced as a 502. +apiVersion: v1 +kind: Namespace +metadata: + name: evolith-core-local +--- +apiVersion: v1 +kind: Secret +metadata: + name: core-auth + namespace: evolith-core-local +type: Opaque +stringData: + EVOLITH_API_KEY: local-dev-key + AGENT_RUNTIME_API_KEY: local-dev-key +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-api + namespace: evolith-core-local + labels: { app: core-api } +spec: + replicas: 1 + selector: { matchLabels: { app: core-api } } + template: + metadata: + labels: { app: core-api } + spec: + # 1001 is the uid the Core's images create and chown their corpus to. + # Pinning 1000 here would reproduce the defect that made the deployed MCP + # unable to read its own policy.wasm and fail-close every tool call + # (evolith_arch32#425). + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + containers: + - name: core-api + image: IMAGE_CORE_API + imagePullPolicy: IfNotPresent + ports: [{ containerPort: 3000, name: http }] + envFrom: [{ secretRef: { name: core-auth } }] + readinessProbe: + httpGet: { path: /health, port: http } + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 24 +--- +apiVersion: v1 +kind: Service +metadata: + name: core-api + namespace: evolith-core-local +spec: + selector: { app: core-api } + ports: [{ port: 80, targetPort: http, name: http }] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-mcp + namespace: evolith-core-local + labels: { app: core-mcp } +spec: + replicas: 1 + selector: { matchLabels: { app: core-mcp } } + template: + metadata: + labels: { app: core-mcp } + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + containers: + - name: core-mcp + image: IMAGE_CORE_MCP + imagePullPolicy: IfNotPresent + ports: [{ containerPort: 3000, name: http }] + envFrom: [{ secretRef: { name: core-auth } }] + readinessProbe: + httpGet: { path: /health, port: http } + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 24 +--- +apiVersion: v1 +kind: Service +metadata: + name: core-mcp + namespace: evolith-core-local +spec: + selector: { app: core-mcp } + ports: [{ port: 80, targetPort: http, name: http }] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-agent-runtime + namespace: evolith-core-local + labels: { app: core-agent-runtime } +spec: + replicas: 1 + selector: { matchLabels: { app: core-agent-runtime } } + template: + metadata: + labels: { app: core-agent-runtime } + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + containers: + - name: core-agent-runtime + image: IMAGE_CORE_AGENT_RUNTIME + imagePullPolicy: IfNotPresent + ports: [{ containerPort: 3000, name: http }] + envFrom: [{ secretRef: { name: core-auth } }] + readinessProbe: + httpGet: { path: /health, port: http } + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 24 +--- +apiVersion: v1 +kind: Service +metadata: + name: core-agent-runtime + namespace: evolith-core-local +spec: + selector: { app: core-agent-runtime } + ports: [{ port: 80, targetPort: http, name: http }]