“jenkins动态获取gitlab分支”这个需求,几乎每个从“手填参数构建”过渡到“标准化流水线”的团队都会撞上。去年我帮一个内部项目做CI改造时,分支从几个feature一下子变成几十个,发版前最痛苦的已经不是写编译脚本,而是每次构建页面里那个分支下拉框怎么“跟着GitLab走”。痛点就一句话:打开Jenkins构建面板时,希望下拉列表里出现的是远程仓库真实存在的分支,而不是靠人肉维护的字符串列表。这篇聊清楚实现方案,同样重要的是把那些踩过的坑和文档里不会写的细节一起讲透。
1. 需求拆解:先搞清楚“动态获取”到底要什么
1.1 两种“动态获取”,方向完全不一样
很多人一提“动态获取GitLab分支”,下意识会想到“GitLab上新建一个分支,Jenkins立刻就有感知,然后自动构建”。这是被动触发,属于持续集成里最常见的一类。它的标准解法一般是Multibranch Pipeline加上Webhook:Jenkins每隔一段时间扫描仓库,发现新分支就自动创建对应的任务,分支删了任务也会消失。这个方式解决的是“自动化、无人值守”,适合每个分支都要完整跑一遍测试、构建、部署的团队。
但还有另一种“动态获取”:团队里一位开发或者测试同学,打开某个已有的构建任务,点“Build with Parameters”,希望从这个下拉列表里选中一个分支来构建。这是主动选择,它不要求Jenkins自动创建任务,也不关心你GitLab到底有多少分支,只关心“我这一刻选择哪个分支发版”。这个场景下,Multibranch Pipeline那一套没法用,因为你只能在一个任务内部把分支作为参数动态填进下拉框。两种解读对应两套完全不同的技术方案,先把这个区分清楚,后面才不会绕弯路。
本文核心讲的是第二种:让人在构建参数面板里动态拉取并选择远程分支。第一种方案会在文章最后作为对比补充说明,不展开写。
1.2 三个常见误区,个个都真实踩过
第一个误区是手工维护分支列表。很多团队一开始用简单的Choice参数,把几十个分支名写死在Jenkinsfile里,或者在Job配置里粘贴一份列表。短期能用,但分支一多就失控:新分支忘了加、删掉的分支还挂在列表里、点进去构建直接失败。我曾经见过一个项目,因为手工列表里遗留了一个两周前删除的分支,测试同学连续点了两次失败,才有人发现列表已经三个月没更新了,白白浪费两个多小时。
第二个误区是试图把“执行Shell获取分支”写在Pipeline的stage里,比如:
stage('Get Branches') { steps { script { def out = sh(returnStdout: true, script: "git ls-remote --heads origin").readLines() echo "分支列表: ${out}" } } }这个stage确实能在构建过程中拿到分支列表,但注意:构建已经开始、参数已经选完,这时候获取列表已经改变不了参数下拉框了。很多人把这两件事搞混,写出来的东西在stage里能打印,但真正到参数面板上,列表还是空的。
第三个误区是装了某个Agent插件就以为万事大吉。比如装了Git Parameter插件,但没有在插件配置里指定可执行Git的路径,或者分支过滤正则写错,最后面板上显示“No branches found”,以为是插件不好使,实际上是配置没配全。
这几种坑背后反映的是同一个问题:参数的动态生成发生在Jenkins进程内部、流水线执行之前,你不能用流水线里的step去喂参数,必须借助专门的参数插件或者构建前机制。理解了这个底层逻辑,后面方案选型就很清晰了。
2. 准备工作:插件、Token与仓库地址的合理组合
2.1 插件选型:选一组够用且不冗余的组合
我建议的默认组合是三个插件配合使用:
- Git Parameter:核心参数插件,直接在参数面板里调用Git命令读取分支。
- Credentials Binding:负责在Pipeline中安全使用凭据。
- Pipeline Utility Steps:处理字符串、读取文件等辅助功能,不是必须,但习惯上会装。
有人会问,既然自己写脚本也能拉分支,为什么还非装Git Parameter?道理很简单,Git Parameter把这个功能封装成了声明式参数,它会在点击“Build with Parameters”的时候去执行git ls-remote,然后把结果转成下拉选项,同时提供分支过滤、默认值、排序模式这些现成选项。你不需要自己处理权限、格式转换、编码这些细节。对大多数团队,这是唯一的正确起点。
至于是不是要装GitLab Plugin,看你的具体需求。如果只用“手动选分支构建”,完全不需要。GitLab Plugin主要是做Merge Request校验、Webhook触发这类深度集成用的。少装插件,一是减少版本兼容问题,二是降低Jenkins系统复杂度。尤其是老版本Jenkins升级时,插件多了非常容易互相冲突。
2.2 Token与凭据:这里最容易埋坑
要让Jenkins读取GitLab分支,本质上就是要有权限访问GitLab仓库。不要在Pipeline里直接硬编码用户名密码或者Access Token,哪怕仓库是内部的也不行。另一个坏习惯是把Token放在Jenkins的系统环境变量里,虽然比硬编码好一点,但所有Job都能读到,权限边界太模糊。
正确的做法在GitLab侧创建Access Token,然后放入Jenkins凭据。
GitLab侧的操作步骤:
- 打开GitLab账号设置,进入Access Tokens页面。
- 填写Token名称,比如
jenkins-branch-reader。 - Scope至少勾选
read_repository,只给仓库读取权限就够了。如果是私有的GitLab服务器,有时还需要勾选read_api才能走API方式;但如果只用Git Parameter,read_repository就足够。 - 设置过期时间。建议不要勾选“no expiration”,一个Token长年不换是很危险的事情,特别是走CI的Token泄露影响面很大。我一般设置90天过期,并在日历里加个提醒。
Jenkins侧的操作步骤:
- 进入“系统管理 → Manage Credentials”。
- 添加“Username with password”类型的凭据。
- Username可以填一个标识名,Password粘贴Token值。
- ID建议写成
gitlab-read-token,这个ID会在后面Pipeline里引用。
这里有一个细节很多人会忽略:如果仓库用的是HTTP(S)地址,那Jenkins在读取分支时依赖的是这个凭据;如果是SSH地址,则依赖Jenkins节点上的SSH Key配置。两种方式不是不能混用,但越混越乱。我习惯统一走HTTP地址加Token凭据,因为Token可以单独控制权限、单独吊销,比SSH Key的维护成本低。
2.3 仓库地址与分支过滤的配置模板
为了方便迁移,我建议在Jenkinsfile里定义几个环境变量,统一配置:
environment { GIT_REPO_URL = 'https://gitlab.example-internal.com/group/project.git' GIT_CREDENTIAL = 'gitlab-read-token' BRANCH_FILTER = 'origin/(main|release-.*|feature-.*)' DEFAULT_BRANCH = 'origin/main' }项目千千万万,分支命名规则也各有不同,但BRANCH_FILTER建议一定写成origin/开头的正则。Git Parameter拉到的分支名是refs/heads/xxx转换后的origin/xxx,如果你不写origin/前缀,过滤经常匹配不上。这也是一个特别隐蔽的坑,后面实测章节会再讲。
3. 核心实现:让分支列表“活”起来
3.1 方案一:Git Parameter插件,最省事的开箱即用
在Jenkinsfile中写参数定义:
pipeline { agent any parameters { gitParameter( name: 'BRANCH_NAME', type: 'PT_BRANCH', branchFilter: 'origin/(main|release-.*|feature-.*)', defaultValue: 'origin/main', sortMode: 'DESCENDING_SMART', description: '选择你要构建的GitLab分支' ) } stages { stage('Print Select') { steps { script { echo "你选择的分支是 ${params.BRANCH_NAME}" } } } } }type: 'PT_BRANCH'表示参数类型是分支,也就是它内部执行的是git ls-remote --heads。branchFilter控制哪些分支能显示,正则基于origin/开头。sortMode里我特别推荐DESCENDING_SMART,它会把带版本号的分支按语义化版本倒序排,这样你最新的v1.2.0自然排在最前面,不用靠人眼找。
构建时,在参数面板选择分支,params.BRANCH_NAME拿到的值通常是以origin/开头的字符串。后面做Checkout时要记得去掉前缀,下面这段就是很常见的处理:
stage('Checkout Code') { steps { script { def targetBranch = params.BRANCH_NAME.replaceFirst('^origin/', '') checkout([ $class: 'GitSCM', branches: [[name: "refs/heads/${targetBranch}"]], extensions: [[$class: 'CleanBeforeCheckout']], userRemoteConfigs: [[ url: env.GIT_REPO_URL, credentialsId: env.GIT_CREDENTIAL ]] ]) } } }第一次接触Git Parameter的人经常会问:分支列表是什么时候刷新的?这里要明确一个机制,Git Parameter是在你点开构建参数面板的那一刻执行一次。也就是说,只要面板开着,GitLab上新加的分支不会自己蹦出来,需要重新打开面板才会刷新。很多团队抱怨“十分钟前建的分支,现在面板里没有”,多半是没理解这个刷新时机。真正常态构建过程中,每次打开面板都会做一次git ls-remote --heads,所以只要重新进面板,新分支自然会出现,这已经足够实时了。
3.2 方案二:Active Choices实现更灵活的脚本分支列表
Git Parameter足够覆盖80%的场景,但有些情况它做不了:比如要根据某个配置文件过滤分支、要按最后提交时间排序、要同时把release分支和tag混在一个列表里。这时候可以选择Active Choices插件,自己写Groovy脚本动态生成选项。
安装Active Choices插件后,参数可以这样写:
parameters { activeChoice( name: 'BRANCH_NAME', description: '动态从远程仓库读取分支', filterLength: 1, groovyScript: ''' def repo = "https://gitlab.example-internal.com/group/project.git" def branches = ["git", "ls-remote", "--heads", repo].execute().text return branches.readLines() .collect { it.replaceAll(".*refs/heads/", "").trim() } .findAll { it == "main" || it.startsWith("release-") } ''' ) }这段脚本使用的是Java的ProcessBuilder,通过execute()直接调用Git命令。需要提醒的是,这个Groovy脚本运行在Jenkins主节点上,而不是构建Agent里。所以主节点上必须装好Git,并且这个脚本环境下没有Pipeline的withCredentials上下文。如果你仓库是私有的,要么在Jenkins服务器上配置一个~/.git-credentials,要么用HTTP URL并把Token拼接进去。拼接Token的方式有泄露风险,不推荐,更好的做法是在主节点上配置好.netrc或者让Active Choices脚本读取Jenkins凭据文件。
相比Git Parameter,Active Choices的好处是自由,但坏处也明显:你要自己管权限、自己处理编码、自己处理异常。一个脚本写不好,分支下拉框直接就报红,还找不到日志,调试成本高。我的建议很直接:除非确实需要复杂逻辑,否则先用Git Parameter,把Active Choices留到“非用不可”的时候。
3.3 方案三:通过GitLab API扩展复杂场景
再说一个升级路径。如果你除了想拉分支,还想同时展示分支的“最后提交时间”“CI状态”“是否受保护”这些信息,单纯Git命令拿不到,那就得走GitLab API。
常见的方式是在Active Choices脚本里调用REST接口:
def apiUrl = "https://gitlab.example-internal.com/api/v4/projects/42/repository/branches?per_page=100&page=1" def token = "里料从凭据读取" def cmd = ["curl", "-s", "--header", "PRIVATE-TOKEN:${token}", apiUrl] def response = cmd.execute().text def branches = new groovy.json.JsonSlurper().parseText(response) return branches.findAll { it.name == "main" || it.name.startsWith("release-") } .collect { it.name }Project ID怎么拿?在GitLab仓库主页面找隐藏数字ID,或者用/api/v4/projects/group%2Fproject接口解析。URL里要把/转成%2F,这是最容易漏的一个细节。
GitLab API的好处是返回JSON,字段丰富;坏处是Token需要read_api权限、有分页问题、还要处理JSON解析库兼容性。一般项目用不到这么重,但如果你要做“分支列表里显示每个分支的Pipeline状态图标”,那API这条路是早晚要走的。
3.4 从“选中分支”到“正确检出”,细节决定成败
分支列表动态了,Checkout反而成了新的踩坑点。很多人在参数面板选好分支,下一步执行git checkout origin/xxx,发现构建结果不稳定。问题多半出在下面几个细节:
第一,如果上次构建在同一个工作区里检出了另一个分支,远程分支的本地跟踪引用可能会冲突。所以我会加上CleanBeforeCheckout或者deleteDir(),保证这次构建的工作区是干净的。第二,如果直接用checkout([$class: 'GitSCM', branches: [[name: params.BRANCH_NAME]]]),要注意BRANCH_NAME可能带origin/前缀,而Git插件的branches.name期望的是refs/heads/xxx这样的完整引用。前缀处理错误,Git会去尝试找本地分支或者直接把构建失败。第三,在Pipeline里,如果是临时想对某个分支做一下编译测试,不需要长期保留分支,我通常直接sh "git fetch origin ${targetBranch} && git checkout FETCH_HEAD",这样工作区处于游离HEAD状态,但构建完就删,不会留下分支垃圾。
关于“游离HEAD有没有影响”的问题,我说下经验:对于纯粹的编译、测试、打包,没有任何影响;但对于后续要基于当前分支继续提交的场景不行,那种场景还是老老实实检出到refs/heads/对应的本地分支。
4. 实操现场:一个可直接复制的完整Pipeline
4.1 一个兼容多种参数的Pipeline模板
这里给一个我在多个项目里用着的模板,已经跑了大半年,结构比较稳。它同时支持分支选择、构建类型选择、还加了一个“是否只读列表”的开关,方便测试。
pipeline { agent { label 'build-agent' } options { timestamps() disableConcurrentBuilds() } environment { GIT_REPO_URL = 'https://gitlab.example-internal.com/group/project.git' GIT_CREDENTIAL = 'gitlab-read-token' } parameters { gitParameter( name: 'BRANCH_NAME', type: 'PT_BRANCH', branchFilter: 'origin/(main|develop|release-.*|feature-.*)', defaultValue: 'origin/main', sortMode: 'DESCENDING_SMART', description: '选择远程分支' ) choice( name: 'BUILD_TYPE', choices: ['package', 'test', 'deploy'], description: '选择构建类型' ) booleanParameter( name: 'CLEAN_WORKSPACE', defaultValue: true, description: '是否清理工作区' ) } stages { stage('Prepare') { steps { script { if (params.CLEAN_WORKSPACE) { deleteDir() } env.TARGET_BRANCH = params.BRANCH_NAME.replaceFirst('^origin/', '') echo "开始构建分支: ${env.TARGET_BRANCH}" } } } stage('Checkout') { steps { script { checkout([ $class: 'GitSCM', branches: [[name: "refs/heads/${env.TARGET_BRANCH}"]], extensions: [[$class: 'CleanBeforeCheckout']], userRemoteConfigs: [[ url: env.GIT_REPO_URL, credentialsId: env.GIT_CREDENTIAL ]] ]) } } } stage('Run') { steps { script { switch (params.BUILD_TYPE) { case 'test': sh "mvn test" break case 'package': sh "mvn -DskipTests package" break case 'deploy': sh "./deploy.sh ${env.TARGET_BRANCH}" break } } } } } post { success { echo "构建成功: branch=${env.TARGET_BRANCH}, type=${params.BUILD_TYPE}" } failure { echo "构建失败,请查看日志" } } }有几个细节我单独强调:
disableConcurrentBuilds():同一任务同时跑两个分支会互相干扰工作区,加了它避免并发冲突。如果你希望不同分支能并行构建,不要加这个,但要改用不同工作区或者多个label节点,否则会死得很难看。CleanBeforeCheckout和手动deleteDir()二选一就行,重复反而容易在特殊目录挂载场景下出问题。我上面代码两个都写了,实际使用时建议删除其中一个,留CleanBeforeCheckout更稳妥。TARGET_BRANCH放在环境变量里,后面所有stage都能直接引用,比每次都去截取params.BRANCH_NAME清爽得多。
4.2 为什么参数面板有时候“转圈圈”很久
Git Parameter本质上每次打开页面都会执行git ls-remote --heads。仓库分支几百个、或者网络到GitLab的延迟高,面板就会卡几秒甚至十几秒。很多同事第一次遇到会以为是Jenkins卡死了,其实它只是在等Git命令返回。
优化办法有几个:
- 在
branchFilter里用更严格的正则,减少匹配分支数量。 - 升级到Git 2.31以上,
ls-remote走protocol v2效率更高。 - 不要用
PT_TAG之类的参数类型去加载海量Tag,Tag一多比分支还慢。
如果你想让参数面板速度更快,又需要展示的字段更多,可以把任务改成FreeStyle Job,配合“Dynamic Choice”插件做一次结果缓存,在一定时间窗口内不重新拉取。但Pipeline里做缓存比较麻烦,我一般不建议为这点时间复杂度买账。实践中,几十个分支的场景,Git Parameter刷一下通常1~2秒,完全可接受。
4.3 分支很多时,排序和分组是体验分水岭
分支多到上百个之后,排序就决定了下拉框好不好用。我见过一个项目,下拉列表第五百多个分支,没有任何排序,找自己要的分支基本靠Ctrl+F。Git Parameter自带sortMode,但很多人没意识去配置,默认乱序。
常用的几种排序模式:
| sortMode | 行为 | 适用场景 |
|---|---|---|
| NONE | 不排序,按Git返回顺序 | 快速验证,不推荐 |
| ASCENDING_SMART | 升序,数字按数值比较 | 一般项目 |
| DESCENDING_SMART | 倒序,版本号优先 | 发版频繁的项目 |
如果你想自己实现排序,比如把main强制排第一位、release分支放前面、feature放后面,那用Active Choices更合适。脚本里可以这样:
return branches.sort { a, b -> if (a == 'main') return -1 if (b == 'main') return 1 if (a.startsWith('release') && !b.startsWith('release')) return -1 if (!a.startsWith('release') && b.startsWith('release')) return 1 return a <=> b }<=>是Groovy的compare运算符,比写compareTo少很多噪音。但要注意,这个排序发生在参数面板打开时,每次打开都会执行一遍,所以尽量别在里面放重量级逻辑。
5. 常见问题与排查技巧实录
下面这几个问题都是我在过去实际推进流水线改造时反复遇到的,列成速查表,方便直接对照。
| 现象 | 典型原因 | 解决办法 |
|---|---|---|
| 面板里分支列表是空的 | 过滤正则没写对;Token权限不足;仓库地址写错 | 先用git ls-remote --heads <url>本机验证;确认分支名是origin/开头;检查Token的read_repository权限 |
| 列表只出现部分分支 | branchFilter正则太严格;分支名带特殊字符 | 适当放宽正则,先在Git Parameter的“Preview”里测正则 |
| 打开面板特别慢 | 分支太多;Git版本太低;网络延迟高 | 收紧过滤正则,升级Git;必要时改用API方案加缓存 |
| 选好分支但Checkout失败 | BRANCH_NAME带origin/前缀;工作区残留旧分支 | Checkout前去掉前缀;加CleanBeforeCheckout;确认branch参数写的是refs/heads/ |
| Token提示权限不足 | Scope勾少了;Token过期了 | 检查过期时间;重新生成Token;确保勾选read_repository |
| 中文分支名显示乱码 | 系统locale不是UTF-8;Groovy脚本读取流编码不对 | Agent上设置LANG=C.UTF-8;脚本里用getText('UTF-8') |
| 参数面板能看到分支,但构建时GitLab又说不存在 | 分支在打开面板后被删了 | 校验分支存在性,或者用try/catch捕获Checkout失败并给出清晰提示 |
有几个排查思路单独说一下。
第一个是本地先验证Git命令。不管你在面板里用什么姿势,最终底层都逃不过Git命令。怀疑分支列表为空,首先在Jenkins节点上执行:
git ls-remote --heads https://gitlab.example-internal.com/group/project.git如果命令本身输出正常,说明问题在插件配置上;如果命令也报错,那就是网络、认证或者仓库地址的问题。这个排查顺序能省掉大量时间。
第二个是跨节点问题。Pipeline在Master上执行参数生成,但实际git fetch发生在Agent上。如果两个节点的Git版本、网络出口、代理设置不一样,就会出现“面板上能选分支,但构建时拉不下来代码”的现象。遇到这种情况,优先在两个节点上分别跑一遍git ls-remote,看结果是否一致。
第三个我要明确表扬一下一个冷门但好用的技巧:在Git Parameter的branchFilter里,正则可以使用origin/(.*)查看所有分支,但你完全可以加!origin/(old-.*)这种排除逻辑?实测在部分版本插件中不支持,更稳妥的方式是写死一长串正向匹配的备选分支。所以规则越简单越不容易踩插件的正则解析差异。
关于手动测试时分支列表看似刷新不过来的问题,我前面已经解释过:参数面板只在打开时刷新。如果做自动化测试,没法反复手动开面板,可以让脚本去调用/job/<job>/descriptorByName/.../fillBranchItems接口拿到同样的结果,这是更“程序员”的验证方式,不过需要额外处理API认证,一般调试用不到。
6. 再补充两类特殊场景的取舍
6.1 Trigger模式:想自动构建,选Multibranch Pipeline
如果你的最终目标不是人工选择,而是GitLab上每有分支变化就自动构建,那别在参数化里死磕。直接新建一个Multibranch Pipeline,Jenkins会自动扫描仓库的分支并生成对应的子任务:
- 分支过滤规则写在“Branch Sources”里。
- 触发方式用Webhook,GitLab推送事件后Jenkins自动执行。
- 每个分支的构建状态会单独展示,和分支一一对应。
有人会问我:能不能把Multibranch Pipeline和Git Parameter结合?有,但没必要。Multibranch Pipeline每个分支本来就是独立流水线,你再在内部加一个“选分支”的参数,逻辑上自相矛盾。我见过的能跑通的混合用法,都是批发布场景,那就是另一个需求了。
6.2 Tag场景:分支和Tag要一起选
有些团队发布时不但要选分支,还要选Tag。Git Parameter支持PT_TAG类型,但不要同时定义两个gitParam然后期望联动。如果Tag和分支混杂的场景比较多,可以用Active Choices写一个下拉框,把两部分列表merge起来。注意Tag名和分支名不能重名,比如公司里习惯把分支命名v1.0.3,那就等着出诡异问题,因为GitLab本身允许分支和Tag同名,但你下拉框里根本分不清选的是哪个。我的建议是严格约定:分支不带纯粹版本号,版本号一律走Tag。
7. 最后分享一点我的实操体感
我用了快一年的Git Parameter方案,最大的感受是:不要追求极致“动态”而选了复杂方案。大多数团队几十个分支,Git Parameter的开箱即用完全够,稳定、好维护、同事上手快。Active Choices虽然灵活,但坏在“自由度高”也意味着“出错点多”,一旦脚本写得花哨,后面维护人会想骂人。我自己只有在需要排序、混选Tag、展示额外字段时才用自写脚本。
如果让我给一个最小配置清单,那就是三件事:Git Parameter插件、read_repository权限的Token、以及一个认真写过的branchFilter。把这三点做到位,“jenkins动态获取gitlab分支”这件事就已经解决了一大半。剩下的精力,不如拿去优化你真正更关心的构建速度和测试覆盖。