feat: add initial multi-environment CI/CD pipeline POC #3
@@ -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