Files
ansible-gitea/.gitlab-ci.include.yml
T
2025-12-05 11:18:26 +07:00

1871 lines
69 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ============================================================================
# OPTIMIZED GITLAB CI/CD PIPELINE FOR NODE.JS APPLICATIONS
# ============================================================================
# Features:
# - Auto-detection: Next.js, NestJS, React.js, Express.js frameworks
# - Flexible Authentication: SSH keys + passwords per environment
# - GitLab Self-hosted Compatible
# - Robust Detection: CI basic → Ansible comprehensive fallback
# - No Auto-Nginx: Manual control with generated templates
# - Single Server: Port management + resource optimization
# - Production Ready: Security, caching, zero-downtime deployment
include:
- project: '$PROJECT_CI_ANSIBLE_PATH'
ref: 'main'
file: '.gitlab-ci/${CI_PROJECT_NAME}.yml'
rules:
- exists:
- '.gitlab-ci/${CI_PROJECT_NAME}.yml'
# ============================================================================
# GLOBAL CONFIGURATION
# ============================================================================
variables:
# Docker and Build Configuration
DOCKER_DRIVER: overlay2
DOCKER_BUILDKIT: "1"
BUILDKIT_PROGRESS: plain
# Auto-detected Application Info
APP_NAME: "${CI_PROJECT_NAME}"
DOCKER_REGISTRY: "${CI_REGISTRY}"
DOCKER_IMAGE: "${CI_REGISTRY_IMAGE}"
DEPLOY_TYPE: "${DEPLOY_TYPE:-docker}"
BUILD_CACHE_ENABLED: "${BUILD_CACHE_ENABLED:-true}"
# GitLab Self-hosted Detection
GITLAB_HOST: "${CI_SERVER_HOST}"
GITLAB_PORT: "${CI_SERVER_PORT:-443}"
GITLAB_ANSIBLE_USERNAME: "${GITLAB_ANSIBLE_USERNAME:-}"
GITLAB_ANSIBLE_TOKEN: "${GITLAB_ANSIBLE_TOKEN:-}"
# Ansible Configuration
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_STDOUT_CALLBACK: "yaml"
ANSIBLE_FORCE_COLOR: "True"
# VAULT
VAULT_ENGINE_NAME: "${CI_PROJECT_NAME}"
VAULT_ADDR: "https://vault.forlife.one"
VAULT_USERNAME: "${CI_VAULT_USERNAME}"
VAULT_PASSWORD: "${CI_VAULT_PASSWORD}"
# Global cache for dependencies
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
- .npm/
- .yarn/
- .pnpm-store/
policy: push
when: on_success
# Pipeline stages
stages:
- prepare
- generate
- test
- security
- build
- deploy
- verify
- cleanup
# ============================================================================
# REUSABLE TEMPLATES
# ============================================================================
.docker_template: &docker_template
image: gitlab.forlife.one:5050/share/docker:24-dind
services:
- gitlab.forlife.one:5050/share/docker:24-dind
before_script:
- apk add --no-cache bash
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- |
echo "⏳ Waiting for Docker daemon..."
for i in {1..10}; do
if docker info > /dev/null 2>&1; then
echo "✅ Docker is ready!"
break
fi
echo "❌ Docker not ready (attempt $i), retrying in 3s..."
sleep 3
done
.ansible_template: &ansible_template
image: gitlab.forlife.one:5050/share/docker:ansible-2.13
before_script:
- apk add --no-cache openssh-client ansible git nodejs npm sshpass wget bash curl jq bc unzip
- curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip
- unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault
- pip install ansible
# Setup SSH agent
- eval $(ssh-agent -s)
# Flexible SSH key setup based on environment
- |
echo "Setting up authentication for environment: ${ENVIRONMENT}"
if [ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]; then
echo "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" | tr -d '\r' | ssh-add -
echo "✓ Using Deployment SSH key"
else
echo "ℹ No SSH key found, will use password authentication"
fi
# Setup SSH configuration
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "Host *" >> ~/.ssh/config
- echo " StrictHostKeyChecking no" >> ~/.ssh/config
- echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config
- echo " LogLevel ERROR" >> ~/.ssh/config
# Add known hosts
- |
if [ -n "$DEPLOYMENT_SERVER_HOST" ]; then
ssh-keyscan -H "$DEPLOYMENT_SERVER_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
fi
# Clone deployment tools - try multiple methods
- |
echo "Attempting to clone deployment tools..."
rm -rf ci
if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with CI_JOB_TOKEN"
elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN"
fi
# Install Ansible requirements
#- ansible-galaxy install -r requirements.yml || echo "No requirements.yml found"
# ============================================================================
# PREPARE STAGE - ENHANCED DETECTION
# ============================================================================
prepare:auto-detect:
stage: prepare
image: gitlab.forlife.one:5050/share/node:18-alpine
before_script:
- apk add --no-cache jq curl
script:
- echo "🔍 Starting auto-detection process..."
# Enhanced Node.js version detection
- |
echo "Detecting Node.js version..."
NODE_VERSION=""
if [ -f ".nvmrc" ]; then
NODE_VERSION=$(cat .nvmrc | tr -d 'v' | tr -d '\n' | head -1)
if [ -n "$NODE_VERSION" ]; then
echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env
echo "✓ Detected Node.js version from .nvmrc: ${NODE_VERSION}"
else
echo "⚠ Empty .nvmrc file, deferring to Ansible"
fi
elif [ -f "package.json" ] && jq -e '.engines.node' package.json > /dev/null 2>&1; then
RAW_VERSION=$(jq -r '.engines.node' package.json)
NODE_VERSION=$(echo "$RAW_VERSION" | grep -oE '[0-9]+' | head -n 1)
if [ -n "$NODE_VERSION" ] && [ "$NODE_VERSION" != "$RAW_VERSION" ]; then
echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env
echo "✓ Detected Node.js version from package.json: ${NODE_VERSION} (from ${RAW_VERSION})"
else
echo "⚠ Complex Node.js version format, deferring to Ansible: ${RAW_VERSION}"
fi
fi
if [ -n "$NODEJS_VERSION" ]; then
NODE_VERSION=$(echo "$NODEJS_VERSION" | grep -oE '[0-9]+' | head -n 1)
echo "✓ NODE_VERSION: ${NODEJS_VERSION}"
echo "NODE_VERSION=${NODEJS_VERSION}" >> deploy.env
fi
# Enhanced framework detection
- |
echo "Detecting framework..."
# Detect PHP / Laravel first (composer.json or artisan file)
if [ -f "composer.json" ] || [ -f "artisan" ]; then
if grep -q '"laravel/framework"' composer.json 2>/dev/null || [ -f "artisan" ]; then
FRAMEWORK="laravel"
CONTAINER_PORT="3000"
echo "✓ Detected Laravel (PHP) framework"
fi
elif [ -f "package.json" ]; then
# Check for common frameworks
if jq -e '.dependencies["@nestjs/core"] or .dependencies["@nestjs/common"]' package.json > /dev/null 2>&1; then
FRAMEWORK="nestjs"
CONTAINER_PORT="3000"
echo "✓ Detected NestJS framework"
elif jq -e '.dependencies["next"]' package.json > /dev/null 2>&1; then
FRAMEWORK="nextjs"
CONTAINER_PORT="3000"
echo "✓ Detected Next.js framework"
elif jq -e '.dependencies["react"] and .dependencies["react-scripts"]' package.json > /dev/null 2>&1; then
FRAMEWORK="reactjs"
CONTAINER_PORT="3000"
echo "✓ Detected React.js (CRA) framework"
elif jq -e '.dependencies["react"] and .dependencies["vite"]' package.json > /dev/null 2>&1; then
FRAMEWORK="reactjs-vite"
CONTAINER_PORT="3000"
echo "✓ Detected React.js (Vite) framework"
elif jq -e '.dependencies["express"]' package.json > /dev/null 2>&1; then
FRAMEWORK="expressjs"
CONTAINER_PORT="3000"
echo "✓ Detected Express.js framework"
elif jq -e '.dependencies["fastify"]' package.json > /dev/null 2>&1; then
FRAMEWORK="fastify"
CONTAINER_PORT="3000"
echo "✓ Detected Fastify framework"
elif jq -e '.dependencies["@angular/core"]' package.json > /dev/null 2>&1; then
FRAMEWORK="angular"
CONTAINER_PORT="3000"
echo "✓ Detected Angular framework"
else
FRAMEWORK=""
CONTAINER_PORT="3000"
echo "ℹ Framework will be detected by Ansible (complex dependencies)"
fi
else
FRAMEWORK=""
CONTAINER_PORT="3000"
echo "ℹ No package.json found, framework will be detected by Ansible"
fi
echo "FRAMEWORK=${FRAMEWORK}" >> deploy.env
echo "CONTAINER_PORT=${CONTAINER_PORT}" >> deploy.env
# Check if this is a frontend project
if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then
export IS_FRONTEND="true"
echo "IS_FRONTEND=true" >> deploy.env
else
echo "IS_FRONTEND=false" >> deploy.env
fi
# Enhanced package manager detection
- |
echo "Detecting package manager..."
if [ -f "composer.json" ]; then
PACKAGE_MANAGER="composer"
echo "✓ Detected Composer (composer.json found)"
elif [ -f "pnpm-lock.yaml" ]; then
PACKAGE_MANAGER="pnpm"
echo "✓ Detected PNPM (pnpm-lock.yaml found)"
elif [ -f "yarn.lock" ]; then
PACKAGE_MANAGER="yarn"
echo "✓ Detected Yarn (yarn.lock found)"
elif [ -f "package-lock.json" ]; then
PACKAGE_MANAGER="npm"
echo "✓ Detected NPM (package-lock.json found)"
elif [ -f "package.json" ]; then
PACKAGE_MANAGER="npm"
echo "✓ Defaulting to NPM (package.json found)"
else
PACKAGE_MANAGER=""
echo "ℹ Package manager will be detected by Ansible"
fi
echo "PACKAGE_MANAGER=${PACKAGE_MANAGER}" >> deploy.env
# Enhanced command detection
- |
echo "Detecting build and start commands..."
if [ -f "package.json" ]; then
BUILD_CMD=$(jq -r '.scripts.build // ""' package.json)
START_CMD=$(jq -r '.scripts.start // ""' package.json)
if [ "$BUILD_CMD" != "" ] && [ "$BUILD_CMD" != "null" ]; then
echo "✓ Detected build command: ${BUILD_CMD}"
else
BUILD_CMD=""
echo "ℹ Build command will be detected by Ansible"
fi
if [ "$START_CMD" != "" ] && [ "$START_CMD" != "null" ]; then
echo "✓ Detected start command: ${START_CMD}"
else
START_CMD=""
echo "ℹ Start command will be detected by Ansible"
fi
else
BUILD_CMD=""
START_CMD=""
echo "ℹ Build and start commands will be detected by Ansible"
fi
echo "BUILD_COMMAND=${BUILD_CMD}" >> deploy.env
echo "START_COMMAND=${START_CMD}" >> deploy.env
# Detection summary
- |
echo ""
echo "📋 AUTO-DETECTION SUMMARY"
echo "========================="
echo "Project: ${CI_PROJECT_NAME}"
echo "Framework: ${FRAMEWORK}"
echo "Node.js: ${NODE_VERSION}"
echo "Package Manager: ${PACKAGE_MANAGER}"
echo "Container Port: ${CONTAINER_PORT}"
echo "Build Command: ${BUILD_CMD}"
echo "Start Command: ${START_CMD}"
echo ""
echo "Note: Complex cases will be handled by Ansible's enhanced detection"
- echo "🌍 Preparing deployment environment..."
- echo "APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}" >> deploy.env
- echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> deploy.env
- echo "GIT_COMMIT=${CI_COMMIT_SHA}" >> deploy.env
- echo "GIT_BRANCH=${CI_COMMIT_REF_NAME}" >> deploy.env
- echo "GIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}" >> deploy.env
- echo "PIPELINE_ID=${CI_PIPELINE_ID}" >> deploy.env
# Pass through detected values with prefixes for Ansible
- echo "CI_DETECTED_NODE_VERSION=${NODE_VERSION:-}" >> deploy.env
- echo "CI_DETECTED_FRAMEWORK=${FRAMEWORK:-}" >> deploy.env
- echo "CI_DETECTED_PACKAGE_MANAGER=${PACKAGE_MANAGER:-}" >> deploy.env
- echo "CI_DETECTED_BUILD_COMMAND=${BUILD_COMMAND:-}" >> deploy.env
- echo "CI_DETECTED_START_COMMAND=${START_COMMAND:-}" >> deploy.env
- echo "CI_DETECTED_CONTAINER_PORT=${CONTAINER_PORT:-3000}" >> deploy.env
- echo "✓ Environment variables prepared"
- |
# Detect environment
if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then
ENV="development"
elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then
ENV="staging"
elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then
ENV="production"
else
ENV="development"
fi
- echo "ENVIRONMENT=${ENVIRONMENT:-$ENV}" >> deploy.env
artifacts:
reports:
dotenv: deploy.env
expire_in: 2 hours
rules:
- if: $CI_COMMIT_TAG
# ============================================================================
# TEST STAGE - FRAMEWORK AWARE
# ============================================================================
# ============================================================================
# SECURITY STAGE
# ============================================================================
# ============================================================================
# GENERATE STAGE - DYNAMIC DOCKERFILE CREATION
# ============================================================================
generate:dockerfile:
<<: *ansible_template
stage: generate
needs: ["prepare:auto-detect", "prepare:frontend-env"]
image: gitlab.forlife.one:5050/share/docker:alpine-latest
before_script:
- apk add --no-cache envsubst git curl jq
script:
- |
echo "Attempting to clone deployment tools..."
rm -rf ci
if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with CI_JOB_TOKEN"
elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN"
fi
- |
echo "🔧 Generating Dockerfile for ${FRAMEWORK:-nodejs} framework..."
# Check if Dockerfile already exists
if [ "$FRAMEWORK" = "LARAVEL" ] && [ -f "Dockerfile" ]; then
echo "✅ Dockerfile already exists, using existing one"
echo "DOCKERFILE_GENERATED=false" >> dockerfile.env
exit 0
fi
if [ ! -d "ci" ]; then
echo "⚠️ ci folder does not exists, using existing one"
exit 0
fi
# Detect environment
if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then
ENV="development"
elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then
ENV="staging"
elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then
ENV="production"
else
ENV="development"
fi
# Determine MEMORY_LIMIT with fallback logic
MEMORY_LIMIT=${MEMORY_LIMIT:-}
if [ -z "$MEMORY_LIMIT" ]; then
echo "🔍 MEMORY_LIMIT not set in CI variables, checking config files..."
# Try project-specific config first
PROJECT_CONFIG="ci/projects/${APP_NAME}/group_vars/${ENV}.yml"
if [ -f "$PROJECT_CONFIG" ]; then
echo "📄 Found project config: $PROJECT_CONFIG"
MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$PROJECT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1)
fi
# Try vault config if still empty
if [ -z "$MEMORY_LIMIT" ]; then
VAULT_CONFIG="ci/group_vars/vault/${ENV}.yml"
if [ -f "$VAULT_CONFIG" ]; then
echo "📄 Found vault config: $VAULT_CONFIG"
MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$VAULT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1)
fi
fi
# Default fallback
if [ -z "$MEMORY_LIMIT" ]; then
echo "⚙️ No MEMORY_LIMIT found, using default: 2g"
MEMORY_LIMIT="2g"
else
echo "✅ Found MEMORY_LIMIT: $MEMORY_LIMIT"
fi
else
echo "✅ Using MEMORY_LIMIT from CI variables: $MEMORY_LIMIT"
fi
# Convert memory limit to number (in MB, without unit)
if echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+g$'; then
MEMORY_GB=$(echo "$MEMORY_LIMIT" | sed 's/g$//')
MEMORY_LIMIT_MB=$((MEMORY_GB * 1024))
echo "📊 Memory limit: ${MEMORY_GB}g = ${MEMORY_LIMIT_MB} (MB)"
elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+m$'; then
MEMORY_LIMIT_MB=$(echo "$MEMORY_LIMIT" | sed 's/m$//')
echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)"
elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+$'; then
MEMORY_LIMIT_MB=$MEMORY_LIMIT
echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)"
else
echo "⚠️ Invalid MEMORY_LIMIT format: $MEMORY_LIMIT, using default 2048"
MEMORY_LIMIT_MB=2048
fi
# Apply 80% factor to MEMORY_LIMIT_MB
MEMORY_LIMIT_MB=$(awk "BEGIN {printf \"%.0f\", $MEMORY_LIMIT_MB * 0.8}")
APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}
# Method 1: Check for local templates first (in ci/roles/docker/templates/dockerfiles/)
TEMPLATE_DIR="ci/roles/docker/templates/dockerfiles"
if [ -d "$TEMPLATE_DIR" ]; then
echo "📁 Found local template directory: $TEMPLATE_DIR"
USE_LOCAL_TEMPLATES=true
else
echo "📥 No local templates, will fetch from ansible repo"
USE_LOCAL_TEMPLATES=false
fi
# Determine framework-specific settings
FRAMEWORK="${FRAMEWORK:-}"
RUN_CMD="${RUN_CMD:-}"
case "$FRAMEWORK" in
"expressjs"|"fastify")
TEMPLATE_FILE="expressjs.Dockerfile.j2"
DEFAULT_PORT="3000"
RUN_CMD='["dumb-init", "node", "dist/main"]'
if [[ "$CI_COMMIT_TAG" == *worker* ]]; then
RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]'
fi
;;
"nestjs")
TEMPLATE_FILE="nestjs.Dockerfile.j2"
DEFAULT_PORT="3000"
RUN_CMD='["dumb-init", "node", "dist/main"]'
if [[ "$CI_COMMIT_TAG" == *worker* ]]; then
RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]'
fi
;;
"nextjs")
TEMPLATE_FILE="nextjs.Dockerfile.j2"
DEFAULT_PORT="3000"
#RUN_CMD='["npm", "start"]'
if [[ "$RUN_CMD" == "" ]]; then
RUN_CMD='["node", "server.js"]'
fi
;;
"reactjs"|"reactjs-vite"|"angular")
TEMPLATE_FILE="reactjs.Dockerfile.j2"
DEFAULT_PORT="3000"
RUN_CMD='[ "serve", "-s", "build" ]'
;;
"laravel")
TEMPLATE_FILE="laravel.Dockerfile.j2"
DEFAULT_PORT="3000"
RUN_CMD='[ "serve", "-s", "build" ]'
;;
*)
TEMPLATE_FILE="nodejs.Dockerfile.j2"
DEFAULT_PORT="3000"
RUN_CMD='["dumb-init", "node", "dist/main"]'
if [[ "$CI_COMMIT_TAG" == *worker* ]]; then
RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]'
fi
;;
esac
if [ "${FRAMEWORK}" = "laravel" ]; then
echo "Determining required PHP version based on Laravel version..."
# 1. Đọc phiên bản Laravel từ composer.json (ví dụ: "^10.0")
LARAVEL_CONSTRAINT=$(jq -r '.require."laravel/framework" // empty' composer.json)
if [ -z "$LARAVEL_CONSTRAINT" ]; then
echo "Laravel framework version not found in composer.json. Using default PHP 8.1"
PHP_VERSION="8.1"
else
# 2. Lấy số phiên bản chính (ví dụ: từ "^10.0" -> "10")
LARAVEL_MAJOR_VERSION=$(echo $LARAVEL_CONSTRAINT | tr -d '^~>=' | cut -d'.' -f1)
PHP_VERSION=""
# 3. Ánh xạ phiên bản Laravel sang phiên bản PHP yêu cầu
case "$LARAVEL_MAJOR_VERSION" in
"13")
PHP_VERSION="8.4"
;;
"12")
PHP_VERSION="8.3"
;;
"11")
PHP_VERSION="8.2"
;;
"10")
PHP_VERSION="8.1"
;;
"9")
PHP_VERSION="8.1"
;;
"8")
PHP_VERSION="8.1"
;;
*)
# Nếu không khớp, đặt một giá trị mặc định an toàn và cảnh báo
echo "Warning: Unsupported or unknown Laravel version ($LARAVEL_MAJOR_VERSION). Falling back to PHP 8.1."
PHP_VERSION="8.1"
;;
esac
echo "Detected Laravel version: $LARAVEL_MAJOR_VERSION -> Required PHP version: $PHP_VERSION"
fi
fi
echo "📦 Target template: ${TEMPLATE_FILE}"
echo "🚀 Framework: ${FRAMEWORK}"
echo "🔌 Port: ${CONTAINER_PORT:-$DEFAULT_PORT}"
# Method 1: Use local templates (if exists)
if [ "$USE_LOCAL_TEMPLATES" = "true" ] && [ -f "$TEMPLATE_DIR/$TEMPLATE_FILE" ]; then
echo "✅ Using local template: $TEMPLATE_DIR/$TEMPLATE_FILE"
TEMPLATE_PATH="$TEMPLATE_DIR/$TEMPLATE_FILE"
# Method 2: Fallback to inline templates
else
echo "⚠️ No template source configured, using inline fallback"
echo "💡 Set ANSIBLE_REPO_URL or TEMPLATE_BASE_URL for external templates"
TEMPLATE_PATH=""
fi
# Process template if found
if [ -n "$TEMPLATE_PATH" ] && [ -f "$TEMPLATE_PATH" ]; then
echo "🔄 Processing template: $TEMPLATE_PATH"
if [ -n "${BUILD_COMMAND:-}" ]; then
BUILD_COMMAND=$(echo "$BUILD_COMMAND" | sed 's/&&/\\\&\\\&/g')
fi
if [ -n "${START_COMMAND:-}" ]; then
START_COMMAND=$(echo "$START_COMMAND" | sed 's/&&/\\\&\\\&/g')
fi
# Set all variables for Jinja2-style replacement
export app_name="${APP_NAME}"
export node_version="${NODE_VERSION:-18}"
export package_manager="${PACKAGE_MANAGER:-npm}"
export build_command="${BUILD_COMMAND:-}"
export start_command="${START_COMMAND:-node index.js}"
export framework_settings_port="${CONTAINER_PORT:-$DEFAULT_PORT}"
export docker_image_tag="${APP_VERSION:-latest}"
export APP_PORT="${framework_settings_port}"
export PHP_VERSION="${PHP_VERSION:-8.1}"
export php_version="${PHP_VERSION:-8.1}"
export MEMORY_LIMIT_MB="${MEMORY_LIMIT_MB:-2048}"
# Add frontend environment variables if applicable
if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then
echo "🌐 Adding frontend environment variables..."
# Process .env file safely
while IFS='=' read -r key value || [ -n "$key" ]; do
# Skip comments and empty lines
case "$key" in
\#*|'')
continue
;;
esac
# Skip empty keys
if [ -z "$key" ]; then
continue
fi
# Clean the value (remove quotes if present)
case "$value" in
\"*\")
value=$(echo "$value" | sed 's/^"//' | sed 's/"$//')
;;
\'*\')
value=$(echo "$value" | sed "s/^'//" | sed "s/'$//")
;;
esac
export "${key}=${value}"
done < .env
fi
# Method A: Simple variable substitution (works for basic templates)
echo "🔧 Method A: Simple variable substitution"
sed -e "s|{{ app_name }}|${app_name}|g" \
-e "s|{{ node_version }}|${node_version}|g" \
-e "s|{{ package_manager }}|${package_manager}|g" \
-e "s|{{ build_command }}|${build_command}|g" \
-e "s|{{ start_command }}|${start_command}|g" \
-e "s|{{ framework_settings.port }}|${framework_settings_port}|g" \
-e "s|{{ framework_settings_port }}|${framework_settings_port}|g" \
-e "s|{{ docker_image_tag }}|${docker_image_tag}|g" \
-e "s|{{ RUN_CMD }}|${RUN_CMD}|g" \
-e "s|{{ php_version }}|${PHP_VERSION:-8.1}|g" \
-e "s|{{ MEMORY_LIMIT_MB }}|${MEMORY_LIMIT_MB:-2048}|g" \
"$TEMPLATE_PATH" > Dockerfile.tmp
# Method B: Advanced replacement for complex Jinja2 (if needed)
if grep -q "{%" "$TEMPLATE_PATH"; then
echo "🔧 Method B: Complex Jinja2 detected, using advanced processing"
# Install j2cli for proper Jinja2 processing (optional)
pip install j2cli 2>/dev/null || {
echo "⚠️ j2cli not available, falling back to sed replacement"
# Handle {% if %} blocks with sed (basic approach)
sed -i '/{% if start_command and start_command != /,/{% else %}/c\
# Using detected start command' Dockerfile.tmp
sed -i '/{% endif %}/d' Dockerfile.tmp
}
# Alternative: Use j2cli if available
if command -v j2 >/dev/null 2>&1; then
echo "✅ Using j2cli for proper Jinja2 processing"
# Create variables file
cat > /tmp/vars.json << EOF
{
"app_name": "${app_name}",
"node_version": "${node_version}",
"package_manager": "${package_manager}",
"build_command": "${build_command}",
"start_command": "${start_command}",
"php_version": "${PHP_VERSION:-8.1}",
"framework_settings_port": "${framework_settings_port}",
"framework_settings": {
"port": "${framework_settings_port}"
},
"docker_image_tag": "${docker_image_tag}",
"RUN_CMD": "${RUN_CMD}",
"MEMORY_LIMIT_MB": "${MEMORY_LIMIT_MB:-2048}"
}
EOF
j2 "$TEMPLATE_PATH" /tmp/vars.json > Dockerfile.tmp
fi
fi
# Final processing with envsubst for any remaining ${VAR} patterns
envsubst < Dockerfile.tmp > Dockerfile
rm -f Dockerfile.tmp
echo "✅ Generated Dockerfile from template: $TEMPLATE_FILE"
else
echo "🔄 Using inline fallback template for ${FRAMEWORK}"
# Fallback to previous inline method
case "$FRAMEWORK" in
"expressjs"|"fastify"|*)
cat > Dockerfile << 'EOF'
# Generated Express.js/Node.js Dockerfile
ARG NODE_VERSION=18
FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base
RUN apk add --no-cache libc6-compat curl dumb-init
WORKDIR /app
FROM base AS deps
COPY package*.json yarn.lock* pnpm-lock.yaml* ./
RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; fi
FROM base AS production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nodejs
COPY --from=builder --chown=nodejs:nodejs /app .
USER nodejs
EXPOSE 3000
CMD ["dumb-init", "node", "index.js"]
EOF
;;
esac
fi
# Show final Dockerfile preview
echo "📄 Generated Dockerfile preview:"
head -20 Dockerfile
echo "..."
echo "DOCKERFILE_GENERATED=true" >> dockerfile.env
echo "TEMPLATE_USED=${TEMPLATE_FILE:-inline}" >> dockerfile.env
artifacts:
paths:
- Dockerfile
reports:
dotenv: dockerfile.env
expire_in: 1 hour
rules:
- if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2"
generate:dockerignore:
<<: *ansible_template
stage: generate
needs: ["prepare:auto-detect"]
image: gitlab.forlife.one:5050/share/docker:alpine-latest
before_script:
- apk add --no-cache git curl
script:
- |
echo "📁 Fetching .dockerignore file..."
# Check if project already has .dockerignore
if [ -f ".dockerignore" ]; then
echo "ℹ️ Project already has .dockerignore file"
exit 0
fi
ANSIBLE_DOCKERIGNORE="ci/.dockerignore"
if [ -f "$ANSIBLE_DOCKERIGNORE" ]; then
echo "✅ Found .dockerignore in Ansible repo"
cp "$ANSIBLE_DOCKERIGNORE" .dockerignore
echo "DOCKERIGNORE_SOURCE=ansible-repo" >> dockerignore.env
fi
artifacts:
paths:
- .dockerignore
reports:
dotenv: dockerignore.env
expire_in: 1 hour
when: always
rules:
- if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2"
# Enhanced prepare stage with frontend env support
prepare:frontend-env:
stage: prepare
needs: ["prepare:auto-detect"]
image: gitlab.forlife.one:5050/share/docker:alpine-latest
before_script:
- apk add --no-cache bash # Đảm bảo có bash
script:
- |
#!/bin/bash
set -e # Exit on error
echo "🎯 Setting up environment with quote validation..."
# Detect environment
if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then
ENV="development"
elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then
ENV="staging"
elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then
ENV="production"
else
ENV="development"
fi
echo "📝 Environment: $ENV"
# Check if it's a frontend framework
is_frontend=false
if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then
is_frontend=true
fi
if [ "$is_frontend" = "true" ]; then
if [[ -f "./.gitlab-ci/${ENV}.env" ]]; then
echo "✅ Found: .gitlab-ci/${ENV}.env"
cp "./.gitlab-ci/${ENV}.env" "./.env"
fi
if [[ -f "./.gitlab/ci/${ENV}.sh" ]]; then
bash ".gitlab/ci/${ENV}.sh"
fi
if [ -f ".env" ]; then
echo "✅ Found: .env"
# 🔧 ADVANCED QUOTE CLEANING
echo "🧹 Cleaning quotes and special characters..."
# Step 1: Create working copy
cp ".env" "temp.env"
# Step 2: Clean problematic content
sed -i 's/\r$//' temp.env # Remove Windows line endings
sed -i '/^$/d' temp.env # Remove empty lines
sed -i '/^[[:space:]]*#/d' temp.env # Remove comments
sed -i 's/^[[:space:]]*//' temp.env # Remove leading spaces
sed -i 's/[[:space:]]*$//' temp.env # Remove trailing spaces
# Step 3: Initialize frontend.env
cat > frontend.env << EOF
TARGET_ENV=${ENV}
FRAMEWORK=${FRAMEWORK}
IS_FRONTEND=true
EOF
else
echo "⚠️ No env file: .env"
cat > frontend.env << EOF
TARGET_ENV=${ENV}
FRAMEWORK=${FRAMEWORK}
IS_FRONTEND=false
EOF
fi
else
echo "⏭️ Backend project"
cat > frontend.env << EOF
TARGET_ENV=${ENV}
FRAMEWORK=${FRAMEWORK}
IS_FRONTEND=false
EOF
fi
# Final validation
echo "🔍 Validating frontend.env syntax..."
# Test if file exists and is readable
if [ ! -f "frontend.env" ]; then
echo "❌ frontend.env file not created"
exit 1
fi
# Basic syntax check - try to read each line
validation_failed=false
while IFS= read -r line || [ -n "$line" ]; do
if [ -n "$line" ] && ! echo "$line" | grep -q "^[A-Z][A-Z0-9_]*="; then
echo "❌ Invalid line format: $line"
validation_failed=true
fi
done < frontend.env
if [ "$validation_failed" = "true" ]; then
echo "❌ Syntax validation failed"
echo "📋 File contents:"
cat frontend.env
exit 1
fi
echo "✅ Syntax validation passed"
echo "📋 Final preview:"
head -10 frontend.env | sed 's/=.*/=***/'
artifacts:
paths:
- .env
- frontend.env # Đảm bảo cả 2 file đều được lưu
reports:
dotenv: frontend.env
expire_in: 2 hours
rules:
- if: $CI_COMMIT_TAG
# ============================================================================
# BUILD STAGE - FRAMEWORK OPTIMIZED
# ============================================================================
build:docker:
<<: *docker_template
stage: build
needs: ["prepare:auto-detect", "prepare:frontend-env", "generate:dockerfile", "generate:dockerignore"]
retry:
max: 1
when:
- script_failure
- runner_system_failure
before_script:
- apk add --no-cache bash envsubst curl jq unzip
- curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip
- unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault && rm -f vault.zip
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- |
echo "⏳ Waiting for Docker daemon..."
for i in {1..10}; do
if docker info > /dev/null 2>&1; then
echo "✅ Docker is ready!"
break
fi
echo "❌ Docker not ready (attempt $i), retrying in 3s..."
sleep 3
done
# Nếu hết retry mà vẫn fail thì exit để GitLab tự retry job
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker vẫn chưa sẵn sàng sau nhiều lần thử. Thoát để GitLab retry job."
MESSAGE="🚨 CI job *failed* in project \`$CI_PROJECT_PATH\` at stage \`$CI_JOB_STAGE\`!\n\nDocker daemon is not ready on runner \`$CI_RUNNER_DESCRIPTION\`.\nCommit: \`$CI_COMMIT_SHORT_SHA\`\nJob URL: $CI_JOB_URL"
curl -s -X POST https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d parse_mode=Markdown \
-d text="$MESSAGE"
exit 1
fi
script:
- |
echo "Attempting to clone deployment tools..."
rm -rf ci
if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with CI_JOB_TOKEN"
elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then
echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN"
fi
- |
#!/bin/bash
set -e # Exit on error
echo "🐳 Building Docker image with framework optimization..."
echo "Framework: ${FRAMEWORK:-}"
echo "Node.js: ${NODE_VERSION:-}"
echo "Package Manager: ${PACKAGE_MANAGER:-}"
echo "Environment: ${ENVIRONMENT:-development}"
# Check Dockerfile status
if [ "${DOCKERFILE_GENERATED:-false}" == "true" ]; then
echo "✅ Using auto-generated Dockerfile for ${FRAMEWORK}"
elif [ -f "Dockerfile" ]; then
echo "✅ Using existing Dockerfile from repository"
else
echo "❌ No Dockerfile available - this should not happen"
exit 1
fi
if [ "${FRAMEWORK}" = "nextjs" ] && [ ! -f "next.config.ts" ] && [ ! -f "next.config.js" ]; then
echo "❌ Không tìm thấy next.config.js hoặc next.config.ts trong dự án Next.js."
echo "❌ Hãy thêm next.config.js/ts và cấu hình output standalone để build Docker image đúng cách."
exit 1
fi
if [ "${FRAMEWORK}" = "expressjs" ]; then
if [ ! -d "src/public" ]; then
mkdir -p "src/public"
fi
fi
if [ "${FRAMEWORK}" = "expressjs" ] || [ "${FRAMEWORK}" = "nestjs" ]; then
if [ ! -f "src/main.ts" ] && [ -f "src/index.ts" ]; then
cp "src/index.ts" "src/main.ts"
fi
fi
if [[ "$IS_FRONTEND" == "true" ]]; then
cd ci
CI_PROJECT_DIR="projects/${APP_NAME}"
if [[ -f "../.gitlab-ci/${ENVIRONMENT}.env" ]]; then
echo "✅ Found: .gitlab-ci/${ENVIRONMENT}.env"
cp "../.gitlab-ci/${ENVIRONMENT}.env" "../.env"
fi
if [[ -f "../.gitlab/ci/${ENVIRONMENT}.sh" ]]; then
cd ../ && bash ".gitlab/ci/${ENVIRONMENT}.sh" && cd ci
fi
if [ ! -d "../public" ]; then
mkdir -p "../public"
fi
cd ../
fi
export VAULT_ADDR="${VAULT_ADDR}"
VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}"
export VAULT_PATH="${VAULT_PATH}"
VAULT_RESPONSE=$(curl --silent --request POST \
--data "{\"password\": \"${VAULT_PASSWORD}\"}" \
"${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}")
#echo "🔐 Vault login response:"
#echo "$VAULT_RESPONSE" | jq
VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token')
if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then
echo "❌ Failed to get Vault token"
exit 1
fi
export VAULT_TOKEN="${VAULT_TOKEN}"
VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "")
# Kiểm tra nếu không lấy được VAULT_OUTPUT
if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then
echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}"
else
SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json')
if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then
echo "🔐 Vault setting for ${VAULT_PATH} will be used!"
# Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE"
# to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"}
# "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE"
#SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""')
SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"')
# Kiểm tra xem có secret nào được tìm thấy không
if [[ -n "$SECRETS_TO_ENV" ]]; then
# Ghi các secret đã được định dạng vào file .env
echo "$SECRETS_TO_ENV" > .env
fi
else
echo "⚠️ No secrets found at ${VAULT_PATH}"
fi
fi
APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}
# Setup Docker Buildx with unique name
BUILDER_NAME="builder-${CI_PIPELINE_ID}-${CI_JOB_ID}"
docker buildx create --use --name "$BUILDER_NAME" || true
# ========================================================================
# FRAMEWORK-SPECIFIC BUILD ARGS PREPARATION
# ========================================================================
echo "🔧 Preparing build arguments..."
# Initialize build args
BUILD_ARGS=""
# Add base build args one by one
BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_VERSION='${NODE_VERSION:-18}'"
BUILD_ARGS="${BUILD_ARGS} --build-arg PACKAGE_MANAGER='${PACKAGE_MANAGER:-npm}'"
BUILD_ARGS="${BUILD_ARGS} --build-arg APP_PORT='${CONTAINER_PORT:-3000}'"
BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV='production'"
BUILD_ARGS="${BUILD_ARGS} --build-arg APP_VERSION='${APP_VERSION}'"
BUILD_ARGS="${BUILD_ARGS} --build-arg FRAMEWORK='${FRAMEWORK:-nodejs}'"
# Add optional build args if they have values
if [ -n "${BUILD_COMMAND:-}" ]; then
ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g")
BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_COMMAND='${ESCAPED}'"
fi
if [ -n "${START_COMMAND:-}" ]; then
ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g")
#BUILD_ARGS="${BUILD_ARGS} --build-arg START_COMMAND='${ESCAPED}'"
fi
if [ -n "${BUILD_DATE:-}" ]; then
BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_DATE='${BUILD_DATE}'"
fi
if [ -n "${GIT_COMMIT:-}" ]; then
BUILD_ARGS="${BUILD_ARGS} --build-arg GIT_COMMIT='${GIT_COMMIT}'"
fi
if [ -n "${PIPELINE_ID:-}" ]; then
BUILD_ARGS="${BUILD_ARGS} --build-arg PIPELINE_ID='${PIPELINE_ID}'"
fi
# Add frontend environment variables if applicable
if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then
echo "🌐 Adding frontend environment variables..."
# Process .env file safely
while IFS='=' read -r key value || [ -n "$key" ]; do
# Skip comments and empty lines
case "$key" in
\#*|'')
continue
;;
esac
# Skip empty keys
if [ -z "$key" ]; then
continue
fi
# Skip keys that are already in base build args to avoid duplicates
case "$key" in
NODE_VERSION|PACKAGE_MANAGER|APP_PORT|PORT|NODE_ENV|APP_VERSION|FRAMEWORK|BUILD_COMMAND|START_COMMAND|BUILD_DATE|GIT_COMMIT|PIPELINE_ID)
echo "⚠️ Skipped duplicate: ${key} (already in base args)"
continue
;;
esac
# Clean the value (remove quotes if present)
case "$value" in
\"*\")
value=$(echo "$value" | sed 's/^"//' | sed 's/"$//')
;;
\'*\')
value=$(echo "$value" | sed "s/^'//" | sed "s/'$//")
;;
esac
# Add to build args with proper quoting
BUILD_ARGS="${BUILD_ARGS} --build-arg ${key}='${value}'"
echo "✓ Added build arg: ${key}=${value:0:30}..."
done < .env
fi
# Debug: Show actual BUILD_ARGS content
echo "🔍 DEBUG: BUILD_ARGS content:"
echo "BUILD_ARGS='${BUILD_ARGS}'"
echo "🔍 DEBUG: First few build args:"
echo "${BUILD_ARGS}" | tr ' ' '\n' | head -10
# ========================================================================
# DOCKER BUILD EXECUTION
# ========================================================================
echo "🚀 Starting Docker build with ${FRAMEWORK} optimization..."
# Setup cache configuration
CACHE_CONFIG=""
if [ "${BUILD_CACHE_ENABLED:-true}" == "true" ]; then
if [ -n "${FRAMEWORK:-}" ]; then
CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK} --cache-to type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK},mode=max"
else
CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache --cache-to type=registry,ref=${DOCKER_IMAGE}:cache,mode=max"
fi
else
CACHE_CONFIG="--no-cache"
fi
# Create output file first (in case of failure) - SIMPLE dotenv format
echo "SKIP_DOCKER_BUILD=false" > docker_build.env
echo "BUILD_SUCCESS=false" >> docker_build.env
echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env
echo "🔨 Executing Docker build command..."
echo "🔍 DEBUG: Command length: ${#BUILD_ARGS} characters"
# Write build args to a file to avoid command line length issues
echo "📝 Writing build args to file for docker buildx..."
echo "$BUILD_ARGS" | tr ' ' '\n' | grep -E "^--build-arg" > /tmp/build_args.txt
echo "📝 Build args file created: $(wc -l < /tmp/build_args.txt) lines"
# Show sample of build args file
echo "🔍 Sample build args:"
head -5 /tmp/build_args.txt
echo "▶️ Starting docker buildx build..."
# Set conditional tag
LATEST_TAG=""
if [ "${ENVIRONMENT:-development}" == "production" ]; then
LATEST_TAG="--tag '${DOCKER_IMAGE}:${APP_VERSION}-latest'"
fi
# Kiểm tra nếu có .env thì thêm vào BUILD_SECRET
if [ -f .env ]; then
echo "✅ Found .env, using as Docker secret"
BUILD_SECRET="--secret id=env,src=.env"
else
echo "⚠️ .env not found, skipping --secret"
BUILD_SECRET=""
fi
if [ "${FRAMEWORK}" = "laravel" ]; then
cp -r ci/roles/docker/templates/nginx ./docker
fi
if [[ "$FRAMEWORK" == "expressjs" ]]; then
if [ ! -d "src/public" ]; then
mkdir -p "src/public"
fi
fi
if [[ "$FRAMEWORK" == "nextjs" ]] && [[ ! -f "../next.config.js" ]]; then
cat > "../next.config.js" <<EOF
/** @type {import('next').NextConfig} */
module.exports = {
output: "standalone",
};
EOF
fi
rm -f frontend.env backend.env docker_build.env vault.zip || true
rm -rf ci || true
# Use eval with the BUILD_ARGS properly expanded
eval "docker buildx build \
${BUILD_SECRET} \
${BUILD_ARGS} \
${CACHE_CONFIG} \
--tag '${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}' \
${LATEST_TAG} \
--target production \
--push \
--platform linux/amd64 \
--progress=plain \
." || {
echo "❌ Docker build failed with exit code: $?"
echo "SKIP_DOCKER_BUILD=false" > docker_build.env
echo "BUILD_SUCCESS=false" >> docker_build.env
echo "DOCKER_IMAGE_BUILT=" >> docker_build.env
rm -f /tmp/build_args.txt
exit 1
}
# Build successful - update dotenv file
echo "SKIP_DOCKER_BUILD=false" > docker_build.env
echo "BUILD_SUCCESS=true" >> docker_build.env
echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env
# Cleanup
rm -f /tmp/build_args.txt
echo "✅ Docker image built successfully:"
echo " - ${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}"
if [ "${ENVIRONMENT:-development}" == "production" ]; then
echo " - ${DOCKER_IMAGE}:${APP_VERSION}-latest"
fi
# Cleanup
docker buildx rm "$BUILDER_NAME" || true
# artifacts:
# reports:
# dotenv: docker_build.env
# expire_in: 1 hour
# when: always
rules:
- if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2"
# ============================================================================
# ADDITIONAL JOBS FOR ENVIRONMENT-SPECIFIC BUILDS
# ============================================================================
# Build for specific environments (if needed)
#build:docker:development:
# extends: build:docker
# variables:
# ENVIRONMENT: "development"
# # NextJS development-specific variables
# NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL_DEV:-http://localhost:3001}"
# NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL_DEV:-http://localhost:3000}"
# # React development-specific variables
# REACT_APP_API_URL: "${REACT_APP_API_URL_DEV:-http://localhost:3001}"
# # Build optimizations for development
# OPTIMIZE_IMAGES: "false"
# COMPRESS_ASSETS: "false"
# ENABLE_BUNDLE_ANALYZER: "false"
# rules:
# - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/ && $DEPLOY_TYPE != "pm2"
# ============================================================================
# DEPLOYMENT STAGES - FLEXIBLE AUTHENTICATION
# ============================================================================
.deploy_template: &deploy_template
<<: *ansible_template
needs: ["prepare:auto-detect", "prepare:frontend-env", "build:docker"]
script:
- |
APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}
echo "🚀 Starting deployment with auto-scaling"
echo "App: ${APP_NAME}"
echo "Version: ${APP_VERSION}"
echo "Framework: ${FRAMEWORK:-}"
echo "Environment: ${ENVIRONMENT:-unknown}"
echo "Node.js: ${NODE_VERSION:-}"
echo "Host: ${DEPLOYMENT_SERVER_HOST:-not-set}:${DEPLOYMENT_SERVER_PORT:-not-set}"
echo "User: ${DEPLOYMENT_SERVER_USERNAME:-not-set}"
echo "Target Replicas: ${DEPLOYMENT_REPLICAS:-1}"
echo "Deployment Strategy: ${DEPLOYMENT_STRATEGY:-rolling}"
# Validate required variables
if [ -z "$DEPLOYMENT_SERVER_HOST" ]; then
echo "❌ DEPLOYMENT_SERVER_HOST is required"
exit 1
fi
cd ci
CI_PROJECT_DIR="projects/${APP_NAME}"
if [ ! -d "$CI_PROJECT_DIR" ]; then
echo "📁 Folder '$CI_PROJECT_DIR' does not exist. Creating..."
mkdir -p "$CI_PROJECT_DIR/templates"
fi
if [[ ! -f "$CI_PROJECT_DIR/vars.yml" ]]; then
touch "$CI_PROJECT_DIR/vars.yml"
fi
if [ ! -d "$CI_PROJECT_DIR/templates" ]; then
mkdir -p "$CI_PROJECT_DIR/templates"
fi
INVENTORY_FILE="inventory/${ENVIRONMENT}.yml"
# Check if inventory file exists
if [[ ! -f "$INVENTORY_FILE" ]]; then
echo "Error: Inventory file not found: $INVENTORY_FILE"
exit 1
fi
AUTH_METHOD=${AUTH_METHOD:-password}
if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]] || [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then
AUTH_METHOD="ssh_key"
fi
ENV_TAG=${ENV_TAG:-""}
if [[ "$ENV_TAG" == "worker" ]]; then
ENV_TAG="-${ENV_TAG}"
fi
ANSIBLE_CMD="ansible-playbook -i $INVENTORY_FILE playbook.yml"
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"app_name=${APP_NAME} deploy_env=${ENVIRONMENT} gitlab_ci_token=${CI_JOB_TOKEN} git_repo=${CI_REPOSITORY_URL} git_tag=${CI_COMMIT_TAG} git_sha=${CI_COMMIT_SHA} git_short_sha=${CI_COMMIT_SHORT_SHA}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_user=${DEPLOYMENT_SERVER_USERNAME} auth_method=${AUTH_METHOD:-password} deploy_strategy=${DEPLOY_TYPE:-docker} env_tag=${ENV_TAG}\""
# Auto-scaling configuration
if [[ -n "${DEPLOYMENT_REPLICAS:-}" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"replicas=${DEPLOYMENT_REPLICAS:-1}\""
fi
if [[ -n "${DEPLOYMENT_MAX_REPLICAS:-}" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"max_replicas=${DEPLOYMENT_MAX_REPLICAS:-9}\""
fi
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"deployment_strategy=${DEPLOYMENT_STRATEGY:-rolling}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"health_check_timeout=${HEALTH_CHECK_TIMEOUT:-30}\""
if [ "${ENVIRONMENT}" = "production" ]; then
if [[ -n "${CPU_LIMIT:-}" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_cpu_limit=${CPU_LIMIT}\""
fi
if [[ -n "${MEMORY_LIMIT:-}" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_memory_limit=${MEMORY_LIMIT}\""
fi
fi
# Add authentication and other parameters
if [[ -n "$DEPLOYMENT_SERVER_HOST" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_host=${DEPLOYMENT_SERVER_HOST}\""
fi
if [[ -n "$DEPLOYMENT_SERVER_PORT" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_port=${DEPLOYMENT_SERVER_PORT}\""
fi
if [[ "$AUTH_METHOD" == "password" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_password=${DEPLOYMENT_SERVER_PASSWORD} server_become_password=${DEPLOYMENT_SERVER_PASSWORD}\""
elif [[ "$AUTH_METHOD" == "ssh_key" ]]; then
if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_path=${DEPLOYMENT_SERVER_SSH_KEY_PATH}\""
fi
if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_content=${DEPLOYMENT_SERVER_SSH_KEY_CONTENT}\""
fi
fi
if [[ -n "$GIT_USER" && -n "$GIT_TOKEN" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"git_username=$GIT_USER git_token=$GIT_TOKEN\""
fi
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"is_frontend=${IS_FRONTEND}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_registry=${CI_REGISTRY}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_project_path=${CI_PROJECT_PATH}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_name=${CI_REGISTRY_IMAGE}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_tag=${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_full=${CI_REGISTRY_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"node_version=${NODE_VERSION:-}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"framework=${FRAMEWORK:-}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"package_manager=${PACKAGE_MANAGER:-}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"build_command=${BUILD_COMMAND:-}\""
#ANSIBLE_CMD=" $ANSIBLE_CMD -e \"start_command=${START_COMMAND:-}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_port=${CONTAINER_PORT:-3000}\""
ANSIBLE_CMD=" $ANSIBLE_CMD -e \"pipeline_id=${PIPELINE_ID}\""
export VAULT_ADDR="${VAULT_ADDR}"
VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}"
export VAULT_PATH="${VAULT_PATH}"
VAULT_RESPONSE=$(curl --silent --request POST \
--data "{\"password\": \"${VAULT_PASSWORD}\"}" \
"${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}")
#echo "🔐 Vault login response:"
#echo "$VAULT_RESPONSE" | jq
VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token')
if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then
echo "❌ Failed to get Vault token"
exit 1
fi
export VAULT_TOKEN="${VAULT_TOKEN}"
VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "")
# Kiểm tra nếu không lấy được VAULT_OUTPUT
if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then
echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}"
else
SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json')
if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then
echo "🔐 Vault setting for ${VAULT_PATH} will be used!"
# Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE"
# to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"}
# "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE"
#SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""')
SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"')
# Kiểm tra xem có secret nào được tìm thấy không
if [[ -n "$SECRETS_TO_ENV" ]]; then
# Ghi các secret đã được định dạng vào file .env
echo "$SECRETS_TO_ENV" > "$CI_PROJECT_DIR/templates/.env.j2"
fi
fi
fi
if [[ -n "$EXTRA_VARS" ]]; then
ANSIBLE_CMD=" $ANSIBLE_CMD --extra-vars '$EXTRA_VARS'"
fi
echo $ANSIBLE_CMD
eval $ANSIBLE_CMD
# --extra-vars '{
# "database_host": "'$DEV_DATABASE_HOST'",
# "database_user": "'$DEV_DATABASE_USER'",
# "database_password": "'$DEV_DATABASE_PASSWORD'",
# "database_name": "myapp_dev",
# "redis_host": "'$DEV_REDIS_HOST'",
# "jwt_secret": "'$DEV_JWT_SECRET'",
# "app_url": "http://'$DEV_SERVER_HOST':3000"
# }'
# Development deployment
deploy:development:
extends: .deploy_template
stage: deploy
environment:
name: development
variables:
ENVIRONMENT: "development"
DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH"
# Auto-scaling configuration for development
DEPLOYMENT_REPLICAS: "$DEVELOPMENT_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$DEVELOPMENT_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/
# Staging deployment
deploy:staging:
extends: .deploy_template
stage: deploy
environment:
name: staging
variables:
ENVIRONMENT: "staging"
DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH"
# Auto-scaling configuration for staging
DEPLOYMENT_REPLICAS: "$STAGING_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$STAGING_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$STAGING_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+$/
# Production deployment
deploy:production:
extends: .deploy_template
stage: deploy
environment:
name: production
variables:
ENVIRONMENT: "production"
DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH"
# Auto-scaling configuration for production
DEPLOYMENT_REPLICAS: "$PRODUCTION_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$PRODUCTION_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual
allow_failure: false
# Development deployment for worker
deploy:development:worker:
extends: .deploy_template
stage: deploy
environment:
name: development
variables:
ENVIRONMENT: "development"
DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH"
ENV_TAG: "worker"
# Worker-specific auto-scaling (typically needs more replicas)
DEPLOYMENT_REPLICAS: "$DEVELOPMENT_WORKER_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_WORKER_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$DEVELOPMENT_WORKER_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+\-worker$/
# Staging deployment for worker
deploy:staging:worker:
extends: .deploy_template
stage: deploy
environment:
name: staging
variables:
ENVIRONMENT: "staging"
DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH"
ENV_TAG: "worker"
# Worker-specific auto-scaling (typically needs more replicas)
DEPLOYMENT_REPLICAS: "$STAGING_WORKER_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$STAGING_WORKER_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$STAGING_WORKER_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+\-worker$/
# Production deployment for worker
deploy:production:worker:
extends: .deploy_template
stage: deploy
environment:
name: production
variables:
ENVIRONMENT: "production"
DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST"
DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT"
DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME"
DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD"
DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT"
DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH"
ENV_TAG: "worker"
# Production worker auto-scaling
DEPLOYMENT_REPLICAS: "$PRODUCTION_WORKER_REPLICAS"
DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_WORKER_MAX_REPLICAS"
DEPLOYMENT_STRATEGY: "$PRODUCTION_WORKER_DEPLOYMENT_STRATEGY"
HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT"
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/
when: manual
allow_failure: false
# ============================================================================
# VERIFICATION STAGE
# ============================================================================
#verify:deployment:
# stage: verify
# image: gitlab.forlife.one:5050/share/docker:alpine-latest
# needs:
# - job: deploy:development
# optional: true
# - job: deploy:staging
# optional: true
# before_script:
# - apk add --no-cache curl jq bc
# script:
# - |
# echo "🔍 Verifying deployment..."
#
# # Determine which environment was deployed
# if [ "$CI_COMMIT_BRANCH" = "dev" ]; then
# VERIFY_HOST="${DEVELOPMENT_HOST}"
# VERIFY_PORT="3001"
# elif [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
# VERIFY_HOST="${STAGING_HOST}"
# VERIFY_PORT="3002"
# else
# echo "ℹ No verification needed for this branch"
# exit 0
# fi
#
# echo "Verifying: http://${VERIFY_HOST}:${VERIFY_PORT}"
#
# # Basic connectivity test
# response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "000")
# if [ "$response" = "200" ] || [ "$response" = "301" ] || [ "$response" = "302" ]; then
# echo "✅ Basic connectivity test passed (HTTP $response)"
# else
# echo "❌ Basic connectivity test failed (HTTP $response)"
# exit 1
# fi
#
# # Performance test
# load_time=$(curl -s -o /dev/null -w "%{time_total}" --max-time 15 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "999")
# echo "⏱️ Response time: ${load_time}s"
#
# if [ "$(echo "$load_time < 5.0" | bc)" = "1" ]; then
# echo "✅ Performance test passed"
# else
# echo "⚠️ Performance test warning: Response time is high (${load_time}s)"
# fi
#
# echo "✅ Deployment verification completed successfully"
#
# rules:
# - if: $CI_COMMIT_BRANCH == "dev"
# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# allow_failure: true
# ============================================================================
# CLEANUP STAGE
# ============================================================================
cleanup:images:
<<: *docker_template
stage: cleanup
script:
- |
echo "🧹 Cleaning up old Docker images..."
# Remove images older than 7 days
docker image prune -af --filter "until=168h" || true
# Keep only last 5 versions per framework
if [ "${FRAMEWORK:-}" != "" ]; then
echo "Cleaning up old ${FRAMEWORK} images..."
# This would require additional logic to clean specific framework images
fi
echo "✅ Cleanup completed"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: delayed
start_in: 2 hours
allow_failure: true
# ============================================================================
# MANUAL OPERATIONS
# ============================================================================
manual:rollback:
<<: *ansible_template
stage: deploy
script:
- |
echo "🔄 Rolling back ${APP_NAME} in ${ENVIRONMENT} environment"
echo "Target version: ${ROLLBACK_VERSION:-previous}"
echo "Target replicas: ${ROLLBACK_REPLICAS:-current}"
cd ci
ansible-playbook \
-i inventory/${ENVIRONMENT}.yml \
playbook.yml \
-e app_name="${APP_NAME}" \
-e deploy_env="${ENVIRONMENT}" \
-e rollback_version="${ROLLBACK_VERSION:-}" \
-e rollback_replicas="${ROLLBACK_REPLICAS:-}" \
-e force_rollback=true \
--tags rollback
echo "✅ Rollback completed"
variables:
ENVIRONMENT: "production"
ROLLBACK_VERSION: "$ROLLBACK_VERSION"
ROLLBACK_REPLICAS: "$ROLLBACK_REPLICAS"
when: manual
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_rollback"
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/ && $CI_COMMIT_BRANCH == "main_rollback_worker"
allow_failure: false
manual:emergency-scale:
<<: *ansible_template
stage: deploy
script:
- |
echo "🚨 Emergency scaling ${APP_NAME} to ${TARGET_REPLICAS} replicas"
echo "Environment: ${SCALE_ENVIRONMENT}"
# Direct script execution for emergency scaling
ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \
"/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh scale ${TARGET_REPLICAS}"
# Show status after scaling
ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \
"/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh status"
variables:
SCALE_ENVIRONMENT: "$SCALE_ENVIRONMENT"
TARGET_REPLICAS: "$TARGET_REPLICAS"
DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST"
DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME"
when: manual
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_scale"
allow_failure: true
# ============================================================================
# NOTIFICATIONS
# ============================================================================
notify:slack:
stage: .post
image: gitlab.forlife.one:5050/share/docker:alpine-latest
before_script:
- apk add --no-cache curl jq
script:
- |
echo "📢 Sending deployment notification with auto-scaling info..."
# Determine status and emoji
if [ "$CI_JOB_STATUS" = "success" ]; then
color="good"
emoji="🎉"
status_text="SUCCESS"
else
color="danger"
emoji="❌"
status_text="FAILED"
fi
# Determine environment
if echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+"; then
env_name="Development"
replicas="${DEV_REPLICAS:-2}"
strategy="${DEV_DEPLOYMENT_STRATEGY:-rolling}"
elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-stg\.[0-9]+"; then
env_name="Staging"
replicas="${STG_REPLICAS:-3}"
strategy="${STG_DEPLOYMENT_STRATEGY:-rolling}"
elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+"; then
env_name="Production"
replicas="${PROD_REPLICAS:-5}"
strategy="${PROD_DEPLOYMENT_STRATEGY:-rolling}"
else
env_name="Unknown"
replicas="2"
strategy="rolling"
fi
# Send Slack notification
curl -X POST -H 'Content-type: application/json' \
--data "{
\"text\": \"${emoji} Auto-Scaling Deployment ${status_text}\",
\"attachments\": [{
\"color\": \"${color}\",
\"fields\": [
{\"title\": \"Application\", \"value\": \"${APP_NAME}\", \"short\": true},
{\"title\": \"Environment\", \"value\": \"${env_name}\", \"short\": true},
{\"title\": \"Framework\", \"value\": \"${FRAMEWORK:-Node.js}\", \"short\": true},
{\"title\": \"Version\", \"value\": \"${APP_VERSION}\", \"short\": true},
{\"title\": \"Replicas\", \"value\": \"${replicas}\", \"short\": true},
{\"title\": \"Strategy\", \"value\": \"${strategy}\", \"short\": true},
{\"title\": \"Node.js\", \"value\": \"${NODE_VERSION:-auto-detected}\", \"short\": true},
{\"title\": \"Branch\", \"value\": \"${CI_COMMIT_REF_NAME}\", \"short\": true},
{\"title\": \"Commit\", \"value\": \"${CI_COMMIT_SHORT_SHA}\", \"short\": true},
{\"title\": \"Pipeline\", \"value\": \"<${CI_PIPELINE_URL}|#${CI_PIPELINE_ID}>\", \"short\": true}
],
\"footer\": \"GitLab CI/CD Auto-Scaling\",
\"ts\": $(date +%s)
}]
}" \
"${SLACK_WEBHOOK_URL}" || echo "Failed to send Slack notification"
rules:
- if: $SLACK_WEBHOOK_URL
when: always
allow_failure: true
# ============================================================================
# GITLAB CI VARIABLES CONFIGURATION EXAMPLES
# ============================================================================
# Example GitLab CI/CD Variables for auto-scaling:
#
# Development Environment:
# DEV_REPLICAS=2
# DEV_MAX_REPLICAS=4
# DEV_DEPLOYMENT_STRATEGY=rolling
# DEV_HEALTH_CHECK_TIMEOUT=30
#
# Staging Environment:
# STG_REPLICAS=3
# STG_MAX_REPLICAS=5
# STG_DEPLOYMENT_STRATEGY=rolling
# STG_HEALTH_CHECK_TIMEOUT=30
#
# Production Environment:
# PROD_REPLICAS=5
# PROD_MAX_REPLICAS=10
# PROD_DEPLOYMENT_STRATEGY=rolling
# PROD_HEALTH_CHECK_TIMEOUT=45
#
# Worker Applications:
# DEV_WORKER_REPLICAS=3
# DEV_WORKER_MAX_REPLICAS=6
# PROD_WORKER_REPLICAS=8
# PROD_WORKER_MAX_REPLICAS=15