diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index d89c212..0ac6e1b 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -1,4 +1,4 @@ -name: Build and Push Frontend +name: Build, Push and Validate Frontend on: push: @@ -11,13 +11,12 @@ on: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 25 steps: - name: Checkout del código uses: actions/checkout@v3 with: - # Descarga el historial necesario para sincronizar origin/main. fetch-depth: 0 - name: Definir Versión Semántica @@ -75,9 +74,7 @@ jobs: exit 1 fi - if ! grep -qE \ - "image:[[:space:]]*${IMAGE_REPOSITORY}:" \ - "${MANIFEST}"; then + if ! grep -qE "image:[[:space:]]*${IMAGE_REPOSITORY}:" "${MANIFEST}"; then echo "ERROR: no se encontró la imagen ${IMAGE_REPOSITORY} en ${MANIFEST}." echo "Líneas image encontradas:" grep -nE "^[[:space:]]*image:" "${MANIFEST}" || true @@ -95,24 +92,241 @@ jobs: if git diff --cached --quiet; then echo "El manifiesto ya contiene la versión ${VERSION}." - exit 0 + break fi git commit -m "chore: release ${VERSION} [skip ci]" if git push origin HEAD:main; then echo "Manifiesto actualizado correctamente con ${VERSION}." - exit 0 + break fi echo "origin/main cambió mientras se realizaba el push." - if [ "${intento}" -lt 3 ]; then - espera=$((intento * 3)) - echo "Reintentando en ${espera} segundos..." - sleep "${espera}" + if [ "${intento}" -eq 3 ]; then + echo "ERROR: no fue posible actualizar main después de 3 intentos." + exit 1 fi + + espera=$((intento * 3)) + echo "Reintentando en ${espera} segundos..." + sleep "${espera}" done - echo "ERROR: no fue posible actualizar main después de 3 intentos." - exit 1 \ No newline at end of file + - name: Validar despliegue y tag ejecutado en Kubernetes + shell: bash + env: + VERSION: ${{ steps.vars.outputs.VERSION }} + IMAGE_REPOSITORY: gitea.cruzcloud.net/devops/ecommerce-frontend + NAMESPACE: ecommerce + DEPLOY_TIMEOUT_SECONDS: '420' + KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} + run: | + set -euo pipefail + + EXPECTED_IMAGE="${IMAGE_REPOSITORY}:${VERSION}" + + echo "==================================================" + echo "Validación posterior al despliegue" + echo "Namespace: ${NAMESPACE}" + echo "Imagen esperada: ${EXPECTED_IMAGE}" + echo "Timeout: ${DEPLOY_TIMEOUT_SECONDS} segundos" + echo "==================================================" + + if [ -z "${KUBE_CONFIG_DATA:-}" ]; then + echo "ERROR: no está configurado el secret KUBE_CONFIG_DATA." + exit 1 + fi + + mkdir -p "${HOME}/.kube" + printf '%s' "${KUBE_CONFIG_DATA}" | base64 --decode > "${HOME}/.kube/config" + chmod 600 "${HOME}/.kube/config" + export KUBECONFIG="${HOME}/.kube/config" + + # Instala kubectl únicamente si el runner no lo incluye. + if ! command -v kubectl >/dev/null 2>&1; then + case "$(uname -m)" in + x86_64) KUBECTL_ARCH="amd64" ;; + aarch64|arm64) KUBECTL_ARCH="arm64" ;; + *) + echo "ERROR: arquitectura no soportada para kubectl: $(uname -m)" + exit 1 + ;; + esac + + KUBECTL_VERSION="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + echo "Instalando kubectl ${KUBECTL_VERSION} (${KUBECTL_ARCH})..." + + curl -fsSLo /tmp/kubectl \ + "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${KUBECTL_ARCH}/kubectl" + curl -fsSLo /tmp/kubectl.sha256 \ + "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${KUBECTL_ARCH}/kubectl.sha256" + + echo "$(cat /tmp/kubectl.sha256) /tmp/kubectl" | sha256sum --check + chmod +x /tmp/kubectl + export PATH="/tmp:${PATH}" + fi + + kubectl version --client + + # También valida conectividad y permisos mínimos sobre el namespace. + kubectl get deployments --namespace "${NAMESPACE}" >/dev/null + + # Argo CD sincroniza de forma asíncrona. Se espera hasta que algún + # Deployment del namespace tenga exactamente la imagen publicada. + DEADLINE=$((SECONDS + DEPLOY_TIMEOUT_SECONDS)) + DEPLOYMENT="" + + while [ "${SECONDS}" -lt "${DEADLINE}" ]; do + MATCHES=() + + while IFS= read -r candidate; do + [ -n "${candidate}" ] || continue + + candidate_images="$( + kubectl get deployment "${candidate}" \ + --namespace "${NAMESPACE}" \ + --output jsonpath='{.spec.template.spec.containers[*].image}' + )" + + for image in ${candidate_images}; do + if [ "${image}" = "${EXPECTED_IMAGE}" ]; then + MATCHES+=("${candidate}") + break + fi + done + done < <( + kubectl get deployments \ + --namespace "${NAMESPACE}" \ + --output jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' + ) + + if [ "${#MATCHES[@]}" -eq 1 ]; then + DEPLOYMENT="${MATCHES[0]}" + echo "Deployment actualizado por Argo CD: ${DEPLOYMENT}" + break + fi + + if [ "${#MATCHES[@]}" -gt 1 ]; then + echo "ERROR: más de un Deployment usa ${EXPECTED_IMAGE}: ${MATCHES[*]}" + exit 1 + fi + + echo "Argo CD aún no ha aplicado ${EXPECTED_IMAGE}. Reintentando en 10 segundos..." + sleep 10 + done + + if [ -z "${DEPLOYMENT}" ]; then + echo "ERROR: Argo CD no aplicó ${EXPECTED_IMAGE} dentro del tiempo límite." + echo "Imágenes actuales en los Deployments de ${NAMESPACE}:" + kubectl get deployments \ + --namespace "${NAMESPACE}" \ + --output custom-columns='DEPLOYMENT:.metadata.name,IMAGES:.spec.template.spec.containers[*].image' + exit 1 + fi + + echo "Esperando que finalice el rollout de deployment/${DEPLOYMENT}..." + kubectl rollout status "deployment/${DEPLOYMENT}" \ + --namespace "${NAMESPACE}" \ + --timeout="${DEPLOY_TIMEOUT_SECONDS}s" + + CONTAINER_NAME="$( + kubectl get deployment "${DEPLOYMENT}" \ + --namespace "${NAMESPACE}" \ + --output go-template="{{range .spec.template.spec.containers}}{{if eq .image \"${EXPECTED_IMAGE}\"}}{{.name}}{{end}}{{end}}" + )" + + if [ -z "${CONTAINER_NAME}" ]; then + echo "ERROR: no se pudo determinar el contenedor que usa ${EXPECTED_IMAGE}." + exit 1 + fi + + SELECTOR="$( + kubectl get deployment "${DEPLOYMENT}" \ + --namespace "${NAMESPACE}" \ + --output go-template='{{range $key, $value := .spec.selector.matchLabels}}{{printf "%s=%s," $key $value}}{{end}}' \ + | sed 's/,$//' + )" + + if [ -z "${SELECTOR}" ]; then + echo "ERROR: no se pudo obtener el selector de deployment/${DEPLOYMENT}." + exit 1 + fi + + echo "Contenedor objetivo: ${CONTAINER_NAME}" + echo "Selector de Pods: ${SELECTOR}" + + EXPECTED_REPLICAS="$( + kubectl get deployment "${DEPLOYMENT}" \ + --namespace "${NAMESPACE}" \ + --output jsonpath='{.spec.replicas}' + )" + + # Solo incluye Pods activos: Running y sin deletionTimestamp. + # Así no se toma como error un Pod antiguo que todavía esté terminando. + mapfile -t PODS < <( + kubectl get pods \ + --namespace "${NAMESPACE}" \ + --selector "${SELECTOR}" \ + --output go-template='{{range .items}}{{if not .metadata.deletionTimestamp}}{{if eq .status.phase "Running"}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}{{end}}' + ) + + if [ "${#PODS[@]}" -eq 0 ]; then + echo "ERROR: no se encontraron Pods Running para deployment/${DEPLOYMENT}." + exit 1 + fi + + VALIDATED_PODS=0 + + for pod in "${PODS[@]}"; do + [ -n "${pod}" ] || continue + + POD_IMAGE="$( + kubectl get pod "${pod}" \ + --namespace "${NAMESPACE}" \ + --output go-template="{{range .spec.containers}}{{if eq .name \"${CONTAINER_NAME}\"}}{{.image}}{{end}}{{end}}" + )" + + POD_READY="$( + kubectl get pod "${pod}" \ + --namespace "${NAMESPACE}" \ + --output go-template="{{range .status.containerStatuses}}{{if eq .name \"${CONTAINER_NAME}\"}}{{.ready}}{{end}}{{end}}" + )" + + IMAGE_ID="$( + kubectl get pod "${pod}" \ + --namespace "${NAMESPACE}" \ + --output go-template="{{range .status.containerStatuses}}{{if eq .name \"${CONTAINER_NAME}\"}}{{.imageID}}{{end}}{{end}}" + )" + + echo "Pod: ${pod}" + echo " Ready: ${POD_READY}" + echo " Imagen declarada: ${POD_IMAGE}" + echo " Image ID: ${IMAGE_ID}" + + if [ "${POD_IMAGE}" != "${EXPECTED_IMAGE}" ]; then + echo "ERROR: ${pod} ejecuta ${POD_IMAGE}; se esperaba ${EXPECTED_IMAGE}." + exit 1 + fi + + if [ "${POD_READY}" != "true" ]; then + echo "ERROR: el contenedor ${CONTAINER_NAME} del Pod ${pod} no está Ready." + exit 1 + fi + + VALIDATED_PODS=$((VALIDATED_PODS + 1)) + done + + if [ "${VALIDATED_PODS}" -ne "${EXPECTED_REPLICAS}" ]; then + echo "ERROR: se esperaban ${EXPECTED_REPLICAS} Pods activos, pero se validaron ${VALIDATED_PODS}." + kubectl get pods --namespace "${NAMESPACE}" --selector "${SELECTOR}" --output wide + exit 1 + fi + + echo "==================================================" + echo "VALIDACIÓN EXITOSA" + echo "Deployment: ${DEPLOYMENT}" + echo "Pods validados: ${VALIDATED_PODS}/${EXPECTED_REPLICAS}" + echo "Tag ejecutado: ${EXPECTED_IMAGE}" + echo "==================================================" \ No newline at end of file