diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f39a7d5..b53a12e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,44 +1,20 @@ # ============================================================================= # 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) +# Executor: Docker (LXC 108 — 71.85.90.8) +# Build/test jobs run in node:18 containers. +# Deploy jobs run in alpine with SSH/rsync, targeting staging (71.85.90.9) +# and production (71.85.90.6) via SSH. # ============================================================================= -# --------------------------------------------------------------------------- -# Variables -# --------------------------------------------------------------------------- variables: PROD_HOST: "71.85.90.6" PROD_USER: "root" PROD_DIR: "/home/cve-dashboard" + STAGING_HOST: "71.85.90.9" + STAGING_USER: "root" 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 @@ -48,46 +24,37 @@ stages: - verify # ============================================================================= -# STAGE 1: Install dependencies +# STAGE 1: Install # ============================================================================= install-backend: stage: install + image: node:18 script: - - npm ci --prefer-offline - cache: - key: ${CI_COMMIT_REF_SLUG} + - npm ci + artifacts: paths: - node_modules/ - policy: pull-push + expire_in: 1 hour install-frontend: stage: install + image: node:18 script: - - cd frontend && npm ci --prefer-offline - cache: - key: ${CI_COMMIT_REF_SLUG} + - cd frontend && npm ci + artifacts: paths: - frontend/node_modules/ - policy: pull-push + expire_in: 1 hour # ============================================================================= -# STAGE 2: Lint / static analysis +# STAGE 2: Lint # ============================================================================= -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 + image: node:18 script: - - npm ci --prefer-offline - node -c backend/server.js - node -c backend/routes/*.js - node -c backend/helpers/*.js @@ -95,27 +62,34 @@ lint-backend: needs: - install-backend +lint-frontend: + stage: lint + image: node:18 + script: + - cd frontend && npx eslint src/ --ignore-pattern '**/__tests__/**' --ignore-pattern '**/*.test.js' --max-warnings 25 + needs: + - install-frontend + # ============================================================================= -# STAGE 3: Tests +# STAGE 3: Test # ============================================================================= test-backend: stage: test + image: node:18 + variables: + DATABASE_URL: $DATABASE_URL script: - - npm ci --prefer-offline - # Source backend .env from the production install so DATABASE_URL is available - # for integration tests. Safe because the runner is on the same machine as the DB. - # Must be on same line as jest because each script line is a separate shell context. - - export $(grep -v '^#' /home/cve-dashboard/backend/.env | xargs) && ./node_modules/.bin/jest --ci --forceExit backend/__tests__/ + - ./node_modules/.bin/jest --ci --forceExit backend/__tests__/ timeout: 5 minutes needs: - install-backend test-frontend: stage: test + image: node:18 script: - - npm ci --prefer-offline - - cd frontend && npm ci --prefer-offline && CI=true npx react-scripts test --watchAll=false --ci + - cd frontend && CI=true npx react-scripts test --watchAll=false --ci timeout: 5 minutes needs: - install-frontend @@ -126,8 +100,9 @@ test-frontend: build-frontend: stage: build + image: node:18 script: - - cd frontend && npm ci --prefer-offline && CI=false REACT_APP_API_BASE=/api REACT_APP_API_HOST="" npm run build + - cd frontend && CI=false REACT_APP_API_BASE=/api REACT_APP_API_HOST="" npm run build artifacts: paths: - frontend/build/ @@ -137,26 +112,30 @@ build-frontend: - lint-frontend # ============================================================================= -# STAGE 5: Deploy +# STAGE 5: Deploy (SSH from container) # ============================================================================= -# --------------------------------------------------------------------------- -# Staging — auto-deploys on main/master to dashboard-dev:3100 -# --------------------------------------------------------------------------- +.deploy-base: &deploy-base + image: alpine:latest + before_script: + - apk add --no-cache openssh-client rsync + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo -e "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null" > ~/.ssh/config + deploy-staging: + <<: *deploy-base stage: deploy rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success environment: name: staging - url: http://localhost:3100 + url: http://71.85.90.9: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 + - echo "Deploying to staging (${STAGING_HOST})..." + - rsync -az --delete --exclude='.git' --exclude='node_modules' --exclude='frontend/node_modules' @@ -165,26 +144,16 @@ deploy-staging: --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 + ./ ${STAGING_USER}@${STAGING_HOST}:${STAGING_DIR}/ + - rsync -az frontend/build/ ${STAGING_USER}@${STAGING_HOST}:${STAGING_DIR}/frontend/build/ + - ssh ${STAGING_USER}@${STAGING_HOST} "cd ${STAGING_DIR} && npm ci --prefer-offline" + - ssh ${STAGING_USER}@${STAGING_HOST} "cd ${STAGING_DIR}/frontend && npm ci --prefer-offline" + - ssh ${STAGING_USER}@${STAGING_HOST} "cd ${STAGING_DIR}/backend && node migrations/run-all.js" + - ssh ${STAGING_USER}@${STAGING_HOST} "systemctl restart cve-backend-staging || systemctl start cve-backend-staging || true" - echo "Staging deploy complete." after_script: - | + apk add --no-cache curl > /dev/null 2>&1 ISSUES=$(git log --format=%B -1 | grep -oP '#\d+' | tr -d '#' | sort -u) for ISSUE in $ISSUES; do curl --silent --request POST \ @@ -197,10 +166,8 @@ deploy-staging: - build-frontend - test-backend -# --------------------------------------------------------------------------- -# Production — manual trigger, SSH to 71.85.90.6 -# --------------------------------------------------------------------------- deploy-production: + <<: *deploy-base stage: deploy rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" @@ -210,10 +177,8 @@ deploy-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' @@ -224,20 +189,17 @@ deploy-production: --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 + ./ ${PROD_USER}@${PROD_HOST}:${PROD_DIR}/ + - rsync -az frontend/build/ ${PROD_USER}@${PROD_HOST}:${PROD_DIR}/frontend/build/ - 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} "test -f /etc/systemd/system/cve-backend.service || true" - ssh ${PROD_USER}@${PROD_HOST} "systemctl daemon-reload && systemctl enable cve-backend && systemctl restart cve-backend" - echo "Production deploy complete." after_script: - | + apk add --no-cache curl > /dev/null 2>&1 ISSUES=$(git log --format=%B -1 | grep -oP '#\d+' | tr -d '#' | sort -u) for ISSUE in $ISSUES; do curl --silent --request POST \ @@ -251,23 +213,22 @@ deploy-production: - test-backend # ============================================================================= -# STAGE 6: Post-deploy verification +# STAGE 6: Verify # ============================================================================= -# --------------------------------------------------------------------------- -# Staging health check -# --------------------------------------------------------------------------- verify-staging: stage: verify + image: alpine:latest rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success script: + - apk add --no-cache curl - 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") + STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://${STAGING_HOST}:3100/api/health 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then echo "Staging health check passed (attempt $i)" break @@ -279,37 +240,28 @@ verify-staging: 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 + COMP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${STAGING_HOST}:3100/api/compliance/items?page=1&limit=1" || echo "000") + [ "$COMP_STATUS" != "200" ] && echo "WARN: Compliance items returned $COMP_STATUS" || true - | - # 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" + VCL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${STAGING_HOST}:3100/api/compliance/vcl/stats" || echo "000") + [ "$VCL_STATUS" != "200" ] && echo "WARN: VCL stats returned $VCL_STATUS" || true - echo "Staging verification passed." needs: - deploy-staging -# --------------------------------------------------------------------------- -# Production health check — rolls back on failure -# --------------------------------------------------------------------------- verify-production: stage: verify + image: alpine:latest rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" when: on_success script: + - apk add --no-cache curl openssh-client + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo -e "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null" > ~/.ssh/config - echo "Verifying production..." - sleep 3 - | @@ -327,7 +279,6 @@ verify-production: 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" @@ -337,24 +288,12 @@ verify-production: 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 + COMP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${PROD_HOST}:3001/api/compliance/items?page=1&limit=1" || echo "000") + [ "$COMP_STATUS" != "200" ] && echo "WARN: Compliance items returned $COMP_STATUS" || true - | - # 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" + VCL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${PROD_HOST}:3001/api/compliance/vcl/stats" || echo "000") + [ "$VCL_STATUS" != "200" ] && echo "WARN: VCL stats returned $VCL_STATUS" || true - echo "Production verification passed." needs: - deploy-production