Przejdź do treści

🔄 Advanced Shell in CI/CD

Integrate shell scripts seamlessly into continuous integration and delivery pipelines with professional-grade practices.

🧭 CI/CD Script Lifecycle

Modern CI/CD pipelines typically follow this flow: 1. Checkout — Retrieve source code 2. Setup — Configure environment 3. Build — Compile/test artifacts 4. Deploy — Release to target environments 5. Verify — Confirm deployment success 6. Report — Publish results and metrics

Each stage often involves shell scripts or commands.


🧪 Professional Script Structure

Standard Header Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env bash
# vim: set ft=sh:

# Script metadata
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly START_TIME="$(date +%s)"

# Exit on error, undefined vars, pipe failures
set -euo pipefail

# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color

# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
log_debug() { [ "${DEBUG:-false}" = true ] && echo -e "[DEBUG] $*" >&2; }

# Error handling
error_exit() {
    local line_no=$1
    local error_code=$2
    local error_msg=$3
    log_error "Error on line $line_no: $error_msg (exit code: $error_code)"
    exit $error_code
}

trap 'error_exit $LINENO $? "Unexpected error"' ERR

🧠 Environment Management

Configuration Loading

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Load configuration with precedence
load_config() {
    local config_files=(
        "/etc/${SCRIPT_NAME%.sh}.conf"
        "${HOME}/.${SCRIPT_NAME%.sh}"
        "./${SCRIPT_NAME%.sh}.conf"
        "./.env"
    )

    for config_file in "${config_files[@]}"; do
        if [ -f "$config_file" ]; then
            log_debug "Loading config from $config_file"
            # shellcheck source=/dev/null
            source "$config_file"
        fi
    done

    # Override with environment variables
    : "${API_URL:=https://api.example.com}"
    : "${TIMEOUT:=30}"
    : "${RETRIES:=3}"
}

load_config

Secret Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Secure secret handling
get_secret() {
    local secret_name="$1"
    local secret_value=""

    # Try multiple sources in order of preference
    if [ -n "${!secret_name:-}" ]; then
        # Environment variable
        secret_value="${!secret_name}"
    elif command -v vault >/dev/null 2>&1; then
        # HashiCorp Vault
        secret_value=$(vault kv get -field=value "secret/$secret_name")
    elif [ -f "/run/secrets/$secret_name" ]; then
        # Docker secrets
        secret_value=$(cat "/run/secrets/$secret_name")
    else
        log_error "Secret $secret_name not found"
        return 1
    fi

    echo "$secret_value"
}

# Usage
API_KEY=$(get_secret "API_KEY")

🧪 Build and Test Automation

