Files
2025-11-29 17:51:12 +08:00

335 lines
6.8 KiB
YAML

# GitLab CI/CD Pipeline for Node.js
# Optimized with caching, parallel execution, and deployment
stages:
- security
- validate
- test
- build
- deploy
# Global variables
variables:
NODE_VERSION: "20"
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/.cache/Cypress"
# Global cache configuration
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
- .cache/Cypress/
policy: pull
# Reusable configuration
.node_template:
image: node:${NODE_VERSION}
before_script:
- node --version
- npm --version
- npm ci
# Validation stage
lint:
extends: .node_template
stage: validate
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
script:
- npm run lint
- npm run format:check
only:
- merge_requests
- main
- develop
# Test stage
unit-test:
extends: .node_template
stage: test
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
script:
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 30 days
integration-test:
extends: .node_template
stage: test
services:
- postgres:15
- redis:7-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
DATABASE_URL: "postgresql://testuser:testpass@postgres:5432/testdb"
REDIS_URL: "redis://redis:6379"
script:
- npm run test:integration
artifacts:
when: always
reports:
junit: test-results/junit.xml
paths:
- test-results/
expire_in: 7 days
# Build stage
build:
extends: .node_template
stage: build
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull
script:
- npm run build
- echo "BUILD_VERSION=$(node -p "require('./package.json').version")" >> build.env
artifacts:
paths:
- dist/
reports:
dotenv: build.env
expire_in: 7 days
only:
- main
- develop
- tags
# Security: Secret Scanning
secret-scan:trufflehog:
stage: security
image: trufflesecurity/trufflehog:latest
script:
- trufflehog filesystem . --json --fail > trufflehog-report.json || true
- |
if [ -s trufflehog-report.json ]; then
echo "❌ Secrets detected!"
cat trufflehog-report.json
exit 1
fi
artifacts:
when: always
paths:
- trufflehog-report.json
expire_in: 30 days
allow_failure: false
only:
- merge_requests
- main
- develop
secret-scan:gitleaks:
stage: security
image: zricethezav/gitleaks:latest
script:
- gitleaks detect --source . --report-format json --report-path gitleaks-report.json
artifacts:
when: always
paths:
- gitleaks-report.json
expire_in: 30 days
allow_failure: true
only:
- merge_requests
- main
- develop
# Security: SAST
sast:semgrep:
stage: security
image: returntocorp/semgrep
script:
- semgrep scan --config=auto --sarif --output=semgrep.sarif .
- semgrep scan --config=p/owasp-top-ten --json --output=semgrep-owasp.json .
artifacts:
reports:
sast: semgrep.sarif
paths:
- semgrep.sarif
- semgrep-owasp.json
expire_in: 30 days
allow_failure: false
only:
- merge_requests
- main
- develop
sast:nodejs:
stage: security
image: node:20-alpine
script:
- npm install -g eslint eslint-plugin-security
- eslint . --plugin=security --format=json --output-file=eslint-security.json || true
artifacts:
paths:
- eslint-security.json
expire_in: 30 days
only:
exists:
- package.json
allow_failure: true
# Security: Dependency Scanning
dependency-scan:npm:
extends: .node_template
stage: security
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
script:
- npm audit --audit-level=moderate --json > npm-audit.json || true
- npm audit --audit-level=high # Fail on high severity
artifacts:
paths:
- npm-audit.json
expire_in: 30 days
allow_failure: false
only:
- merge_requests
- main
- develop
# GitLab built-in security templates
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
# E2E tests (only on main)
e2e-test:
extends: .node_template
stage: test
needs: [build]
dependencies:
- build
script:
- npm run test:e2e
artifacts:
when: always
paths:
- cypress/videos/
- cypress/screenshots/
expire_in: 7 days
only:
- main
# Deploy to staging
deploy:staging:
stage: deploy
image: node:${NODE_VERSION}
needs: [build]
dependencies:
- build
environment:
name: staging
url: https://staging.example.com
on_stop: stop:staging
script:
- echo "Deploying to staging..."
- npm install -g aws-cli
- aws s3 sync dist/ s3://${STAGING_BUCKET}
- aws cloudfront create-invalidation --distribution-id ${STAGING_CF_DIST} --paths "/*"
only:
- develop
when: manual
stop:staging:
stage: deploy
image: node:${NODE_VERSION}
environment:
name: staging
action: stop
script:
- echo "Stopping staging environment..."
when: manual
only:
- develop
# Deploy to production
deploy:production:
stage: deploy
image: node:${NODE_VERSION}
needs: [build, e2e-test]
dependencies:
- build
environment:
name: production
url: https://example.com
before_script:
- echo "Deploying version ${BUILD_VERSION} to production"
script:
- npm install -g aws-cli
- aws s3 sync dist/ s3://${PRODUCTION_BUCKET}
- aws cloudfront create-invalidation --distribution-id ${PRODUCTION_CF_DIST} --paths "/*"
# Health check
- sleep 10
- curl -f https://example.com/health || exit 1
after_script:
- echo "Deployed successfully"
only:
- main
when: manual
# Create release
release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs: [deploy:production]
script:
- echo "Creating release for version ${BUILD_VERSION}"
release:
tag_name: 'v${BUILD_VERSION}'
description: 'Release v${BUILD_VERSION}'
only:
- main
# Workflow rules
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_TAG'
# Additional optimizations
.interruptible_template:
interruptible: true
lint:
extends: [.node_template, .interruptible_template]
unit-test:
extends: [.node_template, .interruptible_template]
integration-test:
extends: [.node_template, .interruptible_template]