sync: workflow CI self-contained + imagen Harbor t-<short> (ADR-061)
Some checks failed
__APP_NAME__ — build & deploy / Calidad + build + push (push) Failing after 2m44s
__APP_NAME__ — build & deploy / Helm upgrade --install (push) Has been skipped

Workflow <app>-build.yaml self-contained — sin uses: cross-repo al repo
privado coralware/IDP. requirements-dev.txt inline. image.repository apunta
al proyecto Harbor dedicado del tenant t-<tenant-id-short> (ADR-019 D-03).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Coralware Template Sync
2026-05-21 22:08:14 -05:00
parent 5d51971270
commit 1c6b8ae182
5 changed files with 128 additions and 37 deletions

View File

@@ -1,5 +1,12 @@
name: __APP_NAME__ — build & deploy name: __APP_NAME__ — build & deploy
# Workflow self-contained del golden path (ADR-061): calidad + build + push +
# helm-deploy inline, SIN `uses:` cross-repo al repo privado `coralware/IDP`.
# El repo del tenant es un repo cliente — su CI no comparte el ciclo de release
# del IDP ni el GitOps de Fleet; se construye leyendo solo su propio repo.
# Corre sobre los runners `[self-hosted, buildkit]` de ci-system (executor=host,
# buildkit remoto — sin Docker daemon local; ADR-043).
on: on:
push: push:
branches: [main] branches: [main]
@@ -7,25 +14,83 @@ on:
- 'code/**' - 'code/**'
- 'helm/**' - 'helm/**'
- '.gitea/workflows/__APP_NAME__-build.yaml' - '.gitea/workflows/__APP_NAME__-build.yaml'
workflow_dispatch: {}
jobs: jobs:
# Tests + build + push a Harbor — patrón ADR-038 reusable workflow. # ── Calidad + build + push de imagen a Harbor ────────────────────────────
build: build:
uses: coralware/IDP/.gitea/workflows/_python-microservice.yaml@main name: Calidad + build + push
with: runs-on: [self-hosted, buildkit]
service_name: __APP_NAME__ outputs:
service_dir: code image_tag: ${{ steps.meta.outputs.tag }}
uses_metrics: false defaults:
coverage_threshold: 80 # menor que el IDP interno (90); el dev sube cuando madure run:
trivy_exit_code: 0 # informacional en MVP; subir a 1 antes de producción working-directory: code
enforce_rollback: false # rollout lo gestiona el job helm-deploy abajo steps:
- name: Checkout
uses: actions/checkout@v4
# Deploy al cluster del tenant — Helm upgrade --install con hook Alembic. - name: Crear venv e instalar dependencias
# El runner corre executor=host (ci-system: container.enabled=false, ADR-043): # --system-site-packages reutiliza ruff/black/bandit/pytest ya
# los steps se ejecutan directo sobre el pod runner, sin Docker daemon. Por eso # preinstalados en la imagen del runner; el venv añade las deps de la app.
# NO se usa `container:` — kubectl ya viene en la imagen del runner; helm se run: |
# instala en runtime (la NetworkPolicy de los runners permite egress 443). python3 -m venv --system-site-packages /tmp/venv-__APP_NAME__
/tmp/venv-__APP_NAME__/bin/pip install --no-cache-dir -r requirements-dev.txt
- name: Lint (ruff)
run: ruff check app
- name: Formato (black --check)
run: black --check app
- name: Seguridad (bandit)
run: bandit -c pyproject.toml -r app --severity-level medium
- name: Tests con cobertura
# Los tests corren sobre SQLite efímero (ver code/tests/conftest.py) —
# el runner no tiene Docker daemon. El deploy real usa MariaDB.
run: |
/tmp/venv-__APP_NAME__/bin/python -m pytest tests/ -v \
--cov=app --cov-report=term-missing --cov-fail-under=80
- name: Metadata de imagen
id: meta
run: echo "tag=${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
- name: Build y push a Harbor
# Proyecto Harbor del tenant: `t-<tenant-id-short>` (ADR-019 D-03).
# Credenciales: robot scoped push/pull SOLO a ese proyecto, sembrado
# por tenant-manager en el repo del tenant (paso 3 del reconcile).
env:
REGISTRY: registry.coralsafety.com
ROBOT_USER: ${{ secrets.HARBOR_ROBOT_USER }}
ROBOT_TOKEN: ${{ secrets.HARBOR_ROBOT_TOKEN }}
IMAGE: t-__TENANT_ID_SHORT__/__APP_NAME__
TAG: ${{ steps.meta.outputs.tag }}
run: |
if [ -z "$ROBOT_USER" ] || [ -z "$ROBOT_TOKEN" ]; then
echo "ERROR: secrets HARBOR_ROBOT_USER/HARBOR_ROBOT_TOKEN no configurados."
echo "tenant-manager debe sembrarlos al crear el repo (paso 3 del reconcile)."
exit 1
fi
printf '%s' "$ROBOT_TOKEN" | docker login "$REGISTRY" -u "$ROBOT_USER" --password-stdin
# Builder buildx remoto — apunta al buildkit daemon de ci-system
# (el runner no tiene Docker daemon local).
if ! docker buildx inspect tenant-builder >/dev/null 2>&1; then
docker buildx create --name tenant-builder --driver remote --use \
"${BUILDKIT_HOST:-tcp://buildkitd.ci-system.svc.cluster.local:2375}"
else
docker buildx use tenant-builder
fi
docker buildx build --platform linux/amd64 \
-t "${REGISTRY}/${IMAGE}:${TAG}" \
--push .
# ── Deploy al cluster del tenant — Helm upgrade --install con hook Alembic ─
# El runner corre executor=host: los steps se ejecutan directo sobre el pod
# runner, sin `container:`. helm se instala en runtime.
helm-deploy: helm-deploy:
name: Helm upgrade --install
needs: build needs: build
runs-on: [self-hosted, buildkit] runs-on: [self-hosted, buildkit]
steps: steps:
@@ -84,10 +149,8 @@ jobs:
- name: Smoke check post-deploy - name: Smoke check post-deploy
# FQDN con tenant-id CON guiones — coincide con el A record que crea # FQDN con tenant-id CON guiones — coincide con el A record que crea
# cloudflare-provisioner (Gap 5) y con el host del Ingress NGINX # cloudflare-provisioner y con el host del Ingress NGINX
# (helm/APP_NAME/values.yaml). Antes usaba TENANT_ID_NODASHES, lo # (helm/APP_NAME/values.yaml).
# que no resolvía al A record canónico — corregido APERTURA COVE
# Sesión 3 (TENANT-INGRESS-CONTROLLER-MISMATCH).
env: env:
INGRESS_HOST: __APP_NAME__-${{ vars.TENANT_ID }}.apps.coralware.cloud INGRESS_HOST: __APP_NAME__-${{ vars.TENANT_ID }}.apps.coralware.cloud
run: | run: |

