Node.js项目CI/CD #

本节通过一个完整的Node.js项目案例,演示如何构建CI/CD流水线。

项目结构 #

text
nodejs-app/
├── src/
│   ├── index.js
│   └── routes/
├── test/
│   └── index.test.js
├── Dockerfile
├── package.json
├── .npmrc
└── Jenkinsfile

Jenkinsfile完整示例 #

groovy
pipeline {
    agent any
    
    environment {
        APP_NAME = 'nodejs-app'
        DOCKER_REGISTRY = 'registry.example.com'
        NODE_VERSION = '18'
    }
    
    tools {
        nodejs "NodeJS ${NODE_VERSION}"
    }
    
    options {
        timeout(time: 20, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '20'))
        timestamps()
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log -1 --pretty=format:"%h - %an, %ar : %s"'
            }
        }
        
        stage('Setup') {
            steps {
                sh 'node --version'
                sh 'npm --version'
            }
        }
        
        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }
        
        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    junit 'test-results/*.xml'
                }
            }
        }
        
        stage('Coverage') {
            steps {
                sh 'npm run coverage'
            }
            post {
                always {
                    publishHTML target: [
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'coverage',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ]
                }
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }
        
        stage('Docker Build') {
            steps {
                script {
                    docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}")
                }
            }
        }
        
        stage('Docker Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'docker-credentials',
                    usernameVariable: 'DOCKER_USER',
                    passwordVariable: 'DOCKER_PASS'
                )]) {
                    sh """
                        docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} ${DOCKER_REGISTRY}
                        docker push ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}
                    """
                }
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh """
                    kubectl set image deployment/${APP_NAME} \
                        ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
                        -n production
                """
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        success {
            slackSend(
                channel: '#builds',
                color: 'good',
                message: "Build Success: ${JOB_NAME} #${BUILD_NUMBER}"
            )
        }
        failure {
            slackSend(
                channel: '#builds',
                color: 'danger',
                message: "Build Failed: ${JOB_NAME} #${BUILD_NUMBER}"
            )
        }
    }
}

Dockerfile #

dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

EXPOSE 3000

USER node

CMD ["node", "dist/index.js"]

package.json #

json
{
  "name": "nodejs-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/index.js",
    "build": "tsc",
    "test": "jest --coverage",
    "lint": "eslint src/**/*.ts",
    "coverage": "jest --coverage --coverageReporters=html"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.0",
    "@types/node": "^18.15.0",
    "eslint": "^8.36.0",
    "jest": "^29.5.0",
    "typescript": "^5.0.0"
  }
}

Jest配置 #

javascript
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js'
  ],
  testMatch: [
    '**/test/**/*.test.js'
  ]
};

多阶段构建优化 #

groovy
stage('Docker Build') {
    steps {
        script {
            // 使用构建缓存
            docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
                def image = docker.build("${APP_NAME}:${BUILD_NUMBER}", 
                    "--build-arg BUILDKIT_INLINE_CACHE=1 " +
                    "--cache-from ${DOCKER_REGISTRY}/${APP_NAME}:latest ."
                )
                image.push()
            }
        }
    }
}

小结 #

  • 使用npm管理依赖
  • 集成ESLint代码检查
  • Jest测试和覆盖率报告
  • Docker多阶段构建优化
  • Slack通知集成
最后更新:2026-03-28