适用于 PowerShell 7.0 及以上版本
在现代软件交付体系中,Azure DevOps 已经成为许多团队的核心协作平台。它集成了代码仓库、CI/CD 流水线、工作项追踪和制品管理等功能,为端到端的 DevOps 实践提供了完整支撑。然而随着团队规模扩大和项目数量增多,仅依靠 Web 界面进行日常管理变得低效——批量创建项目、统一配置流水线、定期生成进度报告等场景迫切需要自动化手段。
PowerShell 凭借对 REST API 的原生支持和强大的对象处理能力,是自动化管理 Azure DevOps 的理想工具。通过脚本调用 Azure DevOps REST API,我们可以将重复性的管理操作编排成可重复执行的工作流,例如一键同步多个项目的仓库配置、自动触发全量回归测试流水线、定时生成冲刺健康度报告。
本文将从实际运维场景出发,介绍如何使用 PowerShell 实现 Azure DevOps 的三大类自动化操作:项目与仓库的批量管理、CI/CD 流水线的触发与监控、工作项的查询与报告生成。每个场景都提供可直接运行的完整脚本和执行结果演示。
Azure DevOps API 连接与项目管理 所有 Azure DevOps 自动化的基础是与 REST API 建立可靠的连接。以下代码封装了一个通用的 API 调用函数,并在此基础上实现项目的批量查询和仓库操作。我们将 Personal Access Token(PAT)存储在环境变量中,通过 Base64 编码构造认证头,确保凭据不会以明文形式出现在脚本中。
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 function Invoke-AzDoRequest { [CmdletBinding ()] param ( [Parameter (Mandatory )] [string ]$Organization , [string ]$Project , [Parameter (Mandatory )] [string ]$ApiPath , [Parameter (Mandatory )] [securestring ]$PatToken , [ValidateSet ('GET' , 'POST' , 'PATCH' , 'PUT' , 'DELETE' )] [string ]$Method = 'GET' , [object ]$Body , [string ]$ApiVersion = '7.1' ) $plainPat = (New-Object PSCredential('user' , $PatToken )).GetNetworkCredential().Password $base64Auth = [Convert ]::ToBase64String( [System.Text.Encoding ]::ASCII.GetBytes(":$plainPat " ) ) $headers = @ { Authorization = "Basic $base64Auth " 'Content-Type' = 'application/json' } $uri = if ($Project ) { "https://dev.azure.com/$Organization /$Project /_apis$ApiPath " } else { "https://dev.azure.com/$Organization /_apis$ApiPath " } $separator = if ($uri -match '\?' ) { '&' } else { '?' } $uri = "$uri $separator `api-version=$ApiVersion " $params = @ { Uri = $uri Headers = $headers Method = $Method } if ($Body ) { $params .Body = ($Body | ConvertTo-Json -Depth 10 -Compress ) } try { Invoke-RestMethod @params } catch { $statusCode = $_ .Exception.Response.StatusCode.value__ $errorReader = [System.IO.StreamReader ]::new($_ .Exception.Response.GetResponseStream()) $errorBody = $errorReader .ReadToEnd() Write-Error "API 请求失败 [HTTP $statusCode ]: $errorBody " } } $pat = ConvertTo-SecureString $env:AZDO_PAT -AsPlainText -Force $allProjects = Invoke-AzDoRequest ` -Organization 'mycompany' ` -ApiPath '/projects' ` -PatToken $pat Write-Host "组织内的项目列表:" Write-Host ("-" * 60 )foreach ($proj in $allProjects .value) { $lastUpdate = [datetime ]::Parse($proj .lastUpdateTime).ToString('yyyy-MM-dd' ) Write-Host " 名称: $ ($proj .name)" Write-Host " 状态: $ ($proj .state) | 上次更新: $lastUpdate " Write-Host " ID: $ ($proj .id)" Write-Host "" } $repos = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath '/git/repositories' ` -PatToken $pat Write-Host "PlatformService 项目的仓库:" foreach ($repo in $repos .value) { $branch = $repo .defaultBranch -replace 'refs/heads/' , '' Write-Host " [$ ($repo .name)] 默认分支: $branch , 大小: $ ([math]::Round($repo .size / 1MB, 1)) MB" }
上述代码定义了 Invoke-AzDoRequest 函数,支持组织级和项目级的 API 调用。函数自动根据 URI 是否已包含查询参数来拼接 api-version,避免重复的 ? 符号。错误处理部分捕获 HTTP 异常并解析响应体,便于排查 API 调用失败的原因。通过该函数可以方便地查询项目列表和仓库信息,为后续的批量管理操作奠定基础。
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 组织内的项目列表 : ------------------------------------------------------------ 名称 : PlatformService 状态 : wellFormed | 上次更新: 2026-01-28 ID : a1b2c3d4-e5f6-7890-abcd-ef1234567890 名称 : DataPipeline 状态 : wellFormed | 上次更新: 2026-01-15 ID : b2c3d4e5-f6a7-8901-bcde-f12345678901 名称 : MobileApp 状态 : wellFormed | 上次更新: 2025-12-20 ID : c3d4e5f6-a7b8-9012-cdef-123456789012 PlatformService 项目的仓库 : [PlatformService.Api] 默认分支 : main, 大小: 24.3 MB [PlatformService.Web] 默认分支 : main, 大小: 18.7 MB [PlatformService.Infra] 默认分支 : develop, 大小: 3.2 MB
流水线管理:触发构建与下载制品 CI/CD 流水线是 Azure DevOps 中最核心的自动化能力之一。在日常运维中,我们经常需要通过脚本触发流水线(例如数据迁移完成后触发部署)、查询构建状态(集成到监控看板),以及下载构建制品(用于自动化测试或灰度发布)。以下代码演示了这三个常见操作的完整实现。
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 $runPayload = @ { resources = @ { repositories = @ { self = @ { refName = 'refs/heads/main' } } } templateParameters = @ { environment = 'staging' enableSmokeTest = 'true' tagVersion = '2.4.1-rc.3' } } $newRun = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath '/pipelines/42/runs' ` -Method POST ` -PatToken $pat ` -Body $runPayload Write-Host "已触发流水线运行:" Write-Host " 运行 ID: $ ($newRun .id)" Write-Host " 流水线: $ ($newRun .pipeline.name)" Write-Host " 状态: $ ($newRun .state)" Write-Host "" $runId = $newRun .id$maxWaitMinutes = 30 $startTime = Get-Date while (((Get-Date ) - $startTime ).TotalMinutes -lt $maxWaitMinutes ) { Start-Sleep -Seconds 20 $runStatus = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath "/pipelines/42/runs/$runId " ` -PatToken $pat $elapsed = ((Get-Date ) - $startTime ).ToString('mm\:ss' ) Write-Host "[$elapsed ] 运行 #$runId 状态: $ ($runStatus .state)" if ($runStatus .state -in 'completed' , 'cancelling' , 'cancelled' ) { Write-Host "" Write-Host "流水线运行结束:" Write-Host " 最终结果: $ ($runStatus .result)" Write-Host " 完成时间: $ ($runStatus .finishedAt)" break } } if ($runStatus .result -eq 'succeeded' ) { $artifacts = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath "/build/builds/$ ($newRun .id)/artifacts" ` -PatToken $pat $downloadDir = Join-Path $HOME "Downloads/AzDo-Artifacts/$ ($newRun .id)" New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null foreach ($artifact in $artifacts .value) { $downloadUrl = $artifact .resource.downloadUrl $fileName = "$ ($artifact .name).zip" $savePath = Join-Path $downloadDir $fileName Write-Host "正在下载制品: $ ($artifact .name) -> $savePath " Invoke-WebRequest -Uri $downloadUrl -Headers @ { Authorization = "Basic $ ([Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(" :$plainPat ")))" } -OutFile $savePath } Write-Host "" Write-Host "所有制品已下载至: $downloadDir " Get-ChildItem $downloadDir | ForEach-Object { Write-Host " $ ($_ .Name) ($ ([math]::Round($_ .Length / 1KB, 1)) KB)" } }
这段脚本按照实际运维流程编排了三个步骤:首先触发流水线并传入模板参数(目标环境、是否执行冒烟测试、版本标签),然后以 20 秒间隔轮询运行状态直到完成,最后仅在构建成功时下载所有制品到本地。轮询部分使用时间差而非固定次数,避免长时间运行的流水线被提前终止。
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 已触发流水线运行: 运行 ID: 1847 流水线: PlatformService-CI 状态: inProgress [00:20] 运行 #1847 状态: inProgress[00:40] 运行 #1847 状态: inProgress[01:00] 运行 #1847 状态: inProgress[01:20] 运行 #1847 状态: inProgress[01:40] 运行 #1847 状态: completed流水线运行结束: 最终结果: succeeded 完成时间: 2026 -02 -03 T08:23 :45.123 Z 正在下载制品: drop -> /Users/wubo/Downloads/AzDo-Artifacts/1847 /drop.zip 正在下载制品: testResults -> /Users/wubo/Downloads/AzDo-Artifacts/1847 /testResults.zip 所有制品已下载至: /Users/wubo/Downloads/AzDo-Artifacts/1847 drop.zip (12456.3 KB) testResults.zip (3287.5 KB)
工作项查询与冲刺报告生成 Azure DevOps Boards 的工作项数据是团队进度和质量的直接反映。定期从 Boards 中提取数据并生成报告,可以帮助团队及时发现阻塞、评估交付速率、辅助 Sprint 回顾。以下代码使用 WIQL(Work Item Query Language)查询当前冲刺的工作项,并生成一份结构化的文本报告。
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 $wiqlQuery = @ { query = @" SELECT [System.Id], [System.WorkItemType], [System.Title], [System.State], [System.AssignedTo], [Microsoft.VSTS.Scheduling.StoryPoints], [Microsoft.VSTS.Common.Priority] FROM WorkItems WHERE [System.IterationPath] = @currentIteration() AND [System.State] NOT IN ('Closed', 'Removed') AND [System.WorkItemType] IN ('User Story', 'Bug', 'Task') ORDER BY [Microsoft.VSTS.Common.Priority] ASC "@ } $queryResult = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath '/wit/wiql' ` -Method POST ` -PatToken $pat ` -Body $wiqlQuery Write-Host "查询到 $ ($queryResult .workItems.Count) 个活跃工作项" Write-Host "" $allIds = $queryResult .workItems | Select-Object -ExpandProperty id$workItems = @ ()$idBatches = $allIds | Group-Object -Property { [math ]::Floor([array ]::IndexOf($allIds , $_ ) / 200 ) }foreach ($batch in $idBatches ) { $idList = ($batch .Group | ForEach-Object { [int ]$_ }) -join ',' $details = Invoke-AzDoRequest ` -Organization 'mycompany' ` -Project 'PlatformService' ` -ApiPath "/wit/workitems?ids=$idList &`$expand=fields" ` -PatToken $pat $workItems += $details .value } Write-Host "=" * 70 Write-Host " PlatformService - Sprint 进度报告" Write-Host " 生成时间: $ (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Host "=" * 70 Write-Host "" $byType = $workItems | Group-Object { $_ .fields.'System.WorkItemType' }Write-Host "[按类型统计]" foreach ($group in $byType ) { $totalPoints = ($group .Group | ForEach-Object { $_ .fields.'Microsoft.VSTS.Scheduling.StoryPoints' } | Where-Object { $_ } | Measure-Object -Sum ).Sum $pointsStr = if ($totalPoints ) { " | 故事点: $totalPoints " } else { '' } Write-Host " $ ($group .Name): $ ($group .Count) 个$pointsStr " } Write-Host "" $byState = $workItems | Group-Object { $_ .fields.'System.State' }Write-Host "[按状态统计]" foreach ($group in $byState ) { $pct = [math ]::Round($group .Count / $workItems .Count * 100 , 1 ) $bar = '#' * [math ]::Floor($pct / 5 ) Write-Host " $ ($group .Name): $ ($group .Count) 个 ($pct %) $bar " } Write-Host "" $byAssignee = $workItems | Group-Object { $assigned = $_ .fields.'System.AssignedTo' if ($assigned ) { $assigned .displayName } else { '未分配' } } | Sort-Object Count -Descending Write-Host "[按负责人统计]" foreach ($group in $byAssignee ) { Write-Host " $ ($group .Name): $ ($group .Count) 个" foreach ($item in $group .Group) { $title = $_ .fields.'System.Title' Write-Host " - #$ ($item .id) $ ($item .fields.'System.Title')" } } Write-Host "" $blockedItems = $workItems | Where-Object { $_ .fields.'System.State' -eq 'Blocked' -or $_ .fields.'System.Tags' -match 'blocked' } if ($blockedItems ) { Write-Host "[警告] 存在 $ ($blockedItems .Count) 个阻塞项:" foreach ($item in $blockedItems ) { Write-Host " !! #$ ($item .id) $ ($item .fields.'System.Title')" } } Write-Host "" Write-Host "=" * 70 Write-Host " 报告结束" Write-Host "=" * 70
这段代码使用 WIQL 查询当前冲刺中所有活跃的工作项,然后通过批量接口获取详细字段。报告生成部分按工作项类型、状态、负责人三个维度进行分组统计,并高亮显示被阻塞的工作项。这种脚本非常适合在 Sprint Standup 会议前自动运行,或通过 CI/CD 定时任务发送到团队频道。
执行结果示例:
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 查询到 24 个活跃工作项 ====================================================================== PlatformService - Sprint 进度报告 生成时间: 2026 -02 -03 08 :30 :15 ====================================================================== [按类型统计] User Story: 8 个 | 故事点: 21 Bug: 6 个 Task: 10 个 [按状态统计] Active: 12 个 (50 %) ############## New: 7 个 (29.2 %) ###### Resolved: 3 个 (12.5 %) ### Blocked: 2 个 (8.3 %) ## [按负责人统计] 张三: 8 个 - #1856 用户注册接口性能优化 - #1861 修复订单列表分页异常 李四: 7 个 - #1858 实现批量导出功能 - #1863 消息推送服务重构 王五: 5 个 - #1859 日志采集模块升级 未分配: 4 个 - #1865 接入新版支付网关 [警告] 存在 2 个阻塞项: !! #1860 第三方认证服务证书过期 !! #1867 等待上游团队提供接口文档 ====================================================================== 报告结束 ======================================================================
注意事项
PAT 权限范围 :创建 PAT 时需根据脚本操作范围选择最小权限集。管理项目需要 Project (Read & Write),操作流水线需要 Build (Read & Execute),查询工作项需要 Work Items (Read)。避免使用全权限 PAT,降低凭据泄露后的影响范围。
API 版本兼容性 :示例中使用 api-version=7.1。Azure DevOps REST API 有 v5.x、v6.x、v7.x 多个主版本,部分接口在不同版本间行为有差异(如流水线 Runs API 在 7.0 后引入了 templateParameters 字段)。生产脚本务必锁定版本号并做兼容性测试。
分页与批量限制 :查询类接口(如项目列表、工作项查询)默认有分页限制,通常单次返回 100-1000 条。需要检查响应中的 continuationToken 字段并循环获取完整数据。工作项批量查询单次上限为 200 个 ID,大批量场景需要自行分批。
并发与速率控制 :Azure DevOps 对同一组织的 API 调用有速率限制(个人用户约每分钟 600 次)。批量操作时建议使用 Start-Sleep 添加间隔,或使用 PowerShell 的 ForEach-Object -Parallel 配合计数器实现受控并发。
错误重试策略 :网络抖动和临时限流会导致偶发的 5xx 或 429 响应。建议封装通用的重试逻辑,对 429 状态码读取 Retry-After 响应头确定等待时间,对 5xx 状态码采用指数退避策略,最多重试 3 次。
敏感信息脱敏 :冲刺报告可能包含员工姓名、工作项内容等信息。如果报告需要发送到外部渠道(如邮件、Slack),注意对 System.AssignedTo 等字段做脱敏处理,或将报告输出到 Azure DevOps 内部的 Wiki 页面,保持访问权限的一致性。