PowerShell 技能连载 - Azure DevOps 自动化

适用于 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'
)

# 将 PAT 转换为 Basic Auth 格式
$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
$uri = if ($Project) {
"https://dev.azure.com/$Organization/$Project/_apis$ApiPath"
} else {
"https://dev.azure.com/$Organization/_apis$ApiPath"
}

# 追加 api-version 参数
$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
$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
# --- 1. 触发流水线构建 ---

$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 ""

# --- 2. 轮询构建状态直到完成 ---

$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
}
}

# --- 3. 下载构建制品 ---

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-03T08:23:45.123Z
正在下载制品: 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
# --- 1. 查询当前冲刺的活跃工作项 ---

$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 ""

# --- 2. 批量获取工作项详情 ---

$allIds = $queryResult.workItems | Select-Object -ExpandProperty id
$workItems = @()

# API 限制单次最多查询 200 个工作项
$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
}

# --- 3. 生成冲刺报告 ---

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 页面,保持访问权限的一致性。

PowerShell 技能连载 - Azure DevOps 集成

适用于 PowerShell 5.1 及以上版本

Azure DevOps 是微软提供的一站式 DevOps 平台,涵盖了 Boards(工作项跟踪)、Repos(代码仓库)、Pipelines(CI/CD 流水线)、Test Plans(测试管理)和 Artifacts(制品管理)五大核心服务。在企业级开发流程中,团队往往需要通过脚本自动化地与 Azure DevOps 交互,例如批量创建工作项、触发流水线、查询构建状态或管理代码仓库分支策略。

虽然 Azure DevOps 提供了功能完善的 Web 界面和 CLI 工具(az devops),但 PowerShell 凭借其强大的对象处理能力和与其他 Windows/Azure 服务的无缝集成,仍然是许多运维和开发团队的首选自动化工具。通过 Azure DevOps REST API,我们可以在 PowerShell 中完成几乎所有的平台操作,并将这些操作编排到更大的自动化工作流中。

本文将介绍如何使用 PowerShell 调用 Azure DevOps REST API,涵盖身份认证与连接管理、工作项(Work Item)的批量操作、Pipeline 的触发与状态监控,以及代码仓库的分支策略管理。每个场景都配有可直接运行的代码示例和执行结果演示。

准备工作:身份认证与连接封装

Azure DevOps REST API 支持多种身份认证方式,其中最常用的是 Personal Access Token(PAT)。为了在脚本中安全地使用 PAT,我们需要将认证逻辑封装成可复用的函数,避免在代码中硬编码凭据。以下代码演示了如何创建一个通用的 Azure DevOps API 调用函数。

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
function Invoke-AzDevOpsApi {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Organization,

[Parameter(Mandatory)]
[string]$Project,

[Parameter(Mandatory)]
[string]$ApiPath,

[Parameter(Mandatory)]
[securestring]$PatToken,

[ValidateSet('GET', 'POST', 'PATCH', 'DELETE')]
[string]$Method = 'GET',

[object]$Body
)

$base64Token = [Convert]::ToBase64String(
[System.Text.Encoding]::ASCII.GetBytes(
':' + (New-Object PSCredential('user', $PatToken).GetNetworkCredential().Password)
)
)

$headers = @{
Authorization = "Basic $base64Token"
'Content-Type' = 'application/json'
}

$uri = "https://dev.azure.com/$Organization/$Project/_apis$ApiPath"

$splat = @{
Uri = $uri
Headers = $headers
Method = $Method
}

if ($Body) {
$splat.Body = ($Body | ConvertTo-Json -Depth 10)
}

Invoke-RestMethod @splat
}

# 从环境变量读取 PAT,避免硬编码
$pat = ConvertTo-SecureString $env:AZDO_PAT -AsPlainText -Force

# 测试连接:获取项目信息
$projectInfo = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '?api-version=7.0' `
-PatToken $pat

Write-Host "项目名称: $($projectInfo.name)"
Write-Host "项目描述: $($projectInfo.description)"
Write-Host "项目 ID: $($projectInfo.id)"

上述代码将 PAT 以 SecureString 形式传入,通过 Base64 编码生成 Basic Auth 头。Invoke-AzDevOpsApi 函数封装了 URI 拼接和请求发送逻辑,后续所有示例都基于此函数调用。

执行结果示例:

1
2
3
项目名称: MyProject
项目描述: 核心业务系统开发项目
项目 ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890

批量创建与查询工作项

Azure DevOps Boards 中的工作项(Work Item)是项目管理的基础单元。当需要从外部系统同步需求、批量创建测试任务或在 Sprint 规划时一次性添加多个用户故事时,手动操作效率极低。以下代码展示了如何批量创建工作项并查询特定条件的工作项列表。

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
# 批量创建用户故事(User Story)
$stories = @(
@{ Title = '实现用户登录 API 接口'; Priority = 1 },
@{ Title = '添加 OAuth2.0 第三方登录支持'; Priority = 2 },
@{ Title = '实现登录失败次数限制策略'; Priority = 3 }
)

foreach ($story in $stories) {
$body = @(
@{
op = 'add'
path = '/fields/System.Title'
value = $story.Title
},
@{
op = 'add'
path = '/fields/Microsoft.VSTS.Common.Priority'
value = $story.Priority
}
)

$result = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/wit/workitems/$User Story?api-version=7.0' `
-Method POST `
-PatToken $pat `
-Body $body

