前言
Jenkins Pipeline 是 Jenkins 的核心能力------用代码定义完整的交付流水线。Pipeline 有两种写法:Declarative(声明式)和 Scripted(脚本式)。本篇详解两种语法的区别、选择策略和编写技巧,让你能独立编写生产级流水线。
一、两种语法的选择
| 维度 | Declarative | Scripted |
|------|-------------|----------|
| 语法 | 结构化 YAML 风格 | Groovy 脚本 |
| 上手难度 | 低 | 中高 |
| 灵活性 | 中(但足够覆盖90%场景) | 高 |
| 可读性 | 强 | 中 |
| 代码审查 | 容易 | 较难 |
| 推荐场景 | 90% 的流水线 | 复杂条件逻辑 |
结论:优先用 Declarative,只有遇到它无法表达的复杂逻辑时才回退到 Scripted。
二、Declarative Pipeline 语法详解
基本结构
pipeline {
agent any // 在任意可用节点执行
environment {
// 环境变量定义
APP_NAME = 'myapp'
VERSION = "${env.BUILD_NUMBER}"
DOCKER_REGISTRY = 'registry.mycompany.com'
}
options {
// 流水线选项
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds() // 禁止并发构建
}
parameters {
// 参数定义
choice(name: 'ENV', choices: ['dev', 'test', 'staging', 'prod'], description: '部署环境')
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像标签')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试')
}
triggers {
// 触发器
cron('H 2 * * *') // 每天凌晨2点执行
}
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
when {
expression { !params.SKIP_TESTS }
}
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/TEST-*.xml'
}
}
}
}
post {
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
emailext to: 'team@mycompany.com', subject: 'Build Failed: ${JOB_NAME} #${BUILD_NUMBER}'
}
always {
cleanWs() // 清理工作空间
}
}
}
agent 指令详解
pipeline {
// 方式1:任意节点
agent any
// 方式2:指定标签的节点
agent { label 'docker-build' }
// 方式3:指定 Docker 镜像(不需要预装工具)
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '-v $HOME/.m2:/root/.m2 -v $HOME/.gradle:/root/.gradle'
label 'docker-build'
}
}
// 方式4:Kubernetes Pod(需要 K8s 插件)
agent {
kubernetes {
label 'mypod'
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ['cat']
volumeMounts:
- mountPath: /root/.m2
name: maven-cache
volumes:
- name: maven-cache
persistentVolumeClaim:
claimName: maven-cache-pvc
'''
}
}
}
**培训要点**:用 Docker agent 是最佳实践------不需要在 Agent 上预装 Maven/Gradle 等工具,直接在 Pipeline 中指定镜像版本,保证构建环境的确定性。
environment 指令
pipeline {
environment {
// 静态值
APP_NAME = 'myapp'
// 引用构建号
BUILD_TAG = "build-${BUILD_NUMBER}"
// 引用凭据(敏感信息)
DB_PASSWORD = credentials('db-password') // 会生成 DB_PASSWORD 和 DB_PASSWORD_USR 变量
// 条件赋值
DEPLOY_ENV = "${env.BRANCH_NAME == 'main' ? 'prod' : 'dev'}"
}
stages {
stage('Config') {
// 局部环境变量,只在此 stage 生效
environment {
LOCAL_VAR = 'stage-specific'
}
steps {
echo "APP_NAME=${APP_NAME}"
echo "DEPLOY_ENV=${DEPLOY_ENV}"
// 使用凭据时用 $ 而非 ${} 在 sh 中
sh 'echo $DB_PASSWORD | wc -c'
}
}
}
}
when 指令:条件执行
stages {
stage('Deploy Prod') {
when {
branch 'main' // 只在 main 分支执行
// expression { env.BRANCH_NAME == 'main' } // 等价写法
}
steps {
sh './deploy.sh prod'
}
}
stage('Deploy Dev') {
when {
branch 'dev'
}
steps {
sh './deploy.sh dev'
}
}
stage('Nightly Build') {
when {
triggeredBy 'TimerTrigger' // 只在定时触发时执行
}
steps {
sh './run-full-test-suite.sh'
}
}
stage('Skip on PR') {
when {
not { changeRequest() } // 非 PR 时执行
}
steps {
sh './heavy-test.sh'
}
}
}
parallel:并行执行
stage('Test') {
parallel {
stage('Unit Test') {
steps {
sh 'mvn test'
}
}
stage('Integration Test') {
steps {
sh 'mvn verify -Pintegration'
}
}
stage('Code Scan') {
steps {
sh 'sonar-scanner'
}
}
}
}
**踩坑提示**:`parallel` 中的每个 stage 默认在同一个 agent 上执行。如果需要在不同 agent 上并行,需要在每个 stage 内部指定 `agent`。
script 块:在 Declarative 中嵌入 Groovy
stage('Dynamic Deploy') {
steps {
script {
// 在 Declarative 中使用 Groovy 逻辑
def services = ['user-service', 'order-service', 'payment-service']
for (service in services) {
echo "Deploying ${service}..."
sh "kubectl set image deployment/${service} app=myapp:${BUILD_NUMBER} -n prod"
}
}
}
}
三、Scripted Pipeline 语法详解
基本结构
// Scripted Pipeline 没有外层 pipeline {} 块
// 直接写 Groovy 脚本
node('docker-build') {
// 定义环境变量
def appName = 'myapp'
def version = env.BUILD_NUMBER
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'mvn clean package -DskipTests'
// 归档构建产物
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
stage('Test') {
sh 'mvn test'
step([$class: 'JUnitResultArchiver', testResults: '**/surefire-reports/TEST-*.xml'])
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh "./deploy.sh prod ${version}"
} else {
sh "./deploy.sh dev ${version}"
}
}
} catch (Exception e) {
// 错误处理
emailext to: 'team@mycompany.com',
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check: ${env.BUILD_URL}"
throw e // 重新抛出,让 Jenkins 标记为失败
} finally {
// 清理
cleanWs()
}
}
Scripted 中的条件与循环
node('docker-build') {
stage('Multi-Service Deploy') {
def services = [
[name: 'user-service', replicas: 3],
[name: 'order-service', replicas: 2],
[name: 'payment-service', replicas: 2]
]
for (svc in services) {
echo "Deploying ${svc.name} with ${svc.replicas} replicas"
sh "kubectl set image deployment/${svc.name} app=myapp:${env.BUILD_NUMBER} -n prod"
sh "kubectl scale deployment/${svc.name} --replicas=${svc.replicas} -n prod"
sh "kubectl rollout status deployment/${svc.name} -n prod --timeout=120s"
}
}
}
并行构建(Scripted 方式)
def branches = [:]
branches['unit-test'] = {
node('java-build') {
stage('Unit Test') {
sh 'mvn test'
}
}
}
branches['integration-test'] = {
node('java-build') {
stage('Integration Test') {
sh 'mvn verify -Pintegration'
}
}
}
branches['code-scan'] = {
node('java-build') {
stage('Code Scan') {
sh 'sonar-scanner'
}
}
}
parallel branches // 并行执行所有分支
四、实战对比:同一个需求的两种写法
需求
根据分支决定部署环境:main→生产,develop→测试,其他→不部署。
Declarative 写法
pipeline {
agent any
stages {
stage('Build') { steps { sh 'mvn clean package' } }
stage('Deploy') {
stages {
stage('Deploy Prod') {
when { branch 'main' }
steps { sh './deploy.sh prod' }
}
stage('Deploy Test') {
when { branch 'develop' }
steps { sh './deploy.sh test' }
}
}
}
}
}
Scripted 写法
node {
stage('Build') {
sh 'mvn clean package'
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh './deploy.sh prod'
} else if (env.BRANCH_NAME == 'develop') {
sh './deploy.sh test'
} else {
echo "Branch ${env.BRANCH_NAME} does not trigger deployment"
}
}
}
**培训要点**:Declarative 的 `when` 让流水线结构更清晰------即使不部署的 stage 也会显示在流水线视图上(标记为 skipped)。Scripted 的 `if/else` 更灵活但不够直观。
五、常用内置变量速查
| 变量 | 说明 | 示例值 |
|------|------|--------|
| env.JOB_NAME | Job 名称 | myapp-pipeline |
| env.BUILD_NUMBER | 构建编号 | 42 |
| env.BUILD_ID | 构建 ID | 2024-01-15_10-30-00 |
| env.BUILD_URL | 构建 URL | http://jenkins/job/myapp/42/ |
| env.WORKSPACE | 工作空间路径 | /home/jenkins/workspace/myapp |
| env.NODE_NAME | 执行节点名 | agent-docker |
| env.JENKINS_URL | Jenkins URL | http://jenkins:8080 |
| env.BRANCH_NAME | 分支名 | main |
| env.GIT_COMMIT | Git commit hash | a1b2c3d4 |
| env.GIT_URL | Git 仓库 URL | https://github.com/org/repo |
六、本篇要点回顾
-
优先用 Declarative,复杂逻辑用
script {}块嵌入 Groovy -
agent指令推荐用 Docker 镜像方式,保证构建环境确定性 -
when指令控制条件执行,替代if/else更结构化 -
parallel实现并行执行,大幅缩短流水线时间 -
credentials()函数安全引用密钥,不要在代码中硬编码 -
post块处理流水线结果通知
下一篇预告:《Jenkinsfile 最佳实践:多分支流水线与共享库》------从语法层面进入工程层面,学习如何管理多个项目的 Jenkinsfile 和复用流水线代码。