feat(ecommerce): release Ari Shopping Frontend v2
Build and Push Frontend / build (push) Successful in 1m57s

This commit is contained in:
2026-07-18 16:34:01 -05:00
parent 9f25b17468
commit 54d2f208ae
25 changed files with 376 additions and 566 deletions
+25 -456
View File
@@ -1,33 +1,29 @@
name: Build, Push and Validate Frontend
name: Build and Push Frontend
on:
push:
branches:
- main
paths:
- 'workloads/ecommerce/index.html'
- 'workloads/ecommerce/Dockerfile'
- 'workloads/ecommerce/**'
- '.gitea/workflows/build.yaml'
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20 # Aumentamos a 20 min para evitar que se corte el clone
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 1 # IMPORTANTE: Solo descarga el último commit, no todo el historial
# 1. NUEVO: Generamos la versión semántica dinámica
- name: Definir Versión Semántica
id: vars
shell: bash
run: |
set -euo pipefail
VERSION="v1.0.${{ gitea.run_number }}"
echo "VERSION=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "Versión generada: ${VERSION}"
# Formato v1.0.X (X = número de ejecución del pipeline)
echo "VERSION=v1.0.${{ github.run_number }}" >> $GITHUB_OUTPUT
- name: Login en Gitea Registry
uses: docker/login-action@v2
@@ -36,6 +32,7 @@ jobs:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
# 2. ACTUALIZADO: Usamos la nueva variable para los tags de Docker
- name: Construir y Subir Imagen
uses: docker/build-push-action@v4
with:
@@ -45,453 +42,25 @@ jobs:
gitea.cruzcloud.net/devops/ecommerce-frontend:${{ steps.vars.outputs.VERSION }}
gitea.cruzcloud.net/devops/ecommerce-frontend:latest
# 3. ACTUALIZADO: Inyectamos la versión limpia en el manifiesto YAML
- name: Actualizar Manifiesto GitOps (CD)
shell: bash
env:
VERSION: ${{ steps.vars.outputs.VERSION }}
MANIFEST: workloads/ecommerce/frontend.yaml
IMAGE_REPOSITORY: gitea.cruzcloud.net/devops/ecommerce-frontend
run: |
set -euo pipefail
git config user.name "Cristian Felipe"
git config user.email "[email protected]"
for intento in 1 2 3; do
echo "--------------------------------------------------"
echo "Intento ${intento}/3"
echo "Versión: ${VERSION}"
echo "Manifiesto: ${MANIFEST}"
echo "--------------------------------------------------"
git fetch origin main
git checkout -B main origin/main
if [ ! -f "${MANIFEST}" ]; then
echo "ERROR: no existe ${MANIFEST}."
exit 1
fi
if ! grep -qE "image:[[:space:]]*${IMAGE_REPOSITORY}:" "${MANIFEST}"; then
echo "ERROR: no se encontró ${IMAGE_REPOSITORY} en ${MANIFEST}."
grep -nE '^[[:space:]]*image:' "${MANIFEST}" || true
exit 1
fi
sed -i -E \
"s|(image:[[:space:]]*${IMAGE_REPOSITORY}:).*|\\1${VERSION}|g" \
"${MANIFEST}"
echo "Imagen resultante:"
grep -nE '^[[:space:]]*image:' "${MANIFEST}" || true
git add "${MANIFEST}"
if git diff --cached --quiet; then
echo "El manifiesto ya contiene ${VERSION}."
break
fi
git commit -m "chore: release ${VERSION} [skip ci]"
if git push origin HEAD:main; then
echo "Manifiesto actualizado correctamente con ${VERSION}."
break
fi
if [ "${intento}" -eq 3 ]; then
echo "ERROR: no fue posible actualizar main después de 3 intentos."
exit 1
fi
espera=$((intento * 3))
echo "origin/main cambió. Reintentando en ${espera} segundos..."
sleep "${espera}"
done
- 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 }}
K8S_SERVER: ${{ secrets.K8S_SERVER }}
K8S_CA_B64: ${{ secrets.K8S_CA_B64 }}
K8S_TOKEN: ${{ secrets.K8S_TOKEN }}
K8S_TLS_SERVER_NAME: ${{ secrets.K8S_TLS_SERVER_NAME }}
run: |
set -euo pipefail
EXPECTED_IMAGE="${IMAGE_REPOSITORY}:${VERSION}"
KUBECONFIG_PATH="${RUNNER_TEMP:-/tmp}/kubeconfig"
CA_PATH="${RUNNER_TEMP:-/tmp}/kubernetes-ca.crt"
echo "=================================================="
echo "Validación posterior al despliegue"
echo "Namespace: ${NAMESPACE}"
echo "Imagen esperada: ${EXPECTED_IMAGE}"
echo "Timeout: ${DEPLOY_TIMEOUT_SECONDS} segundos"
echo "=================================================="
normalize_base64() {
local value="$1"
value="$(printf '%s' "${value}" | tr -d '\r\n\t ')"
if ! printf '%s' "${value}" |
grep -Eq '^[A-Za-z0-9+/]*={0,2}$'; then
return 1
fi
case $(( ${#value} % 4 )) in
0) ;;
2) value="${value}==" ;;
3) value="${value}=" ;;
1) return 1 ;;
esac
printf '%s' "${value}"
}
USE_ATOMIC_KUBECONFIG='false'
KUBE_CONFIG_DATA="$(printf '%s' "${KUBE_CONFIG_DATA:-}" | tr -d '\r\n\t ')"
if [ -n "${KUBE_CONFIG_DATA}" ]; then
echo 'Usando secret atómico KUBE_CONFIG_DATA.'
if ! KUBE_CONFIG_DATA="$(normalize_base64 "${KUBE_CONFIG_DATA}")"; then
echo 'ERROR: KUBE_CONFIG_DATA no contiene Base64 válido.'
exit 1
fi
if ! printf '%s' "${KUBE_CONFIG_DATA}" |
base64 --decode > "${KUBECONFIG_PATH}" 2>/dev/null; then
echo 'ERROR: no se pudo decodificar KUBE_CONFIG_DATA.'
exit 1
fi
if ! grep -q '^kind:[[:space:]]*Config' "${KUBECONFIG_PATH}"; then
echo 'ERROR: KUBE_CONFIG_DATA no contiene un kubeconfig válido.'
exit 1
fi
USE_ATOMIC_KUBECONFIG='true'
# 1. Forzamos a Git a ignorar la historia local y sincronizarse 100% con el servidor
git fetch origin main
git reset --hard origin/main
# 2. Aplicamos el cambio de versión
sed -i -E "s|(image: gitea.cruzcloud.net/devops/ecommerce-frontend:).*|\1${{ steps.vars.outputs.VERSION }}|g" workloads/ecommerce/frontend.yaml
# 3. Solo hacemos commit si realmente hubo un cambio
git add workloads/ecommerce/frontend.yaml
if git diff --cached --quiet; then
echo "No hay cambios en el manifiesto."
else
echo 'KUBE_CONFIG_DATA no está configurado; usando secrets K8S_* de compatibilidad.'
for variable in K8S_SERVER K8S_CA_B64 K8S_TOKEN; do
if [ -z "${!variable:-}" ]; then
echo "ERROR: no está configurado el secret ${variable}."
exit 1
fi
done
K8S_SERVER="$(printf '%s' "${K8S_SERVER}" | tr -d '\r\n\t ')"
K8S_TOKEN="$(printf '%s' "${K8S_TOKEN}" | tr -d '\r\n')"
K8S_CA_B64="$(printf '%s' "${K8S_CA_B64}" | tr -d '\r\n\t ')"
K8S_TLS_SERVER_NAME="$(printf '%s' "${K8S_TLS_SERVER_NAME:-}" | tr -d '\r\n\t ')"
if ! printf '%s' "${K8S_SERVER}" |
grep -Eq '^https://[^[:space:]]+:[0-9]+$'; then
echo 'ERROR: K8S_SERVER no tiene un formato válido.'
echo 'Ejemplo: https://192.168.68.61:46421'
exit 1
fi
if printf '%s' "${K8S_SERVER}" |
grep -Eq '^https://(127\.0\.0\.1|localhost|0\.0\.0\.0)(:|/)'; then
echo 'ERROR: K8S_SERVER usa una dirección no accesible desde el contenedor.'
exit 1
fi
K8S_SERVER_HOST="$(
printf '%s' "${K8S_SERVER}" |
sed -E 's#^https://\[?([^]]+)\]?:[0-9]+$#\1#'
)"
if [ -z "${K8S_TLS_SERVER_NAME}" ]; then
K8S_TLS_SERVER_NAME="${K8S_SERVER_HOST}"
fi
if [ "${K8S_TLS_SERVER_NAME}" != "${K8S_SERVER_HOST}" ]; then
echo 'ERROR: K8S_TLS_SERVER_NAME no coincide con el host de K8S_SERVER.'
echo "Host de K8S_SERVER: ${K8S_SERVER_HOST}"
echo "Valor esperado del secret K8S_TLS_SERVER_NAME: ${K8S_SERVER_HOST}"
exit 1
fi
if ! K8S_CA_B64="$(normalize_base64 "${K8S_CA_B64}")"; then
echo 'ERROR: K8S_CA_B64 no contiene Base64 válido.'
exit 1
fi
if ! printf '%s' "${K8S_CA_B64}" |
base64 --decode > "${CA_PATH}" 2>/dev/null; then
echo 'ERROR: no se pudo decodificar K8S_CA_B64.'
exit 1
fi
if ! grep -q 'BEGIN CERTIFICATE' "${CA_PATH}"; then
echo 'ERROR: K8S_CA_B64 no contiene un certificado PEM.'
exit 1
fi
chmod 600 "${CA_PATH}"
{
echo 'apiVersion: v1'
echo 'kind: Config'
echo 'clusters:'
echo '- name: lab-cluster'
echo ' cluster:'
echo " server: ${K8S_SERVER}"
echo " certificate-authority: ${CA_PATH}"
echo " tls-server-name: ${K8S_TLS_SERVER_NAME}"
echo 'users:'
echo '- name: gitea-postdeploy-validator'
echo ' user:'
echo " token: ${K8S_TOKEN}"
echo 'contexts:'
echo '- name: gitea-postdeploy-validator'
echo ' context:'
echo ' cluster: lab-cluster'
echo ' user: gitea-postdeploy-validator'
echo " namespace: ${NAMESPACE}"
echo 'current-context: gitea-postdeploy-validator'
} > "${KUBECONFIG_PATH}"
fi
chmod 600 "${KUBECONFIG_PATH}"
export KUBECONFIG="${KUBECONFIG_PATH}"
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: $(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
EFFECTIVE_SERVER="$(
kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.server}'
)"
EFFECTIVE_TLS_NAME="$(
kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.tls-server-name}'
)"
EFFECTIVE_CA_B64="$(
kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.certificate-authority-data}'
)"
EFFECTIVE_TOKEN="$(
kubectl config view --raw --minify \
-o jsonpath='{.users[0].user.token}'
)"
if [ -z "${EFFECTIVE_TOKEN}" ]; then
echo 'ERROR: el kubeconfig no contiene un token de autenticación.'
exit 1
fi
echo "API Server efectivo: ${EFFECTIVE_SERVER}"
echo "TLS server name efectivo: ${EFFECTIVE_TLS_NAME:-<host del API Server>}"
if [ -n "${EFFECTIVE_CA_B64}" ]; then
printf '%s' "${EFFECTIVE_CA_B64}" | base64 --decode > "${CA_PATH}"
fi
if [ ! -s "${CA_PATH}" ] || ! grep -q 'BEGIN CERTIFICATE' "${CA_PATH}"; then
echo 'ERROR: el kubeconfig no contiene una CA PEM válida.'
exit 1
fi
if command -v openssl >/dev/null 2>&1; then
echo 'Huella SHA256 de la CA usada por el workflow:'
openssl x509 -in "${CA_PATH}" -noout -fingerprint -sha256
fi
TOKEN_METADATA="$(
printf '%s' "${EFFECTIVE_TOKEN}" |
python3 -c '
import base64
import datetime
import json
import sys
token = sys.stdin.read().strip()
parts = token.split(".")
if len(parts) != 3:
print("formato=no-jwt")
raise SystemExit(0)
payload = parts[1] + "=" * (-len(parts[1]) % 4)
data = json.loads(base64.urlsafe_b64decode(payload.encode()))
subject = data.get("sub", "desconocido")
expiration = data.get("exp")
if expiration is None:
print(f"subject={subject}; expiracion=sin-exp")
else:
when = datetime.datetime.fromtimestamp(
expiration, datetime.timezone.utc
).isoformat()
print(f"subject={subject}; expiracion={when}")
'
)"
echo "Identidad del token: ${TOKEN_METADATA}"
if ! kubectl get deployments \
--namespace "${NAMESPACE}" \
--request-timeout='15s' >/dev/null; then
echo "ERROR: no hay conectividad o permisos para leer Deployments en ${NAMESPACE}."
exit 1
fi
echo "Conectividad y permisos de lectura confirmados."
DEADLINE=$((SECONDS + DEPLOY_TIMEOUT_SECONDS))
DEPLOYMENT=''
while [ "${SECONDS}" -lt "${DEADLINE}" ]; do
mapfile -t MATCHES < <(
kubectl get deployments \
--namespace "${NAMESPACE}" \
--output go-template="{{range .items}}{{\$name := .metadata.name}}{{range .spec.template.spec.containers}}{{if eq .image \"${EXPECTED_IMAGE}\"}}{{\$name}}{{\"\\n\"}}{{end}}{{end}}{{end}}" |
sort -u
)
if [ "${#MATCHES[@]}" -eq 1 ] && [ -n "${MATCHES[0]}" ]; 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."
kubectl get deployments \
--namespace "${NAMESPACE}" \
--output custom-columns='DEPLOYMENT:.metadata.name,IMAGES:.spec.template.spec.containers[*].image'
exit 1
fi
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 encontró el contenedor con ${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/,$//'
)"
EXPECTED_REPLICAS="$(
kubectl get deployment "${DEPLOYMENT}" \
--namespace "${NAMESPACE}" \
--output jsonpath='{.spec.replicas}'
)"
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[@]}" -ne "${EXPECTED_REPLICAS}" ]; then
echo "ERROR: se esperaban ${EXPECTED_REPLICAS} Pods Running y se encontraron ${#PODS[@]}."
kubectl get pods \
--namespace "${NAMESPACE}" \
--selector "${SELECTOR}" \
-o wide
exit 1
fi
VALIDATED_PODS=0
for pod in "${PODS[@]}"; do
DECLARED_IMAGE="$(
kubectl get pod "${pod}" \
--namespace "${NAMESPACE}" \
--output go-template="{{range .spec.containers}}{{if eq .name \"${CONTAINER_NAME}\"}}{{.image}}{{end}}{{end}}"
)"
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: ${READY}"
echo " Imagen declarada: ${DECLARED_IMAGE}"
echo " Image ID: ${IMAGE_ID}"
if [ "${DECLARED_IMAGE}" != "${EXPECTED_IMAGE}" ]; then
echo "ERROR: ${pod} ejecuta ${DECLARED_IMAGE}, no ${EXPECTED_IMAGE}."
exit 1
fi
if [ "${READY}" != 'true' ]; then
echo "ERROR: el contenedor ${CONTAINER_NAME} de ${pod} no está Ready."
exit 1
fi
VALIDATED_PODS=$((VALIDATED_PODS + 1))
done
echo "=================================================="
echo "VALIDACIÓN EXITOSA"
echo "Deployment: ${DEPLOYMENT}"
echo "Pods validados: ${VALIDATED_PODS}/${EXPECTED_REPLICAS}"
echo "Tag ejecutado: ${EXPECTED_IMAGE}"
echo "=================================================="
git commit -m "chore: release ${{ steps.vars.outputs.VERSION }} [skip ci]"
# 4. Forzamos el push usando --force para limpiar cualquier discrepancia
git push origin HEAD:main --force
fi