ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Jenkins动态获取GitLab分支:从手动参数到多分支流水线实战

Jenkins动态获取GitLab分支:从手动参数到多分支流水线实战

先说一句实话:网上搜“jenkins动态获取gitlab分支”,能翻到一大堆插件配置截图,告诉你装个Git Parameter插件,然后填个参数名就完事了。但真到自己上手,会发现要么分支列表刷不出来,要么获取的是远程追踪分支带一大串origin/前缀,要么多分支流水线里压根不知道该怎么过滤。这篇文章我尽量把我自己搭这套东西时的完整思路、踩坑过程、以及最终一直在用的方案讲清楚,不是什么标准教程,但照着做应该能少走不少弯路。

先交代一下背景。我所在的部门用的是GitLab社区版做代码托管,Jenkins做持续集成,之前所有项目的构建任务都是一个项目写死一个分支,比如develop分支有构建任务,master分支再单独复制一份任务,改个分支名。刚开始项目少感觉还行,后来微服务拆了一堆仓库出来,每个仓库还要支持feature分支的临时验证构建,那套“写死分支”的玩法直接就崩了——任务数量爆炸、维护靠翻历史记录、开发天天跑过来问“我这个分支怎么没构建”。

后来下决心把“动态获取GitLab分支”这件事彻底搞定。这里的“动态”其实分两层意思:第一层,构建任务在触发时能实时列出GitLab仓库当前的所有分支,让操作人选一个再构建;第二层,GitLab侧推了新的分支过来,Jenkins能自动感知、自动创建对应的构建任务或构建记录,不需要人工干预。这两层需求不一样,实现方式也完全不一样,很多人没分清,所以折腾半天搞不定。

1. 先分清两类需求:手动选分支与自动感知新分支

这是整个方案里最重要的一步,不分清需求,后面全是白费功夫。

第一类需求最常见:开发人员手上有一个feature分支,想临时跑一次构建验证代码能不能编译过、测试能不能过。这时候他希望打开Jenkins任务页面,能看到一个下拉框,里面列着GitLab仓库当前所有的分支,选中自己要的那个,点构建就行。这种需求本质上是“把分支选择权交给操作人”。

第二类需求更偏自动化:团队希望develop分支每次提交都自动构建,同时新建的release分支、hotfix分支也能自动被识别并触发构建。这种需求不依赖人去选择,而是Jenkins主动去GitLab那边“看”有没有新分支出现。这种需求本质上是“让系统自己发现变化”。

我见过很多同事把这两件事混在一起,用同一个插件想同时实现,结果两个都做不好。比如用Git Parameter插件实现了下拉框,但发现GitLab那边新建的分支不会自动出现在下拉框里,得手动刷新;或者用多分支流水线实现了自动发现,但想让人手动选某个历史分支跑一次构建,又发现没有入口。

实际上这两类需求可以拆开解决,也可以组合使用。我的建议是:如果团队规模不大、构建触发频率不高,先做第一类,即“手动选分支”,成本低、见效快;如果仓库多、分支多、需要严格的门禁管控,再上第二类,即“自动感知新分支”。

2. 第一类需求的标准做法:参数化构建配上Git Parameter插件

先讲手动选分支的做法,因为这是绝大多数团队的起步方案,后续理解多分支流水线也更容易。

2.1 插件的选择与安装

Jenkins里做参数化构建的插件有好几个,比如Git Parameter、Git Branch Parameter、Dynamic Choice Parameter等。我最终用的是Git Parameter,原因很简单:它直接封装了git ls-remote命令,能动态获取远程仓库的分支、标签、修订版本,而且和Jenkins的Git插件配合得最好,不会有变量解析不一致的问题。

安装方式不赘述了,在Jenkins的插件管理页面搜索“Git Parameter”安装即可,注意版本兼容性,老版本Jenkins上装新版插件可能会有警告,但一般不影响使用。

2.2 核心配置:构建参数里加一个分支参数

