Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
fetch-depth: 0
- name: Test the changed-path classifier
shell: bash
run: bash Tools/ci/tests/classify-changes.test.sh
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos-shared-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
fetch-depth: 0
- name: Classify changed paths
id: classify
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/windows-hardening.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
fetch-depth: 0
- name: Classify changed paths
id: classify
shell: bash
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/windows-port-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
fetch-depth: 0
- name: Classify changed paths
id: classify
shell: bash
Expand Down Expand Up @@ -64,3 +64,12 @@ jobs:
- name: Run Windows port validation
shell: pwsh
run: ./Tools/windows/validate.ps1 -Task all -SkipTrayLive -SkipWslRemoteE2E

- name: Retain UIA failure diagnostics
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: uia-live-gate-diagnostics
path: ${{ runner.temp }}\gu-*\logs\*.json
if-no-files-found: warn
retention-days: 7
11 changes: 10 additions & 1 deletion .github/workflows/windows-shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
fetch-depth: 0
- name: Classify changed paths
id: classify
shell: bash
Expand Down Expand Up @@ -52,3 +52,12 @@ jobs:
- name: Validate release packaging
shell: pwsh
run: ./Tools/windows/validate.ps1 -Task packaging

- name: Retain UIA failure diagnostics
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: uia-live-gate-diagnostics
path: ${{ runner.temp }}\gu-*\logs\*.json
if-no-files-found: warn
retention-days: 7
11 changes: 7 additions & 4 deletions Tools/ci/classify-changes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,16 @@ for path in "${paths[@]}"; do
esac

# macOS shared Swift regression: the app, kit, daemon, CLI, the portable Swift
# package, Tuist/SwiftPM/lint configuration, submodules, and the scripts that
# make and the portable setup run.
# package, Tuist/SwiftPM/lint configuration, submodules, icon sources, and the
# scripts that make and the portable setup run.
case "$path" in
graphcode/* | GraphcodeKit/* | MailroomKit/* | graphcoded/* | graphcode-cli/* | \
investigation/spikes/swift-portable/* | \
Package.swift | Package.resolved | Project.swift | Tuist.swift | Tuist/* | \
Makefile | mise.toml | .swiftlint.yml | .swift-format | .gitattributes | \
.gitmodules | ThirdParty/* | scripts/* | Tools/portable-prepare.py | \
Tools/zig-sdk-shim/* | .github/workflows/macos-shared-regression.yml)
Tools/icon/* | Tools/zig-sdk-shim/* | \
.github/workflows/macos-shared-regression.yml)
macos=true; matched=1 ;;
esac

Expand All @@ -133,9 +134,11 @@ for path in "${paths[@]}"; do

[[ $matched -eq 1 ]] && continue

# Paths no gated suite reads. DCO and TDD-evidence still run on every PR.
# Paths no gated suite reads. The source screenshots are still privacy-scanned
# by the ungated investigation job. DCO and TDD-evidence run on every PR.
case "$path" in
*.md | docs/* | screenshots/* | investigation/contracts/* | \
investigation/macos-parity-evidence/evidence/* | \
LICENSE | DCO | .env.example | .github/PULL_REQUEST_TEMPLATE.md | \
.github/ISSUE_TEMPLATE/* | .github/workflows/dco.yml | \
.github/workflows/tdd-evidence.yml)
Expand Down
47 changes: 47 additions & 0 deletions Tools/ci/tests/classify-changes.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ expect "TDD evidence tooling validated by Windows suite" true false false \
Tools/tdd/Test-TddEvidence.ps1
expect "visual baseline manifest" true false false \
investigation/visual-baseline/manifest.json
expect "macOS icon source image" false true false \
Tools/icon/icon-master-1024.png
expect "macOS reference screenshot only" false false false \
investigation/macos-parity-evidence/evidence/empty-welcome-original.png
expect "Windows release workflow" true false false \
.github/workflows/windows-release.yml

Expand Down Expand Up @@ -122,8 +126,51 @@ expect "classifier tests" true true true \

expect "unknown path fails safe" true true true \
some-new-directory/build.sh
expect "unclassified root image fails safe" true true true \
notes/diagram.png
expect "empty change list fails safe" true true true

fixture="$(mktemp -d)"
git -C "$fixture" init -q
git -C "$fixture" config user.name "Classifier Test"
git -C "$fixture" config user.email "classifier@example.invalid"
git -C "$fixture" config core.autocrlf false
printf 'base\n' >"$fixture/README.md"
git -C "$fixture" add README.md
git -C "$fixture" commit -qm "base"
base="$(git -C "$fixture" rev-parse HEAD)"
printf 'documentation\n' >"$fixture/notes.md"
git -C "$fixture" add notes.md
git -C "$fixture" commit -qm "documentation"
head="$(git -C "$fixture" rev-parse HEAD)"
got="$(
cd "$fixture" &&
GITHUB_OUTPUT='' bash "$classifier" --event pull_request --base "$base" --head "$head" 2>/dev/null |
flatten
)"
check "full-history docs-only diff is skipped" \
"windows=false macos=false linux=false" "$got"
rm -rf "$fixture"

# A PR path classifier must have the merge base locally; zero is a literal
# depth, not a falsy workflow-expression operand.
for workflow in \
.github/workflows/linux.yml \
.github/workflows/macos-shared-regression.yml \
.github/workflows/windows-shell.yml \
.github/workflows/windows-port-validation.yml \
.github/workflows/windows-hardening.yml; do
depth="$(awk '
/^ changes:/ { in_changes=1; next }
in_changes && /^ [^ ]/ { exit }
in_changes && /fetch-depth:/ {
sub(/^[[:space:]]*/, "")
print
}
' "$here/../../../$workflow")"
check "$workflow changes checkout has full history" "fetch-depth: 0" "$depth"
done

