mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 01:59:34 +02:00
## Summary of the Pull Request Adds a `PowerRename.UITests.Next` suite powered by winappcli and automates all 18 scenarios from #40663. The suite covers PowerRename settings, search and replace behavior, regular expressions, formatting and filtering options, file-list interactions, and both classic and Windows 11 context-menu workflows. The PR also stabilizes shared `UITestAutomation.Next` runner lifetimes and settings restoration, adds automation IDs for the original and renamed counters, and prepares unsigned CI builds for PowerRename shell testing. CI now signs the sparse context-menu MSIX and the runner/Settings IPC companions with a disposable machine-trusted test identity. ## PR Checklist - [x] Closes: #40663 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Not applicable; no new shipped product binaries are added - [x] JSON for signing: Not applicable - [x] WXS for installer: Not applicable - [x] YML for CI pipeline: The new UI-test project is discovered through the existing `*UITest*.csproj` pipeline flow and is registered in `PowerToys.slnx` - [x] YML for signed pipeline: Not applicable - [x] **Documentation updated:** Not applicable; there are no user-facing behavior or documentation changes ## Detailed Description of the Pull Request / Additional comments ### PowerRename UI tests - Adds a 25-case `PowerRename.UITests.Next` executable covering all 18 checklist items from #40663. - Exercises classic context-menu registration on Windows 10 and Windows 11. - Exercises the signed Windows 11 tier-1 context menu, including icon visibility and real invocation with an Explorer selection. - Covers search/replace preview and application, text formatting, file/folder/subfolder inclusion, filename/extension scope, enumeration, case sensitivity, match-all behavior, regular expressions, file timestamps, Boost syntax, MRU autocomplete, persisted values, and file-list selection/filtering. - Preserves the existing legacy tests. ### Test reliability and automation hooks - Reuses one runner/Settings lifetime across the complete PowerRename suite to avoid repeated cold launches on constrained agents. - Retains and restores global settings for the full class lifetime and verifies that both Settings and the runner remain healthy. - Propagates scope and PowerRename cleanup failures instead of silently leaking process or profile state. - Adds `OriginalCount` and `RenamedCount` automation IDs. These are automation-only metadata and do not change the visible UI. - Uses stable preview samples, exact count targeting, authoritative Explorer selection, readable classic-menu inventories, and live UIA visibility for popup items. ### Unsigned CI build support - Extends the existing sparse-package test signer with required Authenticode companion files. - PowerRename jobs sign `PowerToys.exe` and `PowerToys.Settings.exe` with the same disposable machine-trusted test identity used for sparse MSIX packages. - This preserves Release IPC authentication while allowing Settings module-toggle commands to work on unsigned PR builds. - Windows 11 and ARM64 PowerRename jobs require a validly signed `PowerRenameContextMenuPackage.msix` before tests start. ## Validation Steps Performed ### Builds and discovery - `PowerRename.UITests.Next` Debug x64 build: passed - `PowerRename.UITests.Next` Debug ARM64 cross-build: passed - `PowerRenameUI` Release x64 build: passed - `UITestAutomation.Next.UnitTests` Debug x64 build: passed - `UITestAutomation.Next.UnitTests`: 16/16 passed - Microsoft.Testing.Platform discovery: 25 unique PowerRename test cases ### Local VM matrix All runs used the complete unfiltered `TestCategory=PowerRename` suite and restored the standard user's settings file byte-for-byte. | Guest | Profile | Result | |---|---|---:| | Windows 10 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 10 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | | Windows 11 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 11 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | ### Azure DevOps UI Test Automation - Final build: [155646235 / 20260824.1](https://dev.azure.com/microsoft/Dart/_build/results?buildId=155646235) - Source revision: `4496683104aea622b30179909bd6e95a17d5500f` - ARM64: 25/25 passed - Windows 10 x64: 25/25 passed - Windows 11 x64: 25/25 passed - Total: 75/75 passed, with zero failed, skipped, not-executed, or unanalyzed results - ARM64 and x64 Release product builds succeeded and published their normal artifacts - Signing steps verified the PowerRename sparse MSIX where applicable and both IPC companion executables on every PowerRename test job <img width="372" height="314" alt="image" src="https://github.com/user-attachments/assets/bc303673-0325-4f85-8d70-ef93110baf5b" />
498 lines
16 KiB
PowerShell
498 lines
16 KiB
PowerShell
# Copyright (c) Microsoft Corporation
|
|
# The Microsoft Corporation licenses this file to you under the MIT license.
|
|
# See the LICENSE file in the project root for more information.
|
|
|
|
#requires -Version 7.0
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Validates the prompt-free Azure CLI and Azure DevOps capabilities required by the UI-test pipeline skill.
|
|
|
|
.DESCRIPTION
|
|
Performs only read operations and a pipeline preview. A preview expands the selected YAML without
|
|
creating a build. The script never initiates sign-in, prints a token, or mutates Azure DevOps.
|
|
|
|
.PARAMETER Organization
|
|
Azure DevOps organization name. Defaults to the internal `microsoft` organization.
|
|
|
|
.PARAMETER Project
|
|
Azure DevOps project name. Defaults to `Dart`.
|
|
|
|
.PARAMETER PipelineName
|
|
Enabled pipeline definition to discover. Defaults to `UI Test Automation`.
|
|
|
|
.PARAMETER ProbeBranch
|
|
Existing full Git ref used for the non-mutating preview, for example `refs/heads/main`.
|
|
|
|
.PARAMETER ProbeModule
|
|
One existing UITest project stem used to expand the preview, without brackets.
|
|
|
|
.PARAMETER ProbeBuildId
|
|
Optional completed build from the discovered pipeline. When omitted, the preflight inspects the ten
|
|
newest completed builds and chooses build/test probes automatically.
|
|
|
|
.EXAMPLE
|
|
pwsh .github/skills/ui-tests-pipeline-ci/scripts/Test-AzureDevOpsSetup.ps1
|
|
|
|
.EXAMPLE
|
|
pwsh .github/skills/ui-tests-pipeline-ci/scripts/Test-AzureDevOpsSetup.ps1 `
|
|
-ProbeBranch refs/heads/my-branch `
|
|
-ProbeModule MyModule.UITests
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string] $Organization = 'microsoft',
|
|
|
|
[string] $Project = 'Dart',
|
|
|
|
[string] $PipelineName = 'UI Test Automation',
|
|
|
|
[string] $ProbeBranch = 'refs/heads/main',
|
|
|
|
[string] $ProbeModule = 'FancyZones.UITests.Next',
|
|
|
|
[long] $ProbeBuildId = 0
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'AzureDevOps.ps1')
|
|
|
|
$checks = [Collections.Generic.List[object]]::new()
|
|
|
|
function Add-SetupCheck
|
|
{
|
|
param(
|
|
[string] $Name,
|
|
[ValidateSet('PASS', 'FAIL', 'SKIP')]
|
|
[string] $Status,
|
|
[bool] $Required,
|
|
[string] $Detail
|
|
)
|
|
|
|
$checks.Add([pscustomobject]@{
|
|
Name = $Name
|
|
Status = $Status
|
|
Required = $Required
|
|
Detail = $Detail
|
|
})
|
|
}
|
|
|
|
function Get-FixedError
|
|
{
|
|
param(
|
|
[Management.Automation.ErrorRecord] $ErrorRecord,
|
|
[string] $Fallback
|
|
)
|
|
|
|
if ($ErrorRecord.Exception -and -not [string]::IsNullOrWhiteSpace($ErrorRecord.Exception.Message))
|
|
{
|
|
return $ErrorRecord.Exception.Message
|
|
}
|
|
|
|
return $Fallback
|
|
}
|
|
|
|
$pipelineId = 0
|
|
$probeBuild = $null
|
|
$probeTestBuild = $null
|
|
$buildCandidates = @()
|
|
$probeTestRuns = @()
|
|
|
|
try
|
|
{
|
|
if ($PSVersionTable.PSVersion.Major -lt 7)
|
|
{
|
|
throw "PowerShell 7 or newer is required; found $($PSVersionTable.PSVersion)."
|
|
}
|
|
|
|
Add-SetupCheck 'PowerShell7' 'PASS' $true $PSVersionTable.PSVersion.ToString()
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'PowerShell7' 'FAIL' $true (Get-FixedError $_ 'PowerShell 7 validation failed.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$azCommand = Get-Command az -ErrorAction Stop
|
|
$versionOutput = & az version --output json --only-show-errors 2>&1
|
|
if ($LASTEXITCODE -ne 0)
|
|
{
|
|
throw 'Azure CLI version query failed.'
|
|
}
|
|
|
|
$versionObject = ($versionOutput | Out-String) | ConvertFrom-Json
|
|
$version = [string]$versionObject.PSObject.Properties['azure-cli'].Value
|
|
if ([string]::IsNullOrWhiteSpace($version))
|
|
{
|
|
throw 'Azure CLI did not report its version.'
|
|
}
|
|
|
|
Add-SetupCheck 'AzureCLI' 'PASS' $true "$($azCommand.Source) v$version"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'AzureCLI' 'FAIL' $true (Get-FixedError $_ 'Azure CLI is unavailable.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$extensionOutput = & az extension show `
|
|
--name azure-devops `
|
|
--query '{name:name,version:version}' `
|
|
--output json `
|
|
--only-show-errors 2>&1
|
|
if ($LASTEXITCODE -ne 0)
|
|
{
|
|
throw 'The Azure DevOps CLI extension is not installed.'
|
|
}
|
|
|
|
$extension = ($extensionOutput | Out-String) | ConvertFrom-Json
|
|
Add-SetupCheck 'AzureDevOpsExtension' 'PASS' $true "$($extension.name) v$($extension.version)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'AzureDevOpsExtension' 'FAIL' $true (Get-FixedError $_ 'The Azure DevOps CLI extension is unavailable.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$session = Test-AzDevOpsSession
|
|
Add-SetupCheck `
|
|
'CachedSignInAndToken' `
|
|
'PASS' `
|
|
$true `
|
|
"tenant=$($session.TenantId); userType=$($session.UserType); expires=$($session.TokenExpiresOn)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'CachedSignInAndToken' 'FAIL' $true (Get-FixedError $_ 'Azure CLI sign-in validation failed.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$projectUri = "https://dev.azure.com/$Organization/_apis/projects/$([Uri]::EscapeDataString($Project))?api-version=7.1"
|
|
$projectInfo = (Invoke-AzDevOpsRest -Uri $projectUri -Organization $Organization -Project $Project).Body
|
|
if (-not $projectInfo.id -or $projectInfo.state -ne 'wellFormed')
|
|
{
|
|
throw "Project '$Project' is unavailable or not ready."
|
|
}
|
|
|
|
Add-SetupCheck 'ProjectRead' 'PASS' $true "project=$($projectInfo.name); state=$($projectInfo.state)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'ProjectRead' 'FAIL' $true (Get-FixedError $_ "Cannot read $Organization/$Project.")
|
|
}
|
|
|
|
try
|
|
{
|
|
$encodedPipelineName = [Uri]::EscapeDataString($PipelineName)
|
|
$definitions = @((Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/definitions?name=$encodedPipelineName&api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body.value |
|
|
Where-Object queueStatus -EQ 'enabled')
|
|
if ($definitions.Count -ne 1)
|
|
{
|
|
throw "Expected one enabled '$PipelineName' definition; found $($definitions.Count)."
|
|
}
|
|
|
|
$pipelineId = [int]$definitions[0].id
|
|
Add-SetupCheck `
|
|
'PipelineDefinitionRead' `
|
|
'PASS' `
|
|
$true `
|
|
"id=$pipelineId; revision=$($definitions[0].revision)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'PipelineDefinitionRead' 'FAIL' $true (Get-FixedError $_ 'Pipeline discovery failed.')
|
|
}
|
|
|
|
if ($pipelineId -ne 0)
|
|
{
|
|
try
|
|
{
|
|
if ($ProbeBuildId -ne 0)
|
|
{
|
|
$probeBuild = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds/${ProbeBuildId}?api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
if ([int]$probeBuild.definition.id -ne $pipelineId)
|
|
{
|
|
throw "Probe build $ProbeBuildId does not belong to pipeline $pipelineId."
|
|
}
|
|
|
|
if ($probeBuild.status -ne 'completed')
|
|
{
|
|
throw "Probe build $ProbeBuildId is '$($probeBuild.status)'; use a completed build."
|
|
}
|
|
|
|
$buildCandidates = @($probeBuild)
|
|
}
|
|
else
|
|
{
|
|
$buildPage = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds?definitions=$pipelineId&statusFilter=completed&queryOrder=queueTimeDescending&%24top=10&api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
$buildCandidates = @($buildPage.value)
|
|
if ($buildCandidates.Count -eq 0)
|
|
{
|
|
throw "No completed build was found among the 10 newest runs for pipeline $pipelineId."
|
|
}
|
|
|
|
$probeBuild = $buildCandidates[0]
|
|
}
|
|
|
|
Add-SetupCheck `
|
|
'BuildRead' `
|
|
'PASS' `
|
|
$true `
|
|
"id=$($probeBuild.id); status=$($probeBuild.status); result=$($probeBuild.result)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'BuildRead' 'FAIL' $true (Get-FixedError $_ 'Completed build discovery failed.')
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Add-SetupCheck 'BuildRead' 'SKIP' $true 'Pipeline definition was not resolved.'
|
|
}
|
|
|
|
if ($null -ne $probeBuild)
|
|
{
|
|
try
|
|
{
|
|
$timeline = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds/$($probeBuild.id)/timeline?api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
Add-SetupCheck 'TimelineRead' 'PASS' $true "records=$(@($timeline.records).Count)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'TimelineRead' 'FAIL' $true (Get-FixedError $_ 'Build timeline read failed.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$logs = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds/$($probeBuild.id)/logs?api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
Add-SetupCheck 'BuildLogsRead' 'PASS' $true "logs=$(@($logs.value).Count)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'BuildLogsRead' 'FAIL' $true (Get-FixedError $_ 'Build log read failed.')
|
|
}
|
|
|
|
try
|
|
{
|
|
$artifacts = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds/$($probeBuild.id)/artifacts?api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
Add-SetupCheck 'ArtifactsRead' 'PASS' $true "artifacts=$(@($artifacts.value).Count)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'ArtifactsRead' 'FAIL' $true (Get-FixedError $_ 'Pipeline artifact read failed.')
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach ($candidate in $buildCandidates)
|
|
{
|
|
$candidateBuildUri = [Uri]::EscapeDataString("vstfs:///Build/Build/$($candidate.id)")
|
|
$candidateRuns = @((Invoke-AzDevOpsRest `
|
|
-Uri "_apis/test/runs?buildUri=$candidateBuildUri&api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body.value)
|
|
if ($candidateRuns.Count -gt 0)
|
|
{
|
|
$probeTestBuild = $candidate
|
|
$probeTestRuns = $candidateRuns
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($probeTestRuns.Count -eq 0)
|
|
{
|
|
throw "No Azure Test run was found for the selected probe build set. Pass -ProbeBuildId with a known test-bearing completed build."
|
|
}
|
|
|
|
Add-SetupCheck `
|
|
'TestRunsRead' `
|
|
'PASS' `
|
|
$true `
|
|
"build=$($probeTestBuild.id); runs=$($probeTestRuns.Count)"
|
|
|
|
$probeRunId = [long]$probeTestRuns[0].id
|
|
$resultPage = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/test/Runs/${probeRunId}/results?%24top=1&%24skip=0&api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
$probeResults = @($resultPage.value)
|
|
if ($probeResults.Count -eq 0)
|
|
{
|
|
throw "Probe test run $probeRunId has no results."
|
|
}
|
|
|
|
Add-SetupCheck 'TestResultsRead' 'PASS' $true "run=$probeRunId; pageResults=$($probeResults.Count)"
|
|
|
|
$probeResultId = [long]$probeResults[0].id
|
|
$attachments = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/test/Runs/${probeRunId}/Results/${probeResultId}/attachments?api-version=7.1-preview.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
Add-SetupCheck `
|
|
'TestAttachmentsRead' `
|
|
'PASS' `
|
|
$true `
|
|
"run=$probeRunId; result=$probeResultId; attachments=$(@($attachments.value).Count)"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'AzureTestRead' 'FAIL' $true (Get-FixedError $_ 'Azure Test read failed.')
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Add-SetupCheck 'BuildDependentReads' 'SKIP' $true 'No probe build was resolved.'
|
|
}
|
|
|
|
if ($pipelineId -ne 0)
|
|
{
|
|
try
|
|
{
|
|
if (-not $ProbeBranch.StartsWith('refs/heads/', [StringComparison]::Ordinal))
|
|
{
|
|
throw "ProbeBranch must be a full refs/heads/... ref; received '$ProbeBranch'."
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ProbeModule))
|
|
{
|
|
throw 'ProbeModule cannot be empty.'
|
|
}
|
|
|
|
$previewRequest = @{
|
|
previewRun = $true
|
|
resources = @{ repositories = @{ self = @{ refName = $ProbeBranch } } }
|
|
templateParameters = @{
|
|
buildPlatforms = '- x64'
|
|
enableMsBuildCaching = 'false'
|
|
useVSPreview = 'false'
|
|
useLatestWebView2 = 'false'
|
|
buildSource = 'buildNow'
|
|
specificBuildId = 'xxxx'
|
|
uiTestModules = "[$ProbeModule]"
|
|
}
|
|
}
|
|
$preview = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/pipelines/${pipelineId}/runs?api-version=7.1-preview.1" `
|
|
-Method Post `
|
|
-Body $previewRequest `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
if ([long]$preview.id -ne -1 -or [string]::IsNullOrWhiteSpace([string]$preview.finalYaml))
|
|
{
|
|
throw 'Pipeline preview did not return id=-1 and expanded YAML.'
|
|
}
|
|
|
|
$stages = @([regex]::Matches($preview.finalYaml, '(?m)^- stage: (.+)$') |
|
|
ForEach-Object { $_.Groups[1].Value.Trim() })
|
|
$expectedStages = @('Build_x64', 'Test_x64Win10_FullBuild', 'Test_x64Win11_FullBuild')
|
|
$missingStages = @($expectedStages | Where-Object { $_ -notin $stages })
|
|
if ($missingStages.Count -ne 0)
|
|
{
|
|
throw "Pipeline preview omitted required stages: $($missingStages -join ', '). Expanded stages: $($stages -join ', ')."
|
|
}
|
|
|
|
$moduleAssignments = @($preview.finalYaml -split "`n" |
|
|
Where-Object { $_ -match '\$modulesRaw\s*=' } |
|
|
ForEach-Object { $_.Trim() })
|
|
$expectedModuleAssignment = "`$modulesRaw = '$ProbeModule'"
|
|
if ($moduleAssignments.Count -eq 0 -or
|
|
@($moduleAssignments | Where-Object { $_ -ne $expectedModuleAssignment }).Count -ne 0)
|
|
{
|
|
throw "Pipeline preview did not resolve only module '$ProbeModule'."
|
|
}
|
|
|
|
Add-SetupCheck `
|
|
'PipelinePreview' `
|
|
'PASS' `
|
|
$true `
|
|
"id=$($preview.id); requestedBranch=$ProbeBranch; module=$ProbeModule; stages=$($stages -join ',')"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'PipelinePreview' 'FAIL' $true (Get-FixedError $_ 'Pipeline preview failed.')
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Add-SetupCheck 'PipelinePreview' 'SKIP' $true 'Pipeline definition was not resolved.'
|
|
}
|
|
|
|
try
|
|
{
|
|
if ($null -eq $probeBuild)
|
|
{
|
|
throw 'No probe build is available for a repeated read.'
|
|
}
|
|
|
|
$secondRead = (Invoke-AzDevOpsRest `
|
|
-Uri "_apis/build/builds/$($probeBuild.id)?api-version=7.1" `
|
|
-Organization $Organization `
|
|
-Project $Project).Body
|
|
Add-SetupCheck 'RepeatedPromptFreeRead' 'PASS' $true "id=$($secondRead.id); prompts=0"
|
|
}
|
|
catch
|
|
{
|
|
Add-SetupCheck 'RepeatedPromptFreeRead' 'FAIL' $true (Get-FixedError $_ 'Repeated prompt-free read failed.')
|
|
}
|
|
|
|
$requiredFailures = @($checks | Where-Object { $_.Required -and $_.Status -ne 'PASS' })
|
|
$summary = [pscustomobject]@{
|
|
Ready = $requiredFailures.Count -eq 0
|
|
Organization = $Organization
|
|
Project = $Project
|
|
PipelineName = $PipelineName
|
|
PipelineId = $pipelineId
|
|
ProbeBuildId = if ($probeBuild) { [long]$probeBuild.id } else { $null }
|
|
ProbeTestBuildId = if ($probeTestBuild) { [long]$probeTestBuild.id } else { $null }
|
|
ProbeBranch = $ProbeBranch
|
|
ProbeModule = $ProbeModule
|
|
Checks = $checks.ToArray()
|
|
ProvenWithoutMutation = @(
|
|
'cached Azure CLI sign-in and Azure DevOps token acquisition',
|
|
'project, definition, build, timeline, log, artifact, test-run, result, and attachment reads',
|
|
'Run Pipeline API access and YAML expansion through previewRun=true',
|
|
'repeated prompt-free token-backed REST reads'
|
|
)
|
|
DeliberatelyNotMutated = @(
|
|
'creating an actual pipeline run',
|
|
'canceling a build',
|
|
'retrying or canceling a stage'
|
|
)
|
|
NextStep = if ($requiredFailures.Count -eq 0)
|
|
{
|
|
'Setup is ready. Re-run this preflight after account changes or any 401/403 response.'
|
|
}
|
|
else
|
|
{
|
|
'Setup is not ready. Resolve every required FAIL/SKIP item before queueing or monitoring CI.'
|
|
}
|
|
}
|
|
|
|
$summary | ConvertTo-Json -Depth 8
|
|
if (-not $summary.Ready)
|
|
{
|
|
exit 1
|
|
} |