适用于 PowerShell 7.0 及以上版本,需要 GitHub 账号
2025 年,CI/CD(持续集成/持续部署)已经成为软件交付的标准流程。无论是小型个人项目还是企业级微服务架构,自动化构建、测试和部署的能力都直接影响着交付效率和代码质量。GitHub Actions 作为目前最流行的 CI/CD 平台之一,原生支持 PowerShell 运行环境,这让 PowerShell 用户可以无缝融入 DevOps 工作流。
PowerShell 在 CI/CD 场景中有独特优势:强大的对象管道让数据处理变得简洁,跨平台支持(PowerShell 7+)让同一套脚本可以在 Windows、Linux 和 macOS Runner 上运行,丰富的模块生态则覆盖了从代码质量检查到部署验证的各个环节。本文将从脚本设计原则出发,逐步构建一个完整的 CI/CD 流水线方案。
设计原则:编写可测试的 PowerShell 脚本
CI/CD 流水线的可靠性取决于其中每一段脚本的质量。一段”能跑就行”的脚本在本地可能没问题,但放到自动化流水线中,缺乏错误处理、日志输出和结构化返回值,就会成为调试噩梦。以下是编写 CI/CD 脚本时应遵循的核心原则:
- 单一职责:每个函数只做一件事,方便单独测试和复用
- 显式错误处理:使用
try/catch 捕获异常,绝不吞掉错误
- 结构化输出:返回对象而非格式化文本,方便下游消费
- 参数化配置:所有路径、阈值、环境变量通过参数传入,不硬编码
下面的函数演示了这些原则的实际应用:
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
| function Invoke-ProjectBuild { param( [Parameter(Mandatory)] [string]$ProjectPath,
[string]$Configuration = "Release",
[string]$OutputPath = "./artifacts" )
$ErrorActionPreference = "Stop" $sw = [System.Diagnostics.Stopwatch]::StartNew()
try { if (-not (Test-Path $ProjectPath)) { throw "项目路径不存在: $ProjectPath" }
if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory | Out-Null }
Write-Host "[BUILD] 开始构建 $ProjectPath ($Configuration)..."
$buildResult = & dotnet publish $ProjectPath ` --configuration $Configuration ` --output $OutputPath 2>&1
$sw.Stop()
$result = [PSCustomObject]@{ Success = $LASTEXITCODE -eq 0 Duration = $sw.Elapsed OutputPath = $OutputPath Timestamp = Get-Date }
if (-not $result.Success) { throw "构建失败,退出码: $LASTEXITCODE" }
Write-Host "[BUILD] 构建完成,耗时 $($sw.Elapsed.ToString('mm\:ss'))" return $result } catch { $sw.Stop() Write-Error "[BUILD] 构建异常: $($_.Exception.Message)" throw } }
|
1 2 3 4 5 6 7 8
| PS> Invoke-ProjectBuild -ProjectPath "./src/MyApp.csproj" [BUILD] 开始构建 ./src/MyApp.csproj (Release)... [BUILD] 构建完成,耗时 00:12
Success : True Duration : 00:00:12.3456789 OutputPath : ./artifacts Timestamp : 2025/4/21 10:00:00
|
Pester 单元测试
在 CI/CD 流水线中,自动化测试是质量保障的基石。Pester 是 PowerShell 生态中最成熟的测试框架,支持 Describe/Context/It 三层组织结构,内联 Mock 能力让外部依赖的隔离变得简单。下面的测试覆盖了构建函数的正常路径和异常路径:
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
| BeforeAll { . "$PSScriptRoot/../build/Invoke-ProjectBuild.ps1" }
Describe "Invoke-ProjectBuild" { Context "当项目路径存在时" { BeforeAll { $testProject = Join-Path $TestDrive "TestApp.csproj" Set-Content -Path $testProject -Value "<Project></Project>" }
It "应返回成功的构建结果" { $result = Invoke-ProjectBuild -ProjectPath $testProject ` -OutputPath "$TestDrive/output"
$result | Should -Not -BeNullOrEmpty $result.Success | Should -BeTrue $result.OutputPath | Should -Exist }
It "应在结果中包含耗时信息" { $result = Invoke-ProjectBuild -ProjectPath $testProject ` -OutputPath "$TestDrive/output2"
$result.Duration | Should -BeGreaterThan ([TimeSpan]::Zero) $result.Timestamp | Should -BeGreaterOrEqual (Get-Date).AddMinutes(-1) } }
Context "当项目路径不存在时" { It "应抛出异常" { { Invoke-ProjectBuild -ProjectPath "/nonexistent/project.csproj" } | Should -Throw "*项目路径不存在*" } } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| PS> Invoke-Pester -Path ./tests/Invoke-ProjectBuild.Tests.ps1 -Output Detailed
Starting discovery in 1 files. Discovery finished in 0.23s. [Des] Invoke-ProjectBuild [Ctx] 当项目路径存在时 [It] 应返回成功的构建结果 [It] 应在结果中包含耗时信息 [Ctx] 当项目路径不存在时 [It] 应抛出异常 Tests passed: 3, Failed: 0, Skipped: 0, NotRun: 0 Total time: 1.45s
|
GitHub Actions Workflow 配置
有了可测试的脚本之后,接下来把它集成到 GitHub Actions 中。PowerShell 在 GitHub Actions 的 Windows 和 Ubuntu Runner 上都是预装的,无需额外安装。关键配置点:
- 使用
pwsh shell 确保使用 PowerShell 7(而非 Windows PowerShell 5.1)
- 通过
matrix 策略实现跨平台并行测试
- 使用
actions/cache 缓存模块依赖,加速后续运行
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
| name: PowerShell CI/CD Pipeline
on: push: branches: [main, develop] pull_request: branches: [main]
jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] include: - os: ubuntu-latest shell: pwsh - os: windows-latest shell: pwsh
steps: - name: 检出代码 uses: actions/checkout@v4
- name: 缓存 PowerShell 模块 uses: actions/cache@v4 with: path: ~/.local/share/powershell/Modules key: ps-modules-${{ runner.os }}-${{ hashFiles('**/requirements.psd1') }}
- name: 安装 Pester shell: pwsh run: | Install-Module -Name Pester -Force -Scope CurrentUser Import-Module Pester
- name: 运行 Lint 检查 shell: pwsh run: | Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser $results = Invoke-ScriptAnalyzer -Path ./src -Severity Warning if ($results) { $results | Format-Table -AutoSize Write-Error "Lint 检查未通过" }
- name: 运行 Pester 测试 shell: pwsh run: | $config = New-PesterConfiguration $config.Run.Path = "./tests" $config.Output.Verbosity = "Detailed" $config.TestResult.Enabled = $true $config.TestResult.OutputPath = "testResults.xml" Invoke-Pester -Configuration $config
- name: 上传测试结果 uses: actions/upload-artifact@v4 if: always() with: name: test-results-${{ matrix.os }} path: testResults.xml
|
1 2 3 4 5 6 7 8 9 10
| # GitHub Actions 运行输出摘要 Run Lint 检查 RuleName Severity ScriptName Line Message ---------- -------- ---------- ---- ------- PSUseShouldProcessForStateChangingFunctions Warning build.ps1 12
Run 运行 Pester 测试 [+] DscResource.Tests.ps1 1.23s [+] Invoke-ProjectBuild.Tests.ps1 0.87s Tests passed: 8, Failed: 0
|
部署验证脚本
CI/CD 不止于构建和测试,部署后的验证同样重要。以下函数在部署完成后自动检查服务的健康状态,支持并发检测多个端点,输出结构化的结果摘要。配合流水线中的 continue-on-error: false,一旦任何端点异常就会中断部署。
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
| function Test-DeploymentHealth { param( [Parameter(Mandatory)] [string[]]$Endpoints,
[int]$TimeoutSeconds = 10,
[int]$RetryCount = 3,
[int]$RetryDelaySeconds = 5 )
$results = @()
foreach ($endpoint in $Endpoints) { $attempt = 0 $success = $false
while ($attempt -lt $RetryCount -and -not $success) { $attempt++ $sw = [System.Diagnostics.Stopwatch]::StartNew()
try { $response = Invoke-WebRequest -Uri $endpoint ` -TimeoutSec $TimeoutSeconds ` -UseBasicParsing
$sw.Stop() $success = $response.StatusCode -eq 200
if ($success) { $results += [PSCustomObject]@{ Endpoint = $endpoint Status = "OK" StatusCode = $response.StatusCode LatencyMs = $sw.ElapsedMilliseconds Attempt = $attempt Timestamp = Get-Date } Write-Host "[HEALTH] $endpoint - OK ($($sw.ElapsedMilliseconds)ms)" } } catch { $sw.Stop() Write-Warning "[HEALTH] $endpoint - 第 $attempt 次检测失败: $($_.Exception.Message)"
if ($attempt -lt $RetryCount) { Write-Host " 等待 $RetryDelaySeconds 秒后重试..." Start-Sleep -Seconds $RetryDelaySeconds } } }
if (-not $success) { $results += [PSCustomObject]@{ Endpoint = $endpoint Status = "FAIL" StatusCode = "N/A" LatencyMs = $sw.ElapsedMilliseconds Attempt = $attempt Timestamp = Get-Date } } }
$failedCount = ($results | Where-Object { $_.Status -eq "FAIL" }).Count if ($failedCount -gt 0) { Write-Error "[HEALTH] $failedCount 个端点检测失败!" }
return $results }
|
1 2 3 4 5 6 7 8 9 10
| PS> Test-DeploymentHealth -Endpoints "https://api.example.com/health","https://app.example.com" [HEALTH] https://api.example.com/health - OK (145ms) WARNING: [HEALTH] https://app.example.com - 第 1 次检测失败: 连接超时 等待 5 秒后重试... [HEALTH] https://app.example.com - OK (230ms)
Endpoint Status StatusCode LatencyMs Attempt Timestamp -------- ------ ---------- --------- ------- --------- https://api.example.com/health OK 200 145 1 2025/4/21 10:05:00 https://app.example.com OK 200 230 2 2025/4/21 10:05:06
|
完整流水线:从构建到部署验证
将前面的组件组合在一起,就形成了一个完整的 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 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
| function Invoke-CiCdPipeline { param( [string]$ProjectPath = "./src", [string]$Environment = "staging", [string[]]$HealthEndpoints )
$ErrorActionPreference = "Stop" $pipelineStart = Get-Date $stageResults = @{}
Write-Host "========================================" Write-Host " CI/CD Pipeline - $Environment" Write-Host " 开始时间: $pipelineStart" Write-Host "========================================"
Write-Host "`n[Stage 1/4] 代码质量检查..." $analysisResults = Invoke-ScriptAnalyzer -Path $ProjectPath -Severity Error if ($analysisResults) { $analysisResults | Format-Table -AutoSize throw "代码质量检查未通过" } $stageResults["Lint"] = "PASSED" Write-Host " Lint 检查通过"
Write-Host "`n[Stage 2/4] 运行单元测试..." $pesterConfig = New-PesterConfiguration $pesterConfig.Run.Path = "./tests" $pesterConfig.Output.Verbosity = "Normal" $testResult = Invoke-Pester -Configuration $pesterConfig -PassThru if ($testResult.FailedCount -gt 0) { throw "测试失败: $($testResult.FailedCount) 个用例未通过" } $stageResults["Test"] = "PASSED ($($testResult.PassedCount) passed)" Write-Host " 全部 $($testResult.PassedCount) 个测试通过"
Write-Host "`n[Stage 3/4] 构建项目..." $buildResult = Invoke-ProjectBuild -ProjectPath "$ProjectPath/App.csproj" $stageResults["Build"] = "PASSED ($($buildResult.Duration.ToString('mm\:ss')))" Write-Host " 构建完成"
if ($HealthEndpoints) { Write-Host "`n[Stage 4/4] 部署验证..." $healthResults = Test-DeploymentHealth -Endpoints $HealthEndpoints $stageResults["HealthCheck"] = "PASSED ($($HealthEndpoints.Count) endpoints)" Write-Host " 所有端点健康" }
$totalDuration = (Get-Date) - $pipelineStart Write-Host "`n========================================" Write-Host " Pipeline 完成" Write-Host " 总耗时: $($totalDuration.ToString('mm\:ss'))" Write-Host "========================================"
foreach ($stage in $stageResults.GetEnumerator()) { Write-Host (" {0,-15} {1}" -f $stage.Key, $stage.Value) }
return [PSCustomObject]@{ Success = $true Duration = $totalDuration Stages = $stageResults } }
|
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
| PS> Invoke-CiCdPipeline -Environment "staging" -HealthEndpoints "https://api.example.com/health"
======================================== CI/CD Pipeline - staging 开始时间: 2025/4/21 10:00:00 ========================================
[Stage 1/4] 代码质量检查... Lint 检查通过
[Stage 2/4] 运行单元测试... 全部 8 个测试通过
[Stage 3/4] 构建项目... [BUILD] 开始构建 ./src/App.csproj (Release)... [BUILD] 构建完成,耗时 00:12 构建完成
[Stage 4/4] 部署验证... [HEALTH] https://api.example.com/health - OK (98ms) 所有端点健康
======================================== Pipeline 完成 总耗时: 00:52 ======================================== Lint PASSED Test PASSED (8 passed) Build PASSED (00:12) HealthCheck PASSED (1 endpoints)
|
小结
将 PowerShell 融入 CI/CD 流水线的关键在于:脚本要可测试(Pester 覆盖正常和异常路径)、可观测(结构化日志输出)、可组合(单一职责函数通过编排脚本串联)。GitHub Actions 原生支持 pwsh,使得 PowerShell 脚本可以直接作为 Workflow 步骤运行,无需额外适配。建议在本地先用 Pester 充分测试脚本逻辑,再集成到 Actions Workflow 中,这样能大幅减少调试周期。