本文最后更新于:7 小时后
引言 一直很喜欢自动化的东西,自己的项目都配置有CI/CD;但是公司的项目单独用的TortoiseSVN和Jenkins,运维也没有配置服务端的Piplile;因此自己在TortoiseSVN使用Hook Scripts的客户端触发方法,本机执行Jenkins构建,因为Jenkins直接支持API创建构建,所以整体流程还算顺利
脚本实现的功能:
在每个SVN项目根目录中添加Post-commit后,根据脚本中的配置,按需在指定路径代码更新后触发指定服务的构建;支持参数设置
本文将指导:
任意脚本命令调试方法
Powershell调试方法
API调用Jenkins方法
TortoiseSVN触发Jenkins实用脚本
解决方案 API调用Jenkins 我的Jenkins版本为2.190.1 Jenkins的触发请查看Jenkins API;在网页的最下面有显示;地址为:
[Jenkins地址]/api/
认证方法为Basic认证,API Token请在用户设置中创建
新建构建在Create Job中有说明;
我的CURL命令是这样:
1 2 3 curl --location 'http://192.168.1.23/job/AAA.Web/buildWithParameters' \ --header 'Authorization: Basic XYZZZZXX=' \ --form 'REVISION="HEAD"'
AAA.Web为项目(Job)名称;
Authorization为PostMan使用Basic Auth自动生成,用户名为账号,密码为APIKey
form为我的构建参数
TortoiseSVN提交代码后自动执行脚本官方文档 中有客户端触发事件的详细说明;我这里只需要代码提交后的事件:Post-commit
其中Work Copy Path为SVN本地根路径(每个CheckOut加一条);执行的命令为powershell SVN\AAA\autoJenkins.ps1
这个autoJenkins.ps1文件在后面;保存在SVN本地路径中;因此意思为使用powershell调用这个脚本
这里我先给出我的autoJenkins.ps1脚本文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 param ( [string ]$PATH , [string ]$DEPTH , [string ]$MESSAGEFILE , [string ]$REVISION , [string ]$SVN_ERROR , [string ]$RESULTPATH , [string ]$projectName ) [Console ]::OutputEncoding = [System.Text.UTF8Encoding ]::new()$OutputEncoding = [System.Text.UTF8Encoding ]::new()Write-Host "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') start script" Add-Content svn-hook .log "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') start script" $svnPath = $env:svnPath ;if ([string ]::IsNullOrWhiteSpace($svnPath )) { $svnPath ="D:\svn\repo" }$pathMap = @ ( [PSCustomObject ]@ { Path = "\Program\InterFace\WebService" JobName = "WebService" } )$JENKINS_URL ="http://192.168.1.100:8080" $username = "007" $password = "xxx" function Log { param ([string ]$Message ) Write-Host "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message " Add-Content svn-hook .log "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message " } Log "PATH: $PATH " Log "DEPTH: $DEPTH " Log "MESSAGEFILE: $MESSAGEFILE " Log "REVISION: $REVISION " Log "SVN_ERROR: $SVN_ERROR " Log "RESULTPATH: $RESULTPATH " $script:listProjectName = @ ()function BuildProject { param ( [string ]$projectName , [string ]$hookPath = "" ) if ($script:listProjectName -contains $ProjectName ) { Log "ignore project: $ProjectName " return } $script:listProjectName += $ProjectName $ParentDir = $hookPath $JOB_NAME =$projectName if ([string ]::IsNullOrWhiteSpace($projectName )) { $JOB_NAME =Split-Path $ParentDir -Leaf } Log " url host is :$ {JENKINS_URL} " $uri = "$ ($JENKINS_URL .TrimEnd('/'))/job/$JOB_NAME /buildWithParameters" Log "url: $uri " $authPair = "$ ($username ):$ ($password )" $encodedCredentials = [System.Convert ]::ToBase64String([System.Text.Encoding ]::ASCII.GetBytes($authPair )) $headers = @ { Authorization = "Basic $encodedCredentials " } Invoke-WebRequest -UseBasicParsing ` -Uri $uri ` -Method Post ` -Headers $headers ` -Body @ { REVISION="HEAD" } }function CheckChildUpdate { param ( [string ]$Revision = "413" , [string ]$WatchDir = "/" ) Log "正在查询 SVN 版本: $Revision ..." Log "WatchDir is: $WatchDir " try { [xml ]$svnLog = svn log . -r $Revision -v --xml } catch { Log "SVN 命令执行失败,请检查 SVN 命令行工具是否安装,或 URL/权限是否正确。" exit 1 } Log "get svn log done." if ($null -eq $svnLog .log.logentry) { Log "未找到版本信息 (可能版本号不存在)" exit } $changedPaths = $svnLog .log.logentry.paths.path | ForEach-Object { $_ .InnerText } Log "该版本变更了 $ ($changedPaths .Count) 个文件。" $isHit = $false foreach ($pathTemp in $changedPaths ) { Log "pathTemp is: $pathTemp " $targetPath =Join-Path (Split-Path $Path -Parent ) $pathTemp Log "targetpath: $targetPath " if ($targetPath -like "$WatchDir *" ) { Log " fiind target path: $pathTemp " $isHit = $true break } } return $isHit }function BuildWithCommand { param ( [string ]$Revision = "413" , [string ]$WatchDir = "/" ) Log "for BuildWithCommand search svn version: $Revision ..." Log "WatchDir is: $WatchDir " try { [xml ]$svnLog = svn log . -r $Revision -v --xml } catch { Log "SVN 命令执行失败,请检查 SVN 命令行工具是否安装,或 URL/权限是否正确。" exit 1 } Log "get svn log done." if ($null -eq $svnLog .log.logentry) { Log "未找到版本信息 (可能版本号不存在)" exit } $commitMsg = $svnLog .log.logentry.msg.ToString() Log "Commit Message: $commitMsg " $isHit = $false if ($commitMsg -match "(?i)\[(ci skip|skip ci|no build)\]" ) { Log "检测到跳过指令 [ci skip],停止执行 CI 流程。" exit 0 } if ($commitMsg -match "(?i)\[build:\s*web\]" ) { Log "检测到 [build: web] 指令,强制触发 Web 构建..." } $pattern = "build\s*:\s*([\w\.-]+)" if ($commitMsg -match $pattern ) { $serviceName = $matches [1 ] Log "match build server: [$serviceName ]" if ($serviceName -eq "LifeHelper.Web" ) { } BuildProject $serviceName exit 0 } return $isHit }$myArgs = [System.Environment ]::GetCommandLineArgs()$target = $RESULTPATH for ($i = 0 ; $i -lt $myArgs .Length; $i ++) { Log ("arg[{0}] = `"{1}`"" -f $i , $myArgs [$i ]) } Log "target is: $target " $PATH =$myArgs [1 ] Log "new PATH: $PATH " if (![string ]::IsNullOrWhiteSpace($projectName )) { BuildProject $projectName } BuildWithCommand -Revision $REVISION -WatchDir $target $fullPathMap = [System.Collections.Generic.List [PSCustomObject ]]::new()foreach ($kv in $pathMap ) { $key =$kv .Path; Log "key is $key " $fullPath = Join-Path $svnPath $key Log "full path $ {fullPath}" Log "kv is $kv " $obj = [PSCustomObject ]@ { Path = $fullPath JobName = $kv .JobName } $fullPathMap .Add($obj ); } Log "target is: $targetNorm " $targetNorm = $target Log "targetNorm is: $targetNorm " $matchedValue = $null foreach ($entry in $fullPathMap ) { $keyNorm = [System.IO.Path ]::GetFullPath($entry .Path).TrimEnd('\' ) Log "keyNorm is: $keyNorm " if ($targetNorm .StartsWith($keyNorm , [System.StringComparison ]::OrdinalIgnoreCase)) { $matchedValue = $entry .JobName Log "Matched path: $keyNorm => value: $matchedValue " BuildProject $matchedValue $keyNorm } else { Log "Not matched path: $keyNorm try find by parent" if ($keyNorm .StartsWith($targetNorm , [System.StringComparison ]::OrdinalIgnoreCase)){ $isHit = CheckChildUpdate -Revision $REVISION -WatchDir $keyNorm if ($isHit ){ $matchedValue = $entry .JobName Log "Matched by child path: $keyNorm => value: $matchedValue " BuildProject $matchedValue $keyNorm } } } }if ($null -eq $matchedValue ) { Log "No path matched: $targetNorm " return }Write-Host "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') script end" Add-Content svn-hook .log "$ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') commit done"
配置说明
配置项
说明
svnPath
SVN根目录环境变量
pathMap
SVN监听目录及其对应的Jenkins名称字典;此为SVN目录中的相对路径;
JENKINS_URL
Jenkins服务地址
username
Jenkins服务用户
password
Jenkins服务API Token
因为脚本用到了SVN获取提交记录的方法,因此需要安装SVN CLI;在安装TortoiseSVN时需选择command line client tools
用法 本来是先写这个文章的,结果发现脚本有些问题;经常会报错;于是只能先解决报错;就有了前一篇文章:解决powershell表达式或语句中包含意外的标记问题 ;又经过几次测试使用;改良了脚本;
当有多个不同目录的文件提交到SVN时,它的RESULTPATH会是所有目录的最小父级,因此要能根据SVN提交的目录来触发对应的Job还要从REVISION中遍历所有文件;找到对应的配置匹配的目录触发多个Job
此脚本功能:
根据配置($pathMap)在指定svn目录commit后触发对应的Jenkins Job
使用命令直接触发Jenkins Job,命令:.\autoJenkins.ps1 -projectName "JobName"
从历史版本号触发对应的Jenkins Job,命令:.\autoJenkins.ps1 -REVISION "500" -RESULTPATH "G:\work\SVN\LifeHelper\Program\InterFace\WebService"
在SVN Commit中编写[build: MySpecialJob] 直接触发构建MySpecialJob
Powershell调试方法 在 VS Code中一般调试方法
1.安装 PowerShell 扩展
2.打开 .ps1
3.左侧点红点打断点
4.F5 启动调试
假如需要参数可以在PowerShell Extension的控制台中直接输入带参数的命令触发
在TERMINAL中直接输入脚本带参数命令powershell.exe -File "G:\work\SVN\LifeHelper\autoJenkins.ps1" -Revision 537就可以直接中断调试
调试Post-commit命令的方法 如果直接使用powershell SVN\AAA\autoJenkins.ps1这样的触发命令;因为命令后台执行;假如有意外的错误完全摸不着头脑
那有什么可以实时看到控制台输出及结果的方法吗?
我在这位老哥那里找到了:Using a TortoiseSVN Client Hook Script to Run a Visible PowerShell Script
将powershell SVN\AAA\autoJenkins.ps1命令替换为以下内容
1 powershell.exe -command "& {start-process powershell.exe (([system.environment]::GetCommandLineArgs()|ForEach-Object -process { '\" {0 }\"' -f $_ })[3..([system.environment]::GetCommandLineArgs().Length-1)])}" "-file" "G:\work\SVN\LifeHelper\autoJenkins.ps1"
它可以将启动参数传递给一个新的有前台界面的powershell进程;这样就可以直观看到命令结果了
假如命令太快弹窗一下就消失,可以在代码中添加Read-Host "Press Enter to exit"这样的暂停代码;这样只有在回车确认之后才回继续执行
如果还有一些异常情况;可以本地开启录屏;在录像中逐帧分析报错
结语 实测Post-commit触发器只在svn的命令执行时才有效;比如文件管理器集成的TortoiseSVN GUI操作,SVN Commit命令以及visual studio的VisualSVN插件操作;
第三方集成的svn客户端无法触发;比如IDEA的SVN VCS代码工具无法触发
参考资料