Shell ScriptingReal-World ProjectAdvancedCI/CDDevOpsMay 2026

Shell Scripting Real-World Projects: CI/CD Pipeline Script

Build a complete CI/CD pipeline in bash — lint, unit tests, integration tests, asset build, security scan, and deployment stages, with Slack notifications, build logging, and a webhook trigger server.

Before commercial CI platforms dominated, teams ran their own build-and-deploy pipelines in bash. Understanding how to build one from scratch makes you a better engineer — you know what your CI platform is actually doing, and you can reproduce it locally or replace it entirely when needed.

BASH
#!/usr/bin/env bash
# pipeline.sh — Complete CI/CD pipeline: test → build → deploy

set -euo pipefail

PIPELINE_ID="$(date +%Y%m%d%H%M%S)-${RANDOM}"
REPO_DIR="${1:-$(pwd)}"
BRANCH="${GIT_BRANCH:-$(git -C "${REPO_DIR}" rev-parse --abbrev-ref HEAD)}"
SHA="${GIT_SHA:-$(git -C "${REPO_DIR}" rev-parse --short HEAD)}"
SLACK_URL="${SLACK_WEBHOOK_URL:-}"
BUILD_LOG="/var/log/pipeline/${PIPELINE_ID}.log"
STAGES_PASSED=0
STAGES_TOTAL=0

mkdir -p "$(dirname "${BUILD_LOG}")"

# ── Stage runner ──────────────────────────────────────────
run_stage() {
  local name="$1"; shift
  (( STAGES_TOTAL++ ))
  local start; start=$(date +%s)

  echo ""
  echo "┌─ Stage: ${name} ────────────────────────────────────"
  if "$@" 2>&1 | tee -a "${BUILD_LOG}"; then
    local elapsed=$(( $(date +%s) - start ))
    echo "└─ ✔ ${name} [${elapsed}s]"
    (( STAGES_PASSED++ ))
    return 0
  else
    local elapsed=$(( $(date +%s) - start ))
    echo "└─ ✘ ${name} FAILED [${elapsed}s]"
    [[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
      -d "{\"text\":\"🔴 Pipeline FAILED at stage: ${name} | ${BRANCH}@${SHA}\"}" &>/dev/null
    return 1
  fi
}

echo "Pipeline: ${PIPELINE_ID}"
echo "Branch:   ${BRANCH}@${SHA}"
echo "Log:      ${BUILD_LOG}"
[[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
  -d "{\"text\":\"▶ Pipeline started: ${BRANCH}@${SHA}\"}" &>/dev/null

cd "${REPO_DIR}"

# ── Stage 1: Lint ─────────────────────────────────────────
run_stage "Lint" bash -c '
  shellcheck bin/*.sh scripts/*.sh 2>/dev/null || true
  php -l src/**/*.php 2>&1 | grep -v "No syntax errors" || true
  echo "  Lint: OK"
'

# ── Stage 2: Unit tests ───────────────────────────────────
run_stage "Unit Tests" bash -c '
  export APP_ENV=testing
  ./vendor/bin/phpunit --no-coverage --testdox tests/Unit/
'

# ── Stage 3: Integration tests ────────────────────────────
run_stage "Integration Tests" bash -c '
  export APP_ENV=testing
  ./vendor/bin/phpunit --no-coverage tests/Integration/
'

# ── Stage 4: Build assets ─────────────────────────────────
run_stage "Build Assets" bash -c '
  npm ci --silent
  npm run build
  echo "  Build size: $(du -sh public/build | cut -f1)"
'

# ── Stage 5: Security scan ────────────────────────────────
run_stage "Security Scan" bash -c '
  composer audit --no-interaction 2>&1 | grep -v "^No security"
  npm audit --audit-level=high 2>&1 | tail -5
  echo "  Security scan: OK"
'

# ── Stage 6: Deploy (main branch only) ────────────────────
if [[ "${BRANCH}" == "main" ]]; then
  run_stage "Deploy" bash -c '
    ./scripts/deploy.sh main 2>&1
  '
else
  echo ""
  echo "  Skipping deploy: branch=${BRANCH} (only deploy on main)"
fi

# ── Summary ───────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo "Pipeline: ${PIPELINE_ID}"
echo "Result:   ${STAGES_PASSED}/${STAGES_TOTAL} stages passed"
echo "Log:      ${BUILD_LOG}"

[[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
  -d "{\"text\":\"$(( STAGES_PASSED == STAGES_TOTAL )) && echo ✅ || echo ⚠ Pipeline complete: ${STAGES_PASSED}/${STAGES_TOTAL} | ${BRANCH}@${SHA}\"}" &>/dev/null

(( STAGES_PASSED == STAGES_TOTAL ))
BASH
#!/usr/bin/env bash
# webhook_server.sh — Listen for GitHub webhooks and trigger pipeline

PORT="${WEBHOOK_PORT:-9000}"
SECRET="${WEBHOOK_SECRET:?WEBHOOK_SECRET required}"

while true; do
  # Use netcat to receive one HTTP request
  REQUEST=$(nc -l -p "${PORT}" -q 1 2>/dev/null)

  # Extract signature and body
  SIGNATURE=$(echo "${REQUEST}" | grep -i "X-Hub-Signature-256:" | \
    sed 's/.*: //' | tr -d '\r')
  BODY=$(echo "${REQUEST}" | awk '/^\r?$/{found=1; next} found{print}')

  # Verify HMAC signature
  EXPECTED="sha256=$(echo -n "${BODY}" | \
    openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $2}')"

  if [[ "${SIGNATURE}" == "${EXPECTED}" ]]; then
    BRANCH=$(echo "${BODY}" | python3 -c \
      "import json,sys; d=json.load(sys.stdin); print(d.get('ref','').replace('refs/heads/',''))")
    echo "Webhook: branch=${BRANCH}"
    [[ "${BRANCH}" == "main" ]] && \
      nohup /opt/scripts/pipeline.sh /opt/myapp &>/dev/null &
  else
    echo "Invalid webhook signature — ignoring"
  fi
done
✔ CI/CD pipeline rules — Structure as discrete stages with clear pass/fail. Fail fast — stop the pipeline at the first failure rather than running all stages. Always post Slack notifications at start and end so the team sees progress. Run unit tests before integration tests — they're faster and catch most bugs first. Only deploy from the main branch to prevent accidental production deploys from feature branches. Store build logs to a persistent location for debugging.