# Non-PR events always get full validation, whatever changed.
for event in push merge_group workflow_dispatch schedule; do
got="$(printf 'README.md\n' | GITHUB_OUTPUT='' bash "$classifier" --event "$event" --stdin 2>/dev/null | flatten)"
Expand Down
167 changes: 167 additions & 0 deletions Tools/windows/Tests/ValidationRunner.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ try {
$windowsShellWorkflow = Get-Content (Join-Path $repoRoot ".github\workflows\windows-shell.yml") -Raw
$windowsPortWorkflow = Get-Content `
(Join-Path $repoRoot ".github\workflows\windows-port-validation.yml") -Raw
foreach ($workflow in @($windowsShellWorkflow, $windowsPortWorkflow)) {
if ($workflow -notmatch
'(?s)if: failure\(\).*?actions/upload-artifact@.*?gu-\*.*?logs\\\*\.json') {
throw "RED: Windows CI does not retain failed UIA sandbox diagnostics as an artifact"
}
}
foreach ($workflow in @($windowsWorkflow, $windowsShellWorkflow, $windowsPortWorkflow)) {
if ($workflow -notmatch
"compnerd/gha-setup-swift@397094e75494a93fa8d81db0268dbc8f5d6cf7c6" -or
Expand All @@ -418,6 +424,10 @@ try {
throw "RED: Windows shell CI does not invoke the shell task containing live UI Automation"
}
$runnerSource = Get-Content $runner -Raw
if ($runnerSource -notmatch
'(?s)WINDOWS_SHELL_PRE_UIA_PROCESS_SNAPSHOT=.*?Stop-Process -Id.*?WINDOWS_SHELL_PRE_UIA_CLEANUP=verified.*?Native UI Automation live gate') {
throw "RED: Windows shell validation does not snapshot and reap run-owned product processes before UIA"
}
Test-ZigResolverDiagnostics $runnerSource
Assert-ShellHostPrerequisite $runnerSource
$contractCall = [regex]::Match($runnerSource,
Expand Down Expand Up @@ -461,6 +471,163 @@ try {
throw "RED: Windows shell validation does not execute the UI Automation live gate"
}
$uiaLiveGateSource = Get-Content (Join-Path $repoRoot "Tools\windows\uia-live-gate.ps1") -Raw
if ($uiaLiveGateSource -notmatch 'function Get-UiaStartupFileDiagnostic' -or
$uiaLiveGateSource -notmatch 'RedirectStandardOutput' -or
$uiaLiveGateSource -notmatch 'function Get-UiaPrelaunchDiagnostics' -or
$uiaLiveGateSource -notmatch 'prelaunch-diagnostics\.json' -or
$uiaLiveGateSource -notmatch 'startup-failure\.json') {
throw "RED: UIA startup failure does not capture both child streams, app log, and prelaunch host diagnostics"
}
$prelaunchDiagnostic = $uiaLiveGateSource.LastIndexOf('Get-UiaPrelaunchDiagnostics')
$shellLaunch = $uiaLiveGateSource.IndexOf('Start-Process -FilePath $Shell')
if ($prelaunchDiagnostic -lt 0 -or $shellLaunch -lt 0 -or
$prelaunchDiagnostic -gt $shellLaunch -or
$uiaLiveGateSource -notmatch 'GetCurrentWindowStationName|WindowStation' -or
$uiaLiveGateSource -notmatch 'GetCurrentDesktopName|DesktopName' -or
$uiaLiveGateSource -notmatch 'desktopHeap' -or
$uiaLiveGateSource -notmatch 'sessionId') {
throw "RED: UIA prelaunch diagnostics omit process, desktop heap, window station, or session evidence"
}
if ($uiaLiveGateSource -notmatch 'function Get-UiaOwnedProcessDescendants' -or
$uiaLiveGateSource -notmatch 'UIA_PROCESS_TREE_CLEANUP=verified') {
throw "RED: UIA teardown does not enumerate, reap, and verify all owned descendants"
}
$uiaTokens = $null
$uiaParseErrors = $null
$uiaAst = [Management.Automation.Language.Parser]::ParseInput(
$uiaLiveGateSource, [ref]$uiaTokens, [ref]$uiaParseErrors)
if ($uiaParseErrors.Count -ne 0) {
throw "RED: UIA live gate no longer parses after startup diagnostic changes"
}
foreach ($helperName in @(
"Protect-UiaStartupDiagnosticText",
"Get-UiaStartupDiagnosticValue",
"Get-UiaStartupFileDiagnostic",
"Get-UiaStartupImageHash",
"Write-UiaStartupFailureDiagnostic",
"Get-UiaPrelaunchDiagnostics",
"Get-UiaOwnedProcessDescendants",
"Stop-UiaOwnedProcessTrees"
)) {
$helper = $uiaAst.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq $helperName
}, $true)
if ($null -eq $helper) { throw "RED: UIA startup capture helper is missing: $helperName" }
. ([scriptblock]::Create($helper.Extent.Text))
}
$startupCapturePath = Join-Path $env:TEMP "uia-startup-capture-$PID.log"
try {
[IO.File]::WriteAllText($startupCapturePath, "loader failed`npassword=private-canary`n")
$capture = Get-UiaStartupFileDiagnostic $startupCapturePath "logs\shell-stderr.log"
if ($capture.state -ne "available" -or
$capture.content -notmatch "loader failed" -or
$capture.content -match "private-canary" -or
$capture.readBytes -ne $capture.lengthBytes) {
throw "RED: UIA startup capture does not retain bounded, redacted child output"
}
$truncatedCapture = Get-UiaStartupFileDiagnostic $startupCapturePath `
"logs\shell-stderr.log" 8
if ($truncatedCapture.state -ne "truncated" -or
$truncatedCapture.readBytes -ne 8 -or $truncatedCapture.lengthBytes -le 8) {
throw "RED: UIA startup capture does not bound oversized diagnostics"
}
$missingCapture = Get-UiaStartupFileDiagnostic `
(Join-Path $env:TEMP "uia-missing-$PID.log") "logs\missing.log"
if ($missingCapture.state -ne "missing") {
throw "RED: UIA startup capture does not distinguish a missing child log"
}
$startupLogDirectory = Join-Path $env:TEMP "uia-startup-logs-$PID"
$startupSupportDirectory = Join-Path $env:TEMP "uia-startup-support-$PID"
New-Item -ItemType Directory -Path $startupLogDirectory -Force | Out-Null
New-Item -ItemType Directory -Path $startupSupportDirectory -Force | Out-Null
[IO.File]::WriteAllText(
(Join-Path $startupLogDirectory "shell-stderr.log"),
"loader failed`npassword=stderr-canary`n"
)
[IO.File]::WriteAllText(
(Join-Path $startupLogDirectory "shell-stdout.log"),
"child output captured"
)
[IO.File]::WriteAllText(
(Join-Path $startupSupportDirectory "graphcode-windows.log"),
"app initialization failed`nsecret=app-canary`n"
)
Write-UiaStartupFailureDiagnostic (Get-Process -Id $PID) 0 `
$startupCapturePath $env:TEMP $startupLogDirectory $startupSupportDirectory
$startupRecord = Get-Content -LiteralPath `
(Join-Path $startupLogDirectory "startup-failure.json") -Raw | ConvertFrom-Json
if ($startupRecord.stderr.content -notmatch "loader failed" -or
$startupRecord.stdout.content -notmatch "child output captured" -or
$startupRecord.applicationLog.content -notmatch "app initialization failed" -or
$startupRecord.stderr.content -match "stderr-canary" -or
$startupRecord.applicationLog.content -match "app-canary") {
throw "RED: retained UIA startup report omits or exposes captured child and app output"
}
} finally {
Remove-Item -LiteralPath (Join-Path $env:TEMP "uia-startup-logs-$PID") `
-Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath (Join-Path $env:TEMP "uia-startup-support-$PID") `
-Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $startupCapturePath -Force -ErrorAction SilentlyContinue
}
$hostInfoClassAt = $uiaLiveGateSource.IndexOf("public static class GraphCodeUiaHostInfo")
if ($hostInfoClassAt -lt 0) {
throw "RED: UIA host context native diagnostics type is missing"
}
$hostInfoAddTypeAt = $uiaLiveGateSource.LastIndexOf(
'Add-Type -TypeDefinition @"', $hostInfoClassAt
)
$hostInfoBodyAt = $uiaLiveGateSource.IndexOf("`n", $hostInfoAddTypeAt) + 1
$hostInfoCloseAt = $uiaLiveGateSource.IndexOf('"@', $hostInfoClassAt)
if ($hostInfoAddTypeAt -lt 0 -or $hostInfoBodyAt -le 0 -or
$hostInfoCloseAt -lt 0) {
throw "RED: UIA host context native diagnostics type is missing"
}
$hostInfoBody = $uiaLiveGateSource.Substring(
$hostInfoBodyAt, $hostInfoCloseAt - $hostInfoBodyAt
).TrimEnd("`r", "`n")
Add-Type -TypeDefinition $hostInfoBody
$hostSessionId = [GraphCodeUiaHostInfo]::CurrentSessionId()
if ($hostSessionId -isnot [uint32]) {
throw "RED: UIA host context did not resolve the current Windows session"
}
$hostDiagnostics = Get-UiaPrelaunchDiagnostics
if ($hostDiagnostics.sessionId.state -ne "available" -or
$hostDiagnostics.currentProcessId -ne $PID -or
$hostDiagnostics.desktopHeap.state -ne "usage_unavailable") {
throw "RED: UIA host context diagnostics omitted explicit session or desktop-heap status"
}
$treeFixturePath = Join-Path $env:TEMP "uia-process-tree-$PID.ps1"
$treeRoot = $null
try {
[IO.File]::WriteAllText($treeFixturePath, @'
$start = [Diagnostics.ProcessStartInfo]::new((Join-Path $PSHOME "pwsh.exe"))
$start.ArgumentList.Add("-NoProfile")
$start.ArgumentList.Add("-Command")
$start.ArgumentList.Add("Start-Sleep -Seconds 60")
[void][Diagnostics.Process]::Start($start)
Start-Sleep -Seconds 60
'@)
$treeRoot = Start-Process -FilePath (Join-Path $PSHOME "pwsh.exe") `
-ArgumentList @("-NoProfile", "-File", $treeFixturePath) -PassThru
$treeObserved = $false
for ($attempt = 0; $attempt -lt 20 -and -not $treeObserved; $attempt++) {
Start-Sleep -Milliseconds 100
$treeObserved = @(Get-UiaOwnedProcessDescendants @($treeRoot.Id)).Count -gt 0
}
if (-not $treeObserved) { throw "RED: UIA owned-process traversal missed a controlled child" }
Stop-UiaOwnedProcessTrees @($treeRoot)
if (-not $treeRoot.HasExited) {
throw "RED: UIA owned-process teardown returned before the controlled root exited"
}
} finally {
if ($treeRoot -and -not $treeRoot.HasExited) {
Stop-UiaOwnedProcessTrees @($treeRoot)
}
Remove-Item -LiteralPath $treeFixturePath -Force -ErrorAction SilentlyContinue
}
if ($uiaLiveGateSource -notmatch 'UIA_ROOT_ACCESS' -or
$uiaLiveGateSource -notmatch 'UIA_UPDATE_DIALOG_DIAGNOSTICS' -or
$uiaLiveGateSource -notmatch 'maxSandboxRootUtf16' -or
Expand Down
Loading
Loading