在Jenkins任务的“参数化构建过程”里添加一个参数,类型选择“Git Parameter”:

  • Name填一个变量名,比如BRANCH_NAME,后面构建脚本里就用这个变量引用分支名。
  • Parameter Type选“Branch”,这样拉取的是分支列表。如果选了“Tag”就拉标签列表,“Revision”会拉取所有可用的引用。
  • 在“Branch Filter”里可以写通配符做过滤,比如只想显示release开头的分支,就写release/*。这一步很关键,尤其是仓库里分支特别多的时候,下拉框几百个分支根本没法选,过滤是刚需。

我第一次配置的时候就是卡在这个过滤上,当时不知道有Branch Filter这个字段,结果分支列表里全是origin/xxx这种前缀,后来查了文档才发现,Git Parameter插件默认会把远程分支的origin/前缀去掉,但你如果用了自定义的远程名称或者非标准命名空间,前缀可能不会完全过滤干净。这时候需要自己在Filter里处理。

配置保存之后,再点“Build with Parameters”,就能看到下拉框里列出了所有匹配的分支,选中之后构建就开始了。到这里“手动选分支”的任务其实就已经完成了,很简单对吧。但这里面有个非常隐蔽的坑,我单独拿出来说。

2.3 容易踩的坑:分支参数值跑到构建里变成了乱码

有些同事发现,明明下拉框里选的是feature/login,但构建日志里显示的却是origin/feature/login,甚至有时候是两个值拼接在一起,导致checkout失败。

这个问题的根源在于:Git Parameter插件负责把选中的分支名注入到参数变量里,但Jenkins任务里同时配置了“Source Code Management”里的Git仓库地址和分支。如果你在Git的“Branches to build”里写了*/${BRANCH_NAME},而BRANCH_NAME本身已经被插件处理成了origin/feature/login,那最终拼出来的值就变成了*/origin/feature/login,git自然找不到这个引用。

我最终的写法是:在Git插件的“Branches to build”里只写${BRANCH_NAME},不要加*/前缀。因为Git Parameter插件已经处理过了,它给出的值就是干净的分支名,比如feature/login,Git插件可以直接识别并切换到对应分支。如果你用的参数类型不是Branch而是Revision,那这里可能要按实际情况调整,但常规情况下这个写法最稳。

另外还有一个和凭据相关的点也值得提一下。Git Parameter插件要在构建前拉取分支列表,它需要访问GitLab仓库,所以任务里必须配置好Git的Credentials,而且这个账号需要有对应的仓库访问权限。很多同事遇到“下拉框是空的”第一反应是插件坏了,其实十有八九是凭据权限不够。

2.4 构建脚本里怎么用这个参数

参数值拿到之后,在构建步骤里直接用就行。比如是Shell构建步骤:

echo "当前构建分支:${BRANCH_NAME}" git checkout ${BRANCH_NAME}

如果用的是Pipeline流水线,可以这样传参:

stage('Checkout') { checkout([ $class: 'GitSCM', branches: [[name: "${BRANCH_NAME}"]], userRemoteConfigs: [[url: 'http://gitlab.example.com/group/project.git', credentialsId: 'gitlab-cred']] ]) }

这里有一个细节:branch里不要自己去拼refs/heads/前缀,Jenkins的Git插件会自己处理。你传feature/login,它就知道是refs/heads/feature/login;你传origin/feature/login,它反而可能解析出问题。

3. 第二类需求:用MultiBranch Pipeline自动感知GitLab新分支

手动选分支虽然解决了“动态”的一半问题,但遇到团队分支策略严格、希望新分支自动构建的场景,还是不够。比如团队规定了feature分支合并前必须构建通过,你不能指望开发每次手动去Jenkins点一次构建,这个动作必须自动发生。这时候就要上MultiBranch Pipeline,也就是多分支流水线。

3.1 多分支流水线的本质

多分支流水线不是一个“插件式”的功能,它是Jenkins Pipeline的一种项目类型。创建任务时,类型选择“Multibranch Pipeline”,它会自动扫描代码仓库里的所有分支,并为每一个匹配规则的分支生成一个独立的子任务。每次子任务构建时,它就相当于一个普通的Pipeline任务,只不过分支信息是自动注入的。

它和普通任务最大的区别在于:普通任务是“一个任务对应一个或多个固定分支”,多分支流水线是“一个任务对应整个仓库的所有匹配分支”,并且会自动感知新分支和删除的分支。

听上去很完美对吧?但这里有很多需要设计的地方,不是说建了任务就完事了。

3.2 源码管理配置里的GitLab地址与凭据

创建多分支流水线任务后,在“Branch Sources”里选择Git类型:

  • Project Repository配GitLab仓库地址,注意要选对协议,是HTTP还是SSH。
  • Credentials选择有权限的账号,最好是一个独立的部署账号,不要用个人账号,因为个人账号一旦离职或者权限变动,所有下游构建全挂。
  • Behaviors里面建议加上“Discover branches”和“Discover tags”,根据团队策略决定是否开启。比如只关心所有分支,那就默认的All branches即可;如果只关心以某个前缀开头的分支,可以加一个“Filter by name”策略。

