25 Commits

Author SHA1 Message Date
nietzshn 7e4b8814bd Merge pull request 'feat: add initial multi-environment CI/CD pipeline POC' (#3) from staging into main
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy Production / Build and Push (push) Successful in 14s
CI Pipeline / Build Docker Image (push) Successful in 58s
Deploy Production / Deploy to Production (push) Successful in 9s
CI Pipeline / Security Scan (push) Successful in 10s
Deploy Production / Release Notes (push) Successful in 1s
Reviewed-on: #3
2026-06-02 22:31:08 -06:00
nietzshn 4c972e1be2 Merge pull request 'chore: update Gitea workflow summaries to use GITHUB_STEP_SUMMARY, switch to env variables, and add QA notification step' (#2) from dev into staging
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy Staging / Build and Push (push) Successful in 14s
CI Pipeline / Build Docker Image (push) Successful in 57s
CI Pipeline / Security Scan (push) Successful in 10s
Deploy Staging / Notification (push) Successful in 1s
Deploy Staging / Deploy to Staging (push) Successful in 9s
CI Pipeline / HTML Lint (pull_request) Successful in 9s
CI Pipeline / Build Docker Image (pull_request) Successful in 57s
CI Pipeline / Security Scan (pull_request) Successful in 10s
Reviewed-on: #2
2026-06-02 22:25:33 -06:00
nietzshn 08c40162b0 Merge branch 'staging' into dev
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 14s
CI Pipeline / HTML Lint (pull_request) Successful in 7s
CI Pipeline / Build Docker Image (push) Successful in 58s
Deploy QA / Deploy to QA (push) Successful in 8s
CI Pipeline / Build Docker Image (pull_request) Successful in 56s
Deploy QA / Notification (push) Successful in 1s
CI Pipeline / Security Scan (pull_request) Successful in 11s
CI Pipeline / Security Scan (push) Successful in 11s
2026-06-02 22:25:24 -06:00
nietzshn f48307410e chore: update Gitea workflow summaries to use GITHUB_STEP_SUMMARY, switch to env variables, and add QA notification step
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 14s
CI Pipeline / Build Docker Image (push) Successful in 58s
Deploy QA / Deploy to QA (push) Successful in 9s
CI Pipeline / Security Scan (push) Successful in 11s
Deploy QA / Notification (push) Successful in 1s
CI Pipeline / HTML Lint (pull_request) Successful in 9s
CI Pipeline / Build Docker Image (pull_request) Successful in 57s
CI Pipeline / Security Scan (pull_request) Successful in 12s
2026-06-02 22:09:30 -06:00
nietzshn 5d85b4b741 Merge pull request 'feat: add initial multi-environment CI/CD pipeline POC' (#1) from dev into staging
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy Staging / Build and Push (push) Successful in 15s
CI Pipeline / Build Docker Image (push) Successful in 1m0s
Deploy Staging / Deploy to Staging (push) Successful in 8s
CI Pipeline / Security Scan (push) Successful in 11s
Deploy Staging / Notification (push) Failing after 1s
Reviewed-on: #1
2026-06-02 22:04:02 -06:00
nietzshn d4c3affa2f feat: implement dynamic runtime environment configuration via env-config.js injection and update project documentation
CI Pipeline / HTML Lint (push) Successful in 8s
Deploy QA / Build and Push (push) Successful in 16s
CI Pipeline / Build Docker Image (push) Successful in 1m7s
Deploy QA / Deploy to QA (push) Successful in 9s
CI Pipeline / Security Scan (push) Successful in 12s
CI Pipeline / HTML Lint (pull_request) Successful in 8s
CI Pipeline / Build Docker Image (pull_request) Successful in 58s
CI Pipeline / Security Scan (pull_request) Successful in 12s
2026-06-02 21:59:20 -06:00
nietzshn 0a798cf3b0 refactor: replace appleboy/ssh-action with native ssh command execution in deployment workflows
CI Pipeline / HTML Lint (push) Successful in 8s
Deploy QA / Build and Push (push) Successful in 15s
CI Pipeline / Build Docker Image (push) Successful in 57s
Deploy QA / Deploy to QA (push) Successful in 8s
CI Pipeline / Security Scan (push) Successful in 11s
2026-06-02 21:50:48 -06:00
nietzshn 252fbe5003 refactor: switch ssh remote variable passing to positional arguments for deploy-qa pipeline
CI Pipeline / HTML Lint (push) Successful in 11s
Deploy QA / Build and Push (push) Successful in 21s
CI Pipeline / Build Docker Image (push) Successful in 1m3s
Deploy QA / Deploy to QA (push) Successful in 9s
CI Pipeline / Security Scan (push) Successful in 21s
2026-06-02 21:11:41 -06:00
nietzshn dc86eb2bf2 ci: use ssh-agent with dedicated deploy key (no passphrase) 2026-06-01 22:40:59 -06:00
nietzshn 87faff525c ci: use base64-encoded SSH key to preserve newlines 2026-06-01 22:38:17 -06:00
nietzshn e47445e457 ci: use sshpass with env vars for passphrase-protected SSH keys
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 16s
CI Pipeline / Build Docker Image (push) Successful in 56s
CI Pipeline / Security Scan (push) Successful in 10s
Deploy QA / Deploy to QA (push) Failing after 18s
2026-06-01 22:34:38 -06:00
nietzshn cf1cb65168 ci: replace appleboy ssh action with native ssh-agent
CI Pipeline / HTML Lint (push) Successful in 8s
Deploy QA / Build and Push (push) Successful in 15s
CI Pipeline / Build Docker Image (push) Successful in 57s
Deploy QA / Deploy to QA (push) Failing after 1s
CI Pipeline / Security Scan (push) Successful in 10s
2026-06-01 22:29:56 -06:00
nietzshn d6b17268dd ci: add DEPLOY_PASSPHRASE secret for SSH key passphrase
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Successful in 56s
Deploy QA / Deploy to QA (push) Failing after 3s
CI Pipeline / Security Scan (push) Successful in 10s
2026-06-01 22:22:50 -06:00
nietzshn 76cb35cc98 ci: remove broken summary step
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 15s
CI Pipeline / Build Docker Image (push) Successful in 56s
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Security Scan (push) Successful in 11s
Deploy QA / Notification (push) Failing after 1s
2026-06-01 22:15:37 -06:00
nietzshn 6e72bfed41 fix: upgrade libxml2 to fix CVE-2026-6732
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 14s
CI Pipeline / Build Docker Image (push) Successful in 57s
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Security Scan (push) Successful in 11s
Deploy QA / Notification (push) Failing after 1s
CI Pipeline / Generate Summary (push) Failing after 0s
2026-06-01 22:12:58 -06:00
nietzshn 14dba2c7e8 ci: replace trivy github action with docker-based scan
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 15s
CI Pipeline / Build Docker Image (push) Successful in 56s
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Security Scan (push) Failing after 24s
Deploy QA / Notification (push) Failing after 2s
CI Pipeline / Generate Summary (push) Failing after 1s
2026-06-01 22:08:29 -06:00
nietzshn 4e5d4ef5cb ci: remove flaky health check verification step
CI Pipeline / HTML Lint (push) Successful in 9s
Deploy QA / Build and Push (push) Successful in 12s
CI Pipeline / Build Docker Image (push) Successful in 56s
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Security Scan (push) Failing after 7m26s
Deploy QA / Notification (push) Failing after 1s
CI Pipeline / Generate Summary (push) Failing after 1s
2026-06-01 21:34:16 -06:00
nietzshn d53398ca0c ci: use custom bridge network to avoid port conflicts and get reliable container IP
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Failing after 28m28s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
2026-06-01 20:43:11 -06:00
nietzshn e077fdec07 ci: use random host port to avoid conflict with existing services
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 14s
CI Pipeline / Build Docker Image (push) Failing after 1m35s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
2026-06-01 20:37:35 -06:00
nietzshn 4cdafbc020 ci: use network host mode for health check instead of container IP
CI Pipeline / HTML Lint (push) Successful in 14s
Deploy QA / Build and Push (push) Successful in 23s
CI Pipeline / Build Docker Image (push) Failing after 1m39s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
2026-06-01 20:33:58 -06:00
Nicholas Ceballos cb12d68fbe build(docker): replace envsubst with sed, drop gettext
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Failing after 28m28s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 1s
CI Pipeline / Generate Summary (push) Failing after 0s
Deploy QA / Notification (push) Failing after 1s
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.
2026-06-01 17:25:21 -06:00
Nicholas Ceballos c52dcda03b feat(docker): add env var substitution for nginx config at runtime
CI Pipeline / HTML Lint (push) Successful in 6s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Failing after 1m36s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 2s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
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
2026-06-01 17:11:18 -06:00
Nicholas Ceballos a1cd748e0f ci(gitea): update health check to use container IP
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Failing after 1m35s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 1s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
remove published port mapping and use docker-inspected internal container IP for health checks to avoid port conflicts and reduce exposed network ports
2026-06-01 17:03:24 -06:00
Nicholas Ceballos 4dd96a267d ci: add docker container cleanup before test run
CI Pipeline / HTML Lint (push) Successful in 7s
Deploy QA / Build and Push (push) Successful in 13s
CI Pipeline / Build Docker Image (push) Failing after 37s
CI Pipeline / Security Scan (push) Has been skipped
Deploy QA / Deploy to QA (push) Failing after 1s
CI Pipeline / Generate Summary (push) Failing after 1s
Deploy QA / Notification (push) Failing after 1s
add docker stop and rm commands for ci-test container with || true to avoid name conflicts from leftover containers from prior CI runs
2026-06-01 15:39:45 -06:00
Nicholas Ceballos 1171e15503 feat: add initial multi-environment CI/CD pipeline POC
CI Pipeline / HTML Lint (push) Successful in 1m4s
Deploy QA / Build and Push (push) Successful in 42s
Deploy QA / Deploy to QA (push) Failing after 7s
Deploy QA / Notification (push) Failing after 1s
CI Pipeline / Build Docker Image (push) Failing after 35s
CI Pipeline / Security Scan (push) Has been skipped
CI Pipeline / Generate Summary (push) Failing after 1s
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
2026-06-01 14:23:20 -06:00
11 changed files with 1319 additions and 2 deletions
+84
View File
@@ -0,0 +1,84 @@
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: 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
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
+186
View File
@@ -0,0 +1,186 @@
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
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 "$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 \
-e APP_ENV=production \
-e APP_VERSION=${APP_VERSION} \
-e DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
$REGISTRY_URL/$IMAGE_NAME:$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
EOF
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' >> $GITHUB_STEP_SUMMARY
## Production Deployment Successful :rocket:
**Version:** ${{ needs.build-and-push.outputs.app_version }}
**Commit:** ${{ gitea.sha }}
**Image:** ${{ env.REGISTRY_URL }}/${{ env.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 >> $GITHUB_STEP_SUMMARY || true
cat << 'SUMMARY' >> $GITHUB_STEP_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 \${{ env.REGISTRY_URL }}/\${{ env.IMAGE_NAME }}:stable
```
SUMMARY
+138
View File
@@ -0,0 +1,138 @@
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
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="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 \
-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
echo "::notice::QA deployment healthy"
exit 0
fi
sleep 5
done
echo "::error::QA health check failed"
exit 1
EOF
notify:
name: Notification
runs-on: ubuntu-latest
needs: [build-and-push, deploy]
if: always()
steps:
- name: Write summary
run: |
cat << 'SUMMARY' >> $GITHUB_STEP_SUMMARY
## QA Deployment ${{ needs.deploy.result }}
**Branch:** dev
**Commit:** ${{ gitea.sha }}
**Image:** ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:qa-latest
**URL:** https://practicas.qa.kubistudio.cloud
SUMMARY
+146
View File
@@ -0,0 +1,146 @@
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
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 "$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 \
-e APP_ENV=staging \
-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
EOF
notify:
name: Notification
runs-on: ubuntu-latest
needs: [build-and-push, deploy]
if: always()
steps:
- name: Write summary
run: |
cat << 'SUMMARY' >> $GITHUB_STEP_SUMMARY
## Staging Deployment ${{ needs.deploy.result }}
**Branch:** staging
**Commit:** ${{ gitea.sha }}
**Image:** ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:staging-latest
**URL:** https://practicas.staging.kubistudio.cloud
SUMMARY
+1
View File
@@ -0,0 +1 @@
opencode.json
+37
View File
@@ -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 && apk upgrade --no-cache libxml2
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;"]
+227 -2
View File
@@ -1,3 +1,228 @@
# multi-env-pipeline-poc # CI/CD Multi-Environment Pipeline POC
CI/CD Multi-Environment Strategy (PoC) Prueba de concepto de un pipeline CI/CD multi-ambiente con Gitea Actions, Docker y Nginx.
## 📌 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/*
|
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)
```
### Detalle por Entorno
* **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-<hash>`.
* **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.
* **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.
* **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-<hash>`).
* **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
### 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
```
+29
View File
@@ -0,0 +1,29 @@
#!/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
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 "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -euo pipefail
curl -sf --max-time 3 http://localhost:80/health > /dev/null 2>&1 || exit 1
+58
View File
@@ -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;
}
}
}
+409
View File
@@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kubistudio - Multi-Environment Pipeline POC</title>
<script src="/env-config.js"></script>
<style>
:root {
--accent: #F59E0B;
--accent-bg: rgba(245, 158, 11, 0.1);
--accent-border: rgba(245, 158, 11, 0.3);
--bg: #0f172a;
--card-bg: #1e293b;
--card-border: #334155;
--text: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--success: #10B981;
--danger: #ef4444;
--warning: #F59E0B;
--warning-bg: rgba(245, 158, 11, 0.15);
--warning-text: #fbbf24;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.env-warning {
background: var(--warning-bg);
border-bottom: 1px solid var(--accent-border);
color: var(--warning-text);
text-align: center;
padding: 8px 16px;
font-size: 0.875rem;
font-weight: 500;
display: none;
}
.env-warning.visible { display: block; }
.container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
gap: 24px;
max-width: 720px;
margin: 0 auto;
width: 100%;
}
header {
text-align: center;
width: 100%;
}
.logo-area {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 8px;
}
.logo-icon {
width: 40px;
height: 40px;
background: var(--accent);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
flex-shrink: 0;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.025em;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
background: var(--accent-bg);
color: var(--accent);
border: 1px solid var(--accent-border);
margin-top: 8px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 32px;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
}
.card-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 20px;
font-weight: 600;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
}
.metric {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 16px;
background: rgba(255,255,255,0.03);
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.05);
}
.metric.hidden { display: none; }
.metric-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
font-weight: 600;
}
.metric-value {
font-size: 1rem;
font-weight: 500;
color: var(--text);
word-break: break-all;
}
.metric-value.code {
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
font-size: 0.85rem;
}
.health-section {
width: 100%;
}
.health-status {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-radius: 10px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
font-size: 0.9rem;
}
.health-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
flex-shrink: 0;
transition: background 0.3s;
}
.health-indicator.ok { background: var(--success); }
.health-indicator.fail { background: var(--danger); }
.health-detail {
color: var(--text-secondary);
font-size: 0.8rem;
margin-left: auto;
}
.health-error {
color: var(--danger);
font-size: 0.8rem;
}
footer {
text-align: center;
color: var(--text-muted);
font-size: 0.75rem;
padding: 16px 24px;
border-top: 1px solid var(--card-border);
}
@media (max-width: 480px) {
.container { padding: 16px; gap: 16px; }
.card { padding: 20px; }
.metrics-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
.metric { padding: 10px 12px; }
.metric-value { font-size: 0.85rem; }
h1 { font-size: 1.25rem; }
}
</style>
</head>
<body>
<div id="envWarning" class="env-warning"></div>
<div class="container">
<header>
<div class="logo-area">
<div class="logo-icon" id="logoIcon">K</div>
<h1>Kubistudio Pipeline</h1>
</div>
<div class="badge" id="envBadge">⚙️ Cargando...</div>
</header>
<div class="card" id="mainCard">
<div class="card-title">Informaci&oacute;n del Ambiente</div>
<div class="metrics-grid" id="metricsGrid">
<div class="metric" id="metric-version">
<span class="metric-label">Versi&oacute;n</span>
<span class="metric-value" id="val-version"></span>
</div>
<div class="metric" id="metric-env">
<span class="metric-label">Ambiente</span>
<span class="metric-value" id="val-env"></span>
</div>
<div class="metric" id="metric-deploy">
<span class="metric-label">&Uacute;ltimo Deploy</span>
<span class="metric-value" id="val-deploy"></span>
</div>
<div class="metric hidden" id="metric-commit">
<span class="metric-label">Git Commit</span>
<span class="metric-value code" id="val-commit"></span>
</div>
<div class="metric hidden" id="metric-branch">
<span class="metric-label">Git Branch</span>
<span class="metric-value code" id="val-branch"></span>
</div>
<div class="metric hidden" id="metric-build">
<span class="metric-label">Build Number</span>
<span class="metric-value" id="val-build"></span>
</div>
<div class="metric hidden" id="metric-build-date">
<span class="metric-label">Build Date</span>
<span class="metric-value" id="val-build-date"></span>
</div>
</div>
</div>
<div class="health-section">
<div class="health-status">
<span class="health-indicator" id="healthDot"></span>
<span id="healthLabel">Verificando estado del servicio...</span>
<span class="health-detail" id="healthDetail"></span>
</div>
</div>
</div>
<footer>
&copy; <span id="year"></span> Kubistudio. All rights reserved.
</footer>
<script>
(function() {
var env = window.__ENV__ || {};
var appEnv = env.APP_ENV || 'development';
var appVersion = env.APP_VERSION || '0.0.0';
var buildDate = env.BUILD_DATE || '';
var gitCommit = env.GIT_COMMIT || '';
var gitBranch = env.GIT_BRANCH || '';
var deployTime = env.DEPLOY_TIME || '';
var buildNumber = env.BUILD_NUMBER || '';
var config = {
qa: {
accent: '#F59E0B',
badge: '\uD83E\uDDEA Ambiente de Pruebas',
warning: '\u26A0\uFE0F Este no es un ambiente de producci\u00F3n. Los datos pueden ser modificados o eliminados.',
showAll: true,
logoBg: '#F59E0B'
},
staging: {
accent: '#3B82F6',
badge: '\uD83D\uDD35 Ambiente de Staging',
warning: '\u26A0\uFE0F Release candidate &mdash; pre-producci\u00F3n. Verificar antes de liberar.',
showAll: false,
showCommit: true,
showBranch: false,
showBuild: false,
showBuildDate: true,
logoBg: '#3B82F6'
},
production: {
accent: '#10B981',
badge: '\u2705 Ambiente de Producci\u00F3n',
warning: '',
showAll: false,
showCommit: false,
showBranch: false,
showBuild: false,
showBuildDate: true,
logoBg: '#10B981'
}
};
var envKey = appEnv.toLowerCase();
var cfg = config[envKey] || config.qa;
document.documentElement.style.setProperty('--accent', cfg.accent);
document.documentElement.style.setProperty('--accent-bg', cfg.accent + '18');
document.documentElement.style.setProperty('--accent-border', cfg.accent + '4d');
var logoIcon = document.getElementById('logoIcon');
if (logoIcon) {
logoIcon.style.background = cfg.logoBg;
logoIcon.textContent = envKey.charAt(0).toUpperCase();
}
var badge = document.getElementById('envBadge');
if (badge) badge.textContent = cfg.badge;
var warning = document.getElementById('envWarning');
if (cfg.warning) {
warning.innerHTML = cfg.warning;
warning.classList.add('visible');
}
function setVal(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = val || '—';
}
setVal('val-version', appVersion);
setVal('val-env', appEnv.toUpperCase());
setVal('val-deploy', deployTime ? new Date(deployTime).toLocaleString() : '—');
setVal('val-commit', gitCommit);
setVal('val-branch', gitBranch);
setVal('val-build', buildNumber);
setVal('val-build-date', buildDate ? new Date(buildDate).toLocaleString() : '—');
function toggleMetric(id, show) {
var el = document.getElementById(id);
if (el) el.classList.toggle('hidden', !show);
}
if (cfg.showAll) {
toggleMetric('metric-commit', true);
toggleMetric('metric-branch', true);
toggleMetric('metric-build', true);
toggleMetric('metric-build-date', true);
} else {
toggleMetric('metric-commit', cfg.showCommit);
toggleMetric('metric-branch', cfg.showBranch);
toggleMetric('metric-build', cfg.showBuild);
toggleMetric('metric-build-date', cfg.showBuildDate);
}
var healthDot = document.getElementById('healthDot');
var healthLabel = document.getElementById('healthLabel');
var healthDetail = document.getElementById('healthDetail');
function checkHealth() {
fetch('/health')
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
healthDot.className = 'health-indicator ok';
healthLabel.textContent = 'Servicio operativo';
healthDetail.textContent = data.env ? data.env.toUpperCase() + ' \u00B7 ' + data.version : '';
})
.catch(function(err) {
healthDot.className = 'health-indicator fail';
healthLabel.textContent = 'Error de conexi\u00F3n';
healthDetail.textContent = err.message;
});
}
checkHealth();
setInterval(checkHealth, 30000);
document.getElementById('year').textContent = new Date().getFullYear();
})();
</script>
</body>
</html>