Write-Host "已创建工作项 #$($result.id) - $($result.fields.'System.Title')"
}

# 查询当前迭代中所有未关闭的 Bug
$wiql = @{
query = "SELECT [System.Id], [System.Title], [System.State], [System.AssignedTo] `
FROM WorkItems `
WHERE [System.WorkItemType] = 'Bug' `
AND [System.State] <> 'Closed' `
AND [System.IterationPath] = @currentIteration() `
ORDER BY [System.CreatedDate] DESC"
}

$queryResult = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/wit/wiql?api-version=7.0' `
-Method POST `
-PatToken $pat `
-Body $wiql

$workItemIds = $queryResult.workItems | Select-Object -ExpandProperty id
Write-Host "当前迭代中未关闭的 Bug 数量: $($workItemIds.Count)"

Azure DevOps 创建工作项使用 JSON Patch 格式(op: add),通过 path 指定要设置的字段。WIQL(Work Item Query Language)是类似 SQL 的查询语言,@currentIteration() 函数可以自动定位当前冲刺周期。使用 foreach 循环逐条创建可以清晰地在输出中追踪每条记录的创建结果。

执行结果示例:

1
2
3
4
已创建工作项 #1247 - 实现用户登录 API 接口
已创建工作项 #1248 - 添加 OAuth2.0 第三方登录支持
已创建工作项 #1249 - 实现登录失败次数限制策略
当前迭代中未关闭的 Bug 数量: 8

触发 Pipeline 并监控构建状态

在持续集成/持续部署(CI/CD)流程中,有时需要通过脚本手动触发 Pipeline,例如在完成数据迁移后触发部署流水线,或按需触发特定的测试流水线。以下代码演示了如何触发 Pipeline 构建,并以轮询方式等待构建完成。

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
# 触发 Pipeline 构建
$buildPayload = @{
definition = @{
id = 42
}
parameters = '{"environment":"staging","runTests":true}'
}

$build = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/build/builds?api-version=7.0' `
-Method POST `
-PatToken $pat `
-Body $buildPayload

Write-Host "已触发构建 #$($build.id)"
Write-Host "构建定义: $($build.definition.name)"
Write-Host "初始状态: $($build.status)"
Write-Host "队列时间: $($build.queueTime)"

# 轮询构建状态直到完成
$buildId = $build.id
$maxRetries = 60
$retryCount = 0

while ($retryCount -lt $maxRetries) {
Start-Sleep -Seconds 30

$status = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath "/build/builds/$buildId`?api-version=7.0" `
-PatToken $pat

Write-Host "[$($retryCount + 1)] 构建 #$buildId 状态: $($status.status) - $($status.result)"

if ($status.status -eq 'completed') {
Write-Host "`n构建完成! 最终结果: $($status.result)"
Write-Host "开始时间: $($status.startTime)"
Write-Host "完成时间: $($status.finishTime)"

$duration = [datetime]::Parse($status.finishTime) - [datetime]::Parse($status.startTime)
Write-Host "耗时: $($duration.TotalMinutes.ToString('F1')) 分钟"
break
}

$retryCount++
}

if ($retryCount -ge $maxRetries) {
Write-Warning "等待超时,构建 #$buildId 仍未完成,请手动检查。"
}

这段代码首先通过 POST 请求触发指定 ID 的 Pipeline 定义,同时传递模板参数(environmentrunTests)。触发成功后进入轮询循环,每 30 秒查询一次构建状态,直到状态变为 completed 或达到最大重试次数。循环结束时计算并输出构建耗时,方便排查流水线性能问题。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
已触发构建 #3891
构建定义: MyProject-CI
初始状态: inProgress
队列时间: 2025-11-26T02:15:33.287Z
[1] 构建 #3891 状态: inProgress -
[2] 构建 #3891 状态: inProgress -
[3] 构建 #3891 状态: inProgress -
[4] 构建 #3891 状态: completed - succeeded