我第一次配置的时候没有加任何过滤策略,结果仓库里几十个分支加上几十个合并请求、标签,Jenkins扫描一次要跑好久,而且构建队列里一堆无意义的子任务。后来加了过滤,只保留develop、release/*和feature/*,整个世界清静了。

3.3 Jenkinsfile里怎么拿当前分支名

在多分支流水线里,你不用自己去定义分支参数了,Jenkins会自动把当前分支注入到环境变量BRANCH_NAME里。Pipeline脚本里直接用:

pipeline { agent any stages { stage('打印当前分支') { steps { echo "当前构建分支:${env.BRANCH_NAME}" } } stage('构建') { steps { sh """ if [ "${env.BRANCH_NAME}" = "develop" ]; then echo "开发分支构建" elif [[ "${env.BRANCH_NAME}" == release/* ]]; then echo "发布分支构建" else echo "特性分支构建" fi """ } } } }

注意一个细节:env.BRANCH_NAME在每次构建时会被自动设置为当前分支名,你不需要在参数化构建里再手动添加一个同名参数,否则两个变量会打架,行为会变得很诡异。我就遇到过这个问题:在Pipeline参数里定义了一个BRANCH_NAME,结果子任务的构建一直报错“无法解析BRANCH_NAME”,最后才发现是变量冲突。

3.4 新分支自动被扫描:轮询与Webhook的选择

多分支流水线要自动感知新分支,核心机制是“扫描”。有两种触发方式:

一种是Jenkins定时轮询GitLab仓库,比如每隔5分钟扫描一次,配置在任务的“Scan Multibranch Pipeline Triggers”里,填一个Cron表达式,比如H/5 * * * *。这个做法简单可靠,但缺点是扫描有时间延迟,新分支推上去最多要等5分钟才能触发第一次构建。

另一种是用GitLab Webhook。在GitLab的项目设置里添加一个Webhook,URL指向Jenkins的/multibranch-webhook-trigger/invoke,Token填任务里配置的令牌。这样每次有Push事件时,GitLab会立刻通知Jenkins去扫描。这个方案响应快,但需要保证两个系统之间的网络是通的,而且Token要保管好。

我自己实际使用中采用了折中的方案:Webhook负责实时感知,同时保留一个低频次的轮询作为兜底,防止Webhook因为网络问题漏掉事件。兜底扫描频率设得比较低,比如每小时一次,只用来发现漏网之鱼。

4. 亲测完整的配置实例:从零搭一个支持动态分支的流水线任务

理论说了不少,下面我给一个可以照着抄的实操流程。以一个模拟项目“某跨平台系统”为例,GitLab仓库地址是http://gitlab.example.com/group/demo-platform.git,假设已经配置好了Jenkins和GitLab之间的网络与凭据。

4.1 第一步:准备GitLab访问凭据

在Jenkins的“凭据管理”里添加一个“Username with password”类型的凭据,或者SSH私钥类型的凭据,关键是这个凭据的账号要对目标仓库有读权限。按我个人的经验,用HTTP协议配合账号密码最省心,搭在内网环境尤其合适;SSH虽然安全,但还要处理known_hosts,内网环境没必要给自己找麻烦。

如果你用的是GitLab部署账号,注意不要把账号密码暴露在Jenkins任务配置里明文展示,Jenkins的凭据加密机制会帮你隐藏,但你自己截图发群里的习惯要改一改。

4.2 第二步:创建多分支流水线任务

Jenkins首页 → 新建任务 → 输入名称 → 选择“Multibranch Pipeline”。这里的任务名称建议和仓库名保持一致,后期维护不会乱。

在配置页面里:

  • Branch Sources里选择Git,填Repository URL为http://gitlab.example.com/group/demo-platform.git。
  • Credentials选择刚才添加的凭据。
  • Behaviors里添加“Discover branches”,策略选择“All branches”;添加“Filter by name”,在Include里填(develop|release/.*|feature/.*),排除其他杂七杂八的分支。
  • Build Configuration里Jenkinsfile路径保持默认的Jenkinsfile,如果你的脚本放在根目录且命名为Jenkinsfile,这里就不用动。
  • Scan Triggers里勾上“Periodically if not otherwise run”,填H/30 * * * *做兜底;同时记下这里的Token,后面Webhook要用的。

保存之后,任务会自动执行一次扫描。看到扫描结果里列出若干个分支子任务,说明基础配置已经通了。

4.3 第三步:配置GitLab Webhook加速触发

在GitLab项目页面,进入“Settings → Webhooks”,添加一个新的Webhook:

  • URL填http://jenkins.example.com/multibranch-webhook-trigger/invoke。
  • Secret Token填任务配置里的Token。
  • Trigger勾选Push events和Merge request events。

保存后可以点一下“Test”按钮测试连通性,Jenkins端的日志扫描会触发一次。这一步如果发现测试不通过,大概率是网络不通或者Token不匹配,先自查这两项。

4.4 第四步:写Jenkinsfile支撑不同分支的差异化构建

在代码仓库根目录放一个Jenkinsfile,内容大致如下:

pipeline { agent any options { timestamps() disableConcurrentBuilds() } stages { stage('识别分支') { steps { script { currentBuild.displayName = "${env.BRANCH_NAME} - ${BUILD_NUMBER}" } } } stage('检出代码') { steps { checkout scm } } stage('单元测试') { steps { sh './gradlew test' } } stage('构建产物') { when { anyOf { equals expected: 'develop', actual: "${env.BRANCH_NAME}" matching pattern: 'release/.*', value: "${env.BRANCH_NAME}" } } steps { sh './gradlew assembleRelease' } } } }

这个脚本里有两个关键设计:一是用checkout scm而不是手工指定Git仓库地址,因为多分支流水线会自动注入当前分支的SCM信息;二是用when条件控制只有develop和release分支才构建产物,feature分支只跑测试不构建APK,节省不少构建资源。

4.5 第五步:验证整个流程

完整验证需要做两件事:

第一,在GitLab上新建一个feature/portal-refactor分支,推送到远端。正常情况下,几秒钟内Webhook就会触发Jenkins扫描,扫描发现新分支后自动创建对应的子任务并开始构建。你可以在Jenkins的任务页面看到分支列表里多了一个feature/portal-refactor的子任务,点进去能看到构建日志。

第二,在已有分支(比如develop)上做一次新的提交并推送,观察是否触发了对应的子任务构建。如果之前那套“写死分支”的旧任务还留着,现在可以做对比:旧任务永远构建的是同一个分支,新任务则会根据当前推送的分支自动匹配。这个过程比手动改分支名要稳太多了。

5. 那些年踩过的坑:分支扫描、凭据、命名空间的三座大山

光是把方案跑通还不够,我把自己这几个项目里踩过的坑按频率排个序,提前给大家打个预防针。

5.1 大仓库扫描慢,甚至导致Jenkins卡顿

多分支流水线每次扫描时都会对仓库做一次全量引用的拉取和匹配,仓库大、分支多、标签多的情况下,扫描一次可能要花几十秒甚至几分钟。如果扫描频率设置得过快,比如每1分钟一次,Jenkins主控节点会被拖垮,所有构建都变慢。

我踩过最狠的一次:一个单体仓库大概两千多个分支(有些是历史上遗留下来的远程死分支),我设置了H/2 * * * *的扫描频率,结果Jenkins的处理器直接满负荷,构建排队排了半个多小时。后来我做了三件事才解决:第一,扫描频率降到H/15 * * * *;第二,在过滤器里只保留develop、release/*、feature/*,其余全排除;第三,定期清理GitLab上的死分支,和团队约定超过一定时间没有合并活动的分支要删除。

5.2 凭据权限只够读,不能触发Webhook

Webhook触发扫描时,Jenkins会用配置的凭据去访问GitLab。如果这个凭据只有普通开发者权限,有些操作会受限制,特别是当GitLab仓库启用了推送规则、合并请求审核等安全策略时。

我的建议是:单独建一个“部署账号”,角色的权限只给这个仓库群的Reporter权限,不要给Maintainer权限,降低误操作风险。同时这个账号不参与日常人工操作,密码定期更换。

5.3 分支名里有特殊字符导致脚本崩溃

有些分支名会带/、#、%之类的字符,比如feature/UI#123。这类分支名如果直接拼到Shell脚本里,很容易出问题。比如:

git checkout feature/UI#123

#在Shell里是注释符号,这一行后面的内容全被当成注释了,checkout会失败。

处理办法有两个:一是所有引用分支名的地方必须加双引号,比如git checkout "${BRANCH_NAME}";二是在Jenkinsfile的最前面加一行清理逻辑,把非法字符替换掉或者在日志里做个映射。不同团队的处理习惯不一样,但我们用的是前者,简单直接,问题基本没再出现过。

5.4 命名空间与分支变量的“双重前缀”问题

这个坑和前面提过的origin/前缀有关,但在多分支流水线里还有另一种变体。如果你在Jenkinsfile里自己又拼了一次refs/heads/${BRANCH_NAME},而BRANCH_NAME本身已经带了origin/前缀,最终就会变成refs/heads/origin/feature/login,git命令找不到这个引用。

这个问题在多分支流水线里之所以更容易出现,是因为很多教程会教你在Jenkinsfile里写“通过环境变量拼接分支引用”,但多分支流水线已经帮你处理好了这个问题,你直接用${BRANCH_NAME}就是最干净的值。除非你开了特定的“高级克隆”选项,否则别自己动手做二次处理。

6. 为什么我说别用“Git Parameter + 多分支流水线”的混搭方案

看到这里你可能会想:那我可不可以一边用多分支流水线自动感知,一边又在任务参数里加一个Git Parameter让操作人远程触发某个历史分支的构建?

理论上可以,但我在实际使用中强烈不建议这么干。原因有两点:

第一,多分支流水线的子任务和普通任务的参数化机制不一样。子任务是从父任务扫描出来的,它的参数定义从Jenkinsfile里读取,而不是从任务的“参数化构建”里配置。如果你在父任务的“参数化构建过程”里加了一个Git Parameter,子任务不一定能正确继承,甚至可能导致所有子任务都执行一遍参数解析,构建之前还要去远端拉一次分支列表,效率大打折扣。

第二,这种混搭会让构建记录的可追溯性变差。多分支流水线的子任务天然按分支归类,你在分支页面就能看到这个分支的所有构建历史。但一旦引入手动参数,同一个分支就有两套构建入口,一套是自动感知触发的,一套是手动选的,看起来都是同一个分支在构建,但代码来源、参数环境可能完全不同,排查问题时很容易把人绕晕。

如果团队确实需要“临时对某个历史分支构建一次”,我的做法是给Jenkins加一个额外的自由风格任务,专门用于手动指定分支构建,但它不参与常规的流水线记录,只是作为一个应急工具。这样自动体系不受污染,应急能力也保留着。这个思路在实践里最省心。

7. 动态获取分支的底层逻辑:一次对“构建来源”的重新理解

把方案跑通之后,我回过头来琢磨了一下“jenkins动态获取gitlab分支”这件事其实是在解决什么问题。

过去我们的构建来源是“一个固定配置”:我告诉Jenkins,你去构建develop分支。于是Jenkins像一个听指令办事的人,每次按照这个指令执行。但现在团队的协作节奏变了,分支的创建和合并非常频繁,如果构建来源还是“固定配置”,那相当于每一次分支变化都改一次Jenkins配置,这显然不可持续。

动态获取分支的本质,是把“构建来源”从配置态变成发现态:Jenkins不再被动地等我们告诉它去构建哪个分支,而是主动去发现代码仓库里有哪些分支、哪些分支发生了变化、哪些分支应该被构建。这个转变说起来就一句话,但真正落到工具上,涉及扫描策略、过滤规则、Webhook事件、权限模型等一整套设计。

理解了这一点,再看网上的教程心态就不一样了:你不再是“照着填一堆参数”,而是知道每一步配置都是在回答“Jenkins需要知道什么信息才能发现并构建正确的分支”。

就拿分支过滤来说,很多教程让你填一个正则表达式,你不理解的时候就是盲目照抄,换一个仓库名变了可能就忘了改。但你明白过滤是为了“在扫描阶段减少无效分支进入构建流程”,你就会主动思考:团队分支命名规则是什么?哪些分支需要构建哪些不需要?正则该怎么写才能覆盖当前策略又留出扩展空间?

再比如Webhook,你明白了它是“GitLab主动推事件给Jenkins”,就知道它不是必须的,只是一种更高效的触发方式,本质上和定时轮询做的是同一件事。于是你就会根据团队的网络条件、构建实时性要求来决定要不要配Webhook,而不是看到教程里有就无脑加上。

这种理解层面的东西,可能比配置步骤本身更值钱。因为配置是会变的——插件升级、GitLab版本升级、Jenkins升级,都可能让某个字段的位置和叫法发生变动,但你理解了底层逻辑,换一套界面你照样能找到对应的配置项。

我在实际维护这套流水线的头两个月里,几乎每周都要改一点东西:加一个分支过滤规则、调一次扫描频率、优化一下Jenkinsfile里when条件。如果是死记硬背的配置党,这种迭代速度早就崩溃了。但理解了动态分支的运作机制之后,每次改动都很快能定位到对应的配置项,改完还能预判它会不会影响其他分支的构建。掌握原理比记住步骤重要得多,这句话在Jenkins流水线这件事上体现得淋漓尽致。

所以如果你现在正准备搭这套东西,我的建议是:先花一个小时理解多分支流水线的完整链路,从扫描、匹配、过滤到触发构建,脑子里有一条清晰的链路,再打开Jenkins界面操作。你会发现那些教程里让你填的每一个字段,背后都有一个明确的问题在等你的答案。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进