# ============================================================================= # GitLab CI/CD Pipeline — STEAM Security Dashboard # ============================================================================= # # Pipeline stages: # 1. install — install dependencies for backend and frontend # 2. lint — run linters / static checks # 3. test — run backend (Jest) and frontend (react-scripts) tests # 4. build — produce the production frontend bundle # 5. deploy — deploy to staging (local) or production (SSH to 71.85.90.6) # 6. verify — post-deploy health checks # # Environments: # staging — dashboard-dev:3100 (auto-deploy on main/master) # production — 71.85.90.6:3001 (manual trigger, requires staging verification) # # Executor: shell (runs on dashboard-dev using system Node.js) # ============================================================================= # --------------------------------------------------------------------------- # Variables # --------------------------------------------------------------------------- variables: PROD_HOST: "71.85.90.6" PROD_USER: "root" PROD_DIR: "/home/cve-dashboard" STAGING_DIR: "/home/cve-dashboard-staging" # --------------------------------------------------------------------------- # Global cache — persists node_modules between pipeline runs # --------------------------------------------------------------------------- cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - frontend/node_modules/ policy: pull # --------------------------------------------------------------------------- # Stages # --------------------------------------------------------------------------- stages: - install - lint - test - build - deploy - verify # ============================================================================= # STAGE 1: Install dependencies # ============================================================================= install-backend: stage: install script: - npm ci --prefer-offline cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ policy: pull-push install-frontend: stage: install script: - cd frontend && npm ci --prefer-offline cache: key: ${CI_COMMIT_REF_SLUG} paths: - frontend/node_modules/ policy: pull-push # ============================================================================= # STAGE 2: Lint / static analysis # ============================================================================= lint-frontend: stage: lint script: # Allow up to 25 warnings (mostly unused vars from iterative development). # Errors still block. Unused vars prefixed with _ are suppressed. - cd frontend && npm ci --prefer-offline && npx eslint src/ --ignore-pattern '**/__tests__/**' --ignore-pattern '**/*.test.js' --max-warnings 25 needs: - install-frontend lint-backend: stage: lint script: - npm ci --prefer-offline - node -c backend/server.js - node -c backend/routes/*.js - node -c backend/helpers/*.js - node -c backend/middleware/*.js needs: - install-backend # ============================================================================= # STAGE 3: Tests # ============================================================================= test-backend: stage: test script: - npm ci --prefer-offline - ./node_modules/.bin/jest --ci --forceExit backend/__tests__/ timeout: 5 minutes needs: - install-backend test-frontend: stage: test script: - npm ci --prefer-offline - cd frontend && npm ci --prefer-offline && CI=true npx react-scripts test --watchAll=false --ci timeout: 5 minutes needs: - install-frontend # ============================================================================= # STAGE 4: Build # ============================================================================= build-frontend: stage: build script: - cd frontend && npm ci --prefer-offline && CI=false REACT_APP_API_BASE=/api REACT_APP_API_HOST="" npm run build artifacts: paths: - frontend/build/ expire_in: 7 days needs: - test-frontend - lint-frontend # ============================================================================= # STAGE 5: Deploy # ============================================================================= # --------------------------------------------------------------------------- # Staging — auto-deploys on main/master to dashboard-dev:3100 # --------------------------------------------------------------------------- deploy-staging: stage: deploy rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success environment: name: staging url: http://localhost:3100 script: - echo "Deploying to staging (dashboard-dev:3100)..." # Ensure staging directory exists - mkdir -p ${STAGING_DIR} # Sync code (exclude .git, node_modules, uploads, logs) - rsync -a --delete --exclude='.git' --exclude='node_modules' --exclude='frontend/node_modules' --exclude='frontend/build' --exclude='backend/uploads' --exclude='*.log' --exclude='*.db' --exclude='.env' ${CI_PROJECT_DIR}/ ${STAGING_DIR}/ # Copy built frontend - cp -r ${CI_PROJECT_DIR}/frontend/build ${STAGING_DIR}/frontend/build # Install deps in staging - cd ${STAGING_DIR} && npm ci --prefer-offline - cd ${STAGING_DIR}/frontend && npm ci --prefer-offline # Ensure staging .env exists - | if [ ! -f "${STAGING_DIR}/backend/.env" ]; then cp ${CI_PROJECT_DIR}/backend/.env ${STAGING_DIR}/backend/.env sed -i 's/^PORT=.*/PORT=3100/' ${STAGING_DIR}/backend/.env grep -q "^PORT=" ${STAGING_DIR}/backend/.env || echo "PORT=3100" >> ${STAGING_DIR}/backend/.env fi # Run migrations - cd ${STAGING_DIR}/backend && node migrations/run-all.js # Restart staging service - sudo systemctl restart cve-backend-staging || sudo systemctl start cve-backend-staging || true - echo "Staging deploy complete." after_script: - | ISSUES=$(git log --format=%B -1 | grep -oP '#\d+' | tr -d '#' | sort -u) for ISSUE in $ISSUES; do curl --silent --request POST \ --header "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/issues/${ISSUE}/notes" \ --data-urlencode "body=✅ Deployed to **staging** in pipeline [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL}) (commit \`${CI_COMMIT_SHORT_SHA}\`)" \ > /dev/null 2>&1 || true done needs: - build-frontend - test-backend # --------------------------------------------------------------------------- # Production — manual trigger, SSH to 71.85.90.6 # --------------------------------------------------------------------------- deploy-production: stage: deploy rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: manual environment: name: production url: http://71.85.90.6:3001 script: - echo "Deploying to production (${PROD_HOST})..." # Record current commit on prod for rollback - ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR} && git rev-parse HEAD 2>/dev/null || echo none" > /tmp/prod-prev-commit - echo "Previous production commit:$(cat /tmp/prod-prev-commit)" # Sync code to production (exclude local-only files) - rsync -az --delete --exclude='.git' --exclude='node_modules' --exclude='frontend/node_modules' --exclude='frontend/build' --exclude='backend/uploads' --exclude='*.log' --exclude='*.db' --exclude='.env' --exclude='.compliance-staging' ${CI_PROJECT_DIR}/ ${PROD_USER}@${PROD_HOST}:${PROD_DIR}/ # Copy built frontend - rsync -az ${CI_PROJECT_DIR}/frontend/build/ ${PROD_USER}@${PROD_HOST}:${PROD_DIR}/frontend/build/ # Install deps on production - ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR} && npm ci --prefer-offline" - ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR}/frontend && npm ci --prefer-offline" # Run migrations - ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR}/backend && node migrations/run-all.js" # Restart services — install systemd unit if not present - ssh ${PROD_USER}@${PROD_HOST} "test -f /etc/systemd/system/cve-backend.service" || scp ${CI_PROJECT_DIR}/deploy/cve-backend-production.service ${PROD_USER}@${PROD_HOST}:/etc/systemd/system/cve-backend.service - ssh ${PROD_USER}@${PROD_HOST} "systemctl daemon-reload && systemctl enable cve-backend && systemctl restart cve-backend" - echo "Production deploy complete." after_script: - | ISSUES=$(git log --format=%B -1 | grep -oP '#\d+' | tr -d '#' | sort -u) for ISSUE in $ISSUES; do curl --silent --request POST \ --header "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/issues/${ISSUE}/notes" \ --data-urlencode "body=🚀 Deployed to **production** in pipeline [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL}) (commit \`${CI_COMMIT_SHORT_SHA}\`)" \ > /dev/null 2>&1 || true done needs: - build-frontend - test-backend # ============================================================================= # STAGE 6: Post-deploy verification # ============================================================================= # --------------------------------------------------------------------------- # Staging health check # --------------------------------------------------------------------------- verify-staging: stage: verify rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success script: - echo "Verifying staging..." - sleep 3 - | for i in 1 2 3 4 5; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3100/api/health 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then echo "Staging health check passed (attempt $i)" break fi echo "Staging not ready (status: $STATUS), retrying... (attempt $i/5)" sleep 3 done if [ "$STATUS" != "200" ]; then echo "FAILED: Staging health check failed after 5 attempts" exit 1 fi # --- Post-deploy smoke tests (non-blocking for now) --- # These can be made blocking once stable by changing WARN to FAIL and adding exit 1. - | # Smoke test: compliance items endpoint returns valid JSON COMP_STATUS=$(curl -s -o /tmp/comp-response -w "%{http_code}" http://localhost:3100/api/compliance/items?page=1&limit=1 2>/dev/null || echo "000") if [ "$COMP_STATUS" != "200" ]; then echo "WARN: Compliance items endpoint returned $COMP_STATUS (non-blocking)" fi - | # Smoke test: VCL stats endpoint returns valid JSON VCL_STATUS=$(curl -s -o /tmp/vcl-response -w "%{http_code}" http://localhost:3100/api/compliance/vcl/stats 2>/dev/null || echo "000") if [ "$VCL_STATUS" != "200" ]; then echo "WARN: VCL stats endpoint returned $VCL_STATUS (non-blocking)" fi - | # Smoke test: verify migration ran (compliance_item_history has metric_id column) SCHEMA_CHECK=$(curl -s http://localhost:3100/api/health 2>/dev/null | grep -c '"status":"ok"' || echo "0") echo "Schema health: $SCHEMA_CHECK" - echo "Staging verification passed." needs: - deploy-staging # --------------------------------------------------------------------------- # Production health check — rolls back on failure # --------------------------------------------------------------------------- verify-production: stage: verify rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success script: - echo "Verifying production..." - sleep 3 - | for i in 1 2 3 4 5 6 7 8 9 10; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://${PROD_HOST}:3001/api/health 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then echo "Production health check passed (attempt $i)" break fi echo "Production not ready (status: $STATUS), retrying... (attempt $i/10)" sleep 3 done if [ "$STATUS" != "200" ]; then echo "FAILED: Production health check failed — initiating rollback" PREV_COMMIT=$(cat /tmp/prod-prev-commit 2>/dev/null || echo "") if [ -n "$PREV_COMMIT" ] && [ "$PREV_COMMIT" != "none" ]; then echo "Rolling back to $PREV_COMMIT..." # Re-sync the previous version ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR} && git checkout ${PREV_COMMIT} --force 2>/dev/null" || true ssh ${PROD_USER}@${PROD_HOST} "cd ${PROD_DIR} && npm ci --prefer-offline" ssh ${PROD_USER}@${PROD_HOST} "systemctl restart cve-backend" echo "Rollback complete. Verify manually." else echo "No previous commit recorded — manual intervention required." fi exit 1 fi # --- Post-deploy smoke tests (non-blocking for now) --- # These can be made blocking once stable by changing WARN to FAIL and adding exit 1. - | # Smoke test: compliance items endpoint returns valid JSON COMP_STATUS=$(curl -s -o /tmp/comp-response -w "%{http_code}" http://${PROD_HOST}:3001/api/compliance/items?page=1&limit=1 2>/dev/null || echo "000") if [ "$COMP_STATUS" != "200" ]; then echo "WARN: Compliance items endpoint returned $COMP_STATUS (non-blocking)" fi - | # Smoke test: VCL stats endpoint returns valid JSON VCL_STATUS=$(curl -s -o /tmp/vcl-response -w "%{http_code}" http://${PROD_HOST}:3001/api/compliance/vcl/stats 2>/dev/null || echo "000") if [ "$VCL_STATUS" != "200" ]; then echo "WARN: VCL stats endpoint returned $VCL_STATUS (non-blocking)" fi - | # Smoke test: verify migration ran (compliance_item_history has metric_id column) SCHEMA_CHECK=$(curl -s http://${PROD_HOST}:3001/api/health 2>/dev/null | grep -c '"status":"ok"' || echo "0") echo "Schema health: $SCHEMA_CHECK" - echo "Production verification passed." needs: - deploy-production allow_failure: false