Skip to content

Fix Windows CI path gating and capture UIA startup diagnostics - #506

Merged
coneilen merged 1 commit into
mainfrom
coneilen-microsoft-uia-loader-diagnostics
Sep 29, 2026
Merged

coneilen merged 1 commit into
mainfrom
coneilen-microsoft-uia-loader-diagnostics

Conversation

@coneilen

Copy link
Copy Markdown
Collaborator

Problem

Two related defects, both of which amount to CI doing work it doesn't need to do, and then failing on it.

1. Path-based CI gating has never worked on pull requests

Every gated workflow classifies changed paths in a changes job that checks out with:

fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}

This hits the GitHub Actions falsy-zero trap. On a pull request the expression evaluates true && 0 -> 0, and 0 is falsy in GHA expressions, so 0 || 1 -> 1. The intended full-history checkout never happened; the clone was always shallow.

The consequence, from the classify job of run 36497626173 (windows-shell on #505):

fetch-depth: 1
[command]/usr/bin/git ... fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +9760926f...:refs/remotes/pull/505/merge
classify-changes: could not diff e866bf65...72e7002c; running every suite.
fatal: Invalid symmetric difference expression e866bf65...72e7002c
windows=true
macos=true
linux=true

The shallow clone does not contain the base commit, so git diff base...head fails, and classify-changes.sh correctly fails safe by running every suite. The classifier was never the problem — the checkout was. Net effect: no pull request has ever skipped a gated suite by path classification. A one-file Markdown change paid for the full macOS, Linux and Windows suites.

2. A UIA startup failure produces no usable diagnostics

The windows-shell and windows-spikes jobs have been intermittently failing at the Native UI Automation live gate — four occurrences across three pull requests, with an identical signature:

UIA_STARTUP_FAILURE={"exitSigned":-1073741502,"exitHex":"0xC0000142",
  "executablePath":"...\\graphcode-windows.exe",
  "stderr":{"readBytes":0,"content":"not_read","state":"ownership_unavailable"},
  "stdout":{"state":"not_captured"},
  "applicationLog":{"readBytes":0,"content":"not_read","state":"ownership_unavailable"}}
Exception: Tools\windows\uia-live-gate.ps1:1463
  shell exited with code -1073741502

0xC0000142 is STATUS_DLL_INIT_FAILED: the process is created but dies during loader/DLL initialisation, before reaching a message loop. Critically, the gate captured nothing — child stderr was ownership_unavailable, stdout was not_captured, and the application log went unread. That is precisely why four failures yielded no root cause.

The gate also already admitted its own teardown was unverified:

UIA_SANDBOX_CLEANUP_UNVERIFIED=legacy process teardown does not verify all owned descendants

The two defects compound: because gating never worked, a documentation-only pull request was exposed to a Windows-only loader flake it had no way to trigger.

Change

Gating — .github/workflows/{linux,macos-shared-regression,windows-hardening,windows-port-validation,windows-shell}.yml

  • Every changes job now uses a literal fetch-depth: 0, so the base commit is present and the diff computes.
  • classify-changes.sh fail-safe behaviour is unchanged: an unclassified path, an empty path list, or an uncomputable diff still runs every suite.

Classifier — Tools/ci/classify-changes.sh

  • Now that the path lists actually take effect, two gaps are closed explicitly rather than by blanket rules: Tools/icon/* is classified as a macOS input, and investigation/macos-parity-evidence/evidence/* is classified as documentation.
  • Broad *.png skipping was deliberately rejected as unsafe — icon sources and visual baselines are genuine suite inputs. An arbitrary unknown .png still falls through to "unclassified -> run everything".

UIA diagnostics — Tools/windows/uia-live-gate.ps1, Tools/windows/validate.ps1

  • Records a prelaunch process and desktop-context snapshot (session, window station, desktop).
  • Redirects and captures both child streams, and writes capped, redacted startup JSON including the support log, retained as a 7-day failure artifact.
  • Verifies descendant teardown instead of assuming it.
  • validate.ps1 snapshots product processes at task entry and terminates — then verifies the termination of — only newly created matching processes under this worktree before the UIA gate launches.

Tests — Tools/ci/tests/classify-changes.test.sh, Tools/windows/Tests/ValidationRunner.Tests.ps1

Test evidence

RED: bash Tools/ci/tests/classify-changes.test.sh -> 50 passed, 5 failed; every classifier changes job still carried the falsy && 0 || 1 fetch-depth expression.

GREEN: bash Tools/ci/tests/classify-changes.test.sh -> 58 passed, 0 failed, including a synthetic full-history docs-only diff classified windows=false macos=false linux=false.

REGRESSION: . .\.graphcode-tools\environment.ps1; & .\Tools\windows\Tests\ValidationRunner.Tests.ps1 -> PASS, exercising bounded and redacted stderr/stdout/app-log capture, native session/window-station/desktop reporting, and verified cleanup of a controlled descendant tree; git diff --check origin/main...HEAD -> exit 0.

Independently re-verified by the coordinator from a clean worktree of this branch: the suite reports 58 passed, 0 failed; the docs-only commit ec2dda32 classifies windows=false macos=false linux=false; investigation/macos-parity-evidence/evidence/foo.png classifies as docs; some/random/thing.png still yields windows=true macos=true linux=true with unclassified path ... running every suite; and an uncomputable base...head diff still fails safe to all suites.

What this does not establish

Stated plainly, because the loader failure is the reason this branch exists:

  • The loader root cause remains unconfirmed. Desktop-heap exhaustion was the leading hypothesis; it is not proven. The retained sandboxes from the failing runs were runner-local and the job logs contained no child or application output, so there was nothing left to analyse after the fact.
  • There is no retry and no mitigation here. This change does not claim to eliminate the flake. It makes the next occurrence explicable, and removes one plausible contributor by verifying teardown and clearing stray run-owned processes before launch.
  • The UIA changes are covered by local tests and contracts, not a hosted-runner reproduction. This bug reproduces only on hosted runners.
  • No end-to-end pull request skip has been observed yet. Tools/ci/* is self-gating, so this branch correctly runs every suite and cannot demonstrate a skip. The fixed expression and the classifier tests are the evidence; the first real skip will only be observable on a later docs-only pull request.

Checklist

  • One coherent change; both halves are the same failure story
  • Commit signed off (git commit -s); DCO clean
  • RED: / GREEN: / REGRESSION: lines in command -> result form, from commands actually run
  • RED preserved as observed and not overwritten by the passing run
  • Fail-safe classification behaviour preserved and re-verified
  • No parity ledger rows touched (investigation/ui-parity-matrix.md untouched)
  • No graphcode-windows/src/** changes; no excluded surfaces touched
  • Pinned toolchain used; nothing taken off bare PATH

Fetch complete PR history before changed-path classification, retain fail-safe path mapping, and capture bounded UIA host and process evidence. Verify cleanup of process trees owned by validation before the live gate.

Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coneilen
coneilen merged commit 1915b8a into main Sep 29, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant