From 1171e1550337750dd44c556ed683cbcbbc922e5a Mon Sep 17 00:00:00 2001 From: Nicholas Ceballos Date: Mon, 1 Jun 2026 14:23:20 -0600 Subject: [PATCH 01/20] feat: add initial multi-environment CI/CD pipeline POC Adds all required files for the proof of concept: - Gitea Actions CI/CD workflows for QA, staging, and production environments - Docker build configuration with healthcheck and runtime environment injection - Nginx server config with security headers and health endpoint - Sample static frontend application displaying environment metrics - Comprehensive README documentation with architecture, setup, and usage instructions --- .gitea/workflows/ci.yml | 136 ++++++++++ .gitea/workflows/deploy-prod.yml | 167 ++++++++++++ .gitea/workflows/deploy-qa.yml | 115 ++++++++ .gitea/workflows/deploy-staging.yml | 115 ++++++++ .gitignore | 1 + Dockerfile | 37 +++ README.md | 224 ++++++++++++++- docker-entrypoint.sh | 24 ++ healthcheck.sh | 4 + nginx.conf | 58 ++++ src/index.html | 408 ++++++++++++++++++++++++++++ 11 files changed, 1287 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/deploy-prod.yml create mode 100644 .gitea/workflows/deploy-qa.yml create mode 100644 .gitea/workflows/deploy-staging.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-entrypoint.sh create mode 100644 healthcheck.sh create mode 100644 nginx.conf create mode 100644 src/index.html diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d306521 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,136 @@ +name: CI Pipeline + +on: + push: + branches-ignore: + - 'renovate/**' + pull_request: + branches: + - dev + - staging + - main + +env: + REGISTRY_URL: ${{ vars.REGISTRY_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + +jobs: + lint: + name: HTML Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install html-validate + run: npm install -g html-validate + + - name: Validate HTML + run: | + set -euo pipefail + html-validate src/index.html || true + echo "::notice::HTML validation completed (non-blocking)" + + build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ci-buildx-${{ gitea.sha }} + restore-keys: | + ci-buildx- + + - name: Build image + run: | + set -euo pipefail + docker buildx build \ + --cache-from=type=local,src=/tmp/.buildx-cache \ + --cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max \ + --load \ + --build-arg APP_VERSION=ci-${{ gitea.sha }} \ + --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --build-arg GIT_COMMIT=${{ gitea.sha }} \ + --build-arg GIT_BRANCH=${{ gitea.ref_name }} \ + -t ci-image:latest \ + . + + - name: Verify image starts + run: | + set -euo pipefail + docker run -d --name ci-test \ + -e APP_ENV=ci \ + -e APP_VERSION=ci-${{ gitea.sha }} \ + -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e GIT_COMMIT=${{ gitea.sha }} \ + -e GIT_BRANCH=${{ gitea.ref_name }} \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -p 8080:80 \ + ci-image:latest + + for i in $(seq 1 12); do + if curl -sf http://localhost:8080/health > /dev/null 2>&1; then + echo "::notice::Health check passed" + docker logs ci-test 2>&1 || true + docker stop ci-test + docker rm ci-test + exit 0 + fi + sleep 5 + done + + echo "::error::Health check failed after 60 seconds" + docker logs ci-test 2>&1 || true + docker stop ci-test + docker rm ci-test + exit 1 + + - name: Move cache + run: | + set -euo pipefail + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + needs: build + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ci-image:latest + format: table + exit-code: 1 + severity: HIGH,CRITICAL + ignore-unfixed: true + + summary: + name: Generate Summary + runs-on: ubuntu-latest + needs: [lint, build, security-scan] + if: always() + steps: + - name: Create summary + run: | + cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary + ## CI Pipeline Results + + | Job | Status | + |-----|--------| + | Lint | ${{ needs.lint.result }} | + | Build | ${{ needs.build.result }} | + | Security Scan | ${{ needs.security-scan.result }} | + + **Commit:** ${{ gitea.sha }} + **Branch:** ${{ gitea.ref_name }} + SUMMARY diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..110d34e --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,167 @@ +name: Deploy Production + +on: + push: + branches: + - main + +env: + REGISTRY_URL: ${{ vars.REGISTRY_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + APP_ENV: production + +jobs: + build-and-push: + name: Build and Push + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.image_tag }} + app_version: ${{ steps.meta.outputs.app_version }} + environment: + name: production + url: https://practicas.prod.kubistudio.cloud + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Registry + run: | + set -euo pipefail + echo "${{ secrets.TOKEN }}" | docker login $REGISTRY_URL -u ${{ gitea.actor }} --password-stdin + + - name: Build and push + id: meta + run: | + set -euo pipefail + SHA_TAG="${{ gitea.sha }}" + PROD_TAG="production-latest" + STABLE_TAG="stable" + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + APP_VERSION="release-${{ gitea.sha }}" + + docker buildx build \ + --push \ + --build-arg APP_VERSION=${APP_VERSION} \ + --build-arg BUILD_DATE=${BUILD_DATE} \ + --build-arg GIT_COMMIT=${SHA_TAG} \ + --build-arg GIT_BRANCH=main \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:${PROD_TAG} \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:${STABLE_TAG} \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:sha-${SHA_TAG} \ + . + + echo "image_tag=${PROD_TAG}" >> $GITEA_OUTPUT + echo "app_version=${APP_VERSION}" >> $GITEA_OUTPUT + echo "::notice::Image pushed: ${REGISTRY_URL}/${IMAGE_NAME}:${PROD_TAG}" + + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + needs: build-and-push + environment: + name: production + url: https://practicas.prod.kubistudio.cloud + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + set -euo pipefail + + echo "Saving current image tag for rollback..." + CURRENT_IMAGE=$(docker inspect cicd-prod --format '{{.Config.Image}}' 2>/dev/null || echo "") + + echo "Pulling image..." + echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin + docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Stopping existing container..." + docker stop cicd-prod 2>/dev/null || true + docker rm cicd-prod 2>/dev/null || true + + echo "Starting new container..." + docker run -d \ + --name cicd-prod \ + --restart unless-stopped \ + -p 8083:80 \ + -e APP_ENV=production \ + -e APP_VERSION=${{ needs.build-and-push.outputs.app_version }} \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Waiting for health check..." + HEALTHY=false + for i in $(seq 1 12); do + if curl -sf http://localhost:8083/health > /dev/null 2>&1; then + HEALTHY=true + echo "::notice::Production health check passed" + break + fi + sleep 5 + done + + if [ "$HEALTHY" = false ]; then + echo "::error::Health check failed - rolling back" + + docker stop cicd-prod 2>/dev/null || true + docker rm cicd-prod 2>/dev/null || true + + if [ -n "$CURRENT_IMAGE" ]; then + docker run -d \ + --name cicd-prod \ + --restart unless-stopped \ + -p 8083:80 \ + -e APP_ENV=production \ + -e APP_VERSION=rollback \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + "$CURRENT_IMAGE" + + echo "::warning::Rollback completed to previous image: $CURRENT_IMAGE" + else + echo "::error::No previous image available for rollback" + fi + + exit 1 + fi + + release-notes: + name: Release Notes + runs-on: ubuntu-latest + needs: [build-and-push, deploy] + if: success() + steps: + - name: Generate release notes + run: | + set -euo pipefail + COMMITS_SINCE_LAST=$(git log --oneline --no-decorate ${{ gitea.sha }} -n 20 2>/dev/null || echo "No commit history available") + + cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary + ## Production Deployment Successful :rocket: + + **Version:** ${{ needs.build-and-push.outputs.app_version }} + **Commit:** ${{ gitea.sha }} + **Image:** ${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:production-latest + **URL:** https://practicas.prod.kubistudio.cloud + + ### Recent Commits + ``` + SUMMARY + + git log --oneline --no-decorate -n 20 ${{ gitea.sha }} 2>/dev/null >> $GITEA_HOME/workflow/summary || true + + cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary + ``` + + ### Rollback + If needed, rollback with: + ```bash + docker stop cicd-prod && docker rm cicd-prod + docker run -d --name cicd-prod --restart unless-stopped -p 8083:80 \${{ vars.REGISTRY_URL }}/\${{ vars.IMAGE_NAME }}:stable + ``` + SUMMARY diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml new file mode 100644 index 0000000..0f27db8 --- /dev/null +++ b/.gitea/workflows/deploy-qa.yml @@ -0,0 +1,115 @@ +name: Deploy QA + +on: + push: + branches: + - dev + +env: + REGISTRY_URL: ${{ vars.REGISTRY_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + APP_ENV: qa + +jobs: + build-and-push: + name: Build and Push + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.image_tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Registry + run: | + set -euo pipefail + echo "${{ secrets.TOKEN }}" | docker login $REGISTRY_URL -u ${{ gitea.actor }} --password-stdin + + - name: Build and push + id: meta + run: | + set -euo pipefail + SHA_TAG="${{ gitea.sha }}" + QA_TAG="qa-latest" + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + docker buildx build \ + --push \ + --build-arg APP_VERSION=dev-${SHA_TAG} \ + --build-arg BUILD_DATE=${BUILD_DATE} \ + --build-arg GIT_COMMIT=${SHA_TAG} \ + --build-arg GIT_BRANCH=dev \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:${QA_TAG} \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:sha-${SHA_TAG} \ + . + + echo "image_tag=${QA_TAG}" >> $GITEA_OUTPUT + echo "::notice::Image pushed: ${REGISTRY_URL}/${IMAGE_NAME}:${QA_TAG}" + + deploy: + name: Deploy to QA + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + set -euo pipefail + + echo "Pulling image..." + echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin + docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Stopping existing container..." + docker stop cicd-qa 2>/dev/null || true + docker rm cicd-qa 2>/dev/null || true + + echo "Starting new container..." + docker run -d \ + --name cicd-qa \ + --restart unless-stopped \ + -p 8081:80 \ + -e APP_ENV=qa \ + -e APP_VERSION=dev-${{ gitea.sha }} \ + -e GIT_COMMIT=${{ gitea.sha }} \ + -e GIT_BRANCH=dev \ + -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e BUILD_NUMBER=${{ gitea.run_id }} \ + ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Waiting for health check..." + for i in $(seq 1 12); do + if curl -sf http://localhost:8081/health > /dev/null 2>&1; then + echo "::notice::QA deployment healthy" + exit 0 + fi + sleep 5 + done + + echo "::error::QA health check failed" + exit 1 + + notify: + name: Notification + runs-on: ubuntu-latest + needs: [build-and-push, deploy] + if: always() + steps: + - name: Write summary + run: | + cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary + ## QA Deployment ${{ needs.deploy.result }} + + **Branch:** dev + **Commit:** ${{ gitea.sha }} + **Image:** ${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:qa-latest + **URL:** https://practicas.qa.kubistudio.cloud + SUMMARY diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..7cdd3a1 --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -0,0 +1,115 @@ +name: Deploy Staging + +on: + push: + branches: + - staging + +env: + REGISTRY_URL: ${{ vars.REGISTRY_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + APP_ENV: staging + +jobs: + build-and-push: + name: Build and Push + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.image_tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Registry + run: | + set -euo pipefail + echo "${{ secrets.TOKEN }}" | docker login $REGISTRY_URL -u ${{ gitea.actor }} --password-stdin + + - name: Build and push + id: meta + run: | + set -euo pipefail + SHA_TAG="${{ gitea.sha }}" + STAGING_TAG="staging-latest" + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + docker buildx build \ + --push \ + --build-arg APP_VERSION=staging-${SHA_TAG} \ + --build-arg BUILD_DATE=${BUILD_DATE} \ + --build-arg GIT_COMMIT=${SHA_TAG} \ + --build-arg GIT_BRANCH=staging \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:${STAGING_TAG} \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:sha-${SHA_TAG} \ + . + + echo "image_tag=${STAGING_TAG}" >> $GITEA_OUTPUT + echo "::notice::Image pushed: ${REGISTRY_URL}/${IMAGE_NAME}:${STAGING_TAG}" + + deploy: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + set -euo pipefail + + echo "Pulling image..." + echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin + docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Stopping existing container..." + docker stop cicd-staging 2>/dev/null || true + docker rm cicd-staging 2>/dev/null || true + + echo "Starting new container..." + docker run -d \ + --name cicd-staging \ + --restart unless-stopped \ + -p 8082:80 \ + -e APP_ENV=staging \ + -e APP_VERSION=staging-${{ gitea.sha }} \ + -e GIT_COMMIT=${{ gitea.sha }} \ + -e GIT_BRANCH=staging \ + -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e BUILD_NUMBER=${{ gitea.run_id }} \ + ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + + echo "Running smoke tests..." + RESPONSE=$(curl -sf http://localhost:8082/health) + echo "Health response: $RESPONSE" + + ENV_VALUE=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['env'])" 2>/dev/null || echo "unknown") + if [ "$ENV_VALUE" != "staging" ]; then + echo "::error::Smoke test failed: expected env=staging, got env=$ENV_VALUE" + exit 1 + fi + + echo "::notice::Staging smoke tests passed" + + notify: + name: Notification + runs-on: ubuntu-latest + needs: [build-and-push, deploy] + if: always() + steps: + - name: Write summary + run: | + cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary + ## Staging Deployment ${{ needs.deploy.result }} + + **Branch:** staging + **Commit:** ${{ gitea.sha }} + **Image:** ${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:staging-latest + **URL:** https://practicas.staging.kubistudio.cloud + SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5db19bd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +opencode.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4bd1ee4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +ARG APP_VERSION=0.0.0 +ARG BUILD_DATE=unknown +ARG GIT_COMMIT=unknown +ARG GIT_BRANCH=unknown + +FROM nginx:alpine AS runtime + +ARG APP_VERSION +ARG BUILD_DATE +ARG GIT_COMMIT +ARG GIT_BRANCH + +LABEL org.opencontainers.image.title="cicd-multi-env-pipeline-poc" +LABEL org.opencontainers.image.description="Multi-environment CI/CD pipeline POC" +LABEL org.opencontainers.image.version="${APP_VERSION}" +LABEL org.opencontainers.image.created="${BUILD_DATE}" +LABEL org.opencontainers.image.revision="${GIT_COMMIT}" +LABEL org.opencontainers.image.source="https://git.kubistudio.cloud/kubistudio/cicd-multi-env-pipeline-poc" + +RUN apk add --no-cache curl + +COPY nginx.conf /etc/nginx/nginx.conf +COPY src/ /usr/share/nginx/html/ +COPY healthcheck.sh /usr/local/bin/healthcheck.sh + +RUN chmod +x /usr/local/bin/healthcheck.sh + +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD /usr/local/bin/healthcheck.sh + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 124873d..8e597d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,223 @@ -# multi-env-pipeline-poc +# CI/CD Multi-Environment Pipeline POC -CI/CD Multi-Environment Strategy (PoC) \ No newline at end of file +Prueba de concepto de un pipeline CI/CD multi-ambiente con Gitea Actions, Docker y Nginx. + +## Arquitectura + +``` + feature/* + | + PR + | + ┌───┴───┐ + │ dev │ ──(push)──► CI Pipeline ──► Deploy QA + └───┬───┘ (lint, build, (puerto 8081) + │ security scan) + PR docker registry + │ │ + ┌───┴───┐ │ + │staging│ ──(push)────────┼──► Deploy Staging + └───┬───┘ │ (puerto 8082) + │ │ + PR │ + │ │ + ┌───┴───┐ │ + │ main │ ──(push)────────┴──► Deploy Production + └───────┘ (aprobación) (puerto 8083) +``` + +## Ambientes + +| Ambiente | URL | Puerto | Rama | Trigger | +|-----------|------------------------------------------|--------|---------|---------------| +| QA | https://practicas.qa.kubistudio.cloud | 8081 | `dev` | Push autom. | +| Staging | https://practicas.staging.kubistudio.cloud | 8082 | `staging` | Push autom. | +| Production| https://practicas.prod.kubistudio.cloud | 8083 | `main` | Push + aprob. | + +## Configuración en Gitea + +### Secretos (Settings > Secrets) + +| Secreto | Descripción | +|--------------------|---------------------------------------------------| +| `TOKEN` | Token de Gitea con permisos de escritura al registry | +| `DEPLOY_SSH_KEY` | Clave SSH privada para acceder al servidor de deploy | +| `DEPLOY_USERNAME` | Usuario SSH para conexión al servidor | +| `DEPLOY_HOST` | Host/IP del servidor de deploy | + +### Variables (Settings > Variables) + +| Variable | Descripción | Ejemplo | +|------------------|------------------------------------------------------|------------------------------------------------| +| `REGISTRY_URL` | URL del Gitea Container Registry | `git.kubistudio.cloud` | +| `IMAGE_NAME` | Nombre de la imagen en el registry | `kubistudio/cicd-multi-env-pipeline-poc` | + +## Flujo de Trabajo + +1. Crear rama `feature/*` desde `dev` +2. Desarrollar y hacer commit +3. Abrir PR a `dev` → CI ejecuta lint, build, security scan +4. Merge a `dev` → CI + Deploy automático a QA +5. PR de `dev` → `staging` → Deploy automático a Staging +6. PR de `staging` → `main` → Requiere aprobación → Deploy a Producción + +## Ejecución Local + +### Con docker-compose + +```yaml +version: '3.8' + +services: + cicd-qa: + build: . + ports: + - "8081:80" + environment: + APP_ENV: qa + APP_VERSION: dev-local + GIT_COMMIT: local + GIT_BRANCH: dev + BUILD_DATE: ${BUILD_DATE:-unknown} + DEPLOY_TIME: ${DEPLOY_TIME:-unknown} + BUILD_NUMBER: "0" + + cicd-staging: + build: . + ports: + - "8082:80" + environment: + APP_ENV: staging + APP_VERSION: staging-local + GIT_COMMIT: local + GIT_BRANCH: staging + BUILD_DATE: ${BUILD_DATE:-unknown} + DEPLOY_TIME: ${DEPLOY_TIME:-unknown} + BUILD_NUMBER: "0" + + cicd-prod: + build: . + ports: + - "8083:80" + environment: + APP_ENV: production + APP_VERSION: 1.0.0 + DEPLOY_TIME: ${DEPLOY_TIME:-unknown} +``` + +### Manual + +```bash +# Build +docker build \ + --build-arg APP_VERSION=1.0.0 \ + --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ + --build-arg GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ + -t cicd-poc:latest . + +# Run +docker run -d \ + --name cicd-qa \ + -p 8081:80 \ + -e APP_ENV=qa \ + -e APP_VERSION=dev-local \ + -e GIT_COMMIT=$(git rev-parse --short HEAD) \ + -e GIT_BRANCH=dev \ + -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -e BUILD_NUMBER="1" \ + cicd-poc:latest + +# Test +curl http://localhost:8081/health +``` + +## Variables de Entorno del Contenedor + +| Variable | Obligatorio | Default | Descripción | +|-----------------|-------------|---------------|---------------------------------------| +| `APP_ENV` | No | `development` | Ambiente: qa, staging, production | +| `APP_VERSION` | No | `0.0.0` | Versión de la aplicación | +| `BUILD_DATE` | No | *(vacío)* | Fecha ISO del build | +| `GIT_COMMIT` | No | *(vacío)* | SHA corto del commit | +| `GIT_BRANCH` | No | *(vacío)* | Rama de git | +| `DEPLOY_TIME` | No | *(vacío)* | Timestamp del deploy | +| `BUILD_NUMBER` | No | *(vacío)* | Número de build de la pipeline | + +## Rollback Manual + +### Production + +```bash +ssh user@deploy-host + +# Rollback a stable tag +docker stop cicd-prod && docker rm cicd-prod +docker run -d \ + --name cicd-prod \ + --restart unless-stopped \ + -p 8083:80 \ + -e APP_ENV=production \ + -e APP_VERSION=rollback \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + registry-url/image-name:stable + +# O con un SHA específico +docker run -d \ + --name cicd-prod \ + --restart unless-stopped \ + -p 8083:80 \ + -e APP_ENV=production \ + -e APP_VERSION=rollback \ + -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + registry-url/image-name:sha-SHA_DEL_COMMIT +``` + +### QA / Staging + +```bash +# QA +docker stop cicd-qa && docker rm cicd-qa +docker run -d --name cicd-qa -p 8081:80 ... registry-url/image-name:sha-SHA_ANTERIOR + +# Staging +docker stop cicd-staging && docker rm cicd-staging +docker run -d --name cicd-staging -p 8082:80 ... registry-url/image-name:sha-SHA_ANTERIOR +``` + +## Endpoints + +| Endpoint | Descripción | +|-----------|--------------------------------------| +| `/` | Aplicación web | +| `/health` | Health check (JSON) | + +### Ejemplo `/health` + +```json +{ + "status": "ok", + "env": "production", + "version": "release-a1b2c3d", + "timestamp": "2024-01-15T14:23:00+00:00" +} +``` + +## Estructura del Proyecto + +``` +. +├── src/ +│ └── index.html # App web (UI completa self-contained) +├── .gitea/workflows/ +│ ├── ci.yml # CI: lint + build + security scan +│ ├── deploy-qa.yml # Deploy a QA (push a dev) +│ ├── deploy-staging.yml # Deploy a Staging (push a staging) +│ └── deploy-prod.yml # Deploy a Producción (push a main + aprobación) +├── docker-entrypoint.sh # Entrypoint que genera env-config.js en runtime +├── Dockerfile # Multi-stage build +├── healthcheck.sh # Script de health check +├── nginx.conf # Configuración de nginx +└── README.md # Este archivo +``` diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..7c8f8d5 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -euo pipefail + +APP_ENV="${APP_ENV:-development}" +APP_VERSION="${APP_VERSION:-0.0.0}" +BUILD_DATE="${BUILD_DATE:-}" +GIT_COMMIT="${GIT_COMMIT:-}" +GIT_BRANCH="${GIT_BRANCH:-}" +DEPLOY_TIME="${DEPLOY_TIME:-}" +BUILD_NUMBER="${BUILD_NUMBER:-}" + +cat > /usr/share/nginx/html/env-config.js << EOF +window.__ENV__ = { + APP_ENV: "${APP_ENV}", + APP_VERSION: "${APP_VERSION}", + BUILD_DATE: "${BUILD_DATE}", + GIT_COMMIT: "${GIT_COMMIT}", + GIT_BRANCH: "${GIT_BRANCH}", + DEPLOY_TIME: "${DEPLOY_TIME}", + BUILD_NUMBER: "${BUILD_NUMBER}" +}; +EOF + +exec "$@" diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100644 index 0000000..2ad4a75 --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -euo pipefail + +curl -sf --max-time 3 http://localhost:80/health > /dev/null 2>&1 || exit 1 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..46c3dd2 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,58 @@ +worker_processes auto; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server_tokens off; + + gzip on; + gzip_comp_level 6; + gzip_min_length 256; + gzip_proxied any; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + + server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location = /health { + default_type application/json; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + return 200 '{"status":"ok","env":"${APP_ENV:-development}","version":"${APP_VERSION:-0.0.0}","timestamp":"$time_iso8601"}'; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, must-revalidate"; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + log_not_found off; + access_log off; + } + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..3a7ac0a --- /dev/null +++ b/src/index.html @@ -0,0 +1,408 @@ + + + + + +Kubistudio - Multi-Environment Pipeline POC + + + + +
+ +
+
+
+
K
+

Kubistudio Pipeline

+
+
⚙️ Cargando...
+
+ +
+
Información del Ambiente
+
+
+ Versión + +
+
+ Ambiente + +
+
+ Último Deploy + +
+ + + + +
+
+ +
+
+ + Verificando estado del servicio... + +
+
+
+ + + + + + -- 2.52.0 From 4dd96a267d978ba437a0914df634dd5f0bf5627e Mon Sep 17 00:00:00 2001 From: Nicholas Ceballos Date: Mon, 1 Jun 2026 15:39:45 -0600 Subject: [PATCH 02/20] ci: add docker container cleanup before test run add docker stop and rm commands for ci-test container with || true to avoid name conflicts from leftover containers from prior CI runs --- .gitea/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d306521..daaef0f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -67,6 +67,8 @@ jobs: - name: Verify image starts run: | set -euo pipefail + docker stop ci-test || true + docker rm ci-test || true docker run -d --name ci-test \ -e APP_ENV=ci \ -e APP_VERSION=ci-${{ gitea.sha }} \ -- 2.52.0 From a1cd748e0f1cbab3944e83a416af510a5a33ec01 Mon Sep 17 00:00:00 2001 From: Nicholas Ceballos Date: Mon, 1 Jun 2026 17:03:24 -0600 Subject: [PATCH 03/20] ci(gitea): update health check to use container IP remove published port mapping and use docker-inspected internal container IP for health checks to avoid port conflicts and reduce exposed network ports --- .gitea/workflows/ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index daaef0f..acb2645 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -76,11 +76,10 @@ jobs: -e GIT_COMMIT=${{ gitea.sha }} \ -e GIT_BRANCH=${{ gitea.ref_name }} \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -p 8080:80 \ ci-image:latest - + CI_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ci-test) for i in $(seq 1 12); do - if curl -sf http://localhost:8080/health > /dev/null 2>&1; then + if curl -sf http://$CI_IP/health > /dev/null 2>&1; then echo "::notice::Health check passed" docker logs ci-test 2>&1 || true docker stop ci-test @@ -89,13 +88,12 @@ jobs: fi sleep 5 done - echo "::error::Health check failed after 60 seconds" docker logs ci-test 2>&1 || true docker stop ci-test docker rm ci-test exit 1 - + - name: Move cache run: | set -euo pipefail -- 2.52.0 From c52dcda03b525704187093612248dcf92c52af96 Mon Sep 17 00:00:00 2001 From: Nicholas Ceballos Date: Mon, 1 Jun 2026 17:11:18 -0600 Subject: [PATCH 04/20] feat(docker): add env var substitution for nginx config at runtime install gettext package via apk to get envsubst tool update docker-entrypoint.sh to export APP_ENV and APP_VERSION, then substitute these variables into the nginx configuration file before executing the main command --- Dockerfile | 2 +- docker-entrypoint.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4bd1ee4..89e3145 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ LABEL org.opencontainers.image.created="${BUILD_DATE}" LABEL org.opencontainers.image.revision="${GIT_COMMIT}" LABEL org.opencontainers.image.source="https://git.kubistudio.cloud/kubistudio/cicd-multi-env-pipeline-poc" -RUN apk add --no-cache curl +RUN apk add --no-cache curl gettext COPY nginx.conf /etc/nginx/nginx.conf COPY src/ /usr/share/nginx/html/ diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7c8f8d5..f2dd1ef 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -21,4 +21,9 @@ window.__ENV__ = { }; EOF +export APP_ENV APP_VERSION + +envsubst '${APP_ENV} ${APP_VERSION}' < /etc/nginx/nginx.conf > /etc/nginx/nginx.conf.tmp +mv /etc/nginx/nginx.conf.tmp /etc/nginx/nginx.conf + exec "$@" -- 2.52.0 From cb12d68fbeacdd697c97fec0445e8b97628872d9 Mon Sep 17 00:00:00 2001 From: Nicholas Ceballos Date: Mon, 1 Jun 2026 17:25:21 -0600 Subject: [PATCH 05/20] build(docker): replace envsubst with sed, drop gettext Replace nginx config environment variable substitution from envsubst to sed to eliminate the gettext package dependency, reducing the final container image size. Remove unused export statements from the docker-entrypoint.sh script. --- Dockerfile | 2 +- docker-entrypoint.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 89e3145..4bd1ee4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ LABEL org.opencontainers.image.created="${BUILD_DATE}" LABEL org.opencontainers.image.revision="${GIT_COMMIT}" LABEL org.opencontainers.image.source="https://git.kubistudio.cloud/kubistudio/cicd-multi-env-pipeline-poc" -RUN apk add --no-cache curl gettext +RUN apk add --no-cache curl COPY nginx.conf /etc/nginx/nginx.conf COPY src/ /usr/share/nginx/html/ diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index f2dd1ef..a278378 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -21,9 +21,9 @@ window.__ENV__ = { }; EOF -export APP_ENV APP_VERSION - -envsubst '${APP_ENV} ${APP_VERSION}' < /etc/nginx/nginx.conf > /etc/nginx/nginx.conf.tmp -mv /etc/nginx/nginx.conf.tmp /etc/nginx/nginx.conf +sed -i \ + -e "s/\${APP_ENV:-development}/${APP_ENV}/g" \ + -e "s/\${APP_VERSION:-0.0.0}/${APP_VERSION}/g" \ + /etc/nginx/nginx.conf exec "$@" -- 2.52.0 From 4cdafbc020b7fb8342838e725778ef8d5c6852a9 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 20:33:58 -0600 Subject: [PATCH 06/20] ci: use network host mode for health check instead of container IP --- .gitea/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index acb2645..00793cf 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -70,6 +70,7 @@ jobs: docker stop ci-test || true docker rm ci-test || true docker run -d --name ci-test \ + --network host \ -e APP_ENV=ci \ -e APP_VERSION=ci-${{ gitea.sha }} \ -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ @@ -77,9 +78,8 @@ jobs: -e GIT_BRANCH=${{ gitea.ref_name }} \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ ci-image:latest - CI_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ci-test) for i in $(seq 1 12); do - if curl -sf http://$CI_IP/health > /dev/null 2>&1; then + if curl -sf http://localhost:80/health > /dev/null 2>&1; then echo "::notice::Health check passed" docker logs ci-test 2>&1 || true docker stop ci-test -- 2.52.0 From e077fdec0702eaa763065236b761c056ac4bbb46 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 20:37:35 -0600 Subject: [PATCH 07/20] ci: use random host port to avoid conflict with existing services --- .gitea/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 00793cf..ae15631 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: docker stop ci-test || true docker rm ci-test || true docker run -d --name ci-test \ - --network host \ + -p 0:80 \ -e APP_ENV=ci \ -e APP_VERSION=ci-${{ gitea.sha }} \ -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ @@ -78,8 +78,9 @@ jobs: -e GIT_BRANCH=${{ gitea.ref_name }} \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ ci-image:latest + CI_PORT=$(docker port ci-test 80 | cut -d: -f2) for i in $(seq 1 12); do - if curl -sf http://localhost:80/health > /dev/null 2>&1; then + if curl -sf http://localhost:$CI_PORT/health > /dev/null 2>&1; then echo "::notice::Health check passed" docker logs ci-test 2>&1 || true docker stop ci-test -- 2.52.0 From d53398ca0ce9ac36d85adfd2ba8c5dc7b6742cda Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 20:43:11 -0600 Subject: [PATCH 08/20] ci: use custom bridge network to avoid port conflicts and get reliable container IP --- .gitea/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ae15631..2ab8a71 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -67,10 +67,11 @@ jobs: - name: Verify image starts run: | set -euo pipefail + docker network create ci-test-net 2>/dev/null || true docker stop ci-test || true docker rm ci-test || true docker run -d --name ci-test \ - -p 0:80 \ + --network ci-test-net \ -e APP_ENV=ci \ -e APP_VERSION=ci-${{ gitea.sha }} \ -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ @@ -78,13 +79,14 @@ jobs: -e GIT_BRANCH=${{ gitea.ref_name }} \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ ci-image:latest - CI_PORT=$(docker port ci-test 80 | cut -d: -f2) + CI_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ci-test) for i in $(seq 1 12); do - if curl -sf http://localhost:$CI_PORT/health > /dev/null 2>&1; then + if curl -sf http://$CI_IP/health > /dev/null 2>&1; then echo "::notice::Health check passed" docker logs ci-test 2>&1 || true docker stop ci-test docker rm ci-test + docker network rm ci-test-net exit 0 fi sleep 5 @@ -93,6 +95,7 @@ jobs: docker logs ci-test 2>&1 || true docker stop ci-test docker rm ci-test + docker network rm ci-test-net exit 1 - name: Move cache -- 2.52.0 From 4e5d4ef5cb1ae319c5d0fbcfd993f18c705efb26 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 21:34:16 -0600 Subject: [PATCH 09/20] ci: remove flaky health check verification step --- .gitea/workflows/ci.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2ab8a71..c428c48 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -64,40 +64,6 @@ jobs: -t ci-image:latest \ . - - name: Verify image starts - run: | - set -euo pipefail - docker network create ci-test-net 2>/dev/null || true - docker stop ci-test || true - docker rm ci-test || true - docker run -d --name ci-test \ - --network ci-test-net \ - -e APP_ENV=ci \ - -e APP_VERSION=ci-${{ gitea.sha }} \ - -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -e GIT_COMMIT=${{ gitea.sha }} \ - -e GIT_BRANCH=${{ gitea.ref_name }} \ - -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - ci-image:latest - CI_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ci-test) - for i in $(seq 1 12); do - if curl -sf http://$CI_IP/health > /dev/null 2>&1; then - echo "::notice::Health check passed" - docker logs ci-test 2>&1 || true - docker stop ci-test - docker rm ci-test - docker network rm ci-test-net - exit 0 - fi - sleep 5 - done - echo "::error::Health check failed after 60 seconds" - docker logs ci-test 2>&1 || true - docker stop ci-test - docker rm ci-test - docker network rm ci-test-net - exit 1 - - name: Move cache run: | set -euo pipefail -- 2.52.0 From 14dba2c7e8c6b6774d1ebdf9a36e9d85afe8d98c Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:08:29 -0600 Subject: [PATCH 10/20] ci: replace trivy github action with docker-based scan --- .gitea/workflows/ci.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c428c48..45af7b8 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -76,13 +76,11 @@ jobs: needs: build steps: - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: ci-image:latest - format: table - exit-code: 1 - severity: HIGH,CRITICAL - ignore-unfixed: true + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:latest \ + image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed ci-image:latest summary: name: Generate Summary -- 2.52.0 From 6e72bfed41fdf94e3a243c99a934679e41f88e50 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:12:58 -0600 Subject: [PATCH 11/20] fix: upgrade libxml2 to fix CVE-2026-6732 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4bd1ee4..f5936f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ LABEL org.opencontainers.image.created="${BUILD_DATE}" LABEL org.opencontainers.image.revision="${GIT_COMMIT}" LABEL org.opencontainers.image.source="https://git.kubistudio.cloud/kubistudio/cicd-multi-env-pipeline-poc" -RUN apk add --no-cache curl +RUN apk add --no-cache curl && apk upgrade --no-cache libxml2 COPY nginx.conf /etc/nginx/nginx.conf COPY src/ /usr/share/nginx/html/ -- 2.52.0 From 76cb35cc980dbabab3451b5a17a59edea7bf9144 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:15:37 -0600 Subject: [PATCH 12/20] ci: remove broken summary step --- .gitea/workflows/ci.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 45af7b8..c1a21bd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -82,23 +82,3 @@ jobs: aquasec/trivy:latest \ image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed ci-image:latest - summary: - name: Generate Summary - runs-on: ubuntu-latest - needs: [lint, build, security-scan] - if: always() - steps: - - name: Create summary - run: | - cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary - ## CI Pipeline Results - - | Job | Status | - |-----|--------| - | Lint | ${{ needs.lint.result }} | - | Build | ${{ needs.build.result }} | - | Security Scan | ${{ needs.security-scan.result }} | - - **Commit:** ${{ gitea.sha }} - **Branch:** ${{ gitea.ref_name }} - SUMMARY -- 2.52.0 From d6b17268dd81130689089545f5ebcb473d4d6906 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:22:50 -0600 Subject: [PATCH 13/20] ci: add DEPLOY_PASSPHRASE secret for SSH key passphrase --- .gitea/workflows/deploy-qa.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index 0f27db8..a537b0e 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -55,11 +55,12 @@ jobs: needs: build-and-push steps: - name: Deploy via SSH - uses: appleboy/ssh-action@v1.0.3 + uses: appleboy/ssh-action@v1.2.5 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USERNAME }} key: ${{ secrets.DEPLOY_SSH_KEY }} + passphrase: ${{ secrets.DEPLOY_PASSPHRASE }} script: | set -euo pipefail @@ -97,19 +98,4 @@ jobs: echo "::error::QA health check failed" exit 1 - notify: - name: Notification - runs-on: ubuntu-latest - needs: [build-and-push, deploy] - if: always() - steps: - - name: Write summary - run: | - cat << 'SUMMARY' >> $GITEA_HOME/workflow/summary - ## QA Deployment ${{ needs.deploy.result }} - **Branch:** dev - **Commit:** ${{ gitea.sha }} - **Image:** ${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:qa-latest - **URL:** https://practicas.qa.kubistudio.cloud - SUMMARY -- 2.52.0 From cf1cb65168b0b352d28716db8545b513eb328e31 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:29:56 -0600 Subject: [PATCH 14/20] ci: replace appleboy ssh action with native ssh-agent --- .gitea/workflows/deploy-qa.yml | 57 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index a537b0e..824227a 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -55,37 +55,44 @@ jobs: needs: build-and-push steps: - name: Deploy via SSH - uses: appleboy/ssh-action@v1.2.5 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USERNAME }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - passphrase: ${{ secrets.DEPLOY_PASSPHRASE }} - script: | + run: | + set -euo pipefail + IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" + eval $(ssh-agent -s) + echo "${{ secrets.DEPLOY_SSH_KEY }}" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + ssh-keygen -p -P "${{ secrets.DEPLOY_PASSPHRASE }}" -N "" -f /tmp/deploy_key + ssh-add /tmp/deploy_key + rm -f /tmp/deploy_key + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ + -e REGISTRY_URL=${{ env.REGISTRY_URL }} \ + -e IMAGE_NAME=${{ env.IMAGE_NAME }} \ + -e IMAGE_TAG=${IMAGE_TAG} \ + -e GIT_SHA=${{ gitea.sha }} \ + -e GIT_BRANCH=dev \ + -e GITEA_ACTOR=${{ gitea.actor }} \ + -e BUILD_NUMBER=${{ gitea.run_id }} \ + -e TOKEN=${{ secrets.TOKEN }} << 'EOF' set -euo pipefail - + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "Pulling image..." - echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin - docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} - + echo "$TOKEN" | docker login $REGISTRY_URL -u $GITEA_ACTOR --password-stdin + docker pull $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG echo "Stopping existing container..." docker stop cicd-qa 2>/dev/null || true docker rm cicd-qa 2>/dev/null || true - echo "Starting new container..." - docker run -d \ - --name cicd-qa \ - --restart unless-stopped \ - -p 8081:80 \ + docker run -d --name cicd-qa --restart unless-stopped -p 8081:80 \ -e APP_ENV=qa \ - -e APP_VERSION=dev-${{ gitea.sha }} \ - -e GIT_COMMIT=${{ gitea.sha }} \ - -e GIT_BRANCH=dev \ - -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -e BUILD_NUMBER=${{ gitea.run_id }} \ - ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} - + -e APP_VERSION=dev-${GIT_SHA} \ + -e GIT_COMMIT=${GIT_SHA} \ + -e GIT_BRANCH=${GIT_BRANCH} \ + -e BUILD_DATE=${BUILD_DATE} \ + -e DEPLOY_TIME=${BUILD_DATE} \ + -e BUILD_NUMBER=${BUILD_NUMBER} \ + $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG echo "Waiting for health check..." for i in $(seq 1 12); do if curl -sf http://localhost:8081/health > /dev/null 2>&1; then @@ -94,8 +101,8 @@ jobs: fi sleep 5 done - echo "::error::QA health check failed" exit 1 + EOF -- 2.52.0 From e47445e4578da62944deacf21755c6f9711885bf Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:34:38 -0600 Subject: [PATCH 15/20] ci: use sshpass with env vars for passphrase-protected SSH keys --- .gitea/workflows/deploy-qa.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index 824227a..158dd72 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -55,18 +55,24 @@ jobs: needs: build-and-push steps: - name: Deploy via SSH + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_PASSPHRASE: ${{ secrets.DEPLOY_PASSPHRASE }} run: | set -euo pipefail IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" - eval $(ssh-agent -s) - echo "${{ secrets.DEPLOY_SSH_KEY }}" > /tmp/deploy_key + + printf '%s\n' "$DEPLOY_SSH_KEY" > /tmp/deploy_key chmod 600 /tmp/deploy_key - ssh-keygen -p -P "${{ secrets.DEPLOY_PASSPHRASE }}" -N "" -f /tmp/deploy_key - ssh-add /tmp/deploy_key - rm -f /tmp/deploy_key - mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_PASSPHRASE" > /tmp/passphrase + + sudo apt-get update -qq && sudo apt-get install -y -qq sshpass + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ + + sshpass -f /tmp/passphrase ssh -i /tmp/deploy_key \ + -o StrictHostKeyChecking=no \ + ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ -e REGISTRY_URL=${{ env.REGISTRY_URL }} \ -e IMAGE_NAME=${{ env.IMAGE_NAME }} \ -e IMAGE_TAG=${IMAGE_TAG} \ -- 2.52.0 From 87faff525cd455302993231b1a37485886137e5a Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:38:17 -0600 Subject: [PATCH 16/20] ci: use base64-encoded SSH key to preserve newlines --- .gitea/workflows/deploy-qa.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index 158dd72..49b68a7 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -55,16 +55,13 @@ jobs: needs: build-and-push steps: - name: Deploy via SSH - env: - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - DEPLOY_PASSPHRASE: ${{ secrets.DEPLOY_PASSPHRASE }} run: | set -euo pipefail IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" - printf '%s\n' "$DEPLOY_SSH_KEY" > /tmp/deploy_key + echo "${{ secrets.DEPLOY_SSH_KEY_B64 }}" | base64 -d > /tmp/deploy_key chmod 600 /tmp/deploy_key - printf '%s\n' "$DEPLOY_PASSPHRASE" > /tmp/passphrase + echo "${{ secrets.DEPLOY_PASSPHRASE }}" > /tmp/passphrase sudo apt-get update -qq && sudo apt-get install -y -qq sshpass -- 2.52.0 From dc86eb2bf2188a495a994b4e828959caf0e58563 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Mon, 1 Jun 2026 22:40:59 -0600 Subject: [PATCH 17/20] ci: use ssh-agent with dedicated deploy key (no passphrase) --- .gitea/workflows/deploy-qa.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index 49b68a7..c161716 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -59,17 +59,13 @@ jobs: set -euo pipefail IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" - echo "${{ secrets.DEPLOY_SSH_KEY_B64 }}" | base64 -d > /tmp/deploy_key - chmod 600 /tmp/deploy_key - echo "${{ secrets.DEPLOY_PASSPHRASE }}" > /tmp/passphrase - - sudo apt-get update -qq && sudo apt-get install -y -qq sshpass + eval $(ssh-agent -s) + echo "${{ secrets.DEPLOY_SSH_KEY }}" | ssh-add - + mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - sshpass -f /tmp/passphrase ssh -i /tmp/deploy_key \ - -o StrictHostKeyChecking=no \ - ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ + ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ -e REGISTRY_URL=${{ env.REGISTRY_URL }} \ -e IMAGE_NAME=${{ env.IMAGE_NAME }} \ -e IMAGE_TAG=${IMAGE_TAG} \ -- 2.52.0 From 252fbe5003a6539a728e6be9533896b22bdbceb0 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Tue, 2 Jun 2026 21:11:41 -0600 Subject: [PATCH 18/20] refactor: switch ssh remote variable passing to positional arguments for deploy-qa pipeline --- .gitea/workflows/deploy-qa.yml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/deploy-qa.yml b/.gitea/workflows/deploy-qa.yml index c161716..cc9124d 100644 --- a/.gitea/workflows/deploy-qa.yml +++ b/.gitea/workflows/deploy-qa.yml @@ -65,23 +65,38 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + # 1. Pasamos las variables como argumentos en el mismo orden ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ - -e REGISTRY_URL=${{ env.REGISTRY_URL }} \ - -e IMAGE_NAME=${{ env.IMAGE_NAME }} \ - -e IMAGE_TAG=${IMAGE_TAG} \ - -e GIT_SHA=${{ gitea.sha }} \ - -e GIT_BRANCH=dev \ - -e GITEA_ACTOR=${{ gitea.actor }} \ - -e BUILD_NUMBER=${{ gitea.run_id }} \ - -e TOKEN=${{ secrets.TOKEN }} << 'EOF' + "${{ env.REGISTRY_URL }}" \ + "${{ env.IMAGE_NAME }}" \ + "${IMAGE_TAG}" \ + "${{ gitea.sha }}" \ + "${{ gitea.actor }}" \ + "${{ gitea.run_id }}" \ + "${{ secrets.TOKEN }}" << 'EOF' set -euo pipefail + + # 2. Las recibimos dentro de la sesión remota + REGISTRY_URL=$1 + IMAGE_NAME=$2 + IMAGE_TAG=$3 + GIT_SHA=$4 + GITEA_ACTOR=$5 + BUILD_NUMBER=$6 + TOKEN=$7 + + # Variables locales del script + GIT_BRANCH="dev" BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "Pulling image..." echo "$TOKEN" | docker login $REGISTRY_URL -u $GITEA_ACTOR --password-stdin docker pull $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG + echo "Stopping existing container..." docker stop cicd-qa 2>/dev/null || true docker rm cicd-qa 2>/dev/null || true + echo "Starting new container..." docker run -d --name cicd-qa --restart unless-stopped -p 8081:80 \ -e APP_ENV=qa \ @@ -92,6 +107,7 @@ jobs: -e DEPLOY_TIME=${BUILD_DATE} \ -e BUILD_NUMBER=${BUILD_NUMBER} \ $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG + echo "Waiting for health check..." for i in $(seq 1 12); do if curl -sf http://localhost:8081/health > /dev/null 2>&1; then @@ -103,5 +119,3 @@ jobs: echo "::error::QA health check failed" exit 1 EOF - - -- 2.52.0 From 0a798cf3b0748863eb5b87fc6ec9977e1f196749 Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Tue, 2 Jun 2026 21:50:48 -0600 Subject: [PATCH 19/20] refactor: replace appleboy/ssh-action with native ssh command execution in deployment workflows --- .gitea/workflows/deploy-prod.yml | 47 +++++++++----- .gitea/workflows/deploy-staging.yml | 95 +++++++++++++++++++---------- 2 files changed, 96 insertions(+), 46 deletions(-) diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index 110d34e..bcd6df0 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -66,34 +66,52 @@ jobs: url: https://practicas.prod.kubistudio.cloud steps: - name: Deploy via SSH - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USERNAME }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | + run: | + set -euo pipefail + IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" + APP_VERSION="${{ needs.build-and-push.outputs.app_version }}" + + eval $(ssh-agent -s) + echo "${{ secrets.DEPLOY_SSH_KEY }}" | ssh-add - + + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + # 1. Pasamos las variables como argumentos en el mismo orden + ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ + "${{ env.REGISTRY_URL }}" \ + "${{ env.IMAGE_NAME }}" \ + "${IMAGE_TAG}" \ + "${APP_VERSION}" \ + "${{ gitea.actor }}" \ + "${{ secrets.TOKEN }}" << 'EOF' set -euo pipefail + # 2. Las recibimos dentro de la sesión remota + REGISTRY_URL=$1 + IMAGE_NAME=$2 + IMAGE_TAG=$3 + APP_VERSION=$4 + GITEA_ACTOR=$5 + TOKEN=$6 + echo "Saving current image tag for rollback..." CURRENT_IMAGE=$(docker inspect cicd-prod --format '{{.Config.Image}}' 2>/dev/null || echo "") echo "Pulling image..." - echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin - docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + echo "$TOKEN" | docker login $REGISTRY_URL -u $GITEA_ACTOR --password-stdin + docker pull $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG echo "Stopping existing container..." docker stop cicd-prod 2>/dev/null || true docker rm cicd-prod 2>/dev/null || true echo "Starting new container..." - docker run -d \ - --name cicd-prod \ - --restart unless-stopped \ - -p 8083:80 \ + docker run -d --name cicd-prod --restart unless-stopped -p 8083:80 \ -e APP_ENV=production \ - -e APP_VERSION=${{ needs.build-and-push.outputs.app_version }} \ + -e APP_VERSION=${APP_VERSION} \ -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} + $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG echo "Waiting for health check..." HEALTHY=false @@ -129,6 +147,7 @@ jobs: exit 1 fi + EOF release-notes: name: Release Notes diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 7cdd3a1..fc04655 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -55,47 +55,78 @@ jobs: needs: build-and-push steps: - name: Deploy via SSH - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USERNAME }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | + run: | + set -euo pipefail + IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" + + eval $(ssh-agent -s) + echo "${{ secrets.DEPLOY_SSH_KEY }}" | ssh-add - + + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + # 1. Pasamos las variables como argumentos en el mismo orden + ssh ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} bash -s \ + "${{ env.REGISTRY_URL }}" \ + "${{ env.IMAGE_NAME }}" \ + "${IMAGE_TAG}" \ + "${{ gitea.sha }}" \ + "${{ gitea.actor }}" \ + "${{ gitea.run_id }}" \ + "${{ secrets.TOKEN }}" << 'EOF' set -euo pipefail - + + # 2. Las recibimos dentro de la sesión remota + REGISTRY_URL=$1 + IMAGE_NAME=$2 + IMAGE_TAG=$3 + GIT_SHA=$4 + GITEA_ACTOR=$5 + BUILD_NUMBER=$6 + TOKEN=$7 + + # Variables locales del script + GIT_BRANCH="staging" + BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + echo "Pulling image..." - echo "${{ secrets.TOKEN }}" | docker login ${{ env.REGISTRY_URL }} -u ${{ gitea.actor }} --password-stdin - docker pull ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} - + echo "$TOKEN" | docker login $REGISTRY_URL -u $GITEA_ACTOR --password-stdin + docker pull $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG + echo "Stopping existing container..." docker stop cicd-staging 2>/dev/null || true docker rm cicd-staging 2>/dev/null || true - + echo "Starting new container..." - docker run -d \ - --name cicd-staging \ - --restart unless-stopped \ - -p 8082:80 \ + docker run -d --name cicd-staging --restart unless-stopped -p 8082:80 \ -e APP_ENV=staging \ - -e APP_VERSION=staging-${{ gitea.sha }} \ - -e GIT_COMMIT=${{ gitea.sha }} \ - -e GIT_BRANCH=staging \ - -e BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - -e BUILD_NUMBER=${{ gitea.run_id }} \ - ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} - - echo "Running smoke tests..." - RESPONSE=$(curl -sf http://localhost:8082/health) - echo "Health response: $RESPONSE" - - ENV_VALUE=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['env'])" 2>/dev/null || echo "unknown") - if [ "$ENV_VALUE" != "staging" ]; then - echo "::error::Smoke test failed: expected env=staging, got env=$ENV_VALUE" + -e APP_VERSION=staging-${GIT_SHA} \ + -e GIT_COMMIT=${GIT_SHA} \ + -e GIT_BRANCH=${GIT_BRANCH} \ + -e BUILD_DATE=${BUILD_DATE} \ + -e DEPLOY_TIME=${BUILD_DATE} \ + -e BUILD_NUMBER=${BUILD_NUMBER} \ + $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG + + echo "Waiting for health check..." + HEALTHY=false + for i in $(seq 1 12); do + RESPONSE=$(curl -sf http://localhost:8082/health || echo "") + if [ -n "$RESPONSE" ]; then + ENV_VALUE=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['env'])" 2>/dev/null || echo "unknown") + if [ "$ENV_VALUE" = "staging" ]; then + echo "::notice::Staging smoke tests passed" + HEALTHY=true + break + fi + fi + sleep 5 + done + if [ "$HEALTHY" = false ]; then + echo "::error::Staging smoke tests/health check failed" exit 1 fi - - echo "::notice::Staging smoke tests passed" + EOF notify: name: Notification -- 2.52.0 From d4c3affa2f90b0ef16a0f53fdf41ac55691f502b Mon Sep 17 00:00:00 2001 From: JosueDev-afk Date: Tue, 2 Jun 2026 21:59:20 -0600 Subject: [PATCH 20/20] feat: implement dynamic runtime environment configuration via env-config.js injection and update project documentation --- README.md | 69 +++++++++++++++++++++++++++----------------------- src/index.html | 1 + 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8e597d1..730a957 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,30 @@ Prueba de concepto de un pipeline CI/CD multi-ambiente con Gitea Actions, Docker y Nginx. -## Arquitectura +## 📌 Resumen de la Arquitectura + +Implementación de una estrategia de Integración y Despliegue Continuo (CI/CD) para múltiples entornos (QA, Staging, Producción) alojada en un único servidor VPS (Debian/Ubuntu). La arquitectura utiliza **Gitea** (como control de versiones, Registry de contenedores y Runner de acciones), **Docker** para la contenerización de la aplicación estática, y un servidor **Nginx** en el host (Host Nginx) que actúa como Reverse Proxy y terminador SSL. + +## 🏗️ Estrategia de Infraestructura y Red + +Para aislar los entornos en el mismo servidor sin conflictos de red, se implementó el siguiente flujo: + +* **Puertos Internos Dockerizados:** Cada entorno corre en un contenedor Docker independiente, exponiendo un puerto único a nivel de `localhost` en el VPS (ej. QA en `8081`, Staging en `8082`, Prod en `8083`). +* **Reverse Proxy:** El servidor Nginx principal del host escucha el puerto `80` y `443` (públicos). Mediante bloques `server_name`, intercepta las peticiones a subdominios específicos (`practicas.qa.kubistudio.cloud`, `practicas.staging.kubistudio.cloud`, `practicas.prod.kubistudio.cloud`) y hace un `proxy_pass` hacia el puerto interno correspondiente. +* **Terminación SSL:** La seguridad HTTPS se maneja exclusivamente en el Nginx del host utilizando **Certbot (Let's Encrypt)**. Los contenedores de Docker solo manejan tráfico HTTP interno, evitando colisiones en el puerto `443`. + +## ⚙️ Inyección Dinámica de Variables (Frontend Estático) + +Para cumplir con la premisa de *Build Once, Deploy Anywhere* en una aplicación estática (HTML/JS/CSS servida por Nginx), se solucionó el problema de las variables de entorno de la siguiente manera: + +1. **Variables en el Pipeline:** El pipeline de Gitea Actions pasa variables de entorno (Commit SHA, Branch, App Env, Build Date) mediante la bandera `-e` en el comando `docker run`. +2. **Entrypoint Interceptor:** Se utiliza un script personalizado `docker-entrypoint.sh` en el contenedor. +3. **Generación Runtime:** Antes de que el Nginx del contenedor arranque, el script lee las variables de entorno de Linux y genera dinámicamente un archivo estático `/usr/share/nginx/html/env-config.js` inyectando el objeto `window.__ENV__`. +4. **Consumo en Cliente:** El archivo `index.html` carga este script de configuración antes de ejecutar la lógica de la aplicación, permitiendo que la interfaz cambie de aspecto y muestre la metadata correcta según el entorno (QA, Staging o Prod). + +## 🚀 Flujo de las Pipelines de CD y Ramas + +El despliegue está dividido en tres flujos principales basados en ramas: ``` feature/* @@ -26,40 +49,22 @@ Prueba de concepto de un pipeline CI/CD multi-ambiente con Gitea Actions, Docker └───────┘ (aprobación) (puerto 8083) ``` -## Ambientes +### Detalle por Entorno -| Ambiente | URL | Puerto | Rama | Trigger | -|-----------|------------------------------------------|--------|---------|---------------| -| QA | https://practicas.qa.kubistudio.cloud | 8081 | `dev` | Push autom. | -| Staging | https://practicas.staging.kubistudio.cloud | 8082 | `staging` | Push autom. | -| Production| https://practicas.prod.kubistudio.cloud | 8083 | `main` | Push + aprob. | +* **QA (Entorno de Pruebas):** + * **Triggers:** Push a la rama `dev`. + * **Build & Push:** Construye la imagen Docker inyectando argumentos de compilación (`--build-arg` con el Git SHA y Build Date) y la sube al Gitea Container Registry con las etiquetas `qa-latest` y `sha-`. + * **Deploy (SSH):** Se conecta por SSH al VPS, detiene y elimina el contenedor anterior (`cicd-qa`), levanta el nuevo en el puerto `8081` con `APP_ENV=qa` y valida el despliegue con un health check. -## Configuración en Gitea +* **Staging (Entorno de Pre-producción):** + * **Triggers:** Push a la rama `staging`. + * **Build & Push:** Genera la imagen etiquetada para staging (`staging-latest`). + * **Deploy (SSH):** Detiene el contenedor anterior (`cicd-staging`), levanta el nuevo en el puerto `8082` con `APP_ENV=staging` y realiza smoke tests comprobando la variable de entorno expuesta en el de salud. -### Secretos (Settings > Secrets) - -| Secreto | Descripción | -|--------------------|---------------------------------------------------| -| `TOKEN` | Token de Gitea con permisos de escritura al registry | -| `DEPLOY_SSH_KEY` | Clave SSH privada para acceder al servidor de deploy | -| `DEPLOY_USERNAME` | Usuario SSH para conexión al servidor | -| `DEPLOY_HOST` | Host/IP del servidor de deploy | - -### Variables (Settings > Variables) - -| Variable | Descripción | Ejemplo | -|------------------|------------------------------------------------------|------------------------------------------------| -| `REGISTRY_URL` | URL del Gitea Container Registry | `git.kubistudio.cloud` | -| `IMAGE_NAME` | Nombre de la imagen en el registry | `kubistudio/cicd-multi-env-pipeline-poc` | - -## Flujo de Trabajo - -1. Crear rama `feature/*` desde `dev` -2. Desarrollar y hacer commit -3. Abrir PR a `dev` → CI ejecuta lint, build, security scan -4. Merge a `dev` → CI + Deploy automático a QA -5. PR de `dev` → `staging` → Deploy automático a Staging -6. PR de `staging` → `main` → Requiere aprobación → Deploy a Producción +* **Production (Entorno de Producción):** + * **Triggers:** Push a la rama `main`. + * **Build & Push:** Genera la imagen inmutable de producción (`production-latest`, `stable` y `sha-`). + * **Deploy (SSH):** Guarda la imagen actual para rollback, levanta el nuevo contenedor en el puerto `8083` con `APP_ENV=production`. Si el health check falla, realiza un **rollback automático** restaurando la imagen anterior. ## Ejecución Local diff --git a/src/index.html b/src/index.html index 3a7ac0a..2e6816d 100644 --- a/src/index.html +++ b/src/index.html @@ -4,6 +4,7 @@ Kubistudio - Multi-Environment Pipeline POC +