View File

@@ -56,7 +56,9 @@ def create_app() -> FastAPI:
app = FastAPI( app = FastAPI(
title="__APP_NAME__", title="__APP_NAME__",
version="0.1.0", version="0.1.0",
dependencies=[Depends(_require_auth_dep)] if settings.OPENAPI_REQUIRE_AUTH else [], dependencies=(
[Depends(_require_auth_dep)] if settings.OPENAPI_REQUIRE_AUTH else []
),
) )
@app.middleware("http") @app.middleware("http")

View File

@@ -1,6 +1,16 @@
# Dependencias de desarrollo y CI del golden path web-backend/python.
# Self-contained (ADR-061): el repo del tenant no tiene acceso al monorepo IDP,
# así que las versiones de tooling se inlinean aquí en vez de referenciar
# code/requirements-dev-base.txt. Mantener alineadas a ese archivo (ADR-038).
-r requirements.txt -r requirements.txt
-r ../../code/requirements-dev-base.txt
# Tooling de calidad — versiones canónicas ADR-038
ruff==0.9.10
black==25.1.0
bandit==1.8.3
pytest==8.3.5
pytest-cov==6.0.0
# Específicas del shape — los tests corren sobre SQLite efímero (sin Docker).
httpx==0.27.2 httpx==0.27.2
pytest-asyncio==0.24.0 pytest-asyncio==0.24.0
testcontainers[mariadb]==4.8.2