构建完成! 最终结果: succeeded
开始时间: 2025-11-26T02:15:38.412Z
完成时间: 2025-11-26T02:17:45.891Z
耗时: 2.1 分钟

管理代码仓库分支策略

分支策略(Branch Policy)是保障代码质量的重要手段。在团队协作中,通常要求所有代码变更通过 Pull Request 提交,并设置最低审核人数、构建验证和合并策略。以下代码演示了如何通过 PowerShell 为仓库分支配置分支策略。

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
# 获取仓库 ID
$repo = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/git/repositories?api-version=7.0' `
-PatToken $pat

$targetRepo = $repo.value | Where-Object { $_.name -eq 'MyApp' }
Write-Host "目标仓库: $($targetRepo.name) (ID: $($targetRepo.id))"

# 获取默认分支的 ref
$defaultBranch = $targetRepo.defaultBranch
Write-Host "默认分支: $defaultBranch"

# 获取分支策略配置列表
$policyConfigurations = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/policy/configurations?api-version=7.0' `
-PatToken $pat

# 统计当前项目的策略数量
$enabledPolicies = @($policyConfigurations.value | Where-Object { $_.isEnabled -eq $true })
Write-Host "当前项目已启用的策略数量: $($enabledPolicies.Count)"

# 为 main 分支创建"最少审核人数"策略
$minReviewersPolicy = @{
isEnabled = $true
isBlocking = $true
type = @{
id = 'fa4e907d-c16b-4a4c-90b4-75ae827c5881'
}
settings = @{
minimumApproverCount = 2
creatorVoteCounts = $false
scope = @(
@{
refName = $defaultBranch
matchKind = 'exact'
repositoryId = $targetRepo.id
}
)
}
}

$newPolicy = Invoke-AzDevOpsApi `
-Organization 'mycompany' `
-Project 'MyProject' `
-ApiPath '/policy/configurations?api-version=7.0' `
-Method POST `
-PatToken $pat `
-Body $minReviewersPolicy

Write-Host "已创建策略: 最低审核人数 = 2"
Write-Host "策略 ID: $($newPolicy.id)"
Write-Host "是否阻断: $($newPolicy.isBlocking)"

分支策略的类型通过 GUID 标识。fa4e907d-c16b-4a4c-90b4-75ae828c5881 代表”最少审核人数”策略类型。isBlocking = $true 表示不满足策略要求时无法合并 Pull Request。creatorVoteCounts = $false 确保创建者自身的审核不计入最低审核人数。通过脚本配置策略,可以确保新仓库的分支保护规则与团队规范一致。

执行结果示例:

1
2
3
4
5
6
目标仓库: MyApp (ID: abc12345-6789-def0-1234-567890abcdef)
默认分支: refs/heads/main
当前项目已启用的策略数量: 5
已创建策略: 最低审核人数 = 2
策略 ID: 9876abcd-5432-10fe-dc98-76543210fedc
是否阻断: True

注意事项

  • PAT 安全管理:切勿将 Personal Access Token 硬编码在脚本中。推荐从环境变量($env:AZDO_PAT)或 Azure Key Vault 中读取,并在 CI/CD 流水线中使用变量组(Variable Group)的机密引用功能。
  • API 版本控制:Azure DevOps REST API 要求在每次请求中指定 api-version 参数。建议在生产脚本中固定使用某个已验证的版本号(如 7.07.1),避免因 API 升级导致脚本行为变化。
  • 请求频率限制:Azure DevOps 对 REST API 调用有频率限制(通常为每小时 6000 次,具体取决于组织规模)。批量操作时建议在循环中添加适当延时(如 Start-Sleep -Milliseconds 200),或在遇到 429 状态码时实现指数退避重试。
  • JSON Patch 格式差异:创建工作项使用 JSON Patch 数组格式(op: add),而更新工作项也使用同一格式但允许 op: replaceop: remove 等操作。注意这与常规 REST API 的 PUT/PATCH JSON body 格式不同,混淆两者是常见的调试陷阱。
  • 错误处理Invoke-RestMethod 默认在遇到非 2xx 状态码时抛出异常。建议在调用外层包裹 try/catch 块,并通过 $_.Exception.Response 获取详细的错误响应内容,便于定位问题。
  • 跨平台兼容性:如果需要在 PowerShell 7+ 的 Linux/macOS 环境中运行,注意 ConvertTo-SecureString 在非 Windows 平台上的行为差异。推荐使用跨平台兼容的凭据管理方式,例如直接通过 Invoke-RestMethod-Authentication Bearer 参数传递令牌。