feat: add initial multi-environment CI/CD pipeline POC #1
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
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,223 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
<!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>
|
||||
<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