View File

@@ -1,32 +1,47 @@
""" """
Fixtures para los tests del shape. Fixtures para los tests del shape web-backend/python.
Levanta una MariaDB efímera con testcontainers para que los tests corran Los tests corren contra una BD SQLite efímera en archivo temporal — el runner
contra una BD real (no mocks). El reusable workflow ADR-038 garantiza que CI (executor=host, ADR-043) no tiene Docker daemon, por lo que no se usa
Docker está disponible en el runner. testcontainers (ADR-061). El deploy real usa MariaDB (claim-mariadb.yaml + hook
Alembic); los tests del scaffold solo ejercen /health, /demo y /metrics, sin
tocar Alembic ni modelos de dominio. Al añadir modelos reales, agrega tests de
integración contra tu propia infra.
""" """
import os import os
import tempfile
from collections.abc import Iterator from collections.abc import Iterator
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from testcontainers.mariadb import MariaDbContainer
# La configuración debe existir ANTES de importar app.settings: Settings() exige
# DATABASE_URL al cargar el módulo. Se usa un archivo SQLite temporal (no
# :memory:) para evitar problemas de hilos con el TestClient.
_DB_FD, _DB_PATH = tempfile.mkstemp(suffix=".sqlite", prefix="webapp-test-")
os.close(_DB_FD)
_SQLITE_URL = f"sqlite:///{_DB_PATH}"
os.environ.setdefault("DATABASE_URL", _SQLITE_URL)
os.environ.setdefault("MIGRATION_DATABASE_URL", _SQLITE_URL)
# El frontend estático /demo se sirve desde el árbol del repo durante los tests.
_DEMO_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "app", "static", "demo"))
os.environ.setdefault("STATIC_DEMO_PATH", _DEMO_DIR)
@pytest.fixture(scope="session") @pytest.fixture(scope="session", autouse=True)
def mariadb_url() -> Iterator[str]: def _cleanup_sqlite() -> Iterator[None]:
"""MariaDB efímera por sesión de tests; URL exportada como DATABASE_URL.""" """Borra el archivo SQLite temporal al terminar la sesión de tests."""
with MariaDbContainer("mariadb:11") as mdb: yield
url = mdb.get_connection_url().replace("mysql://", "mysql+pymysql://", 1) try:
os.environ["DATABASE_URL"] = url os.unlink(_DB_PATH)
os.environ["MIGRATION_DATABASE_URL"] = url except OSError:
yield url pass
@pytest.fixture @pytest.fixture
def client(mariadb_url: str) -> Iterator[TestClient]: def client() -> Iterator[TestClient]:
"""TestClient con la app cargada contra la BD efímera.""" """TestClient con la app cargada contra la BD SQLite efímera."""
from app.main import create_app from app.main import create_app
app = create_app() app = create_app()

View File

@@ -4,7 +4,8 @@
replicaCount: 1 replicaCount: 1
image: image:
repository: registry.coralsafety.com/__TENANT_ID_NODASHES__/__APP_NAME__ # Proyecto Harbor dedicado del tenant: `t-<tenant-id-short>` (ADR-019 D-03).
repository: registry.coralsafety.com/t-__TENANT_ID_SHORT__/__APP_NAME__
tag: latest tag: latest
pullPolicy: Always pullPolicy: Always
pullSecret: harbor-tenant-pull pullSecret: harbor-tenant-pull