Dependency Verification

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Verify required tools are available
verify_dependencies() {
    local required_tools=(
        "curl:required for API calls"
        "jq:required for JSON processing"
        "docker:required for container operations"
        "git:required for version control"
    )

    local missing_tools=()

    for tool_desc in "${required_tools[@]}"; do
        local tool="${tool_desc%%:*}"
        local desc="${tool_desc#*:}"

        if ! command -v "$tool" >/dev/null 2>&1; then
            log_error "Missing required tool: $tool ($desc)"
            missing_tools+=("$tool")
        fi
    done

    if [ ${#missing_tools[@]} -gt 0 ]; then
        log_error "Please install missing tools: ${missing_tools[*]}"
        exit 1
    fi

    log_info "All dependencies verified"
}

verify_dependencies

Test Suite Execution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Comprehensive test runner
run_tests() {
    local test_results_dir="${TEST_RESULTS_DIR:-/tmp/test-results}"
    local junit_report="$test_results_dir/junit.xml"

    mkdir -p "$test_results_dir"

    log_info "Running unit tests..."
    if ! npm test -- --reporters=junit --outputFile="$junit_report"; then
        log_error "Unit tests failed"
        return 1
    fi

    log_info "Running integration tests..."
    if ! ./scripts/integration-tests.sh; then
        log_error "Integration tests failed"
        return 1
    fi

    log_info "Running security scans..."
    if ! npm audit; then
        log_warn "Security vulnerabilities found"
        [ "${FAIL_ON_VULNERABILITIES:-false}" = true ] && return 1
    fi

    log_info "All tests passed"
    return 0
}

🧠 Deployment Strategies

Blue-Green Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Blue-green deployment implementation
blue_green_deploy() {
    local app_name="$1"
    local new_version="$2"
    local blue_env="${app_name}-blue"
    local green_env="${app_name}-green"

    # Determine current active environment
    local active_env
    active_env=$(get_active_environment "$app_name")
    local inactive_env
    inactive_env=$(get_inactive_environment "$app_name")

    log_info "Deploying $new_version to $inactive_env"

    # Deploy to inactive environment
    deploy_to_environment "$inactive_env" "$new_version"

    # Health check
    if ! health_check_environment "$inactive_env"; then
        log_error "Health check failed for $inactive_env"
        rollback_deployment "$inactive_env"
        return 1
    fi

    # Switch traffic
    log_info "Switching traffic to $inactive_env"
    switch_traffic "$app_name" "$inactive_env"

    # Cleanup old version
    cleanup_old_version "$active_env"

    log_info "Deployment completed successfully"
}

Rolling Updates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Rolling update implementation
rolling_update() {
    local service_name="$1"
    local new_image="$2"
    local replicas="${3:-3}"
    local max_unavailable="${4:-1}"

    log_info "Starting rolling update for $service_name"

    # Update one replica at a time
    for ((i=1; i<=replicas; i++)); do
        log_info "Updating replica $i/$replicas"

        # Scale down one old replica
        kubectl scale deployment "$service_name" --replicas=$((replicas - max_unavailable))

        # Wait for scale down
        kubectl rollout status deployment "$service_name" --timeout=60s

        # Scale up with new image
        kubectl set image deployment/"$service_name" "$service_name"="$new_image"
        kubectl scale deployment "$service_name" --replicas=$replicas

        # Wait for update
        kubectl rollout status deployment "$service_name" --timeout=60s

        # Health check
        if ! health_check_replica "$service_name" "$i"; then
            log_error "Health check failed for replica $i"
            kubectl rollout undo deployment "$service_name"
            return 1
        fi

        log_info "Replica $i updated successfully"
    done

    log_info "Rolling update completed"
}

🧪 Artifact Management

Build Artifact Creation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Create comprehensive build artifacts
create_artifacts() {
    local version="$1"
    local build_dir="${BUILD_DIR:-./build}"
    local artifacts_dir="${ARTIFACTS_DIR:-./artifacts}"

    mkdir -p "$artifacts_dir"

    # Create version file
    echo "$version" > "$artifacts_dir/VERSION"

    # Create checksums
    find "$build_dir" -type f -exec sha256sum {} \; > "$artifacts_dir/checksums.sha256"

    # Create deployment manifest
    cat > "$artifacts_dir/manifest.json" <<EOF
{
  "version": "$version",
  "build_time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "commit": "$(git rev-parse HEAD)",
  "branch": "$(git rev-parse --abbrev-ref HEAD)",
  "artifacts": [
    $(find "$build_dir" -type f -exec basename {} \; | sed 's/.*/"&"/' | paste -sd,)
  ]
}
EOF

    # Package artifacts
    tar -czf "$artifacts_dir/build-$version.tar.gz" -C "$build_dir" .

    log_info "Artifacts created in $artifacts_dir"
}

Artifact Publishing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Publish artifacts to registry
publish_artifacts() {
    local version="$1"
    local artifacts_dir="${ARTIFACTS_DIR:-./artifacts}"
    local registry_url="${REGISTRY_URL:-https://artifacts.company.com}"

    log_info "Publishing artifacts for version $version"

    # Upload each artifact
    for artifact in "$artifacts_dir"/*; do
        local filename
        filename=$(basename "$artifact")

        log_info "Uploading $filename"

        if ! curl -sf \
            -u "${ARTIFACT_USER}:${ARTIFACT_PASSWORD}" \
            -T "$artifact" \
            "$registry_url/$version/$filename"; then
            log_error "Failed to upload $filename"
            return 1
        fi
    done

    # Mark version as published
    curl -sf \
        -u "${ARTIFACT_USER}:${ARTIFACT_PASSWORD}" \
        -X POST \
        "$registry_url/api/versions/$version/publish"

    log_info "Artifacts published successfully"
}

🧠 Monitoring and Observability

Health Checks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Comprehensive health checking
health_check() {
    local service_url="$1"
    local timeout="${2:-30}"

    # HTTP health check
    if ! curl -sf --max-time "$timeout" "$service_url/health" >/dev/null; then
        log_error "HTTP health check failed"
        return 1
    fi

    # Database connectivity
    if ! nc -z "${DB_HOST:-localhost}" "${DB_PORT:-5432}"; then
        log_error "Database connection failed"
        return 1
    fi

    # Cache connectivity
    if ! redis-cli ping >/dev/null; then
        log_error "Redis connection failed"
        return 1
    fi

    # Custom business logic checks
    if ! custom_health_checks; then
        log_error "Custom health checks failed"
        return 1
    fi

    log_info "All health checks passed"
    return 0
}

Metrics Collection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Collect and report metrics
collect_metrics() {
    local metrics_file="${METRICS_FILE:-/tmp/metrics.json}"

    # System metrics
    local cpu_usage
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)

    local memory_usage
    memory_usage=$(free | grep Mem | awk '{printf "%.2f", $3/$2 * 100.0}')

    local disk_usage
    disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')

    # Application metrics
    local request_count
    request_count=$(get_request_count)

    local error_rate
    error_rate=$(get_error_rate)

    # Generate metrics JSON
    jq -n \
        --arg cpu "$cpu_usage" \
        --arg mem "$memory_usage" \
        --arg disk "$disk_usage" \
        --arg requests "$request_count" \
        --arg errors "$error_rate" \
        '{
            timestamp: (now | todate),
            system: {
                cpu: ($cpu | tonumber),
                memory: ($mem | tonumber),
                disk: ($disk | tonumber)
            },
            application: {
                requests: ($requests | tonumber),
                error_rate: ($errors | tonumber)
            }
        }' > "$metrics_file"

    log_debug "Metrics collected: $(cat "$metrics_file")"
}

🧪 Error Handling and Rollback

Automated Rollback

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Automated rollback mechanism
rollback_on_failure() {
    local deployment_name="$1"
    local max_rollbacks="${MAX_ROLLBACKS:-3}"
    local rollback_count_file="/tmp/${deployment_name}_rollbacks"

    # Track rollback attempts
    local current_rollbacks=0
    if [ -f "$rollback_count_file" ]; then
        current_rollbacks=$(cat "$rollback_count_file")
    fi

    if [ $current_rollbacks -ge $max_rollbacks ]; then
        log_error "Maximum rollbacks ($max_rollbacks) exceeded"
        alert_team "Critical: Deployment $deployment_name failed after $max_rollbacks rollbacks"
        exit 1
    fi

    # Increment rollback counter
    echo $((current_rollbacks + 1)) > "$rollback_count_file"

    log_warn "Initiating rollback for $deployment_name"

    # Perform rollback
    if kubectl rollout undo deployment/"$deployment_name"; then
        log_info "Rollback successful"

        # Notify stakeholders
        send_notification "Deployment rolled back: $deployment_name"

        # Reset rollback counter on success
        rm -f "$rollback_count_file"
    else
        log_error "Rollback failed"
        alert_team "Critical: Rollback failed for $deployment_name"
        exit 1
    fi
}

Failure Notification

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Comprehensive failure notification
notify_failure() {
    local component="$1"
    local error_message="$2"
    local duration="${3:-unknown}"

    local payload
    payload=$(jq -n \
        --arg component "$component" \
        --arg message "$error_message" \
        --arg duration "$duration" \
        --arg commit "$(git rev-parse HEAD 2>/dev/null || echo 'unknown')" \
        --arg branch "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')" \
        --arg build_url "${CI_BUILD_URL:-unknown}" \
        --arg job_id "${CI_JOB_ID:-unknown}" \
        '{
            component: $component,
            message: $message,
            duration: $duration,
            commit: $commit,
            branch: $branch,
            build_url: $build_url,
            job_id: $job_id,
            timestamp: (now | todate)
        }')

    # Send to multiple channels
    send_slack_alert "$payload"
    send_email_alert "$payload"
    send_pagerduty_alert "$payload"

    # Log for debugging
    log_error "Failure notification sent: $payload"
}

🧾 CI/CD Best Practices

Script Quality Assurance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Pre-commit hook for script validation
validate_scripts() {
    local scripts_dir="${1:-.}"

    find "$scripts_dir" -name "*.sh" -type f | while read -r script; do
        echo "Validating $script..."

        # ShellCheck validation
        if ! shellcheck "$script"; then
            echo "ShellCheck failed for $script"
            return 1
        fi

        # Syntax check
        if ! bash -n "$script"; then
            echo "Syntax check failed for $script"
            return 1
        fi

        # Executable check
        if [ ! -x "$script" ]; then
            echo "Warning: $script is not executable"
        fi
    done

    echo "All scripts validated successfully"
}

Performance Optimization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Optimize CI/CD performance
optimize_ci_cd() {
    # Cache dependencies
    if [ -d "node_modules" ] && [ "node_modules" -nt "package-lock.json" ]; then
        echo "Using cached node_modules"
    else
        echo "Installing dependencies"
        npm ci
    fi

    # Parallel test execution
    if command -v parallel >/dev/null 2>&1; then
        find test/ -name "*test.js" | parallel npm run test-file {}
    else
        npm test
    fi

    # Incremental builds
    if git diff --name-only HEAD~1 | grep -E "\.(js|ts|jsx|tsx)$" >/dev/null; then
        echo "JavaScript files changed - running frontend build"
        npm run build-frontend
    fi

    if git diff --name-only HEAD~1 | grep -E "\.(py|pyi)$" >/dev/null; then
        echo "Python files changed - running backend build"
        npm run build-backend
    fi
}

🧾 Summary

  • Structure CI/CD scripts with professional headers
  • Manage configuration and secrets securely
  • Implement comprehensive testing and verification
  • Use blue-green or rolling deployment strategies
  • Create and publish proper build artifacts
  • Monitor health and collect metrics
  • Handle failures with automated rollback
  • Ensure script quality with validation
  • Optimize performance with caching and parallelization
  • Follow security best practices for CI/CD

👉 Continue to: Shell in Containers