Compare commits
23 Commits
eea20775d3
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 08c40162b0 | |||
| f48307410e | |||
| 5d85b4b741 | |||
| d4c3affa2f | |||
| 0a798cf3b0 | |||
| 252fbe5003 | |||
| dc86eb2bf2 | |||
| 87faff525c | |||
| e47445e457 | |||
| cf1cb65168 | |||
| d6b17268dd | |||
| 76cb35cc98 | |||
| 6e72bfed41 | |||
| 14dba2c7e8 | |||
| 4e5d4ef5cb | |||
| d53398ca0c | |||
| e077fdec07 | |||
| 4cdafbc020 | |||
| cb12d68fbe | |||
| c52dcda03b | |||
| a1cd748e0f | |||
| 4dd96a267d | |||
| 1171e15503 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
opencode.json
|
||||
+37
@@ -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;"]
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
@@ -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
@@ -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ón del Ambiente</div>
|
||||
<div class="metrics-grid" id="metricsGrid">
|
||||
<div class="metric" id="metric-version">
|
||||
<span class="metric-label">Versió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">Ú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>
|
||||
© <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 — 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>
|
||||
Reference in New Issue
Block a user