From a97fd0d39fded76fd0f69bf2c600943fb02e946c Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 17:35:35 +0000 Subject: [PATCH 001/126] [ci] Add copilot CI --- eng/pipelines/ci-copilot.yml | 117 +++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 eng/pipelines/ci-copilot.yml diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml new file mode 100644 index 000000000000..41149cfa2815 --- /dev/null +++ b/eng/pipelines/ci-copilot.yml @@ -0,0 +1,117 @@ +# Pipeline for running GitHub Copilot PR Reviewer Agent +# This pipeline installs the Copilot CLI and invokes the PR reviewer agent +# to conduct automated code reviews on pull requests. +# +# For more information, see: +# https://github.com/dotnet/maui/wiki/PR-Reviewer-Agent + +trigger: none # Manual trigger only + +pr: none # Not triggered by PRs + +parameters: + - name: PRNumber + displayName: 'Pull Request Number' + type: string + default: '' + + - name: pool + type: object + default: + name: MAUI-Testing + vmImage: ubuntu-latest + demands: + - Agent.OS -equals Darwin + +# variables: +# - template: /eng/pipelines/common/variables.yml@self + +stages: + - stage: ReviewPR + displayName: 'Review Pull Request' + jobs: + - job: CopilotReview + displayName: 'Run Copilot PR Reviewer Agent' + pool: ${{ parameters.pool }} + steps: + - checkout: self + fetchDepth: 0 + persistCredentials: true + + - script: | + echo "Validating PR Number parameter..." + if [ -z "${{ parameters.PRNumber }}" ]; then + echo "##vso[task.logissue type=error]PRNumber parameter is required" + exit 1 + fi + echo "PR Number: ${{ parameters.PRNumber }}" + displayName: 'Validate Parameters' + + - script: | + echo "Installing Node.js and npm..." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + node --version + npm --version + displayName: 'Install Node.js' + + - script: | + echo "Installing GitHub Copilot CLI..." + npm install -g @githubnext/github-copilot-cli + echo "Copilot CLI installed successfully" + displayName: 'Install GitHub Copilot CLI' + + - script: | + echo "Authenticating with GitHub CLI..." + echo "$(GH_CLI_TOKEN)" | gh auth login --with-token + gh auth status + displayName: 'Authenticate GitHub CLI' + env: + GH_CLI_TOKEN: $(GH_CLI_TOKEN) + + - script: | + echo "Fetching PR #${{ parameters.PRNumber }}..." + git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }} + git checkout pr-${{ parameters.PRNumber }} + echo "Checked out PR branch successfully" + git log -1 --oneline + displayName: 'Checkout PR Branch' + + - script: | + echo "Running Copilot PR Reviewer Agent..." + echo "Reviewing PR #${{ parameters.PRNumber }}..." + + # Invoke the PR reviewer agent using Copilot CLI + # The agent will analyze the PR and generate review feedback + github-copilot-cli chat --agent pr-reviewer "review PR #${{ parameters.PRNumber }}" + displayName: 'Run PR Reviewer Agent' + env: + GITHUB_TOKEN: $(COPILOT_TOKEN) + + - script: | + echo "Posting review comment to PR..." + + # Check if review feedback file was generated + REVIEW_FILE=$(find . -name "Review_Feedback_Issue_*.md" -type f | head -1) + + if [ -n "$REVIEW_FILE" ]; then + echo "Found review file: $REVIEW_FILE" + + # Post the review as a comment on the PR + gh pr comment ${{ parameters.PRNumber }} --body-file "$REVIEW_FILE" + echo "Review comment posted successfully" + else + echo "No review feedback file found, skipping comment" + fi + displayName: 'Post Review Comment' + env: + GITHUB_TOKEN: $(GH_COMMENT_TOKEN) + condition: succeededOrFailed() + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Review Artifacts' + inputs: + targetPath: '$(System.DefaultWorkingDirectory)' + artifact: 'ReviewOutput' + publishLocation: 'pipeline' + condition: succeededOrFailed() From 539bdcfd4a81da0f46efa0fecf073f6b08dec594 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 17:55:31 +0000 Subject: [PATCH 002/126] Install GH --- eng/pipelines/ci-copilot.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 41149cfa2815..b4b2514ad0da 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -49,8 +49,7 @@ stages: - script: | echo "Installing Node.js and npm..." - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - - sudo apt-get install -y nodejs + brew install node node --version npm --version displayName: 'Install Node.js' @@ -61,6 +60,13 @@ stages: echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' + - script: | + echo "Installing GitHub CLI..." + brew install gh + gh --version + echo "GitHub CLI installed successfully" + displayName: 'Install GitHub CLI' + - script: | echo "Authenticating with GitHub CLI..." echo "$(GH_CLI_TOKEN)" | gh auth login --with-token From 62109e968e184652abd46e833910ba774508ed0b Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 18:03:38 +0000 Subject: [PATCH 003/126] Try again --- eng/pipelines/ci-copilot.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index b4b2514ad0da..096d706704cb 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -48,18 +48,14 @@ stages: displayName: 'Validate Parameters' - script: | - echo "Installing Node.js and npm..." - brew install node + echo "Installing Node.js 22..." + brew install node@22 + brew link --overwrite node@22 node --version npm --version + echo "Node.js installed successfully" displayName: 'Install Node.js' - - script: | - echo "Installing GitHub Copilot CLI..." - npm install -g @githubnext/github-copilot-cli - echo "Copilot CLI installed successfully" - displayName: 'Install GitHub Copilot CLI' - - script: | echo "Installing GitHub CLI..." brew install gh @@ -75,6 +71,12 @@ stages: env: GH_CLI_TOKEN: $(GH_CLI_TOKEN) + - script: | + echo "Installing GitHub Copilot CLI..." + npm install -g @github/copilot + echo "Copilot CLI installed successfully" + displayName: 'Install GitHub Copilot CLI' + - script: | echo "Fetching PR #${{ parameters.PRNumber }}..." git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }} @@ -87,9 +89,9 @@ stages: echo "Running Copilot PR Reviewer Agent..." echo "Reviewing PR #${{ parameters.PRNumber }}..." - # Invoke the PR reviewer agent using Copilot CLI - # The agent will analyze the PR and generate review feedback - github-copilot-cli chat --agent pr-reviewer "review PR #${{ parameters.PRNumber }}" + # Invoke the PR reviewer agent using Copilot CLI in programmatic mode + # --allow-all-tools allows Copilot to execute commands without manual approval + copilot -p "review PR #${{ parameters.PRNumber }}" --allow-all-tools --allow-all-paths displayName: 'Run PR Reviewer Agent' env: GITHUB_TOKEN: $(COPILOT_TOKEN) From e0e367a04149fd5a6f33bec6c341f61a0e2d243f Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 18:14:35 +0000 Subject: [PATCH 004/126] skip codeql --- eng/pipelines/ci-copilot.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 096d706704cb..3e4d55e589f2 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -23,8 +23,9 @@ parameters: demands: - Agent.OS -equals Darwin -# variables: -# - template: /eng/pipelines/common/variables.yml@self +variables: + Codeql.Enabled: false + Codeql.SkipTaskAutoInjection: true stages: - stage: ReviewPR From 55c0331314b9bea1de49f6c4c3a2d360ab3b8e54 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 18:37:18 +0000 Subject: [PATCH 005/126] Pipe the output --- eng/pipelines/ci-copilot.yml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 3e4d55e589f2..0810face2216 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -92,7 +92,10 @@ stages: # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # --allow-all-tools allows Copilot to execute commands without manual approval - copilot -p "review PR #${{ parameters.PRNumber }}" --allow-all-tools --allow-all-paths + # Capture output to a file for posting as PR comment + copilot -p "review PR #${{ parameters.PRNumber }}" --allow-all-tools --allow-all-paths 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md + + echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot_review_output.md" displayName: 'Run PR Reviewer Agent' env: GITHUB_TOKEN: $(COPILOT_TOKEN) @@ -100,17 +103,28 @@ stages: - script: | echo "Posting review comment to PR..." - # Check if review feedback file was generated - REVIEW_FILE=$(find . -name "Review_Feedback_Issue_*.md" -type f | head -1) + # Check for the captured output file first + REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot_review_output.md" + + # Also check if Copilot created a Review_Feedback file + FEEDBACK_FILE=$(find . -name "Review_Feedback_*.md" -type f | head -1) + + if [ -n "$FEEDBACK_FILE" ]; then + echo "Found review feedback file: $FEEDBACK_FILE" + REVIEW_FILE="$FEEDBACK_FILE" + fi - if [ -n "$REVIEW_FILE" ]; then - echo "Found review file: $REVIEW_FILE" + if [ -f "$REVIEW_FILE" ] && [ -s "$REVIEW_FILE" ]; then + echo "Posting review from: $REVIEW_FILE" + echo "--- Review Content Preview ---" + head -50 "$REVIEW_FILE" + echo "--- End Preview ---" # Post the review as a comment on the PR gh pr comment ${{ parameters.PRNumber }} --body-file "$REVIEW_FILE" echo "Review comment posted successfully" else - echo "No review feedback file found, skipping comment" + echo "No review output found or file is empty" fi displayName: 'Post Review Comment' env: From 3a81e85e7ad8b1c3f0e090795737741a642292c6 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 12 Dec 2025 18:56:29 +0000 Subject: [PATCH 006/126] Cleanup file --- eng/pipelines/ci-copilot.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 0810face2216..3511e2512e79 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -105,6 +105,7 @@ stages: # Check for the captured output file first REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot_review_output.md" + CLEANED_FILE="$(Build.ArtifactStagingDirectory)/copilot_review_cleaned.md" # Also check if Copilot created a Review_Feedback file FEEDBACK_FILE=$(find . -name "Review_Feedback_*.md" -type f | head -1) @@ -115,14 +116,35 @@ stages: fi if [ -f "$REVIEW_FILE" ] && [ -s "$REVIEW_FILE" ]; then - echo "Posting review from: $REVIEW_FILE" + echo "Cleaning up review output..." + + # Remove intro lines like "I'll review PR #XXXXX for you." and "The code review for PR #XXXXX is complete." + # Remove stats at the end (Total usage est, Total duration, Usage by model, etc.) + sed -n '/^## /,$p' "$REVIEW_FILE" | \ + sed '/^Total usage est:/,$d' | \ + sed '/^Total duration/d' | \ + sed '/^Total code changes/d' | \ + sed '/^Usage by model/,$d' > "$CLEANED_FILE" + + # If cleaning removed everything, fall back to original + if [ ! -s "$CLEANED_FILE" ]; then + echo "Cleaned file is empty, using original" + cp "$REVIEW_FILE" "$CLEANED_FILE" + fi + + echo "Posting review from: $CLEANED_FILE" echo "--- Review Content Preview ---" - head -50 "$REVIEW_FILE" + head -50 "$CLEANED_FILE" echo "--- End Preview ---" # Post the review as a comment on the PR - gh pr comment ${{ parameters.PRNumber }} --body-file "$REVIEW_FILE" - echo "Review comment posted successfully" + # Note: GH_COMMENT_TOKEN needs 'repo' scope or 'pull_requests:write' permission + if gh pr comment ${{ parameters.PRNumber }} --repo dotnet/maui --body-file "$CLEANED_FILE"; then + echo "Review comment posted successfully" + else + echo "##vso[task.logissue type=warning]Failed to post comment. Check that GH_COMMENT_TOKEN has 'repo' or 'pull_requests:write' scope." + echo "Review content is available in the published artifacts." + fi else echo "No review output found or file is empty" fi From 69326b2d5148f6ded4c0a7298bdca7e9ca29622d Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 16:11:03 +0000 Subject: [PATCH 007/126] fix prompt --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 3511e2512e79..5b5d0ec30b34 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -93,7 +93,7 @@ stages: # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # --allow-all-tools allows Copilot to execute commands without manual approval # Capture output to a file for posting as PR comment - copilot -p "review PR #${{ parameters.PRNumber }}" --allow-all-tools --allow-all-paths 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md + copilot --agent pr -p "Review PR #${{ parameters.PRNumber }}" you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot_review_output.md" displayName: 'Run PR Reviewer Agent' From ca59d6d7412778a2755a6e581d17204d116e77f5 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 16:13:26 +0000 Subject: [PATCH 008/126] Fixx --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5b5d0ec30b34..93f77335ce3c 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -93,7 +93,7 @@ stages: # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # --allow-all-tools allows Copilot to execute commands without manual approval # Capture output to a file for posting as PR comment - copilot --agent pr -p "Review PR #${{ parameters.PRNumber }}" you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md + copilot --agent pr -p "Review PR #${{ parameters.PRNumber }}" you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot_review_output.md" displayName: 'Run PR Reviewer Agent' From e8fc0a3666c125f6f5c20eedab726fcac67c801c Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 16:15:49 +0000 Subject: [PATCH 009/126] Fix it again --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 93f77335ce3c..ed99aad8c691 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -93,7 +93,7 @@ stages: # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # --allow-all-tools allows Copilot to execute commands without manual approval # Capture output to a file for posting as PR comment - copilot --agent pr -p "Review PR #${{ parameters.PRNumber }}" you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md + copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot_review_output.md" displayName: 'Run PR Reviewer Agent' From f7b5a5875a7ca4db2ffd2194501f2e6fb19162ec Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 16:23:21 +0000 Subject: [PATCH 010/126] add provisioning --- eng/pipelines/ci-copilot.yml | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ed99aad8c691..a234af4b4d2e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -24,8 +24,11 @@ parameters: - Agent.OS -equals Darwin variables: - Codeql.Enabled: false - Codeql.SkipTaskAutoInjection: true + - template: /eng/pipelines/common/variables.yml@self + - name: Codeql.Enabled + value: false + - name: Codeql.SkipTaskAutoInjection + value: true stages: - stage: ReviewPR @@ -34,6 +37,9 @@ stages: - job: CopilotReview displayName: 'Run Copilot PR Reviewer Agent' pool: ${{ parameters.pool }} + timeoutInMinutes: 120 + variables: + REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE) steps: - checkout: self fetchDepth: 0 @@ -48,6 +54,27 @@ stages: echo "PR Number: ${{ parameters.PRNumber }}" displayName: 'Validate Parameters' + # Provision environment (Xcode, .NET SDK, Android SDK, etc.) + - template: common/provision.yml + parameters: + skipXcode: false + skipProvisionator: true + skipAndroidCommonSdks: false + skipAndroidPlatformApis: false + skipJdk: false + skipSimulatorSetup: false + + # Install .NET and workloads via build.ps1 + - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic + displayName: 'Install .NET and workloads' + retryCountOnTaskFailure: 2 + env: + DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) + PRIVATE_BUILD: $(PrivateBuild) + + - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" + displayName: 'Add .NET to PATH' + - script: | echo "Installing Node.js 22..." brew install node@22 From 6339ba852005afbb907fa9e79382df3d5254835e Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:29:55 +0000 Subject: [PATCH 011/126] again --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a234af4b4d2e..80b928230b19 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -24,7 +24,7 @@ parameters: - Agent.OS -equals Darwin variables: - - template: /eng/pipelines/common/variables.yml@self + # - template: /eng/pipelines/common/variables.yml@self - name: Codeql.Enabled value: false - name: Codeql.SkipTaskAutoInjection From 25f4df299ae49368bd9815f17c7f3aa44f550954 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:35:24 +0000 Subject: [PATCH 012/126] try update --- eng/pipelines/ci-copilot.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 80b928230b19..8da0d12575b4 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -29,6 +29,10 @@ variables: value: false - name: Codeql.SkipTaskAutoInjection value: true + - name: DOTNET_VERSION + value: 10.0.100 + - name: REQUIRED_XCODE + value: 26.0.1 stages: - stage: ReviewPR @@ -38,8 +42,6 @@ stages: displayName: 'Run Copilot PR Reviewer Agent' pool: ${{ parameters.pool }} timeoutInMinutes: 120 - variables: - REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE) steps: - checkout: self fetchDepth: 0 From 747523ed240749c5a4c062dce6cd5cbd3f4308dc Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:39:22 +0000 Subject: [PATCH 013/126] skip certs --- eng/pipelines/common/provision.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index b58ee387b28a..14fcbdcdeaf4 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -32,6 +32,7 @@ parameters: expiryInHours: 1 base64Encode: false skipInternalFeeds: true + skipCertificates: false steps: @@ -199,7 +200,7 @@ steps: timeoutInMinutes: 30 # Provision Additional Software -- ${{ if or(eq(variables['System.TeamProject'], 'DevDiv'), ne(parameters.skipProvisionator, true)) }}: +- ${{ if ne(parameters.skipCertificates, 'true') }}: # Prepare macOS - task: InstallAppleCertificate@2 condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) From 4370f80f4063245423863c97c7591f29eedfbe02 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:39:30 +0000 Subject: [PATCH 014/126] skip certs --- eng/pipelines/ci-copilot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 8da0d12575b4..30539f186e57 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -65,6 +65,7 @@ stages: skipAndroidPlatformApis: false skipJdk: false skipSimulatorSetup: false + skipCertificates: true # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic From 23486ca7f3d553b51117a99995d8fec8dc9b63a4 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:43:20 +0000 Subject: [PATCH 015/126] comment --- eng/pipelines/common/variables.yml | 44 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index ed84d69ca591..2c2bd64a8124 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -57,29 +57,29 @@ variables: - group: MAUI # This is the main MAUI variable group that contains secrets for the apple certificate # Variable groups required for all builds -- ${{ if and(ne(variables['Build.DefinitionName'], 'maui-pr'), ne(variables['Build.DefinitionName'], 'dotnet-maui'), ne(variables['Build.DefinitionName'], 'maui-pr-devicetests'), ne(variables['Build.DefinitionName'], 'maui-pr-uitests')) }}: - - group: maui-provisionator # This is just needed for the provisionator +# - ${{ if and(ne(variables['Build.DefinitionName'], 'maui-pr'), ne(variables['Build.DefinitionName'], 'dotnet-maui'), ne(variables['Build.DefinitionName'], 'maui-pr-devicetests'), ne(variables['Build.DefinitionName'], 'maui-pr-uitests')) }}: +# - group: maui-provisionator # This is just needed for the provisionator -- ${{ if or(eq(variables['System.TeamProject'], 'DevDiv'), eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: - - name: internalProvisioning - value: true - - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: - - name: PrivateBuild - value: false - - name: _RunAsPublic - value: false - - name: _RunAsInternal - value: true - - name: _SignType - value: real +# - ${{ if or(eq(variables['System.TeamProject'], 'DevDiv'), eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: +# - name: internalProvisioning +# value: true +# - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: +# - name: PrivateBuild +# value: false +# - name: _RunAsPublic +# value: false +# - name: _RunAsInternal +# value: true +# - name: _SignType +# value: real - - group: AzureDevOps-Artifact-Feeds-Pats +# - group: AzureDevOps-Artifact-Feeds-Pats -- ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: - - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: - # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT - # DotNet-HelixApi-Access provides: HelixApiAccessToken - - group: Publish-Build-Assets - - group: DotNet-HelixApi-Access - - group: SDL_Settings +# - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: +# - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: +# # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT +# # DotNet-HelixApi-Access provides: HelixApiAccessToken +# - group: Publish-Build-Assets +# - group: DotNet-HelixApi-Access +# - group: SDL_Settings From d4f77f0dca828ee1f97ae52978893fad62be1a51 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 17:43:31 +0000 Subject: [PATCH 016/126] try again --- eng/pipelines/ci-copilot.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 30539f186e57..528ffadbe793 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -24,15 +24,11 @@ parameters: - Agent.OS -equals Darwin variables: - # - template: /eng/pipelines/common/variables.yml@self + - template: /eng/pipelines/common/variables.yml@self - name: Codeql.Enabled value: false - name: Codeql.SkipTaskAutoInjection value: true - - name: DOTNET_VERSION - value: 10.0.100 - - name: REQUIRED_XCODE - value: 26.0.1 stages: - stage: ReviewPR From 7acc3400dd32e05a0f3b216ff910e997f54758d5 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 18:25:40 +0000 Subject: [PATCH 017/126] Update script --- eng/pipelines/ci-copilot.yml | 208 +++++++++++++++++++++++++++++++---- 1 file changed, 185 insertions(+), 23 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 528ffadbe793..824076f0df33 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -74,11 +74,87 @@ stages: - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" displayName: 'Add .NET to PATH' + # Verify environment is ready + - script: | + echo "=== Verifying Build Environment ===" + ERRORS="" + + # Check .NET SDK + echo "Checking .NET SDK..." + if ! dotnet --version; then + ERRORS="${ERRORS}\n- .NET SDK not available" + else + echo "✓ .NET SDK: $(dotnet --version)" + fi + + # Check workloads + echo "Checking MAUI workloads..." + if ! dotnet workload list | grep -q maui; then + ERRORS="${ERRORS}\n- MAUI workload not installed" + else + echo "✓ MAUI workload installed" + fi + + # Check Android SDK + echo "Checking Android SDK..." + if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then + ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set" + else + SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" + if [ ! -d "$SDK_PATH" ]; then + ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH" + else + echo "✓ Android SDK: $SDK_PATH" + fi + fi + + # Check Xcode (macOS only) + if [ "$(uname)" = "Darwin" ]; then + echo "Checking Xcode..." + if ! xcodebuild -version; then + ERRORS="${ERRORS}\n- Xcode not available" + else + echo "✓ Xcode available" + fi + + # Check iOS simulators + echo "Checking iOS simulators..." + if ! xcrun simctl list devices available | grep -q "iPhone"; then + ERRORS="${ERRORS}\n- No iOS simulators available" + else + echo "✓ iOS simulators available" + fi + fi + + # Check Java/JDK + echo "Checking JDK..." + if ! java -version 2>&1; then + ERRORS="${ERRORS}\n- JDK not available" + else + echo "✓ JDK available" + fi + + # Report errors + if [ -n "$ERRORS" ]; then + echo "" + echo "##vso[task.logissue type=error]=== Environment Verification FAILED ===" + echo -e "Missing dependencies:$ERRORS" + echo "##vso[task.logissue type=error]Build environment is not properly configured. See above for details." + exit 1 + fi + + echo "" + echo "=== Environment Verification PASSED ===" + displayName: 'Verify Build Environment' + - script: | echo "Installing Node.js 22..." brew install node@22 brew link --overwrite node@22 - node --version + if ! node --version; then + echo "##vso[task.logissue type=error]Failed to install Node.js" + exit 1 + fi npm --version echo "Node.js installed successfully" displayName: 'Install Node.js' @@ -86,14 +162,24 @@ stages: - script: | echo "Installing GitHub CLI..." brew install gh - gh --version + if ! gh --version; then + echo "##vso[task.logissue type=error]Failed to install GitHub CLI" + exit 1 + fi echo "GitHub CLI installed successfully" displayName: 'Install GitHub CLI' - script: | echo "Authenticating with GitHub CLI..." + if [ -z "$(GH_CLI_TOKEN)" ]; then + echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." + exit 1 + fi echo "$(GH_CLI_TOKEN)" | gh auth login --with-token - gh auth status + if ! gh auth status; then + echo "##vso[task.logissue type=error]GitHub CLI authentication failed" + exit 1 + fi displayName: 'Authenticate GitHub CLI' env: GH_CLI_TOKEN: $(GH_CLI_TOKEN) @@ -101,12 +187,19 @@ stages: - script: | echo "Installing GitHub Copilot CLI..." npm install -g @github/copilot + if ! which copilot; then + echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" + exit 1 + fi echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' - script: | echo "Fetching PR #${{ parameters.PRNumber }}..." - git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }} + if ! git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }}; then + echo "##vso[task.logissue type=error]Failed to fetch PR #${{ parameters.PRNumber }}. Check that the PR exists." + exit 1 + fi git checkout pr-${{ parameters.PRNumber }} echo "Checked out PR branch successfully" git log -1 --oneline @@ -116,12 +209,54 @@ stages: echo "Running Copilot PR Reviewer Agent..." echo "Reviewing PR #${{ parameters.PRNumber }}..." + # Create artifacts directory for Copilot outputs + mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs + # Invoke the PR reviewer agent using Copilot CLI in programmatic mode - # --allow-all-tools allows Copilot to execute commands without manual approval - # Capture output to a file for posting as PR comment - copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot_review_output.md + # Capture exit code to check for failures + set +e + copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + COPILOT_EXIT_CODE=$? + set -e + + echo "Copilot exit code: $COPILOT_EXIT_CODE" + + # Copy any Copilot session files + if [ -d "$HOME/.copilot" ]; then + echo "Copying Copilot session state..." + cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state || true + fi - echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot_review_output.md" + # Copy CustomAgentLogsTmp if it exists + if [ -d "CustomAgentLogsTmp" ]; then + echo "Copying CustomAgentLogsTmp..." + cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Copy any Review_Feedback files + find . -name "Review_Feedback_*.md" -type f -exec cp {} $(Build.ArtifactStagingDirectory)/copilot-logs/ \; 2>/dev/null || true + + # Copy any .github/agent-pr-session files + if [ -d ".github/agent-pr-session" ]; then + echo "Copying agent-pr-session..." + cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Check for failure indicators in output + if [ $COPILOT_EXIT_CODE -ne 0 ]; then + echo "##vso[task.logissue type=error]Copilot CLI exited with code $COPILOT_EXIT_CODE" + # Don't exit yet - let artifacts be published first + echo "##vso[task.setvariable variable=CopilotFailed]true" + fi + + # Check output for common failure patterns + if grep -qi "error\|failed\|exception" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + if grep -qi "simulator.*not\|emulator.*not\|workload.*not\|sdk.*not found" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + echo "##vso[task.logissue type=warning]Copilot encountered environment issues. Check artifacts for details." + fi + fi + + echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" displayName: 'Run PR Reviewer Agent' env: GITHUB_TOKEN: $(COPILOT_TOKEN) @@ -130,11 +265,11 @@ stages: echo "Posting review comment to PR..." # Check for the captured output file first - REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot_review_output.md" - CLEANED_FILE="$(Build.ArtifactStagingDirectory)/copilot_review_cleaned.md" + REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" + CLEANED_FILE="$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_cleaned.md" # Also check if Copilot created a Review_Feedback file - FEEDBACK_FILE=$(find . -name "Review_Feedback_*.md" -type f | head -1) + FEEDBACK_FILE=$(find $(Build.ArtifactStagingDirectory)/copilot-logs -name "Review_Feedback_*.md" -type f | head -1) if [ -n "$FEEDBACK_FILE" ]; then echo "Found review feedback file: $FEEDBACK_FILE" @@ -144,18 +279,26 @@ stages: if [ -f "$REVIEW_FILE" ] && [ -s "$REVIEW_FILE" ]; then echo "Cleaning up review output..." - # Remove intro lines like "I'll review PR #XXXXX for you." and "The code review for PR #XXXXX is complete." - # Remove stats at the end (Total usage est, Total duration, Usage by model, etc.) - sed -n '/^## /,$p' "$REVIEW_FILE" | \ - sed '/^Total usage est:/,$d' | \ + # Clean up the content: + # 1. Strip all ANSI escape codes (color codes like [37m, [39m, etc.) + # 2. Start from the first markdown header (## or #) + # 3. Remove stats at the end (Total usage est, API time, Total session, etc.) + sed 's/\x1b\[[0-9;]*m//g' "$REVIEW_FILE" | \ + sed -n '/^#/,$p' | \ + sed '/^Total usage est/,$d' | \ + sed '/^API time spent/d' | \ + sed '/^Total session time/d' | \ sed '/^Total duration/d' | \ sed '/^Total code changes/d' | \ - sed '/^Usage by model/,$d' > "$CLEANED_FILE" + sed '/^Breakdown by AI model/,$d' | \ + sed '/^Usage by model/,$d' | \ + sed '/^ *claude-/d' | \ + sed '/^ *gpt-/d' > "$CLEANED_FILE" - # If cleaning removed everything, fall back to original + # If cleaning removed everything, fall back to original without ANSI codes if [ ! -s "$CLEANED_FILE" ]; then - echo "Cleaned file is empty, using original" - cp "$REVIEW_FILE" "$CLEANED_FILE" + echo "Cleaned file is empty, using original without ANSI codes" + sed 's/\x1b\[[0-9;]*m//g' "$REVIEW_FILE" > "$CLEANED_FILE" fi echo "Posting review from: $CLEANED_FILE" @@ -172,17 +315,36 @@ stages: echo "Review content is available in the published artifacts." fi else - echo "No review output found or file is empty" + echo "##vso[task.logissue type=warning]No review output found or file is empty" fi displayName: 'Post Review Comment' env: GITHUB_TOKEN: $(GH_COMMENT_TOKEN) condition: succeededOrFailed() + # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 - displayName: 'Publish Review Artifacts' + displayName: 'Publish Copilot Logs' inputs: - targetPath: '$(System.DefaultWorkingDirectory)' - artifact: 'ReviewOutput' + targetPath: '$(Build.ArtifactStagingDirectory)/copilot-logs' + artifact: 'CopilotLogs' publishLocation: 'pipeline' condition: succeededOrFailed() + + # Publish build logs if they exist + - task: PublishPipelineArtifact@1 + displayName: 'Publish Build Logs' + inputs: + targetPath: '$(LogDirectory)' + artifact: 'BuildLogs' + publishLocation: 'pipeline' + condition: and(succeededOrFailed(), ne(variables['LogDirectory'], '')) + + # Fail the pipeline if Copilot failed + - script: | + if [ "$(CopilotFailed)" = "true" ]; then + echo "##vso[task.logissue type=error]Copilot PR review failed. Check CopilotLogs artifact for details." + exit 1 + fi + displayName: 'Check Copilot Result' + condition: succeededOrFailed() From 26447f2ed2e02d03ca1a1dbb6ec5cb772a41cee1 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 18:35:00 +0000 Subject: [PATCH 018/126] Try again --- eng/pipelines/ci-copilot.yml | 53 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 824076f0df33..7162807c3351 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -18,8 +18,8 @@ parameters: - name: pool type: object default: - name: MAUI-Testing - vmImage: ubuntu-latest + name: Azure Pipelines + vmImage: macOS-15 demands: - Agent.OS -equals Darwin @@ -87,14 +87,6 @@ stages: echo "✓ .NET SDK: $(dotnet --version)" fi - # Check workloads - echo "Checking MAUI workloads..." - if ! dotnet workload list | grep -q maui; then - ERRORS="${ERRORS}\n- MAUI workload not installed" - else - echo "✓ MAUI workload installed" - fi - # Check Android SDK echo "Checking Android SDK..." if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then @@ -147,6 +139,47 @@ stages: echo "=== Environment Verification PASSED ===" displayName: 'Verify Build Environment' + # Restore .NET tools (includes xharness) + - script: | + echo "Restoring .NET tools..." + dotnet tool restore + echo "Tools restored successfully" + displayName: 'Restore .NET Tools' + + # List all available simulators/emulators + - script: | + echo "=== Listing Available Simulators/Emulators ===" + + # List iOS simulators using xcrun + echo "" + echo "=== iOS Simulators (xcrun simctl) ===" + xcrun simctl list devices available + + # List iOS simulators using xharness + echo "" + echo "=== iOS Simulators (xharness) ===" + dotnet xharness apple device --list || echo "xharness apple device list failed" + + # List Android emulators using xharness + echo "" + echo "=== Android Emulators (xharness) ===" + dotnet xharness android device --list || echo "xharness android device list failed" + + # List Android AVDs + echo "" + echo "=== Android AVDs (avdmanager) ===" + if [ -n "$ANDROID_HOME" ]; then + "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" + elif [ -n "$ANDROID_SDK_ROOT" ]; then + "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" + else + echo "ANDROID_HOME/ANDROID_SDK_ROOT not set" + fi + + echo "" + echo "=== Simulator/Emulator listing complete ===" + displayName: 'List Simulators and Emulators' + - script: | echo "Installing Node.js 22..." brew install node@22 From bc5e3f1d6d575fd6392ec429a45ab883773fb66a Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 18:41:03 +0000 Subject: [PATCH 019/126] Try list first --- eng/pipelines/ci-copilot.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 7162807c3351..b03cdddbd1cc 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -139,13 +139,6 @@ stages: echo "=== Environment Verification PASSED ===" displayName: 'Verify Build Environment' - # Restore .NET tools (includes xharness) - - script: | - echo "Restoring .NET tools..." - dotnet tool restore - echo "Tools restored successfully" - displayName: 'Restore .NET Tools' - # List all available simulators/emulators - script: | echo "=== Listing Available Simulators/Emulators ===" @@ -179,6 +172,15 @@ stages: echo "" echo "=== Simulator/Emulator listing complete ===" displayName: 'List Simulators and Emulators' + + # Restore .NET tools (includes xharness) + - script: | + echo "Restoring .NET tools..." + dotnet tool restore + echo "Tools restored successfully" + displayName: 'Restore .NET Tools' + + - script: | echo "Installing Node.js 22..." From cc8167429a2e3ba27c304cd9218397ab8b033b4c Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 18:45:58 +0000 Subject: [PATCH 020/126] even before --- eng/pipelines/ci-copilot.yml | 71 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index b03cdddbd1cc..d57382d5405a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -73,7 +73,41 @@ stages: - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" displayName: 'Add .NET to PATH' - + + # List all available simulators/emulators + - script: | + echo "=== Listing Available Simulators/Emulators ===" + + # List iOS simulators using xcrun + echo "" + echo "=== iOS Simulators (xcrun simctl) ===" + xcrun simctl list devices available + + # List iOS simulators using xharness + echo "" + echo "=== iOS Simulators (xharness) ===" + dotnet xharness apple device --list || echo "xharness apple device list failed" + + # List Android emulators using xharness + echo "" + echo "=== Android Emulators (xharness) ===" + dotnet xharness android device --list || echo "xharness android device list failed" + + # List Android AVDs + echo "" + echo "=== Android AVDs (avdmanager) ===" + if [ -n "$ANDROID_HOME" ]; then + "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" + elif [ -n "$ANDROID_SDK_ROOT" ]; then + "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" + else + echo "ANDROID_HOME/ANDROID_SDK_ROOT not set" + fi + + echo "" + echo "=== Simulator/Emulator listing complete ===" + displayName: 'List Simulators and Emulators' + # Verify environment is ready - script: | echo "=== Verifying Build Environment ===" @@ -139,40 +173,7 @@ stages: echo "=== Environment Verification PASSED ===" displayName: 'Verify Build Environment' - # List all available simulators/emulators - - script: | - echo "=== Listing Available Simulators/Emulators ===" - - # List iOS simulators using xcrun - echo "" - echo "=== iOS Simulators (xcrun simctl) ===" - xcrun simctl list devices available - - # List iOS simulators using xharness - echo "" - echo "=== iOS Simulators (xharness) ===" - dotnet xharness apple device --list || echo "xharness apple device list failed" - - # List Android emulators using xharness - echo "" - echo "=== Android Emulators (xharness) ===" - dotnet xharness android device --list || echo "xharness android device list failed" - - # List Android AVDs - echo "" - echo "=== Android AVDs (avdmanager) ===" - if [ -n "$ANDROID_HOME" ]; then - "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" - elif [ -n "$ANDROID_SDK_ROOT" ]; then - "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" - else - echo "ANDROID_HOME/ANDROID_SDK_ROOT not set" - fi - - echo "" - echo "=== Simulator/Emulator listing complete ===" - displayName: 'List Simulators and Emulators' - + # Restore .NET tools (includes xharness) - script: | echo "Restoring .NET tools..." From 98223da95b77d1d4046498e06e3e1b065daab4c9 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 18:55:52 +0000 Subject: [PATCH 021/126] again --- eng/pipelines/ci-copilot.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d57382d5405a..cdebfee79656 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -73,7 +73,7 @@ stages: - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" displayName: 'Add .NET to PATH' - + # List all available simulators/emulators - script: | echo "=== Listing Available Simulators/Emulators ===" @@ -86,12 +86,17 @@ stages: # List iOS simulators using xharness echo "" echo "=== iOS Simulators (xharness) ===" - dotnet xharness apple device --list || echo "xharness apple device list failed" + dotnet xharness apple simulators list --installed || echo "xharness apple simulators list failed" + + # Show Apple device state + echo "" + echo "=== Apple Device State (xharness) ===" + dotnet xharness apple state || echo "xharness apple state failed" - # List Android emulators using xharness + # Show Android device state echo "" - echo "=== Android Emulators (xharness) ===" - dotnet xharness android device --list || echo "xharness android device list failed" + echo "=== Android Device State (xharness) ===" + dotnet xharness android state || echo "xharness android state failed" # List Android AVDs echo "" From 4b50a0ba68fcf4948be182a563714aef4a59ac62 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 23 Jan 2026 19:05:42 +0000 Subject: [PATCH 022/126] Again --- eng/pipelines/ci-copilot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index cdebfee79656..6c2cf3d9d6f4 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -20,8 +20,6 @@ parameters: default: name: Azure Pipelines vmImage: macOS-15 - demands: - - Agent.OS -equals Darwin variables: - template: /eng/pipelines/common/variables.yml@self From 6ff306b3f72c4a98591b8f3a764af18103a9b6bf Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 30 Jan 2026 11:18:16 +0100 Subject: [PATCH 023/126] Enhance Copilot PR review and comment steps Clarify the Copilot PR-review prompt to execute five explicit phases (Understanding, Test Review, Fix Exploration, Alternative Comparison, Final Review). Add a new pipeline step that invokes a Copilot "post-comment" skill, captures its exit code, logs output to $(Build.ArtifactStagingDirectory)/copilot-logs, and sets a PostCommentFailed variable on failure. Ensure artifacts dir exists, surface warnings on failure, and make the original post-comment fallback step run only when the skill step failed. Update step display names and preserve artifact publishing. --- eng/pipelines/ci-copilot.yml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 6c2cf3d9d6f4..f1d695cd1b14 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -254,7 +254,7 @@ stages: # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # Capture exit code to check for failures set +e - copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Please think through every phase you need for doing this review and then execute all the way until the end unless any of the phases fail" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Execute all 5 phases sequentially: Phase 1 (Understanding), Phase 2 (Test Review), Phase 3 (Fix Exploration), Phase 4 (Alternative Comparison), and Phase 5 (Final Review). Complete all phases unless any of them fail." --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md COPILOT_EXIT_CODE=$? set -e @@ -301,7 +301,30 @@ stages: GITHUB_TOKEN: $(COPILOT_TOKEN) - script: | - echo "Posting review comment to PR..." + echo "Posting review comment using skill..." + + # Create artifacts directory if not exists + mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs + + # Use Copilot CLI to invoke the post-comment skill + set +e + copilot -p "Use a skill to post the review feedback as a comment on PR #${{ parameters.PRNumber }}. The review output should be in the CustomAgentLogsTmp or .github/agent-pr-session directories. Clean up ANSI codes and stats before posting." --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_post_comment_output.md + POST_COMMENT_EXIT_CODE=$? + set -e + + echo "Post comment exit code: $POST_COMMENT_EXIT_CODE" + + if [ $POST_COMMENT_EXIT_CODE -ne 0 ]; then + echo "##vso[task.logissue type=warning]Copilot post-comment skill exited with code $POST_COMMENT_EXIT_CODE" + echo "##vso[task.setvariable variable=PostCommentFailed]true" + fi + displayName: 'Post Review Comment Using Skill' + env: + GITHUB_TOKEN: $(COPILOT_TOKEN) + condition: succeededOrFailed() + + - script: | + echo "Posting review comment to PR (fallback)..." # Check for the captured output file first REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" @@ -356,10 +379,10 @@ stages: else echo "##vso[task.logissue type=warning]No review output found or file is empty" fi - displayName: 'Post Review Comment' + displayName: 'Post Review Comment (Fallback)' env: GITHUB_TOKEN: $(GH_COMMENT_TOKEN) - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables['PostCommentFailed'], 'true')) # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 From de8f70dd1fdd5aa4514022c2d5bc5e8197590272 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 30 Jan 2026 23:53:47 +0100 Subject: [PATCH 024/126] Updated the prompt --- eng/pipelines/ci-copilot.yml | 12 +- eng/pipelines/prompts/pr-review-prompt.md | 149 ++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 eng/pipelines/prompts/pr-review-prompt.md diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index f1d695cd1b14..5a79648d877f 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -251,10 +251,20 @@ stages: # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs + # Load prompt from file and substitute PR number + PROMPT_FILE="eng/pipelines/prompts/pr-review-prompt.md" + if [ ! -f "$PROMPT_FILE" ]; then + echo "##vso[task.logissue type=error]Prompt file not found: $PROMPT_FILE" + exit 1 + fi + + # Read prompt and replace placeholder with actual PR number + PROMPT=$(sed "s/\${PR_NUMBER}/${{ parameters.PRNumber }}/g" "$PROMPT_FILE") + # Invoke the PR reviewer agent using Copilot CLI in programmatic mode # Capture exit code to check for failures set +e - copilot --agent pr -p "Review PR #${{ parameters.PRNumber }} you are already on the correct branch so don't switch branches. Execute all 5 phases sequentially: Phase 1 (Understanding), Phase 2 (Test Review), Phase 3 (Fix Exploration), Phase 4 (Alternative Comparison), and Phase 5 (Final Review). Complete all phases unless any of them fail." --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + copilot --agent pr -p "$PROMPT" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md COPILOT_EXIT_CODE=$? set -e diff --git a/eng/pipelines/prompts/pr-review-prompt.md b/eng/pipelines/prompts/pr-review-prompt.md new file mode 100644 index 000000000000..cfb8235052ed --- /dev/null +++ b/eng/pipelines/prompts/pr-review-prompt.md @@ -0,0 +1,149 @@ +Review PR #${PR_NUMBER} + +Follow the 5-phase PR Agent workflow, but leverage the existing agent review state: + +1. **Import prior agent state** instead of re-doing completed phases +2. **Verify the Gate phase** empirically (run tests to confirm FAIL without fix, PASS with fix) +3. **Phase 4 (Fix)** - EXHAUSTIVE exploration: + - Consult 5+ different AI models for diverse fix ideas + - Run try-fix skill with Opus 4.5 for EACH unique idea + - Keep iterating until completely out of alternatives + - Compare ALL candidates to determine best approach +4. **Phase 5 (Report)** - Generate final recommendation with full comparison + +## Work Plan + +### Phase 1: Pre-Flight (Context Gathering) +- [ ] Checkout PR branch (`pr-33687`) +- [ ] Gather PR metadata (title, body, labels, files) +- [ ] Read linked issue #19256 +- [ ] Fetch PR comments and review feedback +- [ ] Check for prior agent review (FOUND: `.github/agent-pr-session/pr-19256.md`) +- [ ] Create local state file importing prior agent's findings +- [ ] Mark Pre-Flight COMPLETE + +### Phase 2: Tests (Verify Reproduction Tests Exist) +- [ ] Confirm PR includes UI tests (already present per file list) +- [ ] Verify test file locations: + - HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs` + - NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs` +- [ ] Verify tests follow naming convention (`Issue19256`) +- [ ] Mark Tests COMPLETE + +### Phase 3: Gate (Test Verification) - MUST PASS +- [ ] Run verification script with `-RequireFullVerification`: + ```bash + pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -RequireFullVerification + ``` +- [ ] Confirm tests FAIL without fix (bug reproduced) +- [ ] Confirm tests PASS with fix (bug fixed) +- [ ] Mark Gate PASSED (or FAILED if tests don't behave correctly) + +### Phase 4: Fix (EXHAUSTIVE Independent Analysis) + +**Goal:** Explore ALL possible alternative solutions until no more ideas remain. + +#### Step 4.1: Multi-Model Brainstorming (Consult 5+ Models) +- [ ] Read `.github/agents/pr/post-gate.md` for Phase 4-5 instructions +- [ ] Consult **Claude Sonnet 4** for fix ideas +- [ ] Consult **Claude Opus 4.5** for fix ideas +- [ ] Consult **GPT-5.2** for fix ideas +- [ ] Consult **GPT-5.1-Codex** for fix ideas +- [ ] Consult **Gemini 3 Pro** for fix ideas +- [ ] Consolidate unique approaches from all models +- [ ] Deduplicate and categorize fix strategies + +#### Step 4.2: Iterative try-fix Exploration (Opus 4.5) - SEQUENTIAL + +**CRITICAL: try-fix attempts MUST be run SEQUENTIALLY, one at a time.** +- Each try-fix must COMPLETE before starting the next +- NO parallel execution - wait for full result before proceeding +- Learn from each attempt to inform the next + +For EACH unique fix idea, run try-fix skill with `claude-opus-4.5`: +- [ ] **Attempt 1:** [First alternative approach] → WAIT FOR COMPLETION +- [ ] **Attempt 2:** [Second alternative approach] → WAIT FOR COMPLETION +- [ ] **Attempt 3:** [Third alternative approach] → WAIT FOR COMPLETION +- [ ] **Attempt 4:** [Continue until exhausted...] → WAIT FOR COMPLETION +- [ ] **Attempt N:** Keep going until try-fix reports "no more ideas" + +**Sequential Iteration Rule:** +1. Start try-fix with ONE idea +2. **WAIT** for try-fix to complete fully (tests run, result recorded) +3. If tests PASS → Record as viable alternative +4. If tests FAIL → Analyze failure reason +5. Use learnings from this attempt to inform the NEXT attempt +6. Start next try-fix (go to step 1) +7. Continue until ALL brainstormed ideas are tested +8. Then ask Opus 4.5 to generate MORE ideas based on all learnings so far +9. Repeat until Opus 4.5 confirms "exhausted all approaches" + +#### Step 4.3: Comparative Analysis +- [ ] Create comparison matrix of ALL fix candidates (PR's + alternatives) +- [ ] Evaluate each on: correctness, simplicity, performance, maintainability +- [ ] Document why PR's fix is/isn't the best approach +- [ ] Select best fix with full justification + +**Exit Criteria for Phase 4:** +- At least 5 models consulted for ideas +- All unique ideas tested via try-fix +- try-fix confirms "no more alternative approaches" +- Comparison matrix complete +- Best fix selected with rationale + +### Phase 5: Report (Final Recommendation) +- [ ] Generate comprehensive review summary +- [ ] Provide final recommendation: APPROVE / REQUEST CHANGES +- [ ] Post review to PR if requested + +## Key Files in This PR + +| File | Type | Purpose | +|------|------|---------| +| `src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs` | Fix | ShowEvent handler to re-apply min/max dates | +| `src/Core/src/Platform/Android/DatePickerExtensions.cs` | Fix | Reset MinDate/MaxDate before setting new values | +| `src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs` | Test | UI test page with dependent DatePickers | +| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs` | Test | NUnit test with screenshot verification | +| `.github/agent-pr-session/pr-19256.md` | Meta | Prior agent review session (all phases COMPLETE) | + +## Prior Agent Review Summary + +The existing agent review file shows: +- **Root Cause:** Known Android platform bug - DatePicker caches MinDate/MaxDate and ignores updates unless reset first +- **Fix Approach:** Reset values before setting + ShowEvent handler to re-apply after dialog initialization +- **Tests:** Screenshot-based verification with two states (FutureDate, EarlierDate) +- **Verdict:** ✅ VALID FIX - follows established 10+ year old Android workaround + +## Notes + +- This is an **Android-only** issue (platform/android label) +- The fix is based on a well-documented workaround from [StackOverflow #19616575](https://stackoverflow.com/questions/19616575) +- Issue has been open since Dec 2023 (regression in 8.0.3) +- PR includes +429 lines (mostly tests and documentation) + +## Risks & Considerations + +1. **Gate verification required** - Must empirically confirm tests behave correctly +2. **Phase 4 is exhaustive** - Will consult 5+ models and iterate try-fix until ALL ideas explored +3. **Android emulator required** - Tests need to run on Android platform +4. **Time investment** - Exhaustive Phase 4 may take significant time but ensures thorough review +5. **Model availability** - Need access to multiple models (Sonnet, Opus, GPT-5.x, Gemini) + +## Models to Consult in Phase 4 + +| Model | Purpose | +|-------|---------| +| `claude-sonnet-4` | Baseline fix ideas | +| `claude-opus-4.5` | Deep analysis + try-fix iterations | +| `gpt-5.2` | Alternative perspective | +| `gpt-5.1-codex` | Code-focused suggestions | +| `gemini-3-pro-preview` | Third-party perspective | + +## Success Criteria + +Phase 4 is complete when: +- ✅ All 5+ models have been consulted +- ✅ Every unique fix idea has been tested via try-fix +- ✅ Opus 4.5 confirms "no more alternative approaches to explore" +- ✅ Comprehensive comparison matrix exists +- ✅ Clear rationale for final fix selection From 0e720a23e8c79f5d00e4b308e276f0500491f7bd Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 31 Jan 2026 15:47:08 +0100 Subject: [PATCH 025/126] Cache PR review prompt before checkout Add a pipeline step (Cache Prompt File) that loads eng/pipelines/prompts/pr-review-prompt.md and copies it to /tmp/copilot-prompts/pr-review-prompt.md before the PR branch checkout. Update the later Copilot step to read the prompt from the cached location and adjust the error message. This prevents failures when the prompt file is absent on the PR branch by ensuring a stable copy is available for the review step. --- eng/pipelines/ci-copilot.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5a79648d877f..8e40f8e0c3a1 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -50,6 +50,21 @@ stages: echo "PR Number: ${{ parameters.PRNumber }}" displayName: 'Validate Parameters' + # Load prompt file before checking out PR branch (file may not exist on PR branch) + - script: | + echo "Loading prompt file..." + PROMPT_FILE="eng/pipelines/prompts/pr-review-prompt.md" + if [ ! -f "$PROMPT_FILE" ]; then + echo "##vso[task.logissue type=error]Prompt file not found: $PROMPT_FILE" + exit 1 + fi + + # Copy prompt to a safe location before branch checkout + mkdir -p /tmp/copilot-prompts + cp "$PROMPT_FILE" /tmp/copilot-prompts/pr-review-prompt.md + echo "Prompt file cached to /tmp/copilot-prompts/pr-review-prompt.md" + displayName: 'Cache Prompt File' + # Provision environment (Xcode, .NET SDK, Android SDK, etc.) - template: common/provision.yml parameters: @@ -251,10 +266,10 @@ stages: # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - # Load prompt from file and substitute PR number - PROMPT_FILE="eng/pipelines/prompts/pr-review-prompt.md" + # Load prompt from cached file (saved before PR checkout) + PROMPT_FILE="/tmp/copilot-prompts/pr-review-prompt.md" if [ ! -f "$PROMPT_FILE" ]; then - echo "##vso[task.logissue type=error]Prompt file not found: $PROMPT_FILE" + echo "##vso[task.logissue type=error]Cached prompt file not found: $PROMPT_FILE" exit 1 fi From 12a788e81d06afea929737525e2bc6f0dc92d7f1 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 31 Jan 2026 16:58:45 +0100 Subject: [PATCH 026/126] Improve iOS simulator auto-selection logic Replace brittle iPhone Xs + iOS 18.5 lookup with a prioritized selection routine. The script now iterates preferred iOS versions and device models, picks the first available preferred device or falls back to the first available iPhone, and finally falls back to any available simulator. It also surfaces runtime info when listing available simulators and reports the selected device name and UDID before booting. --- .github/scripts/shared/Start-Emulator.ps1 | 32 +++++++---------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 9fd9d227331a..35e0219df1b9 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -346,7 +346,7 @@ if ($Platform -eq "android") { # Preferred devices in order of priority $preferredDevices = @("iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro", "iPhone Xs") - # Preferred iOS versions in order (stable preferred, beta fallback) + # Preferred iOS versions in order (newest first) $preferredVersions = @("iOS-18", "iOS-17", "iOS-26") $selectedDevice = $null @@ -363,20 +363,13 @@ if ($Platform -eq "android") { if ($matchingRuntimes) { # Try each preferred device foreach ($deviceName in $preferredDevices) { - $device = $null - $deviceRuntime = $null - foreach ($rt in $matchingRuntimes) { - $found = $rt.Value | Where-Object { $_.name -eq $deviceName -and $_.isAvailable -eq $true } | Select-Object -First 1 - if ($found) { - $device = $found - $deviceRuntime = $rt.Name - break - } - } + $device = $matchingRuntimes | ForEach-Object { + $_.Value | Where-Object { $_.name -eq $deviceName -and $_.isAvailable -eq $true } + } | Select-Object -First 1 if ($device) { $selectedDevice = $device - $selectedVersion = $deviceRuntime + $selectedVersion = ($matchingRuntimes | Select-Object -First 1).Name Write-Info "Found preferred device: $deviceName on $selectedVersion" break } @@ -384,20 +377,13 @@ if ($Platform -eq "android") { # If no preferred device found, take first available iPhone if (-not $selectedDevice) { - $anyiPhone = $null - $iphoneRuntime = $null - foreach ($rt in $matchingRuntimes) { - $found = $rt.Value | Where-Object { $_.name -match "iPhone" -and $_.isAvailable -eq $true } | Select-Object -First 1 - if ($found) { - $anyiPhone = $found - $iphoneRuntime = $rt.Name - break - } - } + $anyiPhone = $matchingRuntimes | ForEach-Object { + $_.Value | Where-Object { $_.name -match "iPhone" -and $_.isAvailable -eq $true } + } | Select-Object -First 1 if ($anyiPhone) { $selectedDevice = $anyiPhone - $selectedVersion = $iphoneRuntime + $selectedVersion = ($matchingRuntimes | Select-Object -First 1).Name Write-Info "Using available iPhone: $($anyiPhone.name) on $selectedVersion" } } From 44b2755448297889f8e757ed9483f6ac742ed8fc Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 31 Jan 2026 17:02:58 +0100 Subject: [PATCH 027/126] Cherry-pick PR commits onto current branch Replace direct checkout of the PR branch with logic that fetches the PR, computes the merge-base against the current branch, and cherry-picks commits from the merge-base..PR_HEAD onto the current branch (using --no-commit). Adds commit counting, a warning when no commits are found, conflict handling with status and diff output, and extra logging (current branch, merge base, commit count, last commit and status). Also updates the pipeline step display name to 'Cherry-pick PR Changes' and tweaks the fetch message. --- eng/pipelines/ci-copilot.yml | 41 ++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 8e40f8e0c3a1..2b7678bb33f9 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -249,15 +249,48 @@ stages: displayName: 'Install GitHub Copilot CLI' - script: | - echo "Fetching PR #${{ parameters.PRNumber }}..." + echo "Fetching PR #${{ parameters.PRNumber }} changes..." if ! git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }}; then echo "##vso[task.logissue type=error]Failed to fetch PR #${{ parameters.PRNumber }}. Check that the PR exists." exit 1 fi - git checkout pr-${{ parameters.PRNumber }} - echo "Checked out PR branch successfully" + + # Get the merge base between current branch and PR branch + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "Current branch: $CURRENT_BRANCH" + + MERGE_BASE=$(git merge-base HEAD pr-${{ parameters.PRNumber }}) + echo "Merge base: $MERGE_BASE" + + # Get the list of commits in the PR (from merge-base to PR head) + PR_COMMITS=$(git rev-list --reverse $MERGE_BASE..pr-${{ parameters.PRNumber }}) + COMMIT_COUNT=$(echo "$PR_COMMITS" | grep -c . || echo "0") + echo "Found $COMMIT_COUNT commit(s) to cherry-pick" + + if [ "$COMMIT_COUNT" -eq "0" ]; then + echo "##vso[task.logissue type=warning]No commits found in PR #${{ parameters.PRNumber }} relative to current branch" + echo "PR may already be merged or branch is up to date" + else + # Cherry-pick each commit from the PR onto the current branch + echo "Cherry-picking PR commits onto $CURRENT_BRANCH..." + for COMMIT in $PR_COMMITS; do + echo "Cherry-picking commit: $(git log -1 --oneline $COMMIT)" + if ! git cherry-pick --no-commit $COMMIT; then + echo "##vso[task.logissue type=error]Failed to cherry-pick commit $COMMIT" + echo "Attempting to show conflict details..." + git status + git diff --name-only --diff-filter=U + exit 1 + fi + done + + echo "Successfully applied $COMMIT_COUNT commit(s) from PR #${{ parameters.PRNumber }}" + fi + + echo "Current state:" git log -1 --oneline - displayName: 'Checkout PR Branch' + git status --short + displayName: 'Cherry-pick PR Changes' - script: | echo "Running Copilot PR Reviewer Agent..." From 7ab081cd9664868bef3e67ceddde9072fe5e97c0 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 31 Jan 2026 18:06:10 +0100 Subject: [PATCH 028/126] Build MSBuild tasks in CI for MAUI Add a CI step to run ./build.ps1 --target=dotnet-buildtasks (Release, diagnostic) to compile MSBuild tasks required for MAUI builds. The step includes a retry on failure and sets DOTNET_TOKEN and PRIVATE_BUILD environment variables for accessing internal artifacts. Placed before the simulator/emulator listing to ensure tasks are available for subsequent MAUI jobs. --- eng/pipelines/ci-copilot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 2b7678bb33f9..4259146cb8a5 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -87,6 +87,14 @@ stages: - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" displayName: 'Add .NET to PATH' + # Build MSBuild tasks (required for MAUI builds) + - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic + displayName: 'Build MSBuild Tasks' + retryCountOnTaskFailure: 1 + env: + DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) + PRIVATE_BUILD: $(PrivateBuild) + # List all available simulators/emulators - script: | echo "=== Listing Available Simulators/Emulators ===" From 4a4f213c8f4c5ebf051a5501282c0f5a2d697da7 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 31 Jan 2026 21:25:00 +0100 Subject: [PATCH 029/126] Remove agent sessions --- .github/agent-pr-session/pr-32289.md | 202 ---------------- .github/agent-pr-session/pr-33134.md | 203 ---------------- .github/agent-pr-session/pr-33380.md | 226 ----------------- .github/agent-pr-session/pr-33392.md | 346 --------------------------- .github/agent-pr-session/pr-33406.md | 247 ------------------- 5 files changed, 1224 deletions(-) delete mode 100644 .github/agent-pr-session/pr-32289.md delete mode 100644 .github/agent-pr-session/pr-33134.md delete mode 100644 .github/agent-pr-session/pr-33380.md delete mode 100644 .github/agent-pr-session/pr-33392.md delete mode 100644 .github/agent-pr-session/pr-33406.md diff --git a/.github/agent-pr-session/pr-32289.md b/.github/agent-pr-session/pr-32289.md deleted file mode 100644 index 33c9e80c1071..000000000000 --- a/.github/agent-pr-session/pr-32289.md +++ /dev/null @@ -1,202 +0,0 @@ -# PR Review: #32289 - Fix handler not disconnected when removing non visible pages using RemovePage() - -**Date:** 2026-01-07 | **Issue:** [#32239](https://github.com/dotnet/maui/issues/32239) | **PR:** [#32289](https://github.com/dotnet/maui/pull/32289) - -## ✅ Status: COMPLETE - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ PASSED | -| 🔧 Fix | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -
-📋 Issue Summary - -**Problem:** When removing pages from a NavigationPage's navigation stack using `NavigationPage.Navigation.RemovePage()`, handlers are not properly disconnected from the removed pages. However, using `ContentPage.Navigation.RemovePage()` correctly disconnects handlers. - -**Root Cause (from PR):** The `RemovePage()` method removes the page from the navigation stack but does not explicitly disconnect its handler. - -**Regression:** Introduced in PR #24887, reproducible from MAUI 9.0.40+ - -**Steps to Reproduce:** -1. Push multiple pages onto a NavigationPage stack -2. Call `NavigationPage.Navigation.RemovePage()` on a non-visible page -3. Observe that the page's handler remains connected (no cleanup) - -**Workaround:** Manually call `.DisconnectHandlers()` after removing the page - -**Platforms Affected:** -- [x] iOS -- [x] Android -- [x] Windows -- [x] MacCatalyst - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Controls/src/Core/NavigationPage/NavigationPage.cs` | Fix | +4 lines | -| `src/Controls/src/Core/NavigationPage/NavigationPage.Legacy.cs` | Fix | +4 lines | -| `src/Controls/src/Core/NavigationProxy.cs` | Fix | -1 line (removed duplicate) | -| `src/Controls/src/Core/Shell/ShellSection.cs` | Fix | +1 line | -| `src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs` | Unit Test | +63 lines | -| `src/Controls/tests/Core.UnitTests/ShellNavigatingTests.cs` | Unit Test | +25 lines | - -**Test Type:** Unit Tests - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- Copilot flagged potential duplicate disconnection logic between NavigationProxy and NavigationPage -- Author responded by removing redundant logic from NavigationProxy and updating ShellSection -- StephaneDelcroix requested unit tests → Author added them -- rmarinho confirmed unit tests cover both `useMaui: true` and `useMaui: false` scenarios - -**Reviewer Feedback:** -- Comments about misleading code comments (fixed) -- Concern about duplicate `DisconnectHandlers()` calls (resolved by moving from NavigationProxy to implementations) -- StephaneDelcroix: Approved after unit tests added -- rmarinho: Approved - confirmed tests cover NavigationPage and Shell scenarios - -**Maintainer Approvals:** -- ✅ StephaneDelcroix (Jan 7, 2026) -- ✅ rmarinho (Jan 7, 2026) - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| -| NavigationPage.cs:914 | Duplicate disconnection with NavigationProxy | Removed from NavigationProxy, now only in NavigationPage | ✅ RESOLVED | -| NavigationPage.Legacy.cs:257 | Same duplicate concern | Same resolution | ✅ RESOLVED | - -**Author Uncertainty:** -- None noted - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes unit tests -- [x] Tests follow naming convention -- [x] Unit tests cover both useMaui: true/false paths -- [x] Unit tests cover Shell navigation - -**Test Files (from PR - Unit Tests):** -- `src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs` (+63 lines) - - `RemovePageDisconnectsHandlerForNonVisiblePage` - Tests removing middle page from 3-page stack (both useMaui: true/false) - - `RemovePageDisconnectsHandlerForRemovedRootPage` - Tests removing root page when another page is on top -- `src/Controls/tests/Core.UnitTests/ShellNavigatingTests.cs` (+25 lines) - - `RemovePageDisconnectsHandlerInShell` - Tests Shell navigation scenario - -**Unit Test Coverage Analysis:** -| Code Path | useMaui: true | useMaui: false | Shell | -|-----------|---------------|----------------|-------| -| Remove middle page | ✅ | ✅ | ✅ | -| Remove root page | ✅ | ✅ | - | - -Coverage is adequate - tests cover all modified code paths. - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ PASSED - -- [x] Tests FAIL without fix (bug reproduced) -- [x] Tests PASS with fix - -**Result:** PASSED ✅ - -**Verification:** Unit tests from PR cover all code paths: -- `RemovePageDisconnectsHandlerForNonVisiblePage(true)` - Maui path (Android/Windows) -- `RemovePageDisconnectsHandlerForNonVisiblePage(false)` - Legacy path (iOS/MacCatalyst) -- `RemovePageDisconnectsHandlerForRemovedRootPage(true/false)` - Root page removal -- `RemovePageDisconnectsHandlerInShell` - Shell navigation - -
- -
-🔧 Fix Candidates - -**Status**: ✅ COMPLETE - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| 1 | try-fix | Add DisconnectHandlers() inside SendHandlerUpdateAsync callback | ❌ FAIL | `NavigationPage.cs` (+3) | **Why failed:** Timing issue - SendHandlerUpdateAsync uses FireAndForget(), so the callback with DisconnectHandlers() runs asynchronously. The test checks Handler immediately after RemovePage() returns, before the async callback executes. | -| 2 | try-fix | Add DisconnectHandlers() synchronously after SendHandlerUpdateAsync in MauiNavigationImpl | ❌ FAIL | `NavigationPage.cs` (+3) | **Why failed:** iOS uses `UseMauiHandler = false`, meaning it uses NavigationImpl (Legacy) NOT MauiNavigationImpl. My fix was in the wrong code path - iOS doesn't execute MauiNavigationImpl at all. | -| 3 | try-fix | Add DisconnectHandlers() at end of Legacy RemovePage method | ✅ PASS | `NavigationPage.Legacy.cs` (+3) | Works! iOS/MacCatalyst use Legacy path. Simpler fix - only 1 file needed for iOS. | -| 4 | try-fix | Approach 2+3 combined (both Maui and Legacy paths) | ✅ PASS (iOS) | `NavigationPage.cs`, `NavigationPage.Legacy.cs` (+6 total) | Works for NavigationPage on all platforms, BUT **misses Shell navigation** which has its own code path. | -| PR | PR #32289 | Add `DisconnectHandlers()` call in RemovePage for non-visible pages | ✅ PASS (Gate) | NavigationPage.cs, NavigationPage.Legacy.cs, NavigationProxy.cs, ShellSection.cs | Original PR - validated by Gate | - -**Note:** try-fix candidates (1, 2, 3...) are added during Phase 4. PR's fix is reference only. - -**Exhausted:** No (stopped after finding working alternative) -**Selected Fix:** PR's fix - -**Deep Analysis (Git History Research):** - -**Historical Timeline:** -1. **PR #24887** (Feb 2025): Fixed Android flickering by avoiding handler removal during PopAsync - inadvertently broke RemovePage scenarios -2. **PR #30049** (June 2025): Attempted fix by adding `page?.DisconnectHandlers()` to `NavigationProxy.OnRemovePage()` - **BUT THIS FIX WAS FUNDAMENTALLY FLAWED** -3. **PR #32289** (Current): Correctly fixes by adding DisconnectHandlers to the NavigationPage implementations - -**Why PR #30049's fix didn't work:** -- `MauiNavigationImpl` and `NavigationImpl` **override** `OnRemovePage()` -- The overrides do NOT call `base.OnRemovePage()` -- Therefore `NavigationProxy.OnRemovePage()` is **NEVER executed** for NavigationPage! -- ContentPage works because it doesn't override - uses the base NavigationProxy directly - -**Why calling base.OnRemovePage() won't work:** -- `MauiNavigationImpl.OnRemovePage()` is a **complete replacement** with its own validation, async flow, etc. -- Calling base would cause double removal and ordering issues - -**Conclusion:** The fix MUST be in the NavigationPage implementations themselves, not in NavigationProxy. PR #32289's approach is architecturally correct. - -**Comparison:** -- **My fix #3** works for iOS/MacCatalyst (Legacy path) - 1 file, 3 lines -- **PR's fix** works for ALL platforms (Legacy + Maui paths) - 3 files, ~10 lines -- **PR #30049's approach** ❌ Doesn't work - fix in NavigationProxy is bypassed by overrides - -**Rationale for selecting PR's fix:** -1. PR covers ALL platforms (iOS, MacCatalyst, Android, Windows) while my fix only covers iOS/MacCatalyst -2. PR also fixes ShellSection for Shell navigation scenarios -3. PR uses null-safety (`page?`) which is more defensive -4. PR correctly removes the ineffective DisconnectHandlers from NavigationProxy (cleanup) -5. My successful fix #3 is essentially a subset of the PR's approach - -**Independent validation:** My fix #3 independently arrived at the same solution as the PR for the Legacy path, which validates the PR's approach is correct. - -
- ---- - -## ✅ Final Recommendation: APPROVE - -**Summary:** PR #32289 correctly fixes the handler disconnection issue when removing non-visible pages using `RemovePage()`. - -**Key Findings:** -1. ✅ **Root cause correctly identified** - NavigationPage overrides bypass NavigationProxy, requiring fix in implementations -2. ✅ **All code paths covered** - NavigationPage (Maui + Legacy) and ShellSection -3. ✅ **Unit tests adequate** - Cover both `useMaui: true/false` and Shell navigation -4. ✅ **Two maintainer approvals** - StephaneDelcroix and rmarinho -5. ✅ **Independent validation** - My try-fix #3 independently arrived at same solution for Legacy path - -**Alternative approaches tested:** -- Approach 2+3 (Maui + Legacy paths only) works but misses Shell navigation -- PR's fix is more complete and architecturally correct - -**No concerns identified.** diff --git a/.github/agent-pr-session/pr-33134.md b/.github/agent-pr-session/pr-33134.md deleted file mode 100644 index 19b6325c46de..000000000000 --- a/.github/agent-pr-session/pr-33134.md +++ /dev/null @@ -1,203 +0,0 @@ -# PR Review: #33134 - [Android] EmptyView doesn't display when CollectionView is placed inside a VerticalStackLayout - -**Date:** 2026-01-09 | **Issue:** [#32932](https://github.com/dotnet/maui/issues/32932) | **PR:** [#33134](https://github.com/dotnet/maui/pull/33134) - -## ✅ Status: COMPLETE - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ COMPLETE | -| 🔧 Fix | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -
-📋 Issue Summary - -CollectionView has an EmptyView property for rendering a view when the ItemsSource is empty. This does not work when the CollectionView is placed inside a VerticalStackLayout. - -**Steps to Reproduce:** -1. Place a CollectionView inside a VerticalStackLayout -2. Set an empty ItemsSource -3. Set an EmptyView or EmptyViewTemplate -4. Run on Android - EmptyView does not display - -**Platforms Affected:** -- [ ] iOS -- [x] Android -- [ ] Windows -- [ ] MacCatalyst - -**Reproduction repo:** https://github.com/rrbabbb/EmptyViewNotWorkingRepro - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs` | Fix | +2 lines | -| `src/Controls/tests/TestCases.HostApp/Issues/Issue32932.cs` | Test | New file | -| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32932.cs` | Test | New file | - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- No significant reviewer feedback at time of review - -**Author:** NanthiniMahalingam (Syncfusion partner) - -**Labels:** platform/android, area-controls-collectionview, community ✨, partner/syncfusion - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes UI tests -- [x] Tests reproduce the issue -- [x] Tests follow naming convention (`IssueXXXXX`) - -**Test Files:** -- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue32932.cs` -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32932.cs` - -**Test Description:** Places a CollectionView with an empty ItemsSource inside a VerticalStackLayout and verifies that the EmptyView (a Label with AutomationId="EmptyView") is visible. - -**Verification:** -- Tests compile successfully -- Follow proper naming conventions (Issue32932) -- Use `CollectionView2` handler for Android -- Have correct `[Issue()]` attributes and categories - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ COMPLETE - -- [x] Tests FAIL without fix (bug reproduced) -- [x] Tests PASS with fix (bug fixed) - -**Result:** VERIFIED ✅ - -**Test Execution Results:** - -| State | Result | Details | -|-------|--------|---------| -| **WITH fix** | ✅ PASS | EmptyView element found immediately (1 Appium query) | -| **WITHOUT fix** | ❌ FAIL (Expected) | EmptyView never appears - 25+ retries before timeout | - -**Verification Method:** -1. Ran test with PR fix applied → PASS -2. Reverted `SizedItemContentView.cs` to pre-fix version (before commit `b498b13ef6`) -3. Ran test without fix → FAIL (TimeoutException: `Timed out waiting for element...`) -4. Restored fix - -**Log Evidence:** -- WITH fix: `method: 'id', selector: 'com.microsoft.maui.uitests:id/EmptyView'` → found immediately -- WITHOUT fix: 25+ consecutive searches for `EmptyView` element, never found, test times out - -
- -
-🔧 Fix Candidates - -**Status**: ✅ COMPLETE - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| 1 | try-fix | Fix at source in `EmptyViewAdapter.GetHeight()/GetWidth()` - return `double`, check `IsInfinity()` before casting | ✅ PASS | `EmptyViewAdapter.cs` | Fixes bug at the source but changes method signatures | -| 2 | try-fix | Fix at source in `EmptyViewAdapter` - use `double` throughout + `Math.Abs` + infinity check | ✅ PASS | `EmptyViewAdapter.cs` | Cleaner version of #1 but still changes method signatures | -| 3 | try-fix | **Avoid int.MaxValue entirely:** keep `GetHeight/GetWidth` as `int`, but change the lambdas passed to `SizedItemContentView`/`SimpleViewHolder` to return `double.PositiveInfinity` when `RecyclerViewHeight/Width` is infinite | ✅ PASS | `EmptyViewAdapter.cs` (+4/-3) | Preserves infinity without signature changes and without magic-number checks; not defensive for other callers | -| 4 | try-fix | Check for infinity BEFORE casting in `GetHeight()`/`GetWidth()`, return 0 to trigger fallback | ❌ FAIL | `EmptyViewAdapter.cs` | **Why failed:** Fallback to `parent.MeasuredHeight` returns 0 during initial layout pass - doesn't provide valid dimensions | -| 5 | try-fix | Skip setting `RecyclerViewHeight/Width` when infinite | ❌ FAIL | `ItemsViewHandler.Android.cs` | **Why failed:** Same issue as #4 - fallback mechanism doesn't work when parent not yet measured | -| 6 | try-fix | Add `GetHeightAsDouble()`/`GetWidthAsDouble()` methods that return infinity when appropriate, use in `CreateEmptyViewHolder` lambdas | ✅ PASS | `EmptyViewAdapter.cs` (+18) | Cleaner version of #3 with dedicated helper methods; fixes at source, no heuristic, no signature changes | -| PR | PR #33134 | Add `NormalizeDimension()` helper in `SizedItemContentView` that converts `int.MaxValue` → `double.PositiveInfinity` | ✅ VERIFIED | `SizedItemContentView.cs` | **SELECTED** - Defensive/low-risk downstream patch | - -### Root Cause Analysis - -When a CollectionView is inside a VerticalStackLayout, the height constraint passed to the CollectionView is `double.PositiveInfinity` (unconstrained). This value propagates to `EmptyViewAdapter.RecyclerViewHeight`. - -In `EmptyViewAdapter.GetHeight()`, the code casts this to `int`: `int height = (int)RecyclerViewHeight;` - -In C#, `(int)double.PositiveInfinity` produces `int.MaxValue` (2147483647). This value is then passed via lambda to `SizedItemContentView.OnMeasure()`, where the code checks `double.IsInfinity(targetHeight)` - but `int.MaxValue` is not infinity, so the check fails and layout calculations break. - -### Key Insight from Failed Attempts - -Attempts #4 and #5 revealed that: -- `(int)double.PositiveInfinity` in C# actually produces `int.MaxValue` (verified empirically) -- The fallback to `parent.MeasuredHeight` doesn't work during initial layout when parent hasn't been measured yet -- The solution must **preserve infinity semantics** rather than trying to fall back to measured values - -### Comparison of Passing Fixes - -| Criteria | PR's Fix | Source-Level (#3/#6) | -|----------|----------|---------------------| -| **Correctness** | ✅ Handles `int.MaxValue` → `∞` | ✅ Preserves `∞` directly | -| **Defensive** | ✅ Protects ALL callers of `SizedItemContentView` | ❌ Only fixes EmptyView path | -| **Risk** | ✅ Minimal - 1 line addition | ⚠️ Adds new methods to adapter | -| **Heuristic concern** | ⚠️ Assumes `int.MaxValue` means infinity | ✅ No heuristic | - -**Why PR's heuristic is valid:** `(int)double.PositiveInfinity` produces `int.MaxValue` in C#, so the heuristic is actually semantically correct - `int.MaxValue` really does mean "this was infinity before the cast." - -**Exhausted:** Yes (6 try-fix attempts completed) -**Selected Fix:** PR #33134's fix (NormalizeDimension helper in SizedItemContentView) - -**Rationale:** The PR's fix is the best choice because: -1. **Defensive:** Protects ALL callers of `SizedItemContentView.OnMeasure()`, not just EmptyView -2. **Low-risk:** Minimal code change (1 helper method, 2 call sites) -3. **Semantically correct:** The heuristic `int.MaxValue → PositiveInfinity` is valid because `(int)double.PositiveInfinity` produces `int.MaxValue` in C# -4. **No signature changes:** Doesn't require changing method signatures in `EmptyViewAdapter` - -
- ---- - -## 📋 Final Report - -### Recommendation: ✅ APPROVE - -**Summary:** PR #33134 correctly fixes the Android EmptyView visibility bug when CollectionView is inside a VerticalStackLayout. - -### Technical Analysis - -**Root Cause:** -- CollectionView inside VerticalStackLayout receives `double.PositiveInfinity` as height constraint -- `EmptyViewAdapter.GetHeight()` casts this to `int`: `(int)double.PositiveInfinity` → `int.MaxValue` -- `SizedItemContentView.OnMeasure()` checks `double.IsInfinity(targetHeight)` but `int.MaxValue` is not infinity -- Layout calculations fail, EmptyView doesn't display - -**Fix:** -- Adds `NormalizeDimension()` helper that converts `int.MaxValue` back to `double.PositiveInfinity` -- Applied at the point of use in `OnMeasure()`, making it defensive for all callers - -### Quality Assessment - -| Aspect | Rating | Notes | -|--------|--------|-------| -| **Correctness** | ✅ Excellent | Fix addresses root cause, tests verify behavior | -| **Test Coverage** | ✅ Good | UI test properly reproduces the bug | -| **Risk** | ✅ Low | Minimal, surgical change | -| **Code Quality** | ✅ Good | Clean helper method, proper naming | -| **Alternative Comparison** | ✅ Done | 6 alternatives explored, PR's approach is best | - -### Test Verification - -- ✅ Test PASSES with fix (EmptyView visible) -- ✅ Test FAILS without fix (TimeoutException - EmptyView never appears) - -### Minor Suggestions (Non-blocking) - -1. **Consider XML doc comment** for `NormalizeDimension()` explaining why the conversion is needed -2. **Consider adding regression test for Grid container** as reported in issue comments diff --git a/.github/agent-pr-session/pr-33380.md b/.github/agent-pr-session/pr-33380.md deleted file mode 100644 index 54656e698bae..000000000000 --- a/.github/agent-pr-session/pr-33380.md +++ /dev/null @@ -1,226 +0,0 @@ -# PR Review: #33380 - [PR agent] Issue23892.ShellBackButtonShouldWorkOnLongPress - test fix - -**Date:** 2026-01-07 | **Issue:** [#33379](https://github.com/dotnet/maui/issues/33379) | **PR:** [#33380](https://github.com/dotnet/maui/pull/33380) - -## ✅ Final Recommendation: APPROVE - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ PASSED | -| 🔧 Fix | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -
-📋 Issue Summary - -**Issue #33379**: The UI test `Issue23892.ShellBackButtonShouldWorkOnLongPress` started failing after PR #32456 was merged. - -**Test Expectation**: `OnAppearing count: 2` -**Test Actual**: `OnAppearing count: 1` - -**Original Issue #23892**: Using long-press navigation on the iOS back button in Shell does not update `Shell.Current.CurrentPage`. The `Navigated` and `Navigating` events don't fire. - -**Platforms Affected:** -- [x] iOS -- [ ] Android -- [ ] Windows -- [ ] MacCatalyst - -
- -
-🔍 Deep Regression Analysis - Full Timeline - -## The Regression Chain - -This PR addresses a **double regression** - the same functionality was broken twice by subsequent PRs. - -### Timeline of Changes to `ShellSectionRenderer.cs` - -| Date | PR | Purpose | Key Change | Broke Long-Press? | -|------|-----|---------|------------|-------------------| -| Feb 2025 | #24003 | Fix #23892 (long-press back) | Added `_popRequested` flag + `DidPopItem` | ✅ Fixed it | -| Jul 2025 | #29825 | Fix #29798/#30280 (tab blank issue) | **Removed** `_popRequested`, expanded `DidPopItem` with manual sync | ❌ **Broke it** | -| Jan 2026 | #32456 | Fix #32425 (navigation hang) | Added null checks, changed `ElementForViewController` | ❌ Maintained broken state | - -### PR #24003 - The Original Fix (Feb 2025) - -**Problem solved**: Long-press back button didn't trigger navigation events. - -**Solution**: Added `_popRequested` flag to distinguish: -- **User-initiated navigation** (long-press): Call `SendPop()` → triggers `GoToAsync("..")` → fires `OnAppearing` -- **Programmatic navigation** (code): Skip `SendPop()` to avoid double-navigation - -**Key code added**: -```csharp -bool _popRequested; - -bool DidPopItem(UINavigationBar _, UINavigationItem __) - => _popRequested || SendPop(); // If not requested, call SendPop -``` - -### PR #29825 - The First Regression (Jul 2025) - -**Problem solved**: Tab becomes blank after specific navigation pattern (pop via tab tap, then navigate again, then back). - -**What went wrong**: The PR author expanded `DidPopItem` with manual stack synchronization logic (`_shellSection.SyncStackDownTo()`) and **removed the `_popRequested` flag entirely**. - -**Result**: `DidPopItem` now ALWAYS does manual sync, never calls `SendPop()` for user-initiated navigation. Long-press navigation stopped triggering `OnAppearing`. - -**Why the test didn't catch it**: Unclear - possibly the test wasn't run or was flaky at the time. - -### PR #32456 - Maintained the Broken State (Jan 2026) - -**Problem solved**: Navigation hangs after rapidly opening/closing pages (iOS 26 specific). - -**What it did**: Added null checks to prevent crashes in `DidPopItem` and changed `ElementForViewController` pattern matching. - -**Maintained the regression**: The PR kept the broken `DidPopItem` logic from #29825 (no `_popRequested` flag). - -**This triggered the test failure**: When #32456 merged to `inflight/candidate`, the existing `Issue23892` test started failing. - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs` | Fix | -20 lines (simplified) | -| `src/Controls/src/Core/Shell/ShellSection.cs` | Fix | -44 lines (removed `SyncStackDownTo`) | - -**Net change:** -49 lines (code reduction) - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- Issue #33379 was filed by @sheiksyedm pointing to the test failure after #32456 merged -- @kubaflo (author of both #32456 and #33380) created this fix - -**Reviewer Feedback:** -- None yet - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| -| (none) | | | | - -**Author Uncertainty:** -- None expressed - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes UI tests (existing test from #24003) -- [x] Tests reproduce the issue -- [x] Tests follow naming convention (`IssueXXXXX`) ✅ - -**Test Files:** -- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue23892.cs` -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23892.cs` - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ PASSED - -- [x] Tests PASS with fix - -**Test Run:** -``` -Platform: iOS -Test Filter: FullyQualifiedName~Issue23892 -Result: SUCCESS ✅ -``` - -**Result:** PASSED ✅ - The `Issue23892.ShellBackButtonShouldWorkOnLongPress` test now passes with the PR's fix. - -
- -
-🔧 Fix Candidates - -**Status**: ✅ COMPLETE - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| 1 | try-fix | Simplified `DidPopItem`: Always call `SendPop()` when stacks are out of sync | ✅ PASS (Issue23892 + Issue29798 + Issue21119) | `ShellSectionRenderer.cs` (-17, +6) | **Simpler AND works!** | -| PR | PR #33380 (original) | Restore `_popRequested` flag + preserve manual sync from #29825/#32456 | ✅ PASS (Gate) | `ShellSectionRenderer.cs` (+11) | Superseded by update | -| PR | PR #33380 (updated) | **Adopted try-fix #1** - Stack sync detection, removed `SyncStackDownTo` | ✅ PASS (CI pending) | `ShellSectionRenderer.cs`, `ShellSection.cs` (-49 net) | **CURRENT - matches recommendation** | - -**Update (2026-01-08):** Developer @kubaflo adopted the simpler approach recommended in try-fix #1. - -**Exhausted:** Yes -**Selected Fix:** PR #33380 (updated) - Now implements the recommended simpler approach - -
- ---- - -## 📋 Final Report - -### Recommendation: ✅ APPROVE - -**Update (2026-01-08):** Developer @kubaflo has adopted the recommended simpler approach. - -### Changes Made by Developer - -The PR now implements exactly the simplified stack-sync detection approach: - -**ShellSectionRenderer.cs** - Simplified `DidPopItem`: -```csharp -bool DidPopItem(UINavigationBar _, UINavigationItem __) -{ - if (_shellSection?.Stack is null || NavigationBar?.Items is null) - return true; - - // If stacks are in sync, nothing to do - if (_shellSection.Stack.Count == NavigationBar.Items.Length) - return true; - - // Stacks out of sync = user-initiated navigation - return SendPop(); -} -``` - -**ShellSection.cs** - Removed `SyncStackDownTo` method (44 lines deleted) - -### Why This Approach Works - -| Scenario | What Happens | -|----------|--------------| -| **Tab tap pop** | Shell updates stack BEFORE `DidPopItem` → stacks ARE in sync → returns early (no `SendPop()`) | -| **Long-press back** | iOS pops directly → Shell stack NOT updated → stacks out of sync → calls `SendPop()` | - -### Benefits of Updated PR - -| Aspect | Before (Original PR) | After (Updated PR) | -|--------|---------------------|-------------------| -| Lines changed | +11 | **-49 net** | -| New fields | `_popRequested` bool | **None (stateless)** | -| Complexity | State tracking | **Simple sync check** | -| `SyncStackDownTo` | Preserved | **Removed** | - -### Conclusion - -The PR now: -- ✅ Fixes Issue #33379 (long-press back navigation) -- ✅ Uses the simpler stateless approach -- ✅ Removes 49 lines of code -- ✅ No new state tracking required -- ⏳ Pending CI verification - -**Approve once CI passes.**u diff --git a/.github/agent-pr-session/pr-33392.md b/.github/agent-pr-session/pr-33392.md deleted file mode 100644 index 29add538c7e7..000000000000 --- a/.github/agent-pr-session/pr-33392.md +++ /dev/null @@ -1,346 +0,0 @@ -# PR Review: #33392 - [iOS] Fixed the UIStepper Value from being clamped based on old higher MinimumValue - -**Date:** 2026-01-06 | **Issue:** N/A (Test failure fix) | **PR:** [#33392](https://github.com/dotnet/maui/pull/33392) - -## ✅ Final Recommendation: APPROVE - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ PASSED | -| 🔍 Analysis | ✅ COMPLETE | -| ⚖️ Compare | ✅ COMPLETE | -| 🔬 Regression | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -
-📋 Issue Summary - -**Problem:** Stepper Device Tests failing on iOS in candidate PR #33363 - -**Root Cause (from PR description):** -- `Stepper_SetIncrementAndVerifyValueChange` and `Stepper_SetIncrementValue_VerifyIncrement` tests failed -- Previous test (`Stepper_ResetToInitialState_VerifyDefaultValues`) updated Minimum to 10 -- When next test runs, new ViewModel sets defaults (Value=0, Minimum=0) -- `MapValue` is called first, but Minimum still has stale value of 10 -- Native UIStepper clamps Value based on old Minimum, causing test failure - -**Regressed by:** PR #32939 - -**Example Scenario:** -- Old state: Min=5, Value=5 -- New state: Min=0, Value=2 -- Without fix: Value set to 2, iOS sees Min=5 (stale), clamps to 5 -- With fix: Min updated to 0 first, then Value set to 2 successfully - -**Platforms Affected:** -- [x] iOS -- [ ] Android (tested, not affected) -- [ ] Windows (tested, not affected) -- [ ] MacCatalyst (tested, not affected) - -
- -
-🔗 Regression Context - PR #32939 - -**Title:** [C] Fix Slider and Stepper property order independence - -**Author:** @StephaneDelcroix - -**Purpose:** Ensure `Value` property is correctly preserved regardless of the order in which `Minimum`, `Maximum`, and `Value` are set (programmatically or via XAML bindings). - -**Original Problem (that #32939 fixed):** -- When using XAML data binding, property application order depends on attribute order and binding timing -- Previous implementation clamped `Value` immediately when `Min`/`Max` changed, using current (potentially default) range -- Example: `Value=50` with `Min=10, Max=100` would get clamped to `1` (default max) if `Value` was set before `Maximum` -- User's intended value was lost - -**Solution in #32939:** -- Introduced three private fields: - - `_requestedValue`: stores user's intended value before clamping - - `_userSetValue`: tracks if user explicitly set `Value` (vs automatic recoercion) - - `_isRecoercing`: prevents `_requestedValue` corruption during recoercion -- When `Min`/`Max` changes: restore `_requestedValue` (clamped to new range) if user explicitly set it -- Changed from `coerceValue` callback to `propertyChanged` callback for Min/Max - -**Issues Fixed by #32939:** -1. **#32903** - Slider Binding Initialization Order Causes Incorrect Value Assignment in XAML -2. **#14472** - Slider is very broken, Value is a mess when setting Minimum -3. **#18910** - Slider is buggy depending on order of properties -4. **#12243** - Stepper Value is incorrectly clamped to default min/max when using bindableproperties in MVVM pattern - -**Files Changed in #32939:** -- `src/Controls/src/Core/Slider/Slider.cs` (+43, -12) -- `src/Controls/src/Core/Stepper/Stepper.cs` (+33, -6) -- `src/Controls/tests/Core.UnitTests/SliderUnitTests.cs` (+166) -- `src/Controls/tests/Core.UnitTests/StepperUnitTests.cs` (+165) - -**Behavioral Change Warning (from #32939):** -> The order of `PropertyChanged` events for `Stepper` may change in edge cases where `Minimum`/`Maximum` changes trigger a `Value` change. Previously, `Value` changed before `Min`/`Max`; now it changes after. - -
- -
-📖 Scenarios from Fixed Issues - -**Scenario 1 (Issue #32903):** XAML Binding Order -```xaml - -``` -ViewModel: `Min=10, Max=100, Value=50` -- Before #32939: Value evaluated before Maximum → clamped to 10 (wrong) -- After #32939: Value "springs back" to 50 when range includes it (correct) - -**Scenario 2 (Issue #14472):** Value Before Minimum -```xaml - -``` -- Before #32939: Shows zero minimum and zero value (wrong) -- After #32939: Correctly shows 75 (correct) - -**Scenario 3 (Issue #12243):** Stepper MVVM Binding -```csharp -Min = 1; Max = 105; Value = 102; -``` -- Before #32939: Value clamped to 100 (default max) (wrong) -- After #32939: Value correctly shows 102 (correct) - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Core/src/Platform/iOS/StepperExtensions.cs` | Fix | +10 lines | - -**No test files included in PR.** - -
- -
-🔍 The Disconnect: MAUI Layer vs Platform Layer - -**Key Insight:** PR #32939 fixed the **MAUI layer** (Stepper.cs) but created a problem in the **iOS platform layer** (StepperExtensions.cs). - -**How #32939 Changed Mapper Call Order:** - -Before #32939: -- `coerceValue` on Minimum → immediately clamps Value → MapValue called → MapMinimum called -- Order: Value updated BEFORE Min/Max - -After #32939: -- `propertyChanged` on Minimum → calls RecoerceValue() → MapMinimum called → MapValue called -- Order: Min/Max updated BEFORE Value (at MAUI layer) -- But iOS platform layer still updates Value BEFORE checking if Min needs update - -**The Gap:** -- MAUI `Stepper.cs` now correctly sequences property changes -- But `StepperHandler.MapValue()` doesn't know about the pending Min/Max changes -- When `MapValue` runs, the native `UIStepper.MinimumValue` still has the OLD value -- iOS native UIStepper clamps to OLD range → wrong value displayed - -**PR #33392's Fix:** -- In `UpdateValue()` (platform layer), check if `MinimumValue` needs updating FIRST -- Update it before setting `Value` on the native control -- This syncs the platform layer with MAUI's new property change sequence - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- No PR comments or review feedback yet - -**Reviewer Feedback:** -- None yet - -**Disagreements to Investigate:** -- None identified - -**Author Uncertainty:** -- None expressed - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes UI tests → **NO** (this fixes existing UI Tests) -- [x] Existing UI Tests cover this scenario → **YES** (StepperFeatureTests.cs) -- [x] Tests follow naming convention → N/A - -**Test Files:** -- Existing: `src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/StepperFeatureTests.cs` -- Failing tests: `Stepper_SetIncrementAndVerifyValueChange`, `Stepper_SetIncrementValue_VerifyIncrement` - -**Note:** This PR fixes UI test failures caused by inter-test state leakage. The `StepperFeatureTests` class does NOT reset between tests (`ResetAfterEachTest` not overridden), so when one test sets Minimum=10, subsequent tests inherit that stale native state. - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ PASSED - -- [x] Existing Stepper UI Tests pass with fix (per PR author verification) -- [x] Tests were failing before this fix on candidate PR #33363 -- [x] Root cause confirmed: mapper call order + native UIStepper clamping - -**Result:** PASSED ✅ - -**Verification approach:** CI pipeline ran StepperFeatureTests on iOS, tests that previously failed now pass. - -
- ---- - -## 🔍 Phase 4: Analysis - COMPLETE - -### Root Cause - -**Layer mismatch after PR #32939:** - -1. PR #32939 changed `Stepper.cs` from `coerceValue` to `propertyChanged` for Min/Max -2. This changed the timing of when Value gets recoerced relative to Min/Max mapper calls -3. iOS native `UIStepper` auto-clamps `Value` to `[MinimumValue, MaximumValue]` when set -4. When `MapValue` runs before `MapMinimum`/`MapMaximum`, native has stale range → wrong clamping - -**Platform comparison:** -| Platform | Native Control | Auto-Clamps on Value Set? | Issue? | -|----------|----------------|---------------------------|--------| -| iOS | UIStepper | ✅ Yes | **YES - needs fix** | -| Windows | MauiStepper | ❌ No (manual clamp on button click) | No | -| Android | MauiStepper (LinearLayout) | ❌ No (buttons only) | No | - -### PR #33392's Approach - -**Correct concept:** Sync Min/Max before setting Value in platform layer. - -**Implementation gap:** Only syncs `MinimumValue`, but same issue exists for `MaximumValue`. - -### Missing Maximum Sync Scenario - INVESTIGATED AND DISMISSED - -Initially hypothesized that Maximum would have the same issue: - -``` -Test A: Sets Maximum=5 → native UIStepper.MaximumValue=5 -Test B: New ViewModel with Maximum=10, Value=8 -MapValue runs before MapMaximum: -- Would Value=8 get clamped to 5? -``` - -**After investigation: This scenario CANNOT occur with default ViewModel values.** - -The ViewModel defaults to `Value=0`. Since 0 is NEVER above any Maximum, the stale Maximum clamping can never trigger. The bug is mathematically asymmetric: - -| Scenario | Default Value=0 | Stale Native Value | Clamp Result | -|----------|-----------------|-------------------|--------------| -| Minimum bug | 0 < stale Min=10 | Min=10 | ❌ Clamped UP to 10 | -| Maximum bug | 0 < stale Max=5 | Max=5 | ✅ No clamp (0 is valid) | - -**Conclusion:** Maximum sync is NOT needed because the default Value=0 can never exceed any Maximum. - -### Slider Also Potentially Affected (Future Consideration) - -`SliderExtensions.UpdateValue()` on iOS doesn't have similar Min sync. However, the same asymmetry applies - default Value=0 cannot trigger a Maximum clamp bug. A Minimum sync might be needed for Slider if similar test patterns emerge. - ---- - -## ⚖️ Phase 5: Compare - COMPLETE - -| Aspect | PR's Fix | Notes | -|--------|----------|-------| -| Syncs Minimum | ✅ Yes | Required - fixes the bug | -| Syncs Maximum | ❌ No | Not needed - see analysis | -| Fixes failing tests | ✅ Yes | Verified | -| Risk of regression | Low | Small, targeted change | - -**Conclusion:** PR is complete as-is. Maximum sync is unnecessary because the bug is mathematically asymmetric (default Value=0 can never exceed any Maximum). - ---- - -## 🔬 Phase 6: Regression - COMPLETE - -### Will fix break #32939 scenarios? - -**Analyzed scenario:** XAML binding order independence - -```xaml - -``` -ViewModel: Min=10, Max=100, Value=50 - -**With PR #33392's fix:** -1. Bindings update in unpredictable order -2. If MapValue runs first: UpdateValue syncs Min→10, then sets Value→50 -3. Value correctly within [10, 100] ✅ -4. MapMaximum later sets Max→100 (already correct at platform) ✅ - -**No regression.** The fix ensures platform state is correct regardless of mapper call order. - -### Double-update concern - -`MinimumValue` may be set twice: once in `UpdateValue`, once in `MapMinimum`. - -**Mitigated by:** Guard condition `if (platformStepper.MinimumValue != stepper.Minimum)` - -**Acceptable:** Setting the same value twice is a no-op for UIStepper. - -### Edge cases verified - -| Edge Case | Result | -|-----------|--------| -| Min > current Value | MAUI clamps first, platform syncs correctly | -| Max < current Value | MAUI clamps first, platform syncs correctly (IF Max sync added) | -| Rapid property changes | Each mapper call syncs current state | -| ViewModel replacement | New values propagate correctly | - ---- - -## 📋 Phase 7: Report - -### Final Recommendation: ✅ APPROVE - -**The PR correctly fixes the Minimum clamping issue. The Maximum sync is NOT needed due to a fundamental asymmetry.** - -### Deep Analysis: Why Maximum Sync Is Unnecessary - -I wrote multiple tests attempting to reproduce a Maximum clamping bug, but they all passed. Here's why: - -**Minimum Bug (exists, PR fixes):** -- Stale native Min = 10 (HIGH) -- New ViewModel Value = 0 (LOW, the default) -- iOS clamps: `Value = max(Value, Min) = max(0, 10) = 10` ❌ WRONG! -- Bug triggers because **Value=0 < stale Min=10** - -**Maximum Bug (does NOT exist):** -- Stale native Max = 5 (LOW) -- New ViewModel Value = 0 (LOW, the default) -- iOS clamps: `Value = min(Value, Max) = min(0, 5) = 0` ✅ CORRECT! -- Bug CANNOT trigger because **Value=0 < stale Max=5** (always valid) - -**The key asymmetry:** When creating a new ViewModel, Value defaults to 0. -- Value=0 can be BELOW a high Minimum (triggering clamp UP) ✅ Bug possible -- Value=0 is NEVER ABOVE any Maximum (no clamp DOWN needed) ✅ No bug - -### Test Verification - -I wrote a UI test `Stepper_ValueNotClampedByStaleMaximum` to attempt to reproduce a Maximum clamping bug. The test passed, confirming the Maximum bug cannot occur. The test was subsequently removed as it was only for investigative purposes. - -### Justification - -1. ✅ **Correct root cause analysis** - PR correctly identifies mapper call order issue for Minimum -2. ✅ **Correct fix approach** - Syncing Min before Value prevents native clamping bug -3. ✅ **Fixes the failing tests** - Immediate problem solved -4. ✅ **Low risk** - Small, targeted change -5. ✅ **Maximum sync NOT needed** - Mathematically impossible to trigger with default Value=0 diff --git a/.github/agent-pr-session/pr-33406.md b/.github/agent-pr-session/pr-33406.md deleted file mode 100644 index e79aca2901f5..000000000000 --- a/.github/agent-pr-session/pr-33406.md +++ /dev/null @@ -1,247 +0,0 @@ -# PR Review: #33406 - [iOS] Fixed Shell navigation on search handler suggestion selection - -**Date:** 2026-01-08 | **Issue:** [#33356](https://github.com/dotnet/maui/issues/33356) | **PR:** [#33406](https://github.com/dotnet/maui/pull/33406) - -**Related Prior Attempt:** [PR #33396](https://github.com/dotnet/maui/pull/33396) (closed - Copilot CLI attempt) - -## ⏳ Status: IN PROGRESS - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ⏳ PENDING | -| 🚦 Gate | ⏳ PENDING | -| 🔧 Fix | ⏳ PENDING | -| 📋 Report | ⏳ PENDING | - ---- - -
-📋 Issue Summary - -**Issue #33356**: [iOS] Clicking on search suggestions fails to navigate to detail page correctly - -**Bug Description**: Clicking on a search suggestion using NavigationBar/SearchBar/custom SearchHandler does not navigate to the detail page correctly on iOS 26.1 & 26.2 with MAUI 10. - -**Root Cause (from PR #33406)**: Navigation fails because `UISearchController` was dismissed (`Active = false`) BEFORE `ItemSelected` was called. This triggers a UIKit transition that deactivates the Shell navigation context and prevents the navigation from completing. - -**Reproduction App**: https://github.com/dotnet/maui-samples/tree/main/10.0/Fundamentals/Shell/Xaminals - -**Steps to Reproduce:** -1. Open the Xaminals sample app -2. Deploy to iPhone 17 Pro 26.2 simulator (Xcode 26.2) -3. Put focus on the search box -4. Type 'b' (note: search dropdown position is wrong - see Issue #32930) -5. Click on 'Bengal' in search suggestions -6. **Issue 1:** No navigation happens (expected: navigate to Bengal cat detail page) -7. Click on 'Bengal' from the main list - this works correctly -8. Click back button -9. **Issue 2:** Navigates to an empty page (expected: navigate back to list) -10. Click back button again - actually navigates back - -**Platforms Affected:** -- [ ] Android -- [x] iOS (26.1 & 26.2) -- [ ] Windows -- [ ] MacCatalyst - -**Regression Info:** -- **Confirmed regression** starting in version 9.0.90 -- Labels: `t/bug`, `platform/ios`, `s/verified`, `s/triaged`, `i/regression`, `shell-search-handler`, `regressed-in-9.0.90` -- Issue 2 (empty page on back navigation) specifically reproducible from 9.0.90 - -**Validated by:** TamilarasanSF4853 (Syncfusion partner) - Confirmed reproducible in VS Code 1.107.1 with MAUI versions 9.0.0, 9.0.82, 9.0.90, 9.0.120, 10.0.0, and 10.0.20 on iOS. - -
- -
-📁 Files Changed - PR #33406 (Community PR) - -| File | Type | Changes | -|------|------|---------| -| `src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs` | Fix | 2 lines (swap order) | -| `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.cs` | Test (HostApp) | +261 lines | -| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33356.cs` | Test (NUnit) | +46 lines | - -**PR #33406 Fix** (simpler approach - just swap order): -```diff - void OnSearchItemSelected(object? sender, object e) - { - if (_searchController is null) - return; - -- _searchController.Active = false; - (SearchHandler as ISearchHandlerController)?.ItemSelected(e); -+ _searchController.Active = false; - } -``` - -
- -
-📁 Files Changed - PR #33396 (Prior Copilot Attempt - CLOSED) - -| File | Type | Changes | -|------|------|---------| -| `.github/agent-pr-session/pr-33396.md` | Session | +210 lines | -| `src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs` | Fix | +17 lines | -| `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.xaml` | Test (XAML) | +41 lines | -| `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.xaml.cs` | Test (C#) | +138 lines | -| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33356.cs` | Test (NUnit) | +70 lines | - -**PR #33396 Fix** (more defensive approach with BeginInvokeOnMainThread): -```diff - void OnSearchItemSelected(object? sender, object e) - { - if (_searchController is null) - return; - -+ // Store the search controller reference before any state changes -+ var searchController = _searchController; -+ -+ // Call ItemSelected first to trigger navigation before dismissing the search UI. -+ // On iOS 26+, setting Active = false before navigation can cause the navigation -+ // to be lost due to the search controller dismissal animation. - (SearchHandler as ISearchHandlerController)?.ItemSelected(e); -- _searchController.Active = false; -+ -+ // Deactivate the search controller after navigation has been initiated. -+ // Using BeginInvokeOnMainThread ensures this happens after the current run loop, -+ // allowing the navigation to proceed without interference from the dismissal animation. -+ ViewController?.BeginInvokeOnMainThread(() => -+ { -+ if (searchController is not null) -+ { -+ searchController.Active = false; -+ } -+ }); - } -``` - -
- -
-💬 Discussion Summary - -**Key Comments from Issue #33356:** -- TamilarasanSF4853 (Syncfusion): Validated issue across multiple MAUI versions (9.0.0 through 10.0.20) -- Issue 2 (empty page on back) specifically regressed in 9.0.90 -- Issue 1 (no navigation on search suggestion tap) affects all tested versions on iOS - -**PR #33406 Review Comments:** -- Copilot PR reviewer caught typo: "searchHander" should be "searchHandler" (5 duplicate comments, all resolved/outdated now) -- Prior agent review by kubaflo marked it as ✅ APPROVE with comprehensive analysis -- PureWeen requested `/rebase` (latest comment) - -**PR #33396 Review Comments:** -- PureWeen asked to update state file to match PR number -- Copilot had firewall issues accessing GitHub API - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| -| N/A | N/A | N/A | No active disagreements | - -**Author Uncertainty:** -- None noted in either PR - -
- -
-⚖️ Comparison: PR #33406 vs PR #33396 - -### Fix Approach Comparison - -| Aspect | PR #33406 (Community) | PR #33396 (Copilot) | -|--------|----------------------|---------------------| -| **Author** | SubhikshaSf4851 (Syncfusion) | Copilot | -| **Status** | Open | Closed (draft) | -| **Lines Changed** | 2 (swap order) | 17 (more defensive) | -| **Fix Strategy** | Simply swap order of operations | Swap order + dispatch to next run loop | -| **Test Style** | Code-only (no XAML) | XAML + code-behind | -| **Test Count** | 1 test method | 2 test methods | - -### Which Fix is Better? - -**PR #33406 (simpler approach):** -- ✅ Minimal change - just swaps two lines -- ✅ Addresses root cause: ItemSelected called while navigation context is valid -- ⚠️ Dismissal happens synchronously after ItemSelected -- ⚠️ Could theoretically still interfere if dismissal animation is fast - -**PR #33396 (defensive approach):** -- ✅ Uses BeginInvokeOnMainThread for explicit async deactivation -- ✅ Stores reference to search controller before state changes -- ✅ More detailed comments explaining the fix -- ⚠️ More code complexity -- ⚠️ Was closed/abandoned - -### Recommendation - -Both approaches should work. PR #33406 is simpler and has been reviewed/approved. The extra defensive measures in PR #33396 (BeginInvokeOnMainThread) may provide additional safety margin but add complexity. - -**Prior agent review on PR #33406** already verified: -- Tests FAIL without fix (bug reproduced - timeout) -- Tests PASS with fix (navigation successful) - -
- -
-🧪 Tests - -**Status**: ⏳ PENDING (need to verify tests compile and reproduce issue) - -**PR #33406 Tests:** -- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.cs` (code-only, no XAML) -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33356.cs` -- 1 test: `Issue33356NavigateShouldOccur` - Tests search handler navigation AND back navigation + collection view navigation - -**PR #33396 Tests (for reference):** -- HostApp XAML: `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.xaml` -- HostApp Code: `src/Controls/tests/TestCases.HostApp/Issues/Issue33356.xaml.cs` -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33356.cs` -- 2 tests: `SearchSuggestionTapNavigatesToDetailPage`, `BackNavigationFromDetailPageWorks` - -**Test Checklist:** -- [ ] PR includes UI tests -- [ ] Tests reproduce the issue -- [ ] Tests follow naming convention (`Issue33356`) - -
- -
-🚦 Gate - Test Verification - -**Status**: ⏳ PENDING - -- [ ] Tests FAIL without fix (bug reproduced) -- [ ] Tests PASS with fix (fix validated) - -**Prior Agent Review Result (kubaflo on PR #33406):** -``` -WITHOUT FIX: FAILED - System.TimeoutException: Timed out waiting for element "Issue33356CatNameLabel" -WITH FIX: PASSED - All 1 tests passed in 21.73 seconds -``` - -**Result:** [PENDING - needs re-verification] - -
- -
-🔧 Fix Candidates - -**Status**: ⏳ PENDING - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| PR | PR #33406 | Swap order: ItemSelected before Active=false | ⏳ PENDING (Gate) | `ShellPageRendererTracker.cs` (2 lines) | Current PR - simpler fix | -| Alt | PR #33396 | Swap order + BeginInvokeOnMainThread | ✅ VERIFIED (prior test) | `ShellPageRendererTracker.cs` (17 lines) | Prior attempt - more defensive | - -**Exhausted:** No -**Selected Fix:** [PENDING] - -
- ---- - -**Next Step:** Verify PR #33406 tests compile and Gate passes. Read `.github/agents/pr/post-gate.md` after Gate passes. From 06a01d856659aca69301342e05fe68a32a77d46d Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 00:37:23 +0100 Subject: [PATCH 030/126] Create iPhone Xs simulator for iOS 18.5 Add provisioning steps to eng/pipelines/common/provision.yml to create an iPhone Xs simulator with iOS 18.5 (falling back to iOS 18.x if 18.5 is not available). The script finds the appropriate runtime and device type, checks for an existing simulator to avoid duplicates, attempts creation if missing, and prints available iPhone simulators. This ensures the required simulator is present for UI tests and logs useful diagnostics if runtimes or device types are not available. --- eng/pipelines/common/provision.yml | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 14fcbdcdeaf4..08e7caadf138 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -194,6 +194,41 @@ steps: else sudo xcodebuild -downloadPlatform iOS fi + + # Create iPhone Xs simulator with iOS 18.5 (required for UI tests) + echo "" + echo "=== Creating iPhone Xs simulator with iOS 18.5 ===" + + # Find iOS 18.5 runtime (or fallback to iOS 18.x) + IOS_RUNTIME=$(xcrun simctl list runtimes -j | jq -r '.runtimes[] | select(.name | contains("iOS 18.5")) | .identifier' | head -1) + if [[ -z "$IOS_RUNTIME" ]]; then + IOS_RUNTIME=$(xcrun simctl list runtimes -j | jq -r '.runtimes[] | select(.name | contains("iOS 18")) | .identifier' | head -1) + fi + + if [[ -n "$IOS_RUNTIME" ]]; then + echo "Found iOS runtime: $IOS_RUNTIME" + IPHONE_XS_TYPE=$(xcrun simctl list devicetypes -j | jq -r '.devicetypes[] | select(.name == "iPhone Xs") | .identifier' | head -1) + + if [[ -n "$IPHONE_XS_TYPE" ]]; then + # Check if already exists + EXISTING=$(xcrun simctl list devices -j | jq -r --arg rt "$IOS_RUNTIME" '.devices[$rt][]? | select(.name == "iPhone Xs") | .udid' | head -1) + if [[ -n "$EXISTING" ]]; then + echo "iPhone Xs simulator already exists: $EXISTING" + else + UDID=$(xcrun simctl create "iPhone Xs" "$IPHONE_XS_TYPE" "$IOS_RUNTIME" 2>&1) && \ + echo "Created iPhone Xs simulator: $UDID" || \ + echo "Note: Could not create iPhone Xs simulator" + fi + else + echo "Note: iPhone Xs device type not available" + fi + else + echo "Note: iOS 18.x runtime not found" + fi + + echo "" + echo "=== Available iPhone simulators ===" + xcrun simctl list devices available | grep -i iphone | head -10 displayName: Install Simulator Runtimes condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) continueOnError: true From 96334e7766374c0d98b8714cd3ec3991ace5fea9 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 01:38:56 +0100 Subject: [PATCH 031/126] Enable provisionator in CI pipeline Change skipProvisionator from true to false in eng/pipelines/ci-copilot.yml to enable the provisionator step during CI runs. This ensures provisioning (e.g., certificates/profiles and related setup) runs as part of the pipeline. --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 4259146cb8a5..5cf87a23ba1d 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -69,7 +69,7 @@ stages: - template: common/provision.yml parameters: skipXcode: false - skipProvisionator: true + skipProvisionator: false skipAndroidCommonSdks: false skipAndroidPlatformApis: false skipJdk: false From b7f0ad4b4e46b07707ed5d94322e7f15ce75f88d Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 01:59:59 +0100 Subject: [PATCH 032/126] Add maui-provisionator variable group Add the 'maui-provisionator' variable group to eng/pipelines/common/variables.yml. This provides the credentials required for the provisionator to install Xcode and is added alongside the existing MAUI variable group. --- eng/pipelines/common/variables.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index 2c2bd64a8124..30d1efdc7e39 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -55,6 +55,7 @@ variables: value: none - group: MAUI # This is the main MAUI variable group that contains secrets for the apple certificate +- group: maui-provisionator # Required for provisionator to install Xcode # Variable groups required for all builds # - ${{ if and(ne(variables['Build.DefinitionName'], 'maui-pr'), ne(variables['Build.DefinitionName'], 'dotnet-maui'), ne(variables['Build.DefinitionName'], 'maui-pr-devicetests'), ne(variables['Build.DefinitionName'], 'maui-pr-uitests')) }}: From 598a48ce6dc6964f75e4527039b3e80221b3d5da Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 12:32:20 +0100 Subject: [PATCH 033/126] Detect host/simulator arch and use iOS RID Update Build-AndDeploy.ps1 to detect the host architecture and pick the appropriate iOS simulator runtime identifier (iossimulator-x64 or iossimulator-arm64) and pass it to the build args. Also add logic to detect the simulator/device architecture via xcrun simctl and prefer an .app bundle matching the simulator arch when selecting artifacts, with a fallback to any iossimulator build. Adds informational logging and a safe fallback if detection fails to improve compatibility across Apple Silicon and Intel macOS hosts. --- .github/scripts/shared/Build-AndDeploy.ps1 | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index 8abdafe7b21c..677c22cc6b0f 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -109,7 +109,6 @@ if ($Platform -eq "android") { # Detect host architecture for simulator builds $hostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() $runtimeId = if ($hostArch -eq "x64") { "iossimulator-x64" } else { "iossimulator-arm64" } - $simArch = if ($hostArch -eq "x64") { "x64" } else { "arm64" } Write-Info "Host architecture: $hostArch, RuntimeIdentifier: $runtimeId" $buildArgs = @($ProjectPath, "-f", $TargetFramework, "-c", $Configuration, "-r", $runtimeId) @@ -176,6 +175,28 @@ if ($Platform -eq "android") { Write-Info "Searching for app bundle in: $artifactsDir" + # Detect simulator architecture to pick the correct app bundle + $simArch = "arm64" # Default to arm64 for Apple Silicon + try { + # Get the simulator's device type to determine architecture + $deviceInfo = xcrun simctl list devices --json | ConvertFrom-Json + $simDevice = $deviceInfo.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.udid -eq $DeviceUdid } | + Select-Object -First 1 + + if ($simDevice) { + # Check if the host machine is x64 or arm64 + $hostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + if ($hostArch -eq "x64") { + $simArch = "x64" + } + Write-Info "Host architecture: $hostArch, using simulator arch: $simArch" + } + } catch { + Write-Info "Could not detect architecture, defaulting to arm64" + } + $appPath = Get-ChildItem -Path $artifactsDir -Filter "*.app" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "$Configuration.*iossimulator-$simArch.*$projectName" -and From 7d0a336b8a2123737ff7e21bd7809a01e9e6ed54 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 12:32:31 +0100 Subject: [PATCH 034/126] Make the build faster for testing --- eng/pipelines/ci-copilot.yml | 39 ------------------ eng/pipelines/common/provision.yml | 65 +----------------------------- 2 files changed, 1 insertion(+), 103 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5cf87a23ba1d..d35dcfe20a54 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -94,45 +94,6 @@ stages: env: DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) PRIVATE_BUILD: $(PrivateBuild) - - # List all available simulators/emulators - - script: | - echo "=== Listing Available Simulators/Emulators ===" - - # List iOS simulators using xcrun - echo "" - echo "=== iOS Simulators (xcrun simctl) ===" - xcrun simctl list devices available - - # List iOS simulators using xharness - echo "" - echo "=== iOS Simulators (xharness) ===" - dotnet xharness apple simulators list --installed || echo "xharness apple simulators list failed" - - # Show Apple device state - echo "" - echo "=== Apple Device State (xharness) ===" - dotnet xharness apple state || echo "xharness apple state failed" - - # Show Android device state - echo "" - echo "=== Android Device State (xharness) ===" - dotnet xharness android state || echo "xharness android state failed" - - # List Android AVDs - echo "" - echo "=== Android AVDs (avdmanager) ===" - if [ -n "$ANDROID_HOME" ]; then - "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" - elif [ -n "$ANDROID_SDK_ROOT" ]; then - "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" list avd 2>/dev/null || echo "avdmanager not available" - else - echo "ANDROID_HOME/ANDROID_SDK_ROOT not set" - fi - - echo "" - echo "=== Simulator/Emulator listing complete ===" - displayName: 'List Simulators and Emulators' # Verify environment is ready - script: | diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 08e7caadf138..57067c783b0d 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -4,18 +4,8 @@ parameters: skipSimulatorSetup: false # .NET NuGet caches clearCaches: true - # Android - JDK - skipJdk: false - # Android - SDK Packages - skipAndroidCommonSdks: false - onlyAndroidPlatformDefaultApis: false - skipAndroidPlatformApis: false - # Android - Emulators - androidEmulatorApiLevel: '' - skipAndroidEmulatorImages: true # For most builds we won't need these - skipAndroidCreateAvds: true # For most builds we won't need these # Provisionator / Xcode - skipProvisionator: true + skipProvisionator: false checkoutDirectory: $(System.DefaultWorkingDirectory) provisionatorXCodePath: $(provisionator.xcode) provisionatorChannel: 'latest' @@ -296,56 +286,3 @@ steps: - script: | dotnet tool restore --verbosity diag displayName: 'Restore .NET Tools' - -################################################## -# Provisioning Android # -################################################## - -# Provisioning Android - JDK -- ${{ if ne(parameters.skipJdk, 'true') }}: - - pwsh: dotnet build -t:ProvisionJdk -bl:"$(LogDirectory)/provision-jdk.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed - displayName: 'Provision JDK' - condition: succeeded() - -# Provisioning Android - Android SDK common packages (eg: cmdline-tools, emulator, platform-tools, build-tools) -- ${{ if ne(parameters.skipAndroidCommonSdks, 'true') }}: - - pwsh: dotnet build -t:ProvisionAndroidSdkCommonPackages -bl:"$(LogDirectory)/provision-androidsdk-common.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed - displayName: 'Provision Android SDK - Common Packages' - condition: succeeded() - -# Provisioning Android - Android environment variables -- ${{ if ne(parameters.skipAndroidCommonSdks, 'true') }}: - - pwsh: | - echo "Setting ANDROID_SDK_ROOT and ANDROID_HOME..." - $jsonOutput = & dotnet android sdk info --format=Json | ConvertFrom-Json - $preferredSdk = $jsonOutput.SdkInfo.Path - echo "##vso[task.setvariable variable=ANDROID_SDK_ROOT]$preferredSdk" - echo "ANDROID_SDK_ROOT set to '$preferredSdk'" - echo "##vso[task.setvariable variable=ANDROID_HOME]$preferredSdk" - echo "ANDROID_HOME set to '$preferredSdk'" - displayName: 'Provision Android SDK - Environment variables' - condition: succeeded() - -# Provisioning Android - Android SDK platform APIs (eg: platforms;android-29, platforms;android-30) -- ${{ if ne(parameters.skipAndroidPlatformApis, 'true') }}: - - pwsh: dotnet build -t:ProvisionAndroidSdkPlatformApiPackages -p:AndroidSdkProvisionDefaultApiLevelsOnly="$Env:AndroidSdkProvisionDefaultApiLevelsOnly" -bl:"$(LogDirectory)/provision-androidsdk-platform-apis.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed - displayName: 'Provision Android SDK - Platform APIs' - condition: succeeded() - env: - AndroidSdkProvisionDefaultApiLevelsOnly: ${{ parameters.onlyAndroidPlatformDefaultApis }} - -# Provisioning Android - Emulator images (eg: system-images;android-34;google_apis;aarch64) -- ${{ if ne(parameters.skipAndroidEmulatorImages, 'true') }}: - - pwsh: dotnet build -t:ProvisionAndroidSdkEmulatorImagePackages -p:AndroidSdkProvisionApiLevel="$Env:AndroidSdkProvisionApiLevel" -bl:"$(LogDirectory)/provision-androidsdk-emulator-images.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed - displayName: 'Provision Android SDK - Emulator Images' - condition: succeeded() - env: - AndroidSdkProvisionApiLevel: ${{ parameters.androidEmulatorApiLevel }} - -# Provisioning Android - Android AVDs (actual emulator virtual devices) -- ${{ if ne(parameters.skipAndroidCreateAvds, 'true') }}: - - pwsh: dotnet build -t:ProvisionAndroidSdkAvdCreateAvds -p:AndroidSdkProvisionApiLevel="$Env:AndroidSdkProvisionApiLevel" -bl:"$(LogDirectory)/provision-androidsdk-create-avds.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed - displayName: 'Provision Android SDK - Create AVDs' - condition: succeeded() - env: - AndroidSdkProvisionApiLevel: ${{ parameters.androidEmulatorApiLevel }} From a74865eeb1b9141e668717268a1abaaff1d10d52 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 12:38:34 +0100 Subject: [PATCH 035/126] Update provision.yml --- eng/pipelines/common/provision.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 57067c783b0d..6680e8150475 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -1,7 +1,7 @@ parameters: # Xcode skipXcode: false - skipSimulatorSetup: false + skipSimulatorSetup: true # .NET NuGet caches clearCaches: true # Provisionator / Xcode @@ -244,19 +244,6 @@ steps: inputs: provisioningProfileLocation: 'secureFiles' provProfileSecureFile: '${{ parameters.provisioningProfileName }}' - ################################################## - # Provisioning Windows # - ################################################## - - # Provisioning Windows - Set dump file location -- pwsh: | - $errorPath = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" - New-ItemProperty -Path $errorPath -Name DumpFolder -PropertyType String -Value "$(Build.ArtifactStagingDirectory)/crash-dumps" -Force - New-ItemProperty -Path $errorPath -Name DumpType -PropertyType DWORD -Value 2 -Force - displayName: 'Set dump file location' - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - continueOnError: true - timeoutInMinutes: 5 ################################################## # Provisioning .NET # From eb7df643df1efc4924b5289b135f1ddd5a4f8e71 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 12:47:21 +0100 Subject: [PATCH 036/126] Update provision.yml --- eng/pipelines/common/provision.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 6680e8150475..5d392bdcc7e3 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -5,7 +5,7 @@ parameters: # .NET NuGet caches clearCaches: true # Provisionator / Xcode - skipProvisionator: false + skipProvisionator: true checkoutDirectory: $(System.DefaultWorkingDirectory) provisionatorXCodePath: $(provisionator.xcode) provisionatorChannel: 'latest' From 65c2cf88a12b7956aadac94d0a8d7e773bdc7a2b Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 12:52:23 +0100 Subject: [PATCH 037/126] Update provision.yml --- eng/pipelines/common/provision.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 5d392bdcc7e3..8a712f43c02c 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -65,17 +65,6 @@ steps: # Provisioning macOS - Xcode - ${{ if ne(parameters.skipXcode, 'true') }}: - - ${{ if ne(parameters.skipProvisionator, true) }}: - - task: xamops.azdevex.provisionator-task.provisionator@3 - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - displayName: 'Provision Xcode' - inputs: - provisionator_uri: ${{ parameters.provisionatorUri }} - provisioning_script: ${{ parameters.checkoutDirectory }}/${{ parameters.provisionatorXCodePath }} - provisioning_extra_args: ${{ parameters.provisionatorExtraArguments }} - github_token: ${{ parameters.gitHubToken }} - env: - PROVISIONATOR_CHANNEL: ${{ parameters.provisionatorChannel }} - script: | echo Remove old Xamarin Settings From 4c56f322be139769ba8f12c3c5b0bd375947dce6 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 1 Feb 2026 13:40:02 +0100 Subject: [PATCH 038/126] Update pr-review-prompt.md --- eng/pipelines/prompts/pr-review-prompt.md | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/eng/pipelines/prompts/pr-review-prompt.md b/eng/pipelines/prompts/pr-review-prompt.md index cfb8235052ed..562fbd20ed42 100644 --- a/eng/pipelines/prompts/pr-review-prompt.md +++ b/eng/pipelines/prompts/pr-review-prompt.md @@ -2,18 +2,31 @@ Review PR #${PR_NUMBER} Follow the 5-phase PR Agent workflow, but leverage the existing agent review state: -1. **Import prior agent state** instead of re-doing completed phases -2. **Verify the Gate phase** empirically (run tests to confirm FAIL without fix, PASS with fix) -3. **Phase 4 (Fix)** - EXHAUSTIVE exploration: +1. **Phase 1: Gate** - Run tests FIRST. If gate fails, STOP IMMEDIATELY. Do not proceed. +2. **Phase 2: Pre-Flight** - Import prior agent state instead of re-doing completed phases +3. **Phase 3: Tests** - Verify reproduction tests exist +4. **Phase 4: Fix** - EXHAUSTIVE exploration: - Consult 5+ different AI models for diverse fix ideas - Run try-fix skill with Opus 4.5 for EACH unique idea - Keep iterating until completely out of alternatives - Compare ALL candidates to determine best approach -4. **Phase 5 (Report)** - Generate final recommendation with full comparison +5. **Phase 5: Report** - Generate final recommendation with full comparison ## Work Plan -### Phase 1: Pre-Flight (Context Gathering) +### Phase 1: Gate (Test Verification) - MUST PASS FIRST ⛔ +**THIS IS A BLOCKING GATE - If tests don't behave correctly, STOP ALL WORK IMMEDIATELY.** + +- [ ] Run verification script with `-RequireFullVerification`: + ```bash + pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -RequireFullVerification + ``` +- [ ] Confirm tests FAIL without fix (bug reproduced) +- [ ] Confirm tests PASS with fix (bug fixed) +- [ ] **IF GATE FAILS**: Stop immediately, do not proceed to any other phase. Report failure and exit. +- [ ] Mark Gate ✅ PASSED (or ❌ FAILED and STOP) + +### Phase 2: Pre-Flight (Context Gathering) - [ ] Checkout PR branch (`pr-33687`) - [ ] Gather PR metadata (title, body, labels, files) - [ ] Read linked issue #19256 @@ -22,7 +35,7 @@ Follow the 5-phase PR Agent workflow, but leverage the existing agent review sta - [ ] Create local state file importing prior agent's findings - [ ] Mark Pre-Flight COMPLETE -### Phase 2: Tests (Verify Reproduction Tests Exist) +### Phase 3: Tests (Verify Reproduction Tests Exist) - [ ] Confirm PR includes UI tests (already present per file list) - [ ] Verify test file locations: - HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs` @@ -30,15 +43,6 @@ Follow the 5-phase PR Agent workflow, but leverage the existing agent review sta - [ ] Verify tests follow naming convention (`Issue19256`) - [ ] Mark Tests COMPLETE -### Phase 3: Gate (Test Verification) - MUST PASS -- [ ] Run verification script with `-RequireFullVerification`: - ```bash - pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -RequireFullVerification - ``` -- [ ] Confirm tests FAIL without fix (bug reproduced) -- [ ] Confirm tests PASS with fix (bug fixed) -- [ ] Mark Gate PASSED (or FAILED if tests don't behave correctly) - ### Phase 4: Fix (EXHAUSTIVE Independent Analysis) **Goal:** Explore ALL possible alternative solutions until no more ideas remain. From 18f36da4f24bce2a1da4334efeece3f7ba7f6cbf Mon Sep 17 00:00:00 2001 From: Shane Date: Sun, 1 Feb 2026 07:20:38 -0600 Subject: [PATCH 039/126] Update ci-copilot.yml to use Review-PR.ps1 script - Replace direct copilot CLI invocation with Review-PR.ps1 - Remove prompt file caching (script has built-in prompt) - Remove cherry-pick step (script handles PR merge) - Use -NoInteractive for CI mode - Retain token handling (GITHUB_TOKEN: COPILOT_TOKEN) --- eng/pipelines/ci-copilot.yml | 82 +++--------------------------------- 1 file changed, 7 insertions(+), 75 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d35dcfe20a54..8efc021cc523 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -50,21 +50,6 @@ stages: echo "PR Number: ${{ parameters.PRNumber }}" displayName: 'Validate Parameters' - # Load prompt file before checking out PR branch (file may not exist on PR branch) - - script: | - echo "Loading prompt file..." - PROMPT_FILE="eng/pipelines/prompts/pr-review-prompt.md" - if [ ! -f "$PROMPT_FILE" ]; then - echo "##vso[task.logissue type=error]Prompt file not found: $PROMPT_FILE" - exit 1 - fi - - # Copy prompt to a safe location before branch checkout - mkdir -p /tmp/copilot-prompts - cp "$PROMPT_FILE" /tmp/copilot-prompts/pr-review-prompt.md - echo "Prompt file cached to /tmp/copilot-prompts/pr-review-prompt.md" - displayName: 'Cache Prompt File' - # Provision environment (Xcode, .NET SDK, Android SDK, etc.) - template: common/provision.yml parameters: @@ -218,74 +203,21 @@ stages: displayName: 'Install GitHub Copilot CLI' - script: | - echo "Fetching PR #${{ parameters.PRNumber }} changes..." - if ! git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }}; then - echo "##vso[task.logissue type=error]Failed to fetch PR #${{ parameters.PRNumber }}. Check that the PR exists." - exit 1 - fi - - # Get the merge base between current branch and PR branch - CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - echo "Current branch: $CURRENT_BRANCH" - - MERGE_BASE=$(git merge-base HEAD pr-${{ parameters.PRNumber }}) - echo "Merge base: $MERGE_BASE" - - # Get the list of commits in the PR (from merge-base to PR head) - PR_COMMITS=$(git rev-list --reverse $MERGE_BASE..pr-${{ parameters.PRNumber }}) - COMMIT_COUNT=$(echo "$PR_COMMITS" | grep -c . || echo "0") - echo "Found $COMMIT_COUNT commit(s) to cherry-pick" - - if [ "$COMMIT_COUNT" -eq "0" ]; then - echo "##vso[task.logissue type=warning]No commits found in PR #${{ parameters.PRNumber }} relative to current branch" - echo "PR may already be merged or branch is up to date" - else - # Cherry-pick each commit from the PR onto the current branch - echo "Cherry-picking PR commits onto $CURRENT_BRANCH..." - for COMMIT in $PR_COMMITS; do - echo "Cherry-picking commit: $(git log -1 --oneline $COMMIT)" - if ! git cherry-pick --no-commit $COMMIT; then - echo "##vso[task.logissue type=error]Failed to cherry-pick commit $COMMIT" - echo "Attempting to show conflict details..." - git status - git diff --name-only --diff-filter=U - exit 1 - fi - done - - echo "Successfully applied $COMMIT_COUNT commit(s) from PR #${{ parameters.PRNumber }}" - fi - - echo "Current state:" - git log -1 --oneline - git status --short - displayName: 'Cherry-pick PR Changes' - - - script: | - echo "Running Copilot PR Reviewer Agent..." + echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - # Load prompt from cached file (saved before PR checkout) - PROMPT_FILE="/tmp/copilot-prompts/pr-review-prompt.md" - if [ ! -f "$PROMPT_FILE" ]; then - echo "##vso[task.logissue type=error]Cached prompt file not found: $PROMPT_FILE" - exit 1 - fi - - # Read prompt and replace placeholder with actual PR number - PROMPT=$(sed "s/\${PR_NUMBER}/${{ parameters.PRNumber }}/g" "$PROMPT_FILE") - - # Invoke the PR reviewer agent using Copilot CLI in programmatic mode - # Capture exit code to check for failures + # Invoke the PR reviewer using our PowerShell script + # The script will merge the PR into the current branch + # -NoInteractive for CI mode (exits after completion) set +e - copilot --agent pr -p "$PROMPT" --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -NoInteractive 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md COPILOT_EXIT_CODE=$? set -e - echo "Copilot exit code: $COPILOT_EXIT_CODE" + echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" # Copy any Copilot session files if [ -d "$HOME/.copilot" ]; then @@ -310,7 +242,7 @@ stages: # Check for failure indicators in output if [ $COPILOT_EXIT_CODE -ne 0 ]; then - echo "##vso[task.logissue type=error]Copilot CLI exited with code $COPILOT_EXIT_CODE" + echo "##vso[task.logissue type=error]Review-PR.ps1 exited with code $COPILOT_EXIT_CODE" # Don't exit yet - let artifacts be published first echo "##vso[task.setvariable variable=CopilotFailed]true" fi From 5fd118dcec4d7f31ce2d37ccb1aa92dc840a2e31 Mon Sep 17 00:00:00 2001 From: Shane Date: Sun, 1 Feb 2026 08:16:08 -0600 Subject: [PATCH 040/126] Add Android/JDK provisioning steps to provision.yml The copilot-ci branch was missing Android provisioning parameters and steps that exist in the main branch. This caused Android builds to silently skip JDK and Android SDK provisioning, resulting in Android tests not being able to run. Added: - skipJdk, skipAndroidCommonSdks, skipAndroidPlatformApis parameters - skipAndroidEmulatorImages, skipAndroidCreateAvds, androidEmulatorApiLevel parameters - Provision JDK step using Provisioning.csproj - Provision Android SDK common packages step - Provision Android SDK environment variables step - Provision Android SDK platform APIs step - Provision Android SDK emulator images step (disabled by default) - Provision Android SDK create AVDs step (disabled by default) --- eng/pipelines/common/provision.yml | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 8a712f43c02c..2b09582e2051 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -4,6 +4,16 @@ parameters: skipSimulatorSetup: true # .NET NuGet caches clearCaches: true + # Android - JDK + skipJdk: false + # Android - SDK Packages + skipAndroidCommonSdks: false + onlyAndroidPlatformDefaultApis: false + skipAndroidPlatformApis: false + # Android - Emulators + androidEmulatorApiLevel: '' + skipAndroidEmulatorImages: true # For most builds we won't need these + skipAndroidCreateAvds: true # For most builds we won't need these # Provisionator / Xcode skipProvisionator: true checkoutDirectory: $(System.DefaultWorkingDirectory) @@ -262,3 +272,56 @@ steps: - script: | dotnet tool restore --verbosity diag displayName: 'Restore .NET Tools' + +################################################## +# Provisioning Android # +################################################## + +# Provisioning Android - JDK +- ${{ if ne(parameters.skipJdk, 'true') }}: + - pwsh: dotnet build -t:ProvisionJdk -bl:"$(LogDirectory)/provision-jdk.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed + displayName: 'Provision JDK' + condition: succeeded() + +# Provisioning Android - Android SDK common packages (eg: cmdline-tools, emulator, platform-tools, build-tools) +- ${{ if ne(parameters.skipAndroidCommonSdks, 'true') }}: + - pwsh: dotnet build -t:ProvisionAndroidSdkCommonPackages -bl:"$(LogDirectory)/provision-androidsdk-common.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed + displayName: 'Provision Android SDK - Common Packages' + condition: succeeded() + +# Provisioning Android - Android environment variables +- ${{ if ne(parameters.skipAndroidCommonSdks, 'true') }}: + - pwsh: | + echo "Setting ANDROID_SDK_ROOT and ANDROID_HOME..." + $jsonOutput = & dotnet android sdk info --format=Json | ConvertFrom-Json + $preferredSdk = $jsonOutput.SdkInfo.Path + echo "##vso[task.setvariable variable=ANDROID_SDK_ROOT]$preferredSdk" + echo "ANDROID_SDK_ROOT set to '$preferredSdk'" + echo "##vso[task.setvariable variable=ANDROID_HOME]$preferredSdk" + echo "ANDROID_HOME set to '$preferredSdk'" + displayName: 'Provision Android SDK - Environment variables' + condition: succeeded() + +# Provisioning Android - Android SDK platform APIs (eg: platforms;android-29, platforms;android-30) +- ${{ if ne(parameters.skipAndroidPlatformApis, 'true') }}: + - pwsh: dotnet build -t:ProvisionAndroidSdkPlatformApiPackages -p:AndroidSdkProvisionDefaultApiLevelsOnly="$Env:AndroidSdkProvisionDefaultApiLevelsOnly" -bl:"$(LogDirectory)/provision-androidsdk-platform-apis.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed + displayName: 'Provision Android SDK - Platform APIs' + condition: succeeded() + env: + AndroidSdkProvisionDefaultApiLevelsOnly: ${{ parameters.onlyAndroidPlatformDefaultApis }} + +# Provisioning Android - Emulator images (eg: system-images;android-34;google_apis;aarch64) +- ${{ if ne(parameters.skipAndroidEmulatorImages, 'true') }}: + - pwsh: dotnet build -t:ProvisionAndroidSdkEmulatorImagePackages -p:AndroidSdkProvisionApiLevel="$Env:AndroidSdkProvisionApiLevel" -bl:"$(LogDirectory)/provision-androidsdk-emulator-images.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed + displayName: 'Provision Android SDK - Emulator Images' + condition: succeeded() + env: + AndroidSdkProvisionApiLevel: ${{ parameters.androidEmulatorApiLevel }} + +# Provisioning Android - Android AVDs (actual emulator virtual devices) +- ${{ if ne(parameters.skipAndroidCreateAvds, 'true') }}: + - pwsh: dotnet build -t:ProvisionAndroidSdkAvdCreateAvds -p:AndroidSdkProvisionApiLevel="$Env:AndroidSdkProvisionApiLevel" -bl:"$(LogDirectory)/provision-androidsdk-create-avds.binlog" ${{ parameters.checkoutDirectory }}/src/Provisioning/Provisioning.csproj -v:detailed + displayName: 'Provision Android SDK - Create AVDs' + condition: succeeded() + env: + AndroidSdkProvisionApiLevel: ${{ parameters.androidEmulatorApiLevel }} From 03a33c3aab8baf6ac72b8e318d356fd16fef78e9 Mon Sep 17 00:00:00 2001 From: Shane Date: Sun, 1 Feb 2026 13:24:15 -0600 Subject: [PATCH 041/126] Add Android emulator provisioning and boot step - Enable skipAndroidEmulatorImages: false to download emulator images - Enable skipAndroidCreateAvds: false to create AVD - Set androidEmulatorApiLevel: 34 for API level 34 emulator - Add step to boot Android emulator using Cake script before Copilot runs This should fix the issue where Android UI tests couldn't run because no emulator was available. --- eng/pipelines/ci-copilot.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 8efc021cc523..c5535c51acc1 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -60,6 +60,10 @@ stages: skipJdk: false skipSimulatorSetup: false skipCertificates: true + # Android emulator setup + skipAndroidEmulatorImages: false + skipAndroidCreateAvds: false + androidEmulatorApiLevel: '34' # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic @@ -153,7 +157,17 @@ stages: echo "Tools restored successfully" displayName: 'Restore .NET Tools' - + # Boot Android emulator for UI tests + - pwsh: | + Write-Host "=== Booting Android Emulator ===" + # Use the cake script to boot the emulator + ./build.ps1 -Script eng/devices/android.cake --target=boot --device="android-emulator-64_34" --verbosity=diagnostic + displayName: 'Boot Android Emulator' + continueOnError: true + timeoutInMinutes: 10 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) - script: | echo "Installing Node.js 22..." From 3ed6c2f2b33549bad9ade1b1a712cd6c57ba9a17 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 00:27:28 +0100 Subject: [PATCH 042/126] It should work --- .github/scripts/shared/Start-Emulator.ps1 | 35 ++-- eng/pipelines/ci-copilot.yml | 52 +++++- eng/pipelines/common/provision.yml | 192 ---------------------- 3 files changed, 72 insertions(+), 207 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 35e0219df1b9..9dbea5ad06dd 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -188,21 +188,36 @@ if ($Platform -eq "android") { Write-Info "Starting emulator: $selectedAvd" Write-Info "This may take 1-2 minutes..." - # Use swiftshader for software rendering (more reliable on CI without GPU) - # Redirect output to a log file for debugging - $emulatorLog = Join-Path ([System.IO.Path]::GetTempPath()) "emulator-$selectedAvd.log" - + # CRITICAL: Must use nohup to properly detach emulator process + # This prevents STDIO stream inheritance issues in CI environments if ($IsWindows) { Start-Process $emulatorBin -ArgumentList "-avd", $selectedAvd, "-no-snapshot-load", "-no-boot-anim", "-gpu", "swiftshader_indirect" -WindowStyle Hidden } else { - # macOS/Linux: Use nohup to detach from terminal - # Use -no-snapshot (not -no-snapshot-load) to ensure clean emulator state for CI/testing. - # This disables both snapshot load and save, so each boot is a cold boot. - # Trade-off: slower boots, but guarantees no stale state between test runs. - $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" + # macOS/Linux: Use nohup to fully detach the emulator process + # This ensures the process doesn't inherit STDIO streams and can run independently + $androidHome = $env:ANDROID_HOME + if (-not $androidHome) { + $androidHome = $env:ANDROID_SDK_ROOT + } + if (-not $androidHome) { + $androidHome = "$env:HOME/Library/Android/sdk" + } + + $emulatorBin = Join-Path $androidHome "emulator/emulator" + + if (-not (Test-Path $emulatorBin)) { + Write-Error "Emulator binary not found at: $emulatorBin" + Write-Info "Please ensure ANDROID_HOME or ANDROID_SDK_ROOT is set correctly." + exit 1 + } + + # Use nohup to fully detach the emulator process from the terminal + # Redirect all output to /dev/null to prevent STDIO inheritance issues + $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" bash -c $startScript - Write-Info "Emulator started in background. Log file: $emulatorLog" + + Write-Info "Emulator started in background (fully detached with nohup)" } # Give the emulator process time to start diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index c5535c51acc1..4fc3dd262f3c 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -158,13 +158,38 @@ stages: displayName: 'Restore .NET Tools' # Boot Android emulator for UI tests - - pwsh: | - Write-Host "=== Booting Android Emulator ===" - # Use the cake script to boot the emulator - ./build.ps1 -Script eng/devices/android.cake --target=boot --device="android-emulator-64_34" --verbosity=diagnostic + - script: | + echo "=== Booting Android Emulator ===" + + # Start emulator in background, detached from terminal + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & + EMULATOR_PID=$! + echo "Emulator started with PID: $EMULATOR_PID" + + # Wait for device to appear + echo "Waiting for emulator device..." + adb wait-for-device + + # Wait for boot_completed + echo "Waiting for emulator to finish booting..." + timeout=120 + waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + sleep 2 + waited=$((waited + 2)) + echo "Waiting for boot... ($waited/$timeout seconds)" + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not boot in time" + adb devices -l + exit 1 + fi + done + + echo "=== Emulator booted successfully! ===" + adb devices -l displayName: 'Boot Android Emulator' continueOnError: true - timeoutInMinutes: 10 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) @@ -181,6 +206,23 @@ stages: echo "Node.js installed successfully" displayName: 'Install Node.js' + - script: | + echo "Installing Appium and UiAutomator2 driver..." + npm install -g appium + appium driver install uiautomator2 + + # Verify installation + if ! which appium; then + echo "##vso[task.logissue type=error]Failed to install Appium" + exit 1 + fi + + echo "Appium version: $(appium --version)" + echo "Appium drivers installed:" + appium driver list --installed + echo "Appium installed successfully" + displayName: 'Install Appium' + - script: | echo "Installing GitHub CLI..." brew install gh diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 2b09582e2051..36cd3105d0e0 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -52,198 +52,6 @@ steps: expiryInHours: ${{ parameters.expiryInHours }} base64Encode: ${{ parameters.base64Encode }} -################################################## -# Provisioning macOS # -################################################## - -#check if the _tool directory exists and remove it -- pwsh: | - $dotnetToolsPath = "$(Agent.ToolsDirectory)/dotnet" - Write-Host "Agent-Cleanser: Agent tools path for .NET installations: ${dotnetToolsPath}" - try { - if ([IO.Directory]::Exists($dotnetToolsPath)) { - Write-Host "Agent-Cleanser: Deleting: ${dotnetToolsPath}" - [IO.Directory]::Delete($dotnetToolsPath, $true) - } else { - Write-Host "Agent-Cleanser: Skipping cleansing of .NET: No .NET installations found under the agent tools directory: $(Agent.ToolsDirectory)" - } - } catch { - Write-Host "ERROR: EXCEPTION: $($_.Exception.Message)" - } - displayName: Clean $(Agent.ToolsDirectory)/dotnet directory - condition: eq(variables['Agent.OS'], 'Darwin') - -# Provisioning macOS - Xcode -- ${{ if ne(parameters.skipXcode, 'true') }}: - - - script: | - echo Remove old Xamarin Settings - rm -f ~/Library/Preferences/Xamarin/Settings.plist - rm -f ~/Library/Preferences/maui/Settings.plist - echo Mac OS version: - sw_vers -productVersion - echo - echo Installed Xcode versions: - XCODE_LIST=($(ls -1 /Applications | grep '^Xcode_' | sed 's/^Xcode_//;s/.app$//')) - for v in "${XCODE_LIST[@]}"; do echo " $v"; done - echo - echo currently selected xcode: - xcrun xcode-select --print-path - echo - echo selecting latest xcode... - # Use REQUIRED_XCODE for devdiv team project or if CollectionUri contains xamarin, XCODE for others (e.g., maui-pr) - if [[ "$(System.TeamProject)" == "devdiv" || "$(System.CollectionUri)" == *"xamarin"* ]]; then - XCODE_VERSION=$(REQUIRED_XCODE) - else - XCODE_VERSION=$(XCODE) - fi - - # Check if the specified Xcode version exists, if not try fallbacks - # Fallback order: exact version -> major.minor -> major - XCODE_PATH="" - ORIGINAL_VERSION="$XCODE_VERSION" - - # Build list of versions to try - VERSIONS_TO_TRY=("$XCODE_VERSION") - - # Add major.minor fallback (e.g., 26.0.1 -> 26.0) - if [[ "$XCODE_VERSION" =~ ^([0-9]+\.[0-9]+)\.[0-9]+$ ]]; then - VERSIONS_TO_TRY+=("${BASH_REMATCH[1]}") - fi - - # Add major fallback (e.g., 26.0.1 or 26.0 -> 26) - if [[ "$XCODE_VERSION" =~ ^([0-9]+)\. ]]; then - VERSIONS_TO_TRY+=("${BASH_REMATCH[1]}") - fi - - echo "Will try Xcode versions in order: ${VERSIONS_TO_TRY[*]}" - - for VERSION in "${VERSIONS_TO_TRY[@]}"; do - CANDIDATE_PATH="/Applications/Xcode_${VERSION}.app" - echo "Checking for Xcode ${VERSION} at ${CANDIDATE_PATH}..." - if [[ -d "$CANDIDATE_PATH" ]]; then - echo "Found Xcode version ${VERSION} at ${CANDIDATE_PATH}" - XCODE_VERSION="$VERSION" - XCODE_PATH="$CANDIDATE_PATH" - break - else - echo "Xcode version ${VERSION} not found" - fi - done - - if [[ -z "$XCODE_PATH" ]]; then - echo "ERROR: No suitable Xcode version found for requested version ${ORIGINAL_VERSION}" - echo "Tried: ${VERSIONS_TO_TRY[*]}" - exit 0 - fi - - sudo xcode-select -s "$XCODE_PATH" - xcrun xcode-select --print-path - xcodebuild -version - sudo xcodebuild -license accept - - sudo xcodebuild -runFirstLaunch - displayName: Select Xcode Version - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - timeoutInMinutes: 30 - - - ${{ if ne(parameters.skipSimulatorSetup, 'true') }}: - - script: | - echo installing simulator runtimes... - # Use REQUIRED_XCODE for devdiv team project or if CollectionUri contains xamarin, XCODE for others (e.g., maui-pr) - if [[ "$(System.TeamProject)" == "devdiv" || "$(System.CollectionUri)" == *"xamarin"* ]]; then - XCODE_VERSION=$(REQUIRED_XCODE) - else - XCODE_VERSION=$(XCODE) - fi - # if we're using Xcode 26.0[.?], then explicitly install the iOS 26.0 simulator (the iOS 26.0.1 simulator doesn't work for us) - # also install the universal simulator version, so that this bot can run x64 apps in the simulator. - if [[ "${XCODE_VERSION}" =~ ^26[.]0.*$ ]]; then - RC=0 - sudo xcodebuild -downloadPlatform iOS -architectureVariant universal -buildVersion 26.0 || RC=$? - if [[ "$RC" == "70" ]]; then - echo "xcodebuild exited with exit code 70, which seems to mean not all is bad? Assuming things will work..." - elif [[ "$RC" == "0" ]]; then - echo "Successfully installed the requested iOS simulator" - else - echo "Failed to install simulator runtime, deleting all simulator runtimes and trying again..." - sudo xcrun simctl runtime delete all - # simulator runtimes are deleted asynchronously, so wait until they're all gone (but max 60 seconds) - for i in $(seq 1 60); do - sleep 1 - C=$(xcrun simctl runtime list -j | jq '. | length') - if [[ $C == 0 ]]; then - echo " still $C simulators left..." - break - fi - done - echo "Re-trying simulator runtime installation, if this doesn't work, a reboot might be required" - sudo xcodebuild -downloadPlatform iOS -architectureVariant universal -buildVersion 26.0 - fi - else - sudo xcodebuild -downloadPlatform iOS - fi - - # Create iPhone Xs simulator with iOS 18.5 (required for UI tests) - echo "" - echo "=== Creating iPhone Xs simulator with iOS 18.5 ===" - - # Find iOS 18.5 runtime (or fallback to iOS 18.x) - IOS_RUNTIME=$(xcrun simctl list runtimes -j | jq -r '.runtimes[] | select(.name | contains("iOS 18.5")) | .identifier' | head -1) - if [[ -z "$IOS_RUNTIME" ]]; then - IOS_RUNTIME=$(xcrun simctl list runtimes -j | jq -r '.runtimes[] | select(.name | contains("iOS 18")) | .identifier' | head -1) - fi - - if [[ -n "$IOS_RUNTIME" ]]; then - echo "Found iOS runtime: $IOS_RUNTIME" - IPHONE_XS_TYPE=$(xcrun simctl list devicetypes -j | jq -r '.devicetypes[] | select(.name == "iPhone Xs") | .identifier' | head -1) - - if [[ -n "$IPHONE_XS_TYPE" ]]; then - # Check if already exists - EXISTING=$(xcrun simctl list devices -j | jq -r --arg rt "$IOS_RUNTIME" '.devices[$rt][]? | select(.name == "iPhone Xs") | .udid' | head -1) - if [[ -n "$EXISTING" ]]; then - echo "iPhone Xs simulator already exists: $EXISTING" - else - UDID=$(xcrun simctl create "iPhone Xs" "$IPHONE_XS_TYPE" "$IOS_RUNTIME" 2>&1) && \ - echo "Created iPhone Xs simulator: $UDID" || \ - echo "Note: Could not create iPhone Xs simulator" - fi - else - echo "Note: iPhone Xs device type not available" - fi - else - echo "Note: iOS 18.x runtime not found" - fi - - echo "" - echo "=== Available iPhone simulators ===" - xcrun simctl list devices available | grep -i iphone | head -10 - displayName: Install Simulator Runtimes - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - continueOnError: true - timeoutInMinutes: 30 - -# Provision Additional Software -- ${{ if ne(parameters.skipCertificates, 'true') }}: - # Prepare macOS - - task: InstallAppleCertificate@2 - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - displayName: 'Install Apple Certificate ${{ parameters.certName }}' - continueOnError: true - inputs: - certSecureFile: '${{ parameters.certName }}' - certPwd: ${{ parameters.certPass }} - keychain: 'temp' - opensslPkcsArgs: '${{ parameters.openSslArgs }}' - - - task: InstallAppleProvisioningProfile@1 - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - displayName: 'Install Apple Provisioning Profile ${{ parameters.provisioningProfileName }}' - continueOnError: true - inputs: - provisioningProfileLocation: 'secureFiles' - provProfileSecureFile: '${{ parameters.provisioningProfileName }}' - ################################################## # Provisioning .NET # ################################################## From d75e77f9b9ac4a4cb45c882fcad58ff649c535f5 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 01:45:28 +0100 Subject: [PATCH 043/126] Enable Android emulators and set API level to 34 Update CI provisioning defaults to enable Android emulator setup: set androidEmulatorApiLevel to '34', and change skipAndroidEmulatorImages and skipAndroidCreateAvds from true to false so emulator images are fetched and AVDs are created during provisioning. This allows pipelines that require Android emulators to run without additional overrides. --- eng/pipelines/common/provision.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 36cd3105d0e0..35b38e66e76e 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -11,9 +11,9 @@ parameters: onlyAndroidPlatformDefaultApis: false skipAndroidPlatformApis: false # Android - Emulators - androidEmulatorApiLevel: '' - skipAndroidEmulatorImages: true # For most builds we won't need these - skipAndroidCreateAvds: true # For most builds we won't need these + androidEmulatorApiLevel: '34' + skipAndroidEmulatorImages: false # For most builds we won't need these + skipAndroidCreateAvds: false # For most builds we won't need these # Provisionator / Xcode skipProvisionator: true checkoutDirectory: $(System.DefaultWorkingDirectory) From ffe9ebffc42a84b0f00ff1996a667fd69e3821e6 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 12:29:42 +0100 Subject: [PATCH 044/126] Improve ADB reliability and Appium setup Add ADB restart/retry and error handling to Android build and emulator scripts to mitigate intermittent "Broken pipe"/ADB install failures: Build-AndDeploy.ps1 now retries dotnet build (with maxRetries=2), restarts ADB, verifies device connectivity and inspects build output for ADB-related errors before retrying; Start-Emulator.ps1 now restarts ADB up front and verifies connectivity after boot. Add a Write-Warning helper in shared-utils.ps1 for consistent warning logging. Update ci-copilot.yml to install Appium and the UiAutomator2 driver in CI, expose npm global bin on PATH, and prepend it for subsequent pipeline steps so Appium is available. --- .github/scripts/shared/Build-AndDeploy.ps1 | 43 ++++++++++++++++++++-- .github/scripts/shared/Start-Emulator.ps1 | 42 +++++++++++++++------ .github/scripts/shared/shared-utils.ps1 | 2 +- eng/pipelines/ci-copilot.yml | 15 ++++++++ 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index 677c22cc6b0f..bed1ebaf582b 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -85,11 +85,46 @@ if ($Platform -eq "android") { Write-Info "Build command: dotnet build $($buildArgs -join ' ')" $buildStartTime = Get-Date + $maxRetries = 2 + $retryCount = 0 + $buildExitCode = 1 + + while ($retryCount -lt $maxRetries -and $buildExitCode -ne 0) { + if ($retryCount -gt 0) { + Write-Info "Retry attempt $retryCount of $($maxRetries - 1)..." + Write-Info "Restarting ADB server before retry..." + adb kill-server 2>$null + Start-Sleep -Seconds 2 + adb start-server 2>$null + Start-Sleep -Seconds 3 + + # Verify device is still connected + $deviceCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 + if ($deviceCheck -notmatch "ping") { + Write-Error "Device $DeviceUdid not responding after ADB restart" + exit 1 + } + Write-Success "ADB connection restored, retrying build..." + } + + # Build and deploy in one step (Run target handles both) + $buildOutput = & dotnet build @buildArgs 2>&1 + $buildOutput | ForEach-Object { Write-Host $_ } + + $buildExitCode = $LASTEXITCODE + $retryCount++ + + # Check for broken pipe error - retry if found + if ($buildExitCode -ne 0) { + $brokenPipe = $buildOutput | Select-String -Pattern "Broken pipe|ADB0010|InstallFailedException" -Quiet + if (-not $brokenPipe) { + # Not a broken pipe error, don't retry + break + } + Write-Warning "Detected ADB broken pipe error, will attempt recovery..." + } + } - # Build and deploy in one step (Run target handles both) - & dotnet build @buildArgs - - $buildExitCode = $LASTEXITCODE $buildDuration = (Get-Date) - $buildStartTime if ($buildExitCode -ne 0) { diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 9dbea5ad06dd..5135b93265a1 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -50,18 +50,16 @@ if ($Platform -eq "android") { exit 1 } - # Get Android SDK path - $androidSdkRoot = $env:ANDROID_SDK_ROOT - if (-not $androidSdkRoot) { - $androidSdkRoot = $env:ANDROID_HOME - } - if (-not $androidSdkRoot) { - $androidSdkRoot = "$env:HOME/Library/Android/sdk" - } - - # Track which AVD to boot (may be set from DeviceUdid parameter if it's an AVD name) - $selectedAvd = $null + # Restart ADB server to ensure clean connection state + # This prevents "Broken pipe" errors from stale connections + Write-Info "Restarting ADB server for clean connection..." + adb kill-server 2>$null + Start-Sleep -Seconds 2 + adb start-server 2>$null + Start-Sleep -Seconds 2 + Write-Success "ADB server restarted" + # Get device UDID if not provided OR if it's an AVD name that needs to be booted # Check if DeviceUdid is an AVD name (not an emulator-XXXX format) if ($DeviceUdid -and $DeviceUdid -notmatch "^emulator-\d+$") { # DeviceUdid is likely an AVD name - check if it's in the AVD list @@ -341,6 +339,28 @@ if ($Platform -eq "android") { } } + # Verify ADB connectivity is healthy (prevents "Broken pipe" errors during build) + Write-Info "Verifying ADB connectivity to device..." + $connectivityCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 + if ($connectivityCheck -notmatch "ping") { + Write-Info "ADB connectivity issue detected, restarting ADB server..." + adb kill-server 2>$null + Start-Sleep -Seconds 2 + adb start-server 2>$null + Start-Sleep -Seconds 3 + + # Retry connectivity check + $retryCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 + if ($retryCheck -notmatch "ping") { + Write-Error "ADB connectivity failed after restart. Device may need to be rebooted." + Write-Info "Try: adb -s $DeviceUdid reboot" + exit 1 + } + Write-Success "ADB connectivity restored after restart" + } else { + Write-Success "ADB connectivity verified" + } + Write-Success "Using Android device: $DeviceUdid" #endregion diff --git a/.github/scripts/shared/shared-utils.ps1 b/.github/scripts/shared/shared-utils.ps1 index 21477d0d09bb..9db166521ad4 100644 --- a/.github/scripts/shared/shared-utils.ps1 +++ b/.github/scripts/shared/shared-utils.ps1 @@ -24,7 +24,7 @@ function Write-Success { Write-Host "✅ $Message" -ForegroundColor Green } -function Write-Warn { +function Write-Warning { param([string]$Message) Write-Host "⚠️ $Message" -ForegroundColor Yellow } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 4fc3dd262f3c..2eff1fe5e15b 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -208,7 +208,16 @@ stages: - script: | echo "Installing Appium and UiAutomator2 driver..." + + # Get npm global bin directory and add to PATH + NPM_BIN=$(npm config get prefix)/bin + echo "NPM global bin directory: $NPM_BIN" + export PATH="$NPM_BIN:$PATH" + + # Install Appium globally npm install -g appium + + # Install UiAutomator2 driver for Android appium driver install uiautomator2 # Verify installation @@ -217,9 +226,15 @@ stages: exit 1 fi + APPIUM_PATH=$(which appium) + echo "Appium path: $APPIUM_PATH" echo "Appium version: $(appium --version)" echo "Appium drivers installed:" appium driver list --installed + + # Export PATH for subsequent steps (Azure DevOps specific) + echo "##vso[task.prependpath]$NPM_BIN" + echo "Appium installed successfully" displayName: 'Install Appium' From 8a34f174bff90f0d24613ff0a38dd3f64b553db9 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 13:39:57 +0100 Subject: [PATCH 045/126] Add verify-tests-fail step to CI pipeline Add a PowerShell task to eng/pipelines/ci-copilot.yml that runs the verify-tests-fail.ps1 script for Android to confirm UI tests fail before applying a fix. The step calls .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 with the PRNumber parameter, logs the exit code, emits a VSO warning on nonzero exit, and is configured with continueOnError: true, a 30-minute timeout, and ANDROID_SDK_ROOT / JAVA_HOME_17_X64 environment variables. --- eng/pipelines/ci-copilot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 2eff1fe5e15b..441df32f0fe5 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -194,6 +194,25 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) + # Verify UI tests fail without fix (on Android emulator) + - pwsh: | + Write-Host "=== Verifying UI Tests Fail Without Fix ===" + + # Run the verify-tests-fail script on Android + $result = & pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -PRNumber ${{ parameters.PRNumber }} + + Write-Host "Verification script completed with exit code: $LASTEXITCODE" + + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $LASTEXITCODE" + } + displayName: 'Verify UI Tests Fail Without Fix' + continueOnError: true + timeoutInMinutes: 30 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + - script: | echo "Installing Node.js 22..." brew install node@22 From 789eda37515a2d2c8e48bcd1c84c6ed4ac1aff37 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 13:41:26 +0100 Subject: [PATCH 046/126] Update ci-copilot.yml --- eng/pipelines/ci-copilot.yml | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 441df32f0fe5..d7bbd7e7c25d 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -157,42 +157,42 @@ stages: echo "Tools restored successfully" displayName: 'Restore .NET Tools' - # Boot Android emulator for UI tests - - script: | - echo "=== Booting Android Emulator ===" - - # Start emulator in background, detached from terminal - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & - EMULATOR_PID=$! - echo "Emulator started with PID: $EMULATOR_PID" - - # Wait for device to appear - echo "Waiting for emulator device..." - adb wait-for-device - - # Wait for boot_completed - echo "Waiting for emulator to finish booting..." - timeout=120 - waited=0 - while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do - sleep 2 - waited=$((waited + 2)) - echo "Waiting for boot... ($waited/$timeout seconds)" - if [ $waited -ge $timeout ]; then - echo "##vso[task.logissue type=error]Emulator did not boot in time" - adb devices -l - exit 1 - fi - done - - echo "=== Emulator booted successfully! ===" - adb devices -l - displayName: 'Boot Android Emulator' - continueOnError: true - timeoutInMinutes: 15 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) + # # Boot Android emulator for UI tests + # - script: | + # echo "=== Booting Android Emulator ===" + + # # Start emulator in background, detached from terminal + # nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & + # EMULATOR_PID=$! + # echo "Emulator started with PID: $EMULATOR_PID" + + # # Wait for device to appear + # echo "Waiting for emulator device..." + # adb wait-for-device + + # # Wait for boot_completed + # echo "Waiting for emulator to finish booting..." + # timeout=120 + # waited=0 + # while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + # sleep 2 + # waited=$((waited + 2)) + # echo "Waiting for boot... ($waited/$timeout seconds)" + # if [ $waited -ge $timeout ]; then + # echo "##vso[task.logissue type=error]Emulator did not boot in time" + # adb devices -l + # exit 1 + # fi + # done + + # echo "=== Emulator booted successfully! ===" + # adb devices -l + # displayName: 'Boot Android Emulator' + # continueOnError: true + # timeoutInMinutes: 15 + # env: + # ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + # JAVA_HOME: $(JAVA_HOME_17_X64) # Verify UI tests fail without fix (on Android emulator) - pwsh: | From 50aee9f08533c0bec9d665c7cfaef626de6c5ac3 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 14:04:26 +0100 Subject: [PATCH 047/126] Add verbose Android emulator and test steps Improve CI diagnostics and robustness for Android UI tests by adding verbose wrapper steps and better error handling. Updated verify-tests-fail invocation to check script existence, capture and propagate exit codes, and emit detailed logs; added explicit steps to start the Android emulator, build & deploy the HostApp (finding emulator UDID via adb), and build-and-run HostApp with tests, each with verbose output, exit-code reporting, and timeouts. These changes surface failures and script exceptions to help debug flaky UI test runs. --- eng/pipelines/ci-copilot.yml | 138 ++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d7bbd7e7c25d..9d398fb28eed 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -197,15 +197,33 @@ stages: # Verify UI tests fail without fix (on Android emulator) - pwsh: | Write-Host "=== Verifying UI Tests Fail Without Fix ===" + Write-Host "Working directory: $(Get-Location)" + Write-Host "Script path: .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" + + # Check script exists + if (-not (Test-Path ".github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1")) { + Write-Host "##vso[task.logissue type=error]Script not found!" + exit 1 + } # Run the verify-tests-fail script on Android - $result = & pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -PRNumber ${{ parameters.PRNumber }} + # Use Invoke-Expression to run in same process and capture all output + $ErrorActionPreference = "Continue" + try { + & "./.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" -Platform android -PRNumber "${{ parameters.PRNumber }}" 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } - Write-Host "Verification script completed with exit code: $LASTEXITCODE" + Write-Host "Verification script completed with exit code: $exitCode" - if ($LASTEXITCODE -ne 0) { - Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $LASTEXITCODE" + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $exitCode" } + exit $exitCode displayName: 'Verify UI Tests Fail Without Fix' continueOnError: true timeoutInMinutes: 30 @@ -213,6 +231,118 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) + # Start Android Emulator (verbose logging) + - pwsh: | + Write-Host "=== Starting Android Emulator ===" + Write-Host "Working directory: $(Get-Location)" + + $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + exit 1 + } + + $ErrorActionPreference = "Continue" + try { + & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } + + Write-Host "Start-Emulator completed with exit code: $exitCode" + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=warning]Start-Emulator failed with code $exitCode" + } + exit $exitCode + displayName: 'Start Android Emulator (Verbose)' + continueOnError: true + timeoutInMinutes: 10 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + + # Build and Deploy HostApp (verbose logging) + - pwsh: | + Write-Host "=== Building and Deploying HostApp ===" + Write-Host "Working directory: $(Get-Location)" + + $scriptPath = ".github/scripts/shared/Build-AndDeploy.ps1" + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + exit 1 + } + + # Get emulator UDID + $udid = (adb devices | Select-String -Pattern "^(emulator-\d+)" | ForEach-Object { $_.Matches[0].Groups[1].Value } | Select-Object -First 1) + if (-not $udid) { + Write-Host "##vso[task.logissue type=error]No Android emulator found" + adb devices -l + exit 1 + } + Write-Host "Using emulator: $udid" + + $ErrorActionPreference = "Continue" + try { + & "./$scriptPath" -Platform android ` + -ProjectPath "src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj" ` + -TargetFramework "net10.0-android" ` + -DeviceUdid $udid ` + -Verbose 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } + + Write-Host "Build-AndDeploy completed with exit code: $exitCode" + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=warning]Build-AndDeploy failed with code $exitCode" + } + exit $exitCode + displayName: 'Build and Deploy HostApp (Verbose)' + continueOnError: true + timeoutInMinutes: 30 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + + # Build and Run HostApp with Tests (verbose logging) + - pwsh: | + Write-Host "=== Building and Running HostApp ===" + Write-Host "Working directory: $(Get-Location)" + + $scriptPath = ".github/scripts/BuildAndRunHostApp.ps1" + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + exit 1 + } + + $ErrorActionPreference = "Continue" + try { + & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } + + Write-Host "BuildAndRunHostApp completed with exit code: $exitCode" + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=warning]BuildAndRunHostApp failed with code $exitCode" + } + exit $exitCode + displayName: 'Build and Run HostApp (Verbose)' + continueOnError: true + timeoutInMinutes: 45 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + - script: | echo "Installing Node.js 22..." brew install node@22 From d464c4c5f761a0c39a0fa42d51786816a5b23694 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 15:36:04 +0100 Subject: [PATCH 048/126] Increase emulator timeout and adjust CI steps Increase Android emulator adb wait timeout from 120s to 600s and refactor CI pipeline steps: first pwsh step now runs .github/scripts/shared/Start-Emulator.ps1 with -Verbose (10 min timeout) and clearer missing-script logs; second step runs the verify-tests-fail script with the PRNumber parameter (30 min timeout). These changes reduce emulator startup flakiness and improve logging and separation of responsibilities in the CI job. --- .github/scripts/shared/Start-Emulator.ps1 | 7 +++- eng/pipelines/ci-copilot.yml | 48 +++++++++++------------ 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 5135b93265a1..744f036fd940 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -218,8 +218,11 @@ if ($Platform -eq "android") { Write-Info "Emulator started in background (fully detached with nohup)" } - # Give the emulator process time to start - Start-Sleep -Seconds 5 + # Wait for emulator to appear in adb devices + Write-Info "Waiting for emulator to start..." + $timeout = 600 + $elapsed = 0 + $emulatorStarted = $false # Check if emulator process is running if ($IsWindows) { diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 9d398fb28eed..1eb43f2eb890 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -194,23 +194,20 @@ stages: # ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) # JAVA_HOME: $(JAVA_HOME_17_X64) - # Verify UI tests fail without fix (on Android emulator) + # Start Android Emulator (verbose logging) - pwsh: | - Write-Host "=== Verifying UI Tests Fail Without Fix ===" + Write-Host "=== Starting Android Emulator ===" Write-Host "Working directory: $(Get-Location)" - Write-Host "Script path: .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" - # Check script exists - if (-not (Test-Path ".github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1")) { - Write-Host "##vso[task.logissue type=error]Script not found!" + $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" exit 1 } - # Run the verify-tests-fail script on Android - # Use Invoke-Expression to run in same process and capture all output $ErrorActionPreference = "Continue" try { - & "./.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" -Platform android -PRNumber "${{ parameters.PRNumber }}" 2>&1 | ForEach-Object { Write-Host $_ } + & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } $exitCode = $LASTEXITCODE } catch { Write-Host "##vso[task.logissue type=error]Exception: $_" @@ -218,33 +215,35 @@ stages: $exitCode = 1 } - Write-Host "Verification script completed with exit code: $exitCode" - + Write-Host "Start-Emulator completed with exit code: $exitCode" if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $exitCode" + Write-Host "##vso[task.logissue type=warning]Start-Emulator failed with code $exitCode" } exit $exitCode - displayName: 'Verify UI Tests Fail Without Fix' + displayName: 'Start Android Emulator (Verbose)' continueOnError: true - timeoutInMinutes: 30 + timeoutInMinutes: 10 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) - # Start Android Emulator (verbose logging) + # Verify UI tests fail without fix (on Android emulator) - pwsh: | - Write-Host "=== Starting Android Emulator ===" + Write-Host "=== Verifying UI Tests Fail Without Fix ===" Write-Host "Working directory: $(Get-Location)" + Write-Host "Script path: .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" - $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" - if (-not (Test-Path $scriptPath)) { - Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + # Check script exists + if (-not (Test-Path ".github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1")) { + Write-Host "##vso[task.logissue type=error]Script not found!" exit 1 } + # Run the verify-tests-fail script on Android + # Use Invoke-Expression to run in same process and capture all output $ErrorActionPreference = "Continue" try { - & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } + & "./.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" -Platform android -PRNumber "${{ parameters.PRNumber }}" 2>&1 | ForEach-Object { Write-Host $_ } $exitCode = $LASTEXITCODE } catch { Write-Host "##vso[task.logissue type=error]Exception: $_" @@ -252,14 +251,15 @@ stages: $exitCode = 1 } - Write-Host "Start-Emulator completed with exit code: $exitCode" + Write-Host "Verification script completed with exit code: $exitCode" + if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]Start-Emulator failed with code $exitCode" + Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $exitCode" } exit $exitCode - displayName: 'Start Android Emulator (Verbose)' + displayName: 'Verify UI Tests Fail Without Fix' continueOnError: true - timeoutInMinutes: 10 + timeoutInMinutes: 30 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From a503a97a37c54c98681c2fd9d904812d64f281b2 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 16:04:40 +0100 Subject: [PATCH 049/126] Make AVD list safe and add PR cherry-pick step Wraps emulator -list-avds output in an array in Start-Emulator.ps1 to avoid string indexing issues when a single AVD is returned. Adds a new pipeline step in eng/pipelines/ci-copilot.yml that fetches a PR, computes the merge-base against origin/main, enumerates commits in the PR, and cherry-picks them into the workspace (using --no-commit and a simple conflict resolution strategy) so CI can run tests against the PR changes. Includes git user config and status output to help debugging. --- .github/scripts/shared/Start-Emulator.ps1 | 24 +++++++++++++- eng/pipelines/ci-copilot.yml | 39 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 744f036fd940..93f51e76f458 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -99,7 +99,29 @@ if ($Platform -eq "android") { exit 1 } - # Get list of available AVDs (if not already set from parameter) + # Get list of available AVDs + # Force array even with single result to avoid string indexing issues + $avdList = @(emulator -list-avds) + + if (-not $avdList -or $avdList.Count -eq 0) { + Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio." + Write-Info "To create an AVD:" + Write-Info " 1. Open Android Studio" + Write-Info " 2. Go to Tools > Device Manager" + Write-Info " 3. Click 'Create Device' and follow the wizard" + exit 1 + } + + Write-Info "Available emulators: $($avdList -join ', ')" + + # Selection priority: + # 1. API 30 Nexus device + # 2. Any API 30 device + # 3. Any Nexus device + # 4. First available device + + # $selectedAvd may already be set if AVD name was provided + # Only run auto-selection if not already set if (-not $selectedAvd) { $avdList = emulator -list-avds 2>$null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 1eb43f2eb890..a558cad85a4d 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -227,6 +227,45 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) + # Cherry-pick PR changes to current branch + - script: | + echo "=== Cherry-picking PR #${{ parameters.PRNumber }} changes ===" + + # Configure git + git config user.email "copilot@microsoft.com" + git config user.name "GitHub Copilot" + + # Fetch the PR + echo "Fetching PR #${{ parameters.PRNumber }}..." + git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }} + + # Get the merge base between main and the PR + MERGE_BASE=$(git merge-base origin/main pr-${{ parameters.PRNumber }}) + echo "Merge base: $MERGE_BASE" + + # Get list of commits in the PR (from merge base to PR head) + COMMITS=$(git rev-list --reverse $MERGE_BASE..pr-${{ parameters.PRNumber }}) + COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ') + echo "Found $COMMIT_COUNT commits to cherry-pick" + + # Cherry-pick each commit + for COMMIT in $COMMITS; do + echo "Cherry-picking commit: $COMMIT" + git cherry-pick --no-commit $COMMIT || { + echo "##vso[task.logissue type=warning]Cherry-pick conflict for $COMMIT, attempting to continue..." + git checkout --theirs . 2>/dev/null || true + git add -A + } + done + + # Show what changed + echo "=== Files changed from PR ===" + git status --short + + echo "=== Cherry-pick complete ===" + displayName: 'Cherry-pick PR Changes' + continueOnError: false + # Verify UI tests fail without fix (on Android emulator) - pwsh: | Write-Host "=== Verifying UI Tests Fail Without Fix ===" From b4e4ead1f4761f799bdc6dd96ee30608cefa107e Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 16:30:32 +0100 Subject: [PATCH 050/126] Fix Android emulator start: add -no-window flag, use adb wait-for-device --- .github/scripts/shared/Start-Emulator.ps1 | 115 +++++----------------- 1 file changed, 26 insertions(+), 89 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 93f51e76f458..92a93c07cb4e 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -211,7 +211,7 @@ if ($Platform -eq "android") { # CRITICAL: Must use nohup to properly detach emulator process # This prevents STDIO stream inheritance issues in CI environments if ($IsWindows) { - Start-Process $emulatorBin -ArgumentList "-avd", $selectedAvd, "-no-snapshot-load", "-no-boot-anim", "-gpu", "swiftshader_indirect" -WindowStyle Hidden + Start-Process "emulator" -ArgumentList "-avd", $selectedAvd, "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim" -WindowStyle Hidden } else { # macOS/Linux: Use nohup to fully detach the emulator process @@ -233,132 +233,69 @@ if ($Platform -eq "android") { } # Use nohup to fully detach the emulator process from the terminal + # Include -no-window for headless CI environments # Redirect all output to /dev/null to prevent STDIO inheritance issues - $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" + $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" bash -c $startScript Write-Info "Emulator started in background (fully detached with nohup)" } - # Wait for emulator to appear in adb devices - Write-Info "Waiting for emulator to start..." - $timeout = 600 + # Use adb wait-for-device first (like the working bash script) + Write-Info "Waiting for emulator device to appear..." + adb wait-for-device + + # Wait for emulator to appear in adb devices with proper state + Write-Info "Waiting for emulator to be ready..." + $timeout = 120 $elapsed = 0 $emulatorStarted = $false - # Check if emulator process is running - if ($IsWindows) { - $emulatorProcs = (Get-Process -Name "emulator*","qemu*" -ErrorAction SilentlyContinue | - Where-Object { $_.CommandLine -match [regex]::Escape($selectedAvd) }).Id -join "`n" - } else { - $emulatorProcs = bash -c "pgrep -f 'qemu.*$selectedAvd' || pgrep -f 'emulator.*$selectedAvd' || true" 2>&1 - } - if ([string]::IsNullOrWhiteSpace($emulatorProcs)) { - Write-Error "Emulator process did not start. Checking log..." - if (Test-Path $emulatorLog) { - Get-Content $emulatorLog | Select-Object -Last 50 | ForEach-Object { Write-Info " $_" } - } - exit 1 - } - Write-Info "Emulator process started (PIDs: $emulatorProcs)" - - # Wait for device to appear with timeout - # Timeout of 120s (2 min) - if the emulator hasn't registered an ADB device by then, it's not going to - Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 120 - $deviceWaited = 0 - - while ($deviceWaited -lt $deviceTimeout) { - # Match any emulator device line - $devices = adb devices | Select-String "^emulator-\d+\s+device" + while ($elapsed -lt $timeout) { + $devices = adb devices | Select-String "emulator.*device$" if ($devices.Count -gt 0) { $DeviceUdid = ($devices[0].Line -split '\s+')[0] Write-Info "Emulator detected: $DeviceUdid" break } - # Check for offline state - $offlineDevices = adb devices | Select-String "^emulator-\d+\s+offline" - if ($offlineDevices.Count -gt 0) { - Write-Info "Device found but offline, waiting..." - } - - Start-Sleep -Seconds 5 - $deviceWaited += 5 + Start-Sleep -Seconds 2 + $elapsed += 2 - if ($deviceWaited % 30 -eq 0) { - Write-Info "Still waiting... ($deviceWaited seconds elapsed)" - # Show emulator log tail if taking too long - if ((Test-Path $emulatorLog)) { - Write-Info "Emulator log (last 5 lines):" - Get-Content $emulatorLog | Select-Object -Last 5 | ForEach-Object { Write-Info " $_" } - } + if ($elapsed % 10 -eq 0) { + Write-Info "Still waiting... ($elapsed seconds elapsed)" } } - if (-not $DeviceUdid) { - Write-Error "Emulator failed to start within $deviceTimeout seconds. Please try starting it manually." - Write-Info "Current adb devices:" + if (-not $emulatorStarted) { + Write-Error "Emulator failed to start within $timeout seconds. Please try starting it manually." adb devices -l - if (Test-Path $emulatorLog) { - Write-Info "Emulator log (last 30 lines):" - Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } - } exit 1 } - # Wait for boot to complete + # Wait for boot to complete (poll sys.boot_completed like bash script) Write-Info "Waiting for emulator to finish booting..." $bootTimeout = 600 $bootElapsed = 0 while ($bootElapsed -lt $bootTimeout) { - $bootStatus = adb -s $DeviceUdid shell getprop sys.boot_completed 2>$null + $bootStatus = adb shell getprop sys.boot_completed 2>$null if ($bootStatus -match "1") { Write-Success "Emulator fully booted: $DeviceUdid" break } - Start-Sleep -Seconds 5 - $bootElapsed += 5 + Start-Sleep -Seconds 2 + $bootElapsed += 2 - if ($bootElapsed % 30 -eq 0) { - Write-Info "Still booting... ($bootElapsed seconds elapsed)" + if ($bootElapsed % 10 -eq 0) { + Write-Info "Waiting for boot... ($bootElapsed/$bootTimeout seconds)" } } - if ($bootElapsed -ge $bootTimeout) { + if (-not $bootCompleted) { Write-Error "Emulator failed to complete boot within $bootTimeout seconds." - Write-Info "You can check status with: adb -s $DeviceUdid shell getprop sys.boot_completed" - if (Test-Path $emulatorLog) { - Write-Info "Emulator log (last 30 lines):" - Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } - } - exit 1 - } - - # Wait for package manager service to be available (critical for app installation) - Write-Info "Waiting for package manager service..." - $pmTimeout = 120 - $pmWaited = 0 - - while ($pmWaited -lt $pmTimeout) { - $pmOutput = adb -s $DeviceUdid shell pm list packages 2>$null - if ($pmOutput -match "package:") { - Write-Info "Package manager service is ready" - break - } - Start-Sleep -Seconds 3 - $pmWaited += 3 - if ($pmWaited % 15 -eq 0) { - Write-Info "Waiting for package manager... ($pmWaited seconds elapsed)" - } - } - - if ($pmWaited -ge $pmTimeout) { - Write-Error "Package manager service did not start within $pmTimeout seconds." - Write-Info "Checking services:" - adb -s $DeviceUdid shell service list 2>$null | Select-Object -First 20 | ForEach-Object { Write-Info " $_" } + adb devices -l exit 1 } } From 7c81b1e32ae01eeb0823f9cde464fd3330b6c01b Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 18:04:24 +0100 Subject: [PATCH 051/126] Simplify Android build and emulator scripts Streamline Android CI scripts: remove the build retry/ADB-restart loop in .github/scripts/shared/Build-AndDeploy.ps1 and invoke dotnet build once, preserving the exit code and duration. Rework .github/scripts/shared/Start-Emulator.ps1 to match the CI bash behavior: resolve Android SDK path, detect running emulator, start the emulator with nohup/bash (detached), wait for device availability and sys.boot_completed, and remove the previous extensive AVD-selection and ADB restart/connectivity recovery logic. These changes simplify flow and align Windows/macOS/Linux startup with CI expectations, reducing complexity and flakiness. --- .github/scripts/shared/Build-AndDeploy.ps1 | 43 +-- .github/scripts/shared/Start-Emulator.ps1 | 324 ++++----------------- 2 files changed, 67 insertions(+), 300 deletions(-) diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index bed1ebaf582b..677c22cc6b0f 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -85,46 +85,11 @@ if ($Platform -eq "android") { Write-Info "Build command: dotnet build $($buildArgs -join ' ')" $buildStartTime = Get-Date - $maxRetries = 2 - $retryCount = 0 - $buildExitCode = 1 - - while ($retryCount -lt $maxRetries -and $buildExitCode -ne 0) { - if ($retryCount -gt 0) { - Write-Info "Retry attempt $retryCount of $($maxRetries - 1)..." - Write-Info "Restarting ADB server before retry..." - adb kill-server 2>$null - Start-Sleep -Seconds 2 - adb start-server 2>$null - Start-Sleep -Seconds 3 - - # Verify device is still connected - $deviceCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 - if ($deviceCheck -notmatch "ping") { - Write-Error "Device $DeviceUdid not responding after ADB restart" - exit 1 - } - Write-Success "ADB connection restored, retrying build..." - } - - # Build and deploy in one step (Run target handles both) - $buildOutput = & dotnet build @buildArgs 2>&1 - $buildOutput | ForEach-Object { Write-Host $_ } - - $buildExitCode = $LASTEXITCODE - $retryCount++ - - # Check for broken pipe error - retry if found - if ($buildExitCode -ne 0) { - $brokenPipe = $buildOutput | Select-String -Pattern "Broken pipe|ADB0010|InstallFailedException" -Quiet - if (-not $brokenPipe) { - # Not a broken pipe error, don't retry - break - } - Write-Warning "Detected ADB broken pipe error, will attempt recovery..." - } - } + # Build and deploy in one step (Run target handles both) + & dotnet build @buildArgs + + $buildExitCode = $LASTEXITCODE $buildDuration = (Get-Date) - $buildStartTime if ($buildExitCode -ne 0) { diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 92a93c07cb4e..555a595f7eaf 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -43,6 +43,9 @@ Write-Step "Detecting and starting $Platform device..." if ($Platform -eq "android") { #region Android Device Detection and Startup + # This matches the CI bash script approach exactly + + Write-Info "=== Booting Android Emulator ===" # Check adb if (-not (Get-Command "adb" -ErrorAction SilentlyContinue)) { @@ -50,280 +53,79 @@ if ($Platform -eq "android") { exit 1 } - # Restart ADB server to ensure clean connection state - # This prevents "Broken pipe" errors from stale connections - Write-Info "Restarting ADB server for clean connection..." - adb kill-server 2>$null - Start-Sleep -Seconds 2 - adb start-server 2>$null - Start-Sleep -Seconds 2 - Write-Success "ADB server restarted" - - # Get device UDID if not provided OR if it's an AVD name that needs to be booted - # Check if DeviceUdid is an AVD name (not an emulator-XXXX format) - if ($DeviceUdid -and $DeviceUdid -notmatch "^emulator-\d+$") { - # DeviceUdid is likely an AVD name - check if it's in the AVD list - $avdList = emulator -list-avds 2>$null - if ($avdList -contains $DeviceUdid) { - Write-Info "DeviceUdid '$DeviceUdid' is an AVD name. Will boot this emulator..." - $selectedAvd = $DeviceUdid - $DeviceUdid = $null # Clear so we boot and get actual device ID below - } else { - Write-Error "DeviceUdid '$DeviceUdid' is not a valid emulator ID or AVD name." - Write-Info "Available AVDs: $($avdList -join ', ')" - exit 1 - } + # Get Android SDK path + $androidSdkRoot = $env:ANDROID_SDK_ROOT + if (-not $androidSdkRoot) { + $androidSdkRoot = $env:ANDROID_HOME + } + if (-not $androidSdkRoot) { + $androidSdkRoot = "$env:HOME/Library/Android/sdk" } - if (-not $DeviceUdid) { - Write-Info "Auto-detecting Android device..." + # Check for already running device + $runningDevices = adb devices | Select-String "emulator.*device$" + if ($runningDevices.Count -gt 0) { + $DeviceUdid = ($runningDevices[0] -split '\s+')[0] + Write-Success "Found running Android device: $DeviceUdid" + } + else { + # Start emulator in background, detached from terminal (exactly like CI) + $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" + $avdName = if ($DeviceUdid) { $DeviceUdid } else { "Emulator_34" } - # Check for running devices first - # Note: adb devices output can be: - # emulator-5554 device (basic) - # emulator-5554 device product:... model:... (with -l flag or some environments) - # We match any line starting with emulator- and containing "device" as the state - $runningDevices = adb devices | Select-String "^emulator-\d+\s+device" + Write-Info "Starting emulator: $avdName" + $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" + bash -c $startScript + # Wait for device to appear (adb wait-for-device) + Write-Info "Waiting for emulator device..." + adb wait-for-device + + # Get the device ID + $runningDevices = adb devices | Select-String "emulator.*device$" if ($runningDevices.Count -gt 0) { - # Use first running device - extract just the emulator-XXXX part - $DeviceUdid = ($runningDevices[0].Line -split '\s+')[0] - Write-Success "Found running Android device: $DeviceUdid" - } - else { - Write-Info "No running devices found. Looking for available emulators..." - - # Check if emulator command exists - if (-not (Get-Command "emulator" -ErrorAction SilentlyContinue)) { - Write-Error "No running Android devices and 'emulator' command not found. Please start an emulator or connect a device." - exit 1 - } - - # Get list of available AVDs - # Force array even with single result to avoid string indexing issues - $avdList = @(emulator -list-avds) - - if (-not $avdList -or $avdList.Count -eq 0) { - Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio." - Write-Info "To create an AVD:" - Write-Info " 1. Open Android Studio" - Write-Info " 2. Go to Tools > Device Manager" - Write-Info " 3. Click 'Create Device' and follow the wizard" - exit 1 - } - - Write-Info "Available emulators: $($avdList -join ', ')" - - # Selection priority: - # 1. API 30 Nexus device - # 2. Any API 30 device - # 3. Any Nexus device - # 4. First available device - - # $selectedAvd may already be set if AVD name was provided - # Only run auto-selection if not already set - if (-not $selectedAvd) { - $avdList = emulator -list-avds 2>$null - - if (-not $avdList -or $avdList.Count -eq 0) { - Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio." - Write-Info "To create an AVD:" - Write-Info " 1. Open Android Studio" - Write-Info " 2. Go to Tools > Device Manager" - Write-Info " 3. Click 'Create Device' and follow the wizard" - exit 1 - } - - Write-Info "Available emulators: $($avdList -join ', ')" - - # Selection priority: - # 1. API 34 device (matches CI provisioning) - # 2. API 30 Nexus device - # 3. Any API 30 device - # 4. Any Nexus device - # 5. First available device - - # Try to find API 34 device (CI default) - $api34Device = $avdList | Where-Object { $_ -match "34|API.*34" } | Select-Object -First 1 - if ($api34Device) { - $selectedAvd = $api34Device - Write-Info "Selected API 34 device: $selectedAvd" - } - - # Try to find API 30 Nexus device - if (-not $selectedAvd) { - $api30Nexus = $avdList | Where-Object { $_ -match "API.*30" -and $_ -match "Nexus" } | Select-Object -First 1 - if ($api30Nexus) { - $selectedAvd = $api30Nexus - Write-Info "Selected API 30 Nexus device: $selectedAvd" - } - } - - # Try to find any API 30 device - if (-not $selectedAvd) { - $api30Device = $avdList | Where-Object { $_ -match "API.*30" } | Select-Object -First 1 - if ($api30Device) { - $selectedAvd = $api30Device - Write-Info "Selected API 30 device: $selectedAvd" - } - } - - # Try to find any Nexus device - if (-not $selectedAvd) { - $nexusDevice = $avdList | Where-Object { $_ -match "Nexus" } | Select-Object -First 1 - if ($nexusDevice) { - $selectedAvd = $nexusDevice - Write-Info "Selected Nexus device: $selectedAvd" - } - } - - # Fall back to first available device - if (-not $selectedAvd) { - $selectedAvd = $avdList[0] - Write-Info "Selected first available device: $selectedAvd" - } - } - - # Start emulator with selected AVD - $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" - if ($IsWindows) { - $emulatorBin = "$emulatorBin.exe" - } - - # Check emulator binary exists - if (-not (Test-Path $emulatorBin)) { - # Fallback: try to find emulator on PATH - $emulatorCmd = Get-Command emulator -ErrorAction SilentlyContinue - if ($emulatorCmd) { - $emulatorBin = $emulatorCmd.Source - Write-Info "Using emulator from PATH: $emulatorBin" - } else { - Write-Error "Emulator binary not found at: $emulatorBin" - Write-Info "Looking for emulator in SDK..." - Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } - exit 1 - } - } - - Write-Info "Starting emulator: $selectedAvd" - Write-Info "This may take 1-2 minutes..." - - # CRITICAL: Must use nohup to properly detach emulator process - # This prevents STDIO stream inheritance issues in CI environments - if ($IsWindows) { - Start-Process "emulator" -ArgumentList "-avd", $selectedAvd, "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim" -WindowStyle Hidden - } - else { - # macOS/Linux: Use nohup to fully detach the emulator process - # This ensures the process doesn't inherit STDIO streams and can run independently - $androidHome = $env:ANDROID_HOME - if (-not $androidHome) { - $androidHome = $env:ANDROID_SDK_ROOT - } - if (-not $androidHome) { - $androidHome = "$env:HOME/Library/Android/sdk" - } - - $emulatorBin = Join-Path $androidHome "emulator/emulator" - - if (-not (Test-Path $emulatorBin)) { - Write-Error "Emulator binary not found at: $emulatorBin" - Write-Info "Please ensure ANDROID_HOME or ANDROID_SDK_ROOT is set correctly." - exit 1 - } - - # Use nohup to fully detach the emulator process from the terminal - # Include -no-window for headless CI environments - # Redirect all output to /dev/null to prevent STDIO inheritance issues - $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" - bash -c $startScript - - Write-Info "Emulator started in background (fully detached with nohup)" - } - - # Use adb wait-for-device first (like the working bash script) - Write-Info "Waiting for emulator device to appear..." - adb wait-for-device - - # Wait for emulator to appear in adb devices with proper state - Write-Info "Waiting for emulator to be ready..." - $timeout = 120 - $elapsed = 0 - $emulatorStarted = $false - - while ($elapsed -lt $timeout) { - $devices = adb devices | Select-String "emulator.*device$" - if ($devices.Count -gt 0) { - $DeviceUdid = ($devices[0].Line -split '\s+')[0] - Write-Info "Emulator detected: $DeviceUdid" - break - } - - Start-Sleep -Seconds 2 - $elapsed += 2 - - if ($elapsed % 10 -eq 0) { - Write-Info "Still waiting... ($elapsed seconds elapsed)" - } - } - - if (-not $emulatorStarted) { - Write-Error "Emulator failed to start within $timeout seconds. Please try starting it manually." - adb devices -l - exit 1 - } - - # Wait for boot to complete (poll sys.boot_completed like bash script) - Write-Info "Waiting for emulator to finish booting..." - $bootTimeout = 600 - $bootElapsed = 0 - - while ($bootElapsed -lt $bootTimeout) { - $bootStatus = adb shell getprop sys.boot_completed 2>$null - if ($bootStatus -match "1") { - Write-Success "Emulator fully booted: $DeviceUdid" - break - } - - Start-Sleep -Seconds 2 - $bootElapsed += 2 - - if ($bootElapsed % 10 -eq 0) { - Write-Info "Waiting for boot... ($bootElapsed/$bootTimeout seconds)" - } + $DeviceUdid = ($runningDevices[0] -split '\s+')[0] + } else { + # Device might still be in "offline" state, wait a bit more + Start-Sleep -Seconds 5 + $runningDevices = adb devices | Select-String "emulator" + if ($runningDevices.Count -gt 0) { + $DeviceUdid = ($runningDevices[0] -split '\s+')[0] } - - if (-not $bootCompleted) { - Write-Error "Emulator failed to complete boot within $bootTimeout seconds." - adb devices -l - exit 1 + } + + if (-not $DeviceUdid) { + Write-Error "Emulator did not start" + adb devices -l + exit 1 + } + + Write-Info "Emulator started with device ID: $DeviceUdid" + + # Wait for boot_completed (exactly like CI) + Write-Info "Waiting for emulator to finish booting..." + $timeout = 120 + $waited = 0 + + while ($waited -lt $timeout) { + $bootStatus = adb shell getprop sys.boot_completed 2>$null + if ($bootStatus -match "1") { + break } + Start-Sleep -Seconds 2 + $waited += 2 + Write-Info "Waiting for boot... ($waited/$timeout seconds)" } - } - - # Verify ADB connectivity is healthy (prevents "Broken pipe" errors during build) - Write-Info "Verifying ADB connectivity to device..." - $connectivityCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 - if ($connectivityCheck -notmatch "ping") { - Write-Info "ADB connectivity issue detected, restarting ADB server..." - adb kill-server 2>$null - Start-Sleep -Seconds 2 - adb start-server 2>$null - Start-Sleep -Seconds 3 - # Retry connectivity check - $retryCheck = adb -s $DeviceUdid shell echo "ping" 2>&1 - if ($retryCheck -notmatch "ping") { - Write-Error "ADB connectivity failed after restart. Device may need to be rebooted." - Write-Info "Try: adb -s $DeviceUdid reboot" + if ($waited -ge $timeout) { + Write-Error "Emulator did not boot in time" + adb devices -l exit 1 } - Write-Success "ADB connectivity restored after restart" - } else { - Write-Success "ADB connectivity verified" } - Write-Success "Using Android device: $DeviceUdid" + Write-Success "=== Emulator booted successfully! ===" + adb devices -l #endregion From 8e3931717851a63de0471f0622803fac403ee554 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 18:07:31 +0100 Subject: [PATCH 052/126] Update ci-copilot.yml --- eng/pipelines/ci-copilot.yml | 66 +++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a558cad85a4d..3e6133d77ae9 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -65,6 +65,19 @@ stages: skipAndroidCreateAvds: false androidEmulatorApiLevel: '34' + # Start Android Emulator EARLY (boots in background while other steps run) + # This saves ~2-3 minutes by parallelizing boot with .NET install and build tasks + - script: | + echo "=== Starting Android Emulator (background) ===" + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & + echo "Emulator starting in background (PID: $!)" + echo "Boot will complete while other steps run..." + displayName: 'Start Android Emulator (Background)' + continueOnError: true + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic displayName: 'Install .NET and workloads' @@ -194,35 +207,34 @@ stages: # ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) # JAVA_HOME: $(JAVA_HOME_17_X64) - # Start Android Emulator (verbose logging) - - pwsh: | - Write-Host "=== Starting Android Emulator ===" - Write-Host "Working directory: $(Get-Location)" - - $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" - if (-not (Test-Path $scriptPath)) { - Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" - exit 1 - } - - $ErrorActionPreference = "Continue" - try { - & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } - $exitCode = $LASTEXITCODE - } catch { - Write-Host "##vso[task.logissue type=error]Exception: $_" - Write-Host $_.ScriptStackTrace - $exitCode = 1 - } + # Wait for Android Emulator to finish booting (started earlier in background) + - script: | + echo "=== Waiting for Android Emulator to be ready ===" + + # Wait for device to appear + echo "Waiting for emulator device..." + adb wait-for-device + + # Wait for boot_completed + echo "Waiting for emulator to finish booting..." + timeout=120 + waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + sleep 2 + waited=$((waited + 2)) + echo "Waiting for boot... ($waited/$timeout seconds)" + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not boot in time" + adb devices -l + exit 1 + fi + done - Write-Host "Start-Emulator completed with exit code: $exitCode" - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]Start-Emulator failed with code $exitCode" - } - exit $exitCode - displayName: 'Start Android Emulator (Verbose)' + echo "=== Emulator booted successfully! ===" + adb devices -l + displayName: 'Wait for Android Emulator' continueOnError: true - timeoutInMinutes: 10 + timeoutInMinutes: 5 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From 917649ff5eb5640955afed8c47f434874ef9c2ca Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 18:31:35 +0100 Subject: [PATCH 053/126] Update ci-copilot.yml --- eng/pipelines/ci-copilot.yml | 95 +++++++++++++++++------------------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 3e6133d77ae9..ed0ca1aa4f32 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -65,15 +65,42 @@ stages: skipAndroidCreateAvds: false androidEmulatorApiLevel: '34' - # Start Android Emulator EARLY (boots in background while other steps run) - # This saves ~2-3 minutes by parallelizing boot with .NET install and build tasks - - script: | - echo "=== Starting Android Emulator (background) ===" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & - echo "Emulator starting in background (PID: $!)" - echo "Boot will complete while other steps run..." - displayName: 'Start Android Emulator (Background)' - continueOnError: true + # Start Android Emulator EARLY (using Start-Emulator.ps1 script) + - pwsh: | + Write-Host "=== Starting Android Emulator ===" + Write-Host "Working directory: $(Get-Location)" + Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" + Write-Host "JAVA_HOME: $env:JAVA_HOME" + + $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" + Write-Host "Script path: $scriptPath" + + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + exit 1 + } + Write-Host "Script exists: OK" + + $ErrorActionPreference = "Continue" + try { + Write-Host "Invoking Start-Emulator.ps1 -Platform android..." + & "./$scriptPath" -Platform android 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + Write-Host "Script returned exit code: $exitCode" + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } + + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=error]Start-Emulator failed with code $exitCode" + exit $exitCode + } + + Write-Host "=== Android Emulator Started Successfully ===" + displayName: 'Start Android Emulator' + timeoutInMinutes: 10 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) @@ -170,46 +197,14 @@ stages: echo "Tools restored successfully" displayName: 'Restore .NET Tools' - # # Boot Android emulator for UI tests - # - script: | - # echo "=== Booting Android Emulator ===" - - # # Start emulator in background, detached from terminal - # nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & - # EMULATOR_PID=$! - # echo "Emulator started with PID: $EMULATOR_PID" - - # # Wait for device to appear - # echo "Waiting for emulator device..." - # adb wait-for-device - - # # Wait for boot_completed - # echo "Waiting for emulator to finish booting..." - # timeout=120 - # waited=0 - # while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do - # sleep 2 - # waited=$((waited + 2)) - # echo "Waiting for boot... ($waited/$timeout seconds)" - # if [ $waited -ge $timeout ]; then - # echo "##vso[task.logissue type=error]Emulator did not boot in time" - # adb devices -l - # exit 1 - # fi - # done - - # echo "=== Emulator booted successfully! ===" - # adb devices -l - # displayName: 'Boot Android Emulator' - # continueOnError: true - # timeoutInMinutes: 15 - # env: - # ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - # JAVA_HOME: $(JAVA_HOME_17_X64) - - # Wait for Android Emulator to finish booting (started earlier in background) + # Boot Android emulator for UI tests - script: | - echo "=== Waiting for Android Emulator to be ready ===" + echo "=== Booting Android Emulator ===" + + # Start emulator in background, detached from terminal + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & + EMULATOR_PID=$! + echo "Emulator started with PID: $EMULATOR_PID" # Wait for device to appear echo "Waiting for emulator device..." @@ -232,9 +227,9 @@ stages: echo "=== Emulator booted successfully! ===" adb devices -l - displayName: 'Wait for Android Emulator' + displayName: 'Boot Android Emulator' continueOnError: true - timeoutInMinutes: 5 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From 7bc8410dded5f9b0a85fc53209307aea0c99bf79 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 11:51:55 -0600 Subject: [PATCH 054/126] Fix Android emulator boot by using ARM64 macOS image The emulator was timing out because it was using x86_64 system images on an Intel macOS agent, which requires binary translation and is extremely slow without KVM (Linux-only). Switch to macOS-15-arm64 which: - Uses Apple Silicon with native Hypervisor.framework support - Auto-provisions arm64-v8a system images (detected in Provisioning.csproj) - Matches the pattern used by ci-uitests.yml and ci-device-tests.yml --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ed0ca1aa4f32..93d3e7fd579b 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -19,7 +19,7 @@ parameters: type: object default: name: Azure Pipelines - vmImage: macOS-15 + vmImage: macOS-15-arm64 variables: - template: /eng/pipelines/common/variables.yml@self From ac0039d0f8b9f91cf9875817927223d40c2235c4 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Feb 2026 19:14:53 +0100 Subject: [PATCH 055/126] Fix Android emulator startup with better timeout handling and diagnostics - Replace adb wait-for-device with custom polling loop (prevents infinite hang) - Add GPU swiftshader_indirect for software rendering on CI (more reliable) - Add emulator process verification after startup - Add emulator log capture for debugging failures - Add AVD list verification before startup - Increase timeout to 180s for boot_completed - Increase CI task timeout to 15 minutes - Better error messages with log snippets on failure --- .github/scripts/shared/Start-Emulator.ps1 | 86 ++++++++++++++++++----- eng/pipelines/ci-copilot.yml | 2 +- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 555a595f7eaf..5ffb38efa840 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -73,30 +73,78 @@ if ($Platform -eq "android") { $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" $avdName = if ($DeviceUdid) { $DeviceUdid } else { "Emulator_34" } + # Check emulator binary exists + if (-not (Test-Path $emulatorBin)) { + Write-Error "Emulator binary not found at: $emulatorBin" + Write-Info "Looking for emulator in SDK..." + Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } + exit 1 + } + + # Check AVD exists + Write-Info "Available AVDs:" + $avdListOutput = & "$androidSdkRoot/cmdline-tools/latest/bin/avdmanager" list avd 2>&1 + Write-Info $avdListOutput + Write-Info "Starting emulator: $avdName" - $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 &" + Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect" + + # Use swiftshader for software rendering (more reliable on CI without GPU) + # Redirect output to a log file for debugging + $emulatorLog = "/tmp/emulator-$avdName.log" + $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" bash -c $startScript - # Wait for device to appear (adb wait-for-device) - Write-Info "Waiting for emulator device..." - adb wait-for-device + # Give the emulator process time to start + Start-Sleep -Seconds 5 - # Get the device ID - $runningDevices = adb devices | Select-String "emulator.*device$" - if ($runningDevices.Count -gt 0) { - $DeviceUdid = ($runningDevices[0] -split '\s+')[0] - } else { - # Device might still be in "offline" state, wait a bit more - Start-Sleep -Seconds 5 + # Check if emulator process is running + $emulatorProcs = bash -c "pgrep -f 'qemu.*$avdName' || pgrep -f 'emulator.*$avdName' || true" 2>&1 + if ([string]::IsNullOrWhiteSpace($emulatorProcs)) { + Write-Error "Emulator process did not start. Checking log..." + if (Test-Path $emulatorLog) { + Get-Content $emulatorLog | Select-Object -Last 50 | ForEach-Object { Write-Info " $_" } + } + exit 1 + } + Write-Info "Emulator process started (PIDs: $emulatorProcs)" + + # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) + Write-Info "Waiting for emulator device to appear..." + $deviceTimeout = 120 + $deviceWaited = 0 + $DeviceUdid = $null + + while ($deviceWaited -lt $deviceTimeout) { $runningDevices = adb devices | Select-String "emulator" if ($runningDevices.Count -gt 0) { - $DeviceUdid = ($runningDevices[0] -split '\s+')[0] + $firstDevice = ($runningDevices[0] -split '\s+')[0] + $deviceState = ($runningDevices[0] -split '\s+')[1] + if ($deviceState -eq "device") { + $DeviceUdid = $firstDevice + break + } + Write-Info "Device found but state is '$deviceState', waiting..." + } + Start-Sleep -Seconds 5 + $deviceWaited += 5 + Write-Info "Waiting for device... ($deviceWaited/$deviceTimeout seconds)" + + # Show emulator log tail if taking too long + if ($deviceWaited -ge 30 -and ($deviceWaited % 30 -eq 0) -and (Test-Path $emulatorLog)) { + Write-Info "Emulator log (last 10 lines):" + Get-Content $emulatorLog | Select-Object -Last 10 | ForEach-Object { Write-Info " $_" } } } if (-not $DeviceUdid) { - Write-Error "Emulator did not start" + Write-Error "Emulator device did not appear in time" + Write-Info "Current adb devices:" adb devices -l + if (Test-Path $emulatorLog) { + Write-Info "Emulator log (last 30 lines):" + Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + } exit 1 } @@ -104,22 +152,26 @@ if ($Platform -eq "android") { # Wait for boot_completed (exactly like CI) Write-Info "Waiting for emulator to finish booting..." - $timeout = 120 + $timeout = 180 $waited = 0 while ($waited -lt $timeout) { - $bootStatus = adb shell getprop sys.boot_completed 2>$null + $bootStatus = adb -s $DeviceUdid shell getprop sys.boot_completed 2>$null if ($bootStatus -match "1") { break } - Start-Sleep -Seconds 2 - $waited += 2 + Start-Sleep -Seconds 5 + $waited += 5 Write-Info "Waiting for boot... ($waited/$timeout seconds)" } if ($waited -ge $timeout) { Write-Error "Emulator did not boot in time" adb devices -l + if (Test-Path $emulatorLog) { + Write-Info "Emulator log (last 30 lines):" + Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + } exit 1 } } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 93d3e7fd579b..84701a206ff7 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -100,7 +100,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 10 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From e189817500dedf8cfd0ae8d59a8978e8d46c5435 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 12:17:18 -0600 Subject: [PATCH 056/126] Fix 'Boot Android Emulator' step with same improvements Apply the same fixes to the inline bash script that were applied to Start-Emulator.ps1: - Add -gpu swiftshader_indirect for software rendering - Replace adb wait-for-device with custom polling (prevents hang) - Add emulator process verification after startup - Add log capture to /tmp/emulator-boot.log - Increase boot timeout to 180s - Add device detection timeout (60s) - Show log snippets on failure --- eng/pipelines/ci-copilot.yml | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 84701a206ff7..e9e0381f1244 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -100,7 +100,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 15 + timeoutInMinutes: 10 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) @@ -201,14 +201,39 @@ stages: - script: | echo "=== Booting Android Emulator ===" - # Start emulator in background, detached from terminal - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim > /dev/null 2>&1 & + # Use swiftshader for software rendering (more reliable on CI without GPU) + # Start emulator in background with logging for debugging + EMULATOR_LOG="/tmp/emulator-boot.log" + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" - # Wait for device to appear + # Give emulator a moment to start and verify process is running + sleep 3 + if ! pgrep -f 'qemu-system' > /dev/null; then + echo "##vso[task.logissue type=error]Emulator process did not start" + echo "=== Emulator Log ===" + cat "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + exit 1 + fi + echo "Emulator process verified running" + + # Wait for device to appear with timeout (don't use adb wait-for-device - can hang forever) echo "Waiting for emulator device..." - adb wait-for-device + device_timeout=60 + device_waited=0 + while ! adb devices | grep -q "emulator.*device"; do + sleep 2 + device_waited=$((device_waited + 2)) + if [ $device_waited -ge $device_timeout ]; then + echo "##vso[task.logissue type=error]Emulator device did not appear in time" + echo "=== Emulator Log (last 50 lines) ===" + tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + adb devices -l + exit 1 + fi + done + echo "Emulator device detected" # Wait for boot_completed echo "Waiting for emulator to finish booting..." @@ -220,6 +245,8 @@ stages: echo "Waiting for boot... ($waited/$timeout seconds)" if [ $waited -ge $timeout ]; then echo "##vso[task.logissue type=error]Emulator did not boot in time" + echo "=== Emulator Log (last 50 lines) ===" + tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" adb devices -l exit 1 fi @@ -229,7 +256,7 @@ stages: adb devices -l displayName: 'Boot Android Emulator' continueOnError: true - timeoutInMinutes: 15 + timeoutInMinutes: 10 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From f3c9398f88d93f872444e2aa46e5ea3dfa9c1eff Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 12:35:21 -0600 Subject: [PATCH 057/126] Disable hardware acceleration for Android emulator on Azure hosted agents --- .github/scripts/shared/Start-Emulator.ps1 | 5 +++-- eng/pipelines/ci-copilot.yml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 5ffb38efa840..1b757a849ec1 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -87,12 +87,13 @@ if ($Platform -eq "android") { Write-Info $avdListOutput Write-Info "Starting emulator: $avdName" - Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect" + Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect" + # Use -accel off to disable hardware acceleration (HVF not available on Azure hosted ARM64 agents) # Use swiftshader for software rendering (more reliable on CI without GPU) # Redirect output to a log file for debugging $emulatorLog = "/tmp/emulator-$avdName.log" - $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" + $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" bash -c $startScript # Give the emulator process time to start diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index e9e0381f1244..8cd38fd83340 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -201,10 +201,11 @@ stages: - script: | echo "=== Booting Android Emulator ===" + # Use -accel off to disable hardware acceleration (HVF not available on Azure hosted ARM64 agents) # Use swiftshader for software rendering (more reliable on CI without GPU) # Start emulator in background with logging for debugging EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" From f9f7db165998bb990ffc5e8686ce9d9bd417c539 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 12:45:42 -0600 Subject: [PATCH 058/126] Force TCG software emulation for Android emulator (-qemu -accel tcg) --- .github/scripts/shared/Start-Emulator.ps1 | 6 +++--- eng/pipelines/ci-copilot.yml | 26 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 1b757a849ec1..bcbe56fc3ea8 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -87,13 +87,13 @@ if ($Platform -eq "android") { Write-Info $avdListOutput Write-Info "Starting emulator: $avdName" - Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect" + Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg" - # Use -accel off to disable hardware acceleration (HVF not available on Azure hosted ARM64 agents) + # Use -qemu -accel tcg to force TCG (software) emulation since HVF is not available on Azure hosted ARM64 agents # Use swiftshader for software rendering (more reliable on CI without GPU) # Redirect output to a log file for debugging $emulatorLog = "/tmp/emulator-$avdName.log" - $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" + $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg > '$emulatorLog' 2>&1 &" bash -c $startScript # Give the emulator process time to start diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 8cd38fd83340..9e08e0e6de07 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -65,6 +65,28 @@ stages: skipAndroidCreateAvds: false androidEmulatorApiLevel: '34' + # Disable hardware acceleration in AVD config (HVF not available on Azure hosted agents) + - script: | + echo "=== Disabling hardware acceleration in AVD config ===" + AVD_CONFIG="$HOME/.android/avd/Emulator_34.avd/config.ini" + if [ -f "$AVD_CONFIG" ]; then + echo "Found AVD config: $AVD_CONFIG" + echo "Current config:" + cat "$AVD_CONFIG" + echo "" + echo "Adding hw.cpu.ncore=2 and removing any accelerator settings..." + # Remove any existing accelerator settings and add our own + grep -v "hw.accelerator" "$AVD_CONFIG" > "$AVD_CONFIG.tmp" && mv "$AVD_CONFIG.tmp" "$AVD_CONFIG" + echo "hw.cpu.ncore=2" >> "$AVD_CONFIG" + echo "" + echo "Updated config:" + cat "$AVD_CONFIG" + else + echo "##vso[task.logissue type=warning]AVD config not found at: $AVD_CONFIG" + ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found" + fi + displayName: 'Configure AVD for Software Emulation' + # Start Android Emulator EARLY (using Start-Emulator.ps1 script) - pwsh: | Write-Host "=== Starting Android Emulator ===" @@ -201,11 +223,11 @@ stages: - script: | echo "=== Booting Android Emulator ===" - # Use -accel off to disable hardware acceleration (HVF not available on Azure hosted ARM64 agents) + # Use -qemu -accel tcg to force TCG (software) emulation (HVF not available on Azure hosted ARM64 agents) # Use swiftshader for software rendering (more reliable on CI without GPU) # Start emulator in background with logging for debugging EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" From b9b799ae23eb9cbf9b6f8fd5f8498bb77cc239f4 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 12:55:13 -0600 Subject: [PATCH 059/126] Add QEMU_ACCEL=tcg environment variable to force TCG emulation --- eng/pipelines/ci-copilot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 9e08e0e6de07..5a33af2a6edb 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -126,6 +126,7 @@ stages: env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) + QEMU_ACCEL: tcg # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic @@ -283,6 +284,7 @@ stages: env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) + QEMU_ACCEL: tcg # Cherry-pick PR changes to current branch - script: | From 949272982a378feb8bbcc802265c5ae572a2defb Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 13:16:23 -0600 Subject: [PATCH 060/126] Switch to Intel macOS agent (macOS-15) for Android emulator support --- .github/scripts/shared/Start-Emulator.ps1 | 6 +++--- eng/pipelines/ci-copilot.yml | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index bcbe56fc3ea8..2ddca7985ae7 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -87,13 +87,13 @@ if ($Platform -eq "android") { Write-Info $avdListOutput Write-Info "Starting emulator: $avdName" - Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg" + Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect" - # Use -qemu -accel tcg to force TCG (software) emulation since HVF is not available on Azure hosted ARM64 agents + # Use swiftshader for software graphics rendering (more reliable on CI without GPU) # Use swiftshader for software rendering (more reliable on CI without GPU) # Redirect output to a log file for debugging $emulatorLog = "/tmp/emulator-$avdName.log" - $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg > '$emulatorLog' 2>&1 &" + $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" bash -c $startScript # Give the emulator process time to start diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5a33af2a6edb..0e2e2e9b6c92 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -19,7 +19,7 @@ parameters: type: object default: name: Azure Pipelines - vmImage: macOS-15-arm64 + vmImage: macOS-15 variables: - template: /eng/pipelines/common/variables.yml@self @@ -126,7 +126,6 @@ stages: env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) - QEMU_ACCEL: tcg # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic @@ -224,11 +223,11 @@ stages: - script: | echo "=== Booting Android Emulator ===" - # Use -qemu -accel tcg to force TCG (software) emulation (HVF not available on Azure hosted ARM64 agents) + # Use swiftshader for software graphics rendering (HVF not available on Azure hosted ARM64 agents) # Use swiftshader for software rendering (more reliable on CI without GPU) # Start emulator in background with logging for debugging EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -qemu -accel tcg > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" @@ -284,7 +283,6 @@ stages: env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) - QEMU_ACCEL: tcg # Cherry-pick PR changes to current branch - script: | From e980847f1f7d2b483fe6e0d6c3f047e96d571a95 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 13:36:32 -0600 Subject: [PATCH 061/126] Switch to macOS-14 and increase emulator boot timeout to 180s --- .github/scripts/shared/Start-Emulator.ps1 | 3 ++- eng/pipelines/ci-copilot.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 2ddca7985ae7..804eeefdc6a8 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -111,8 +111,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) + # Increased timeout to 180s as emulator on CI can take 2+ minutes to boot Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 120 + $deviceTimeout = 180 $deviceWaited = 0 $DeviceUdid = $null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 0e2e2e9b6c92..759196e77c2f 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -19,7 +19,7 @@ parameters: type: object default: name: Azure Pipelines - vmImage: macOS-15 + vmImage: macOS-14 variables: - template: /eng/pipelines/common/variables.yml@self From 31f5485bddfa06e28285f81330189b7294151e27 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 14:53:11 -0600 Subject: [PATCH 062/126] Skip Platform APIs and Common SDKs provisioning (pre-installed on macOS image) --- eng/pipelines/ci-copilot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 759196e77c2f..098a2905530a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -55,8 +55,8 @@ stages: parameters: skipXcode: false skipProvisionator: false - skipAndroidCommonSdks: false - skipAndroidPlatformApis: false + skipAndroidCommonSdks: true + skipAndroidPlatformApis: true skipJdk: false skipSimulatorSetup: false skipCertificates: true From 299b6784d643154f11a60479858fc0a82edbcbdc Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 15:03:25 -0600 Subject: [PATCH 063/126] Increase emulator boot timeout to 300s/15min --- .github/scripts/shared/Start-Emulator.ps1 | 4 ++-- eng/pipelines/ci-copilot.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 804eeefdc6a8..9bed871f5f0e 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -111,9 +111,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) - # Increased timeout to 180s as emulator on CI can take 2+ minutes to boot + # Increased timeout to 300s as emulator on CI can take 2-5 minutes to boot on slow hardware Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 180 + $deviceTimeout = 300 $deviceWaited = 0 $DeviceUdid = $null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 098a2905530a..d092ced7ff1e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -122,7 +122,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 10 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) @@ -279,7 +279,7 @@ stages: adb devices -l displayName: 'Boot Android Emulator' continueOnError: true - timeoutInMinutes: 10 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From 465e22f22cd76e3e4a5ccd9a57f0bbce550605fb Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 15:17:50 -0600 Subject: [PATCH 064/126] Increase emulator timeout to 30 minutes --- .github/scripts/shared/Start-Emulator.ps1 | 4 ++-- eng/pipelines/ci-copilot.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 9bed871f5f0e..c4bf10c4a504 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -111,9 +111,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) - # Increased timeout to 300s as emulator on CI can take 2-5 minutes to boot on slow hardware + # Increased timeout to 1800s (30 min) as emulator on CI can take a long time to boot Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 300 + $deviceTimeout = 1800 $deviceWaited = 0 $DeviceUdid = $null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d092ced7ff1e..bcb47ea40b44 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -122,7 +122,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 15 + timeoutInMinutes: 35 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From 7ce6e141750d0970a5786282289020ffcf27b729 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 16:12:27 -0600 Subject: [PATCH 065/126] Remove redundant steps, reduce emulator timeout to 15min --- .github/scripts/shared/Start-Emulator.ps1 | 4 +- eng/pipelines/ci-copilot.yml | 124 +--------------------- 2 files changed, 3 insertions(+), 125 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index c4bf10c4a504..b9fde62e7690 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -111,9 +111,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) - # Increased timeout to 1800s (30 min) as emulator on CI can take a long time to boot + # Timeout of 900s (15 min) - emulator on CI takes ~10 minutes to boot Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 1800 + $deviceTimeout = 900 $deviceWaited = 0 $DeviceUdid = $null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index bcb47ea40b44..a101fea0c90d 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -122,7 +122,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 35 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) @@ -284,128 +284,6 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) - # Cherry-pick PR changes to current branch - - script: | - echo "=== Cherry-picking PR #${{ parameters.PRNumber }} changes ===" - - # Configure git - git config user.email "copilot@microsoft.com" - git config user.name "GitHub Copilot" - - # Fetch the PR - echo "Fetching PR #${{ parameters.PRNumber }}..." - git fetch origin pull/${{ parameters.PRNumber }}/head:pr-${{ parameters.PRNumber }} - - # Get the merge base between main and the PR - MERGE_BASE=$(git merge-base origin/main pr-${{ parameters.PRNumber }}) - echo "Merge base: $MERGE_BASE" - - # Get list of commits in the PR (from merge base to PR head) - COMMITS=$(git rev-list --reverse $MERGE_BASE..pr-${{ parameters.PRNumber }}) - COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ') - echo "Found $COMMIT_COUNT commits to cherry-pick" - - # Cherry-pick each commit - for COMMIT in $COMMITS; do - echo "Cherry-picking commit: $COMMIT" - git cherry-pick --no-commit $COMMIT || { - echo "##vso[task.logissue type=warning]Cherry-pick conflict for $COMMIT, attempting to continue..." - git checkout --theirs . 2>/dev/null || true - git add -A - } - done - - # Show what changed - echo "=== Files changed from PR ===" - git status --short - - echo "=== Cherry-pick complete ===" - displayName: 'Cherry-pick PR Changes' - continueOnError: false - - # Verify UI tests fail without fix (on Android emulator) - - pwsh: | - Write-Host "=== Verifying UI Tests Fail Without Fix ===" - Write-Host "Working directory: $(Get-Location)" - Write-Host "Script path: .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" - - # Check script exists - if (-not (Test-Path ".github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1")) { - Write-Host "##vso[task.logissue type=error]Script not found!" - exit 1 - } - - # Run the verify-tests-fail script on Android - # Use Invoke-Expression to run in same process and capture all output - $ErrorActionPreference = "Continue" - try { - & "./.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1" -Platform android -PRNumber "${{ parameters.PRNumber }}" 2>&1 | ForEach-Object { Write-Host $_ } - $exitCode = $LASTEXITCODE - } catch { - Write-Host "##vso[task.logissue type=error]Exception: $_" - Write-Host $_.ScriptStackTrace - $exitCode = 1 - } - - Write-Host "Verification script completed with exit code: $exitCode" - - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]verify-tests-fail script exited with code $exitCode" - } - exit $exitCode - displayName: 'Verify UI Tests Fail Without Fix' - continueOnError: true - timeoutInMinutes: 30 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) - - # Build and Deploy HostApp (verbose logging) - - pwsh: | - Write-Host "=== Building and Deploying HostApp ===" - Write-Host "Working directory: $(Get-Location)" - - $scriptPath = ".github/scripts/shared/Build-AndDeploy.ps1" - if (-not (Test-Path $scriptPath)) { - Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" - exit 1 - } - - # Get emulator UDID - $udid = (adb devices | Select-String -Pattern "^(emulator-\d+)" | ForEach-Object { $_.Matches[0].Groups[1].Value } | Select-Object -First 1) - if (-not $udid) { - Write-Host "##vso[task.logissue type=error]No Android emulator found" - adb devices -l - exit 1 - } - Write-Host "Using emulator: $udid" - - $ErrorActionPreference = "Continue" - try { - & "./$scriptPath" -Platform android ` - -ProjectPath "src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj" ` - -TargetFramework "net10.0-android" ` - -DeviceUdid $udid ` - -Verbose 2>&1 | ForEach-Object { Write-Host $_ } - $exitCode = $LASTEXITCODE - } catch { - Write-Host "##vso[task.logissue type=error]Exception: $_" - Write-Host $_.ScriptStackTrace - $exitCode = 1 - } - - Write-Host "Build-AndDeploy completed with exit code: $exitCode" - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]Build-AndDeploy failed with code $exitCode" - } - exit $exitCode - displayName: 'Build and Deploy HostApp (Verbose)' - continueOnError: true - timeoutInMinutes: 30 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) - # Build and Run HostApp with Tests (verbose logging) - pwsh: | Write-Host "=== Building and Running HostApp ===" From 88cff8926abc30174b936f5025273840fff54576 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 18:01:05 -0600 Subject: [PATCH 066/126] Remove Build and Run HostApp step (PR Reviewer handles this) --- eng/pipelines/ci-copilot.yml | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a101fea0c90d..58e226925a99 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -284,39 +284,6 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) - # Build and Run HostApp with Tests (verbose logging) - - pwsh: | - Write-Host "=== Building and Running HostApp ===" - Write-Host "Working directory: $(Get-Location)" - - $scriptPath = ".github/scripts/BuildAndRunHostApp.ps1" - if (-not (Test-Path $scriptPath)) { - Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" - exit 1 - } - - $ErrorActionPreference = "Continue" - try { - & "./$scriptPath" -Platform android -Verbose 2>&1 | ForEach-Object { Write-Host $_ } - $exitCode = $LASTEXITCODE - } catch { - Write-Host "##vso[task.logissue type=error]Exception: $_" - Write-Host $_.ScriptStackTrace - $exitCode = 1 - } - - Write-Host "BuildAndRunHostApp completed with exit code: $exitCode" - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]BuildAndRunHostApp failed with code $exitCode" - } - exit $exitCode - displayName: 'Build and Run HostApp (Verbose)' - continueOnError: true - timeoutInMinutes: 45 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) - - script: | echo "Installing Node.js 22..." brew install node@22 From 3a61aec5482952b4d230483735d3809b674cb730 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 18:02:43 -0600 Subject: [PATCH 067/126] Wait for package manager service before declaring emulator ready --- .github/scripts/shared/Start-Emulator.ps1 | 26 +++++++++++++++++++++++ eng/pipelines/ci-copilot.yml | 18 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index b9fde62e7690..ff7b5ee52cd5 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -176,6 +176,32 @@ if ($Platform -eq "android") { } exit 1 } + + Write-Info "Boot completed flag set" + + # Wait for package manager service to be available (critical for app installation) + Write-Info "Waiting for package manager service..." + $pmTimeout = 120 + $pmWaited = 0 + + while ($pmWaited -lt $pmTimeout) { + $pmOutput = adb -s $DeviceUdid shell pm list packages 2>$null + if ($pmOutput -match "package:") { + break + } + Start-Sleep -Seconds 3 + $pmWaited += 3 + Write-Info "Waiting for package manager... ($pmWaited/$pmTimeout seconds)" + } + + if ($pmWaited -ge $pmTimeout) { + Write-Error "Package manager service did not start" + Write-Info "Checking services:" + adb -s $DeviceUdid shell service list 2>$null | Select-Object -First 20 | ForEach-Object { Write-Info " $_" } + exit 1 + } + + Write-Info "Package manager service is ready" } Write-Success "=== Emulator booted successfully! ===" diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 58e226925a99..85d937ba8694 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -274,6 +274,24 @@ stages: exit 1 fi done + echo "Boot completed flag set" + + # Wait for package manager service to be available (critical for app installation) + echo "Waiting for package manager service..." + pm_timeout=120 + pm_waited=0 + while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 3 + pm_waited=$((pm_waited + 3)) + echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" + if [ $pm_waited -ge $pm_timeout ]; then + echo "##vso[task.logissue type=error]Package manager service did not start" + echo "=== Checking services ===" + adb shell service list 2>/dev/null | head -20 || echo "Cannot list services" + exit 1 + fi + done + echo "Package manager service is ready" echo "=== Emulator booted successfully! ===" adb devices -l From 28173a35a90a1ff9e82ae2f1bdca4cc0ee7c9d35 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 18:27:29 -0600 Subject: [PATCH 068/126] Increase emulator timeout to 30 minutes --- .github/scripts/shared/Start-Emulator.ps1 | 4 ++-- eng/pipelines/ci-copilot.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index ff7b5ee52cd5..d851e4ae47e2 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -111,9 +111,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) - # Timeout of 900s (15 min) - emulator on CI takes ~10 minutes to boot + # Timeout of 1800s (30 min) - emulator on CI can take a very long time to boot Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 900 + $deviceTimeout = 1800 $deviceWaited = 0 $DeviceUdid = $null diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 85d937ba8694..ac0f6f623536 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -122,7 +122,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' - timeoutInMinutes: 15 + timeoutInMinutes: 35 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) JAVA_HOME: $(JAVA_HOME_17_X64) From 4078332559a41d761da3711fd63c4b3096835d54 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 21:06:19 -0600 Subject: [PATCH 069/126] Restart emulator just before PR Reviewer to ensure freshness --- eng/pipelines/ci-copilot.yml | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ac0f6f623536..cc7cc8b2e3d0 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -381,6 +381,82 @@ stages: echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' + # Restart Android emulator to ensure it's fresh before PR Reviewer runs + # The emulator can become unstable after running for a long time + - script: | + echo "=== Restarting Android Emulator for PR Reviewer ===" + + # Kill any existing emulator + echo "Killing existing emulator..." + pkill -f 'qemu-system' 2>/dev/null || true + sleep 5 + + # Restart ADB server + echo "Restarting ADB server..." + adb kill-server + sleep 2 + adb start-server + sleep 2 + + # Start fresh emulator + EMULATOR_LOG="/tmp/emulator-fresh.log" + echo "Starting fresh emulator..." + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + EMULATOR_PID=$! + echo "Emulator started with PID: $EMULATOR_PID" + + # Wait for device to appear + echo "Waiting for emulator device..." + device_timeout=300 + device_waited=0 + while ! adb devices | grep -q "emulator.*device"; do + sleep 5 + device_waited=$((device_waited + 5)) + if [ $device_waited -ge $device_timeout ]; then + echo "##vso[task.logissue type=error]Emulator device did not appear in time" + cat "$EMULATOR_LOG" 2>/dev/null | tail -30 + exit 1 + fi + echo "Waiting for device... ($device_waited/$device_timeout seconds)" + done + echo "Emulator device detected" + + # Wait for boot_completed + echo "Waiting for boot_completed..." + boot_timeout=180 + boot_waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + sleep 5 + boot_waited=$((boot_waited + 5)) + if [ $boot_waited -ge $boot_timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not boot in time" + exit 1 + fi + done + echo "Boot completed" + + # Wait for package manager + echo "Waiting for package manager service..." + pm_timeout=120 + pm_waited=0 + while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 5 + pm_waited=$((pm_waited + 5)) + if [ $pm_waited -ge $pm_timeout ]; then + echo "##vso[task.logissue type=error]Package manager service did not start" + adb shell service list 2>/dev/null | head -20 + exit 1 + fi + echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" + done + + echo "=== Fresh Android Emulator Ready! ===" + adb devices -l + displayName: 'Restart Android Emulator (Fresh)' + timeoutInMinutes: 15 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + - script: | echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." From ae2f366f883aea7cb3d8eab4a90b4a6a9f5de996 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 3 Feb 2026 22:45:08 -0600 Subject: [PATCH 070/126] Fix Android device ID parsing in Start-Emulator.ps1 - Fix regex from 'emulator.*device$' to '^emulator-\d+\s+device' The old regex didn't match adb output with extended device info - Use .Line property when splitting MatchInfo objects Splitting MatchInfo directly can produce unexpected results - Add comments explaining the adb output format variations --- .github/scripts/shared/Start-Emulator.ps1 | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index d851e4ae47e2..1d4f790f5ab5 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -63,9 +63,14 @@ if ($Platform -eq "android") { } # Check for already running device - $runningDevices = adb devices | Select-String "emulator.*device$" + # Note: adb devices output can be: + # emulator-5554 device (basic) + # emulator-5554 device product:... model:... (with -l flag or some environments) + # We match any line starting with emulator- and containing "device" as the state + $runningDevices = adb devices | Select-String "^emulator-\d+\s+device" if ($runningDevices.Count -gt 0) { - $DeviceUdid = ($runningDevices[0] -split '\s+')[0] + # Extract just the emulator-XXXX part (first column) + $DeviceUdid = ($runningDevices[0].Line -split '\s+')[0] Write-Success "Found running Android device: $DeviceUdid" } else { @@ -118,10 +123,14 @@ if ($Platform -eq "android") { $DeviceUdid = $null while ($deviceWaited -lt $deviceTimeout) { - $runningDevices = adb devices | Select-String "emulator" + # Match any emulator device line + $runningDevices = adb devices | Select-String "^emulator-\d+" if ($runningDevices.Count -gt 0) { - $firstDevice = ($runningDevices[0] -split '\s+')[0] - $deviceState = ($runningDevices[0] -split '\s+')[1] + # Use .Line to get the actual string content + $line = $runningDevices[0].Line + $parts = $line -split '\s+' + $firstDevice = $parts[0] + $deviceState = $parts[1] if ($deviceState -eq "device") { $DeviceUdid = $firstDevice break From f536f49fe915d7ce127ca0f68c2f10fea5349e77 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 00:16:21 -0600 Subject: [PATCH 071/126] Increase Android emulator boot timeout to 600s (10 min) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI boot times on Intel macOS can vary significantly. Previous 180s timeout was too tight - observed >3 min boots causing failures. Changes: - ci-copilot.yml: boot_timeout 120s/180s → 600s (both emulator steps) - Start-Emulator.ps1: timeout 180s → 600s - android.cake: EmulatorBootTimeoutSeconds 2*60 → 10*60 Consulted 3 models - consensus was 600s provides adequate headroom for CI variance while staying within step timeout (15 min). --- .github/scripts/shared/Start-Emulator.ps1 | 2 +- eng/pipelines/ci-copilot.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 1d4f790f5ab5..cba4a2d4bde4 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -163,7 +163,7 @@ if ($Platform -eq "android") { # Wait for boot_completed (exactly like CI) Write-Info "Waiting for emulator to finish booting..." - $timeout = 180 + $timeout = 600 $waited = 0 while ($waited -lt $timeout) { diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index cc7cc8b2e3d0..bd67b4708eaa 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -260,7 +260,7 @@ stages: # Wait for boot_completed echo "Waiting for emulator to finish booting..." - timeout=120 + timeout=600 waited=0 while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do sleep 2 @@ -423,7 +423,7 @@ stages: # Wait for boot_completed echo "Waiting for boot_completed..." - boot_timeout=180 + boot_timeout=600 boot_waited=0 while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do sleep 5 From e4cbf17867f5bbbf482fe007e894d279a12c76b7 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 08:24:03 -0600 Subject: [PATCH 072/126] Restore AVD auto-detection, pass Emulator_34 explicitly in CI - Restored auto-detection logic from main branch for local dev - Priority: API 34 > API 30 Nexus > API 30 > Nexus > First available - CI now passes -DeviceUdid Emulator_34 explicitly - Preserved all fixes: regex, timeouts, package manager check, GPU flags --- .github/scripts/shared/Start-Emulator.ps1 | 372 ++++++++++++++-------- eng/pipelines/ci-copilot.yml | 4 +- 2 files changed, 235 insertions(+), 141 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index cba4a2d4bde4..0f98b7a9597c 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -43,9 +43,6 @@ Write-Step "Detecting and starting $Platform device..." if ($Platform -eq "android") { #region Android Device Detection and Startup - # This matches the CI bash script approach exactly - - Write-Info "=== Booting Android Emulator ===" # Check adb if (-not (Get-Command "adb" -ErrorAction SilentlyContinue)) { @@ -62,159 +59,256 @@ if ($Platform -eq "android") { $androidSdkRoot = "$env:HOME/Library/Android/sdk" } - # Check for already running device - # Note: adb devices output can be: - # emulator-5554 device (basic) - # emulator-5554 device product:... model:... (with -l flag or some environments) - # We match any line starting with emulator- and containing "device" as the state - $runningDevices = adb devices | Select-String "^emulator-\d+\s+device" - if ($runningDevices.Count -gt 0) { - # Extract just the emulator-XXXX part (first column) - $DeviceUdid = ($runningDevices[0].Line -split '\s+')[0] - Write-Success "Found running Android device: $DeviceUdid" - } - else { - # Start emulator in background, detached from terminal (exactly like CI) - $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" - $avdName = if ($DeviceUdid) { $DeviceUdid } else { "Emulator_34" } - - # Check emulator binary exists - if (-not (Test-Path $emulatorBin)) { - Write-Error "Emulator binary not found at: $emulatorBin" - Write-Info "Looking for emulator in SDK..." - Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } + # Track which AVD to boot (may be set from DeviceUdid parameter if it's an AVD name) + $selectedAvd = $null + + # Check if DeviceUdid is an AVD name (not an emulator-XXXX format) + if ($DeviceUdid -and $DeviceUdid -notmatch "^emulator-\d+$") { + # DeviceUdid is likely an AVD name - check if it's in the AVD list + $avdList = emulator -list-avds 2>$null + if ($avdList -contains $DeviceUdid) { + Write-Info "DeviceUdid '$DeviceUdid' is an AVD name. Will boot this emulator..." + $selectedAvd = $DeviceUdid + $DeviceUdid = $null # Clear so we boot and get actual device ID below + } else { + Write-Error "DeviceUdid '$DeviceUdid' is not a valid emulator ID or AVD name." + Write-Info "Available AVDs: $($avdList -join ', ')" exit 1 } + } + + if (-not $DeviceUdid) { + Write-Info "Auto-detecting Android device..." - # Check AVD exists - Write-Info "Available AVDs:" - $avdListOutput = & "$androidSdkRoot/cmdline-tools/latest/bin/avdmanager" list avd 2>&1 - Write-Info $avdListOutput - - Write-Info "Starting emulator: $avdName" - Write-Info "Emulator command: $emulatorBin -avd $avdName -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect" - - # Use swiftshader for software graphics rendering (more reliable on CI without GPU) - # Use swiftshader for software rendering (more reliable on CI without GPU) - # Redirect output to a log file for debugging - $emulatorLog = "/tmp/emulator-$avdName.log" - $startScript = "nohup '$emulatorBin' -avd '$avdName' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" - bash -c $startScript - - # Give the emulator process time to start - Start-Sleep -Seconds 5 + # Check for running devices first + # Note: adb devices output can be: + # emulator-5554 device (basic) + # emulator-5554 device product:... model:... (with -l flag or some environments) + # We match any line starting with emulator- and containing "device" as the state + $runningDevices = adb devices | Select-String "^emulator-\d+\s+device" - # Check if emulator process is running - $emulatorProcs = bash -c "pgrep -f 'qemu.*$avdName' || pgrep -f 'emulator.*$avdName' || true" 2>&1 - if ([string]::IsNullOrWhiteSpace($emulatorProcs)) { - Write-Error "Emulator process did not start. Checking log..." - if (Test-Path $emulatorLog) { - Get-Content $emulatorLog | Select-Object -Last 50 | ForEach-Object { Write-Info " $_" } - } - exit 1 + if ($runningDevices.Count -gt 0) { + # Use first running device - extract just the emulator-XXXX part + $DeviceUdid = ($runningDevices[0].Line -split '\s+')[0] + Write-Success "Found running Android device: $DeviceUdid" } - Write-Info "Emulator process started (PIDs: $emulatorProcs)" - - # Wait for device to appear with timeout (don't use adb wait-for-device - it can hang forever) - # Timeout of 1800s (30 min) - emulator on CI can take a very long time to boot - Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 1800 - $deviceWaited = 0 - $DeviceUdid = $null - - while ($deviceWaited -lt $deviceTimeout) { - # Match any emulator device line - $runningDevices = adb devices | Select-String "^emulator-\d+" - if ($runningDevices.Count -gt 0) { - # Use .Line to get the actual string content - $line = $runningDevices[0].Line - $parts = $line -split '\s+' - $firstDevice = $parts[0] - $deviceState = $parts[1] - if ($deviceState -eq "device") { - $DeviceUdid = $firstDevice - break + else { + Write-Info "No running devices found. Looking for available emulators..." + + # Check if emulator command exists + if (-not (Get-Command "emulator" -ErrorAction SilentlyContinue)) { + Write-Error "No running Android devices and 'emulator' command not found. Please start an emulator or connect a device." + exit 1 + } + + # Get list of available AVDs (if not already set from parameter) + if (-not $selectedAvd) { + $avdList = emulator -list-avds 2>$null + + if (-not $avdList -or $avdList.Count -eq 0) { + Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio." + Write-Info "To create an AVD:" + Write-Info " 1. Open Android Studio" + Write-Info " 2. Go to Tools > Device Manager" + Write-Info " 3. Click 'Create Device' and follow the wizard" + exit 1 + } + + Write-Info "Available emulators: $($avdList -join ', ')" + + # Selection priority: + # 1. API 34 device (matches CI provisioning) + # 2. API 30 Nexus device + # 3. Any API 30 device + # 4. Any Nexus device + # 5. First available device + + # Try to find API 34 device (CI default) + $api34Device = $avdList | Where-Object { $_ -match "34|API.*34" } | Select-Object -First 1 + if ($api34Device) { + $selectedAvd = $api34Device + Write-Info "Selected API 34 device: $selectedAvd" + } + + # Try to find API 30 Nexus device + if (-not $selectedAvd) { + $api30Nexus = $avdList | Where-Object { $_ -match "API.*30" -and $_ -match "Nexus" } | Select-Object -First 1 + if ($api30Nexus) { + $selectedAvd = $api30Nexus + Write-Info "Selected API 30 Nexus device: $selectedAvd" + } + } + + # Try to find any API 30 device + if (-not $selectedAvd) { + $api30Device = $avdList | Where-Object { $_ -match "API.*30" } | Select-Object -First 1 + if ($api30Device) { + $selectedAvd = $api30Device + Write-Info "Selected API 30 device: $selectedAvd" + } + } + + # Try to find any Nexus device + if (-not $selectedAvd) { + $nexusDevice = $avdList | Where-Object { $_ -match "Nexus" } | Select-Object -First 1 + if ($nexusDevice) { + $selectedAvd = $nexusDevice + Write-Info "Selected Nexus device: $selectedAvd" + } + } + + # Fall back to first available device + if (-not $selectedAvd) { + $selectedAvd = $avdList[0] + Write-Info "Selected first available device: $selectedAvd" } - Write-Info "Device found but state is '$deviceState', waiting..." } - Start-Sleep -Seconds 5 - $deviceWaited += 5 - Write-Info "Waiting for device... ($deviceWaited/$deviceTimeout seconds)" - # Show emulator log tail if taking too long - if ($deviceWaited -ge 30 -and ($deviceWaited % 30 -eq 0) -and (Test-Path $emulatorLog)) { - Write-Info "Emulator log (last 10 lines):" - Get-Content $emulatorLog | Select-Object -Last 10 | ForEach-Object { Write-Info " $_" } + # Start emulator with selected AVD + $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" + + # Check emulator binary exists + if (-not (Test-Path $emulatorBin)) { + Write-Error "Emulator binary not found at: $emulatorBin" + Write-Info "Looking for emulator in SDK..." + Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } + exit 1 } - } - - if (-not $DeviceUdid) { - Write-Error "Emulator device did not appear in time" - Write-Info "Current adb devices:" - adb devices -l - if (Test-Path $emulatorLog) { - Write-Info "Emulator log (last 30 lines):" - Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + + Write-Info "Starting emulator: $selectedAvd" + Write-Info "This may take 1-2 minutes..." + + # Use swiftshader for software rendering (more reliable on CI without GPU) + # Redirect output to a log file for debugging + $emulatorLog = "/tmp/emulator-$selectedAvd.log" + + if ($IsWindows) { + Start-Process $emulatorBin -ArgumentList "-avd", $selectedAvd, "-no-snapshot-load", "-no-boot-anim", "-gpu", "swiftshader_indirect" -WindowStyle Hidden } - exit 1 - } - - Write-Info "Emulator started with device ID: $DeviceUdid" - - # Wait for boot_completed (exactly like CI) - Write-Info "Waiting for emulator to finish booting..." - $timeout = 600 - $waited = 0 - - while ($waited -lt $timeout) { - $bootStatus = adb -s $DeviceUdid shell getprop sys.boot_completed 2>$null - if ($bootStatus -match "1") { - break + else { + # macOS/Linux: Use nohup to detach from terminal + $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" + bash -c $startScript + Write-Info "Emulator started in background. Log file: $emulatorLog" } + + # Give the emulator process time to start Start-Sleep -Seconds 5 - $waited += 5 - Write-Info "Waiting for boot... ($waited/$timeout seconds)" - } - - if ($waited -ge $timeout) { - Write-Error "Emulator did not boot in time" - adb devices -l - if (Test-Path $emulatorLog) { - Write-Info "Emulator log (last 30 lines):" - Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + + # Check if emulator process is running + $emulatorProcs = bash -c "pgrep -f 'qemu.*$selectedAvd' || pgrep -f 'emulator.*$selectedAvd' || true" 2>&1 + if ([string]::IsNullOrWhiteSpace($emulatorProcs)) { + Write-Error "Emulator process did not start. Checking log..." + if (Test-Path $emulatorLog) { + Get-Content $emulatorLog | Select-Object -Last 50 | ForEach-Object { Write-Info " $_" } + } + exit 1 } - exit 1 - } - - Write-Info "Boot completed flag set" - - # Wait for package manager service to be available (critical for app installation) - Write-Info "Waiting for package manager service..." - $pmTimeout = 120 - $pmWaited = 0 - - while ($pmWaited -lt $pmTimeout) { - $pmOutput = adb -s $DeviceUdid shell pm list packages 2>$null - if ($pmOutput -match "package:") { - break + Write-Info "Emulator process started (PIDs: $emulatorProcs)" + + # Wait for device to appear with timeout + # Timeout of 600s (10 min) - emulator can take a while to boot, especially with software rendering + Write-Info "Waiting for emulator device to appear..." + $deviceTimeout = 600 + $deviceWaited = 0 + + while ($deviceWaited -lt $deviceTimeout) { + # Match any emulator device line + $devices = adb devices | Select-String "^emulator-\d+\s+device" + if ($devices.Count -gt 0) { + $DeviceUdid = ($devices[0].Line -split '\s+')[0] + Write-Info "Emulator detected: $DeviceUdid" + break + } + + # Check for offline state + $offlineDevices = adb devices | Select-String "^emulator-\d+\s+offline" + if ($offlineDevices.Count -gt 0) { + Write-Info "Device found but offline, waiting..." + } + + Start-Sleep -Seconds 5 + $deviceWaited += 5 + + if ($deviceWaited % 30 -eq 0) { + Write-Info "Still waiting... ($deviceWaited seconds elapsed)" + # Show emulator log tail if taking too long + if ((Test-Path $emulatorLog)) { + Write-Info "Emulator log (last 5 lines):" + Get-Content $emulatorLog | Select-Object -Last 5 | ForEach-Object { Write-Info " $_" } + } + } + } + + if (-not $DeviceUdid) { + Write-Error "Emulator failed to start within $deviceTimeout seconds. Please try starting it manually." + Write-Info "Current adb devices:" + adb devices -l + if (Test-Path $emulatorLog) { + Write-Info "Emulator log (last 30 lines):" + Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + } + exit 1 + } + + # Wait for boot to complete + Write-Info "Waiting for emulator to finish booting..." + $bootTimeout = 600 + $bootElapsed = 0 + + while ($bootElapsed -lt $bootTimeout) { + $bootStatus = adb -s $DeviceUdid shell getprop sys.boot_completed 2>$null + if ($bootStatus -match "1") { + Write-Success "Emulator fully booted: $DeviceUdid" + break + } + + Start-Sleep -Seconds 5 + $bootElapsed += 5 + + if ($bootElapsed % 30 -eq 0) { + Write-Info "Still booting... ($bootElapsed seconds elapsed)" + } + } + + if ($bootElapsed -ge $bootTimeout) { + Write-Error "Emulator failed to complete boot within $bootTimeout seconds." + Write-Info "You can check status with: adb -s $DeviceUdid shell getprop sys.boot_completed" + if (Test-Path $emulatorLog) { + Write-Info "Emulator log (last 30 lines):" + Get-Content $emulatorLog | Select-Object -Last 30 | ForEach-Object { Write-Info " $_" } + } + exit 1 + } + + # Wait for package manager service to be available (critical for app installation) + Write-Info "Waiting for package manager service..." + $pmTimeout = 120 + $pmWaited = 0 + + while ($pmWaited -lt $pmTimeout) { + $pmOutput = adb -s $DeviceUdid shell pm list packages 2>$null + if ($pmOutput -match "package:") { + Write-Info "Package manager service is ready" + break + } + Start-Sleep -Seconds 3 + $pmWaited += 3 + if ($pmWaited % 15 -eq 0) { + Write-Info "Waiting for package manager... ($pmWaited seconds elapsed)" + } + } + + if ($pmWaited -ge $pmTimeout) { + Write-Error "Package manager service did not start within $pmTimeout seconds." + Write-Info "Checking services:" + adb -s $DeviceUdid shell service list 2>$null | Select-Object -First 20 | ForEach-Object { Write-Info " $_" } + exit 1 } - Start-Sleep -Seconds 3 - $pmWaited += 3 - Write-Info "Waiting for package manager... ($pmWaited/$pmTimeout seconds)" - } - - if ($pmWaited -ge $pmTimeout) { - Write-Error "Package manager service did not start" - Write-Info "Checking services:" - adb -s $DeviceUdid shell service list 2>$null | Select-Object -First 20 | ForEach-Object { Write-Info " $_" } - exit 1 } - - Write-Info "Package manager service is ready" } - Write-Success "=== Emulator booted successfully! ===" - adb devices -l + Write-Success "Using Android device: $DeviceUdid" #endregion diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index bd67b4708eaa..e309295f79fd 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -105,8 +105,8 @@ stages: $ErrorActionPreference = "Continue" try { - Write-Host "Invoking Start-Emulator.ps1 -Platform android..." - & "./$scriptPath" -Platform android 2>&1 | ForEach-Object { Write-Host $_ } + Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_34..." + & "./$scriptPath" -Platform android -DeviceUdid Emulator_34 2>&1 | ForEach-Object { Write-Host $_ } $exitCode = $LASTEXITCODE Write-Host "Script returned exit code: $exitCode" } catch { From 92dd3a1dda9c6917416b205cb6e46b9e951168b6 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 16:28:10 -0600 Subject: [PATCH 073/126] Use Review-PR.ps1 switches for posting comments - Add -Platform android -RunFinalize -PostSummaryComment to Review-PR.ps1 call - Remove separate 'Post Review Comment Using Skill' step - Remove 'Post Review Comment (Fallback)' step - Script now handles all comment posting internally --- eng/pipelines/ci-copilot.yml | 88 +----------------------------------- 1 file changed, 2 insertions(+), 86 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index e309295f79fd..6e5d9214dbc2 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -466,9 +466,9 @@ stages: # Invoke the PR reviewer using our PowerShell script # The script will merge the PR into the current branch - # -NoInteractive for CI mode (exits after completion) + # -PostSummaryComment and -RunFinalize handle posting comments set +e - pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -NoInteractive 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform android -RunFinalize -PostSummaryComment 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md COPILOT_EXIT_CODE=$? set -e @@ -514,90 +514,6 @@ stages: env: GITHUB_TOKEN: $(COPILOT_TOKEN) - - script: | - echo "Posting review comment using skill..." - - # Create artifacts directory if not exists - mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - - # Use Copilot CLI to invoke the post-comment skill - set +e - copilot -p "Use a skill to post the review feedback as a comment on PR #${{ parameters.PRNumber }}. The review output should be in the CustomAgentLogsTmp or .github/agent-pr-session directories. Clean up ANSI codes and stats before posting." --yolo 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_post_comment_output.md - POST_COMMENT_EXIT_CODE=$? - set -e - - echo "Post comment exit code: $POST_COMMENT_EXIT_CODE" - - if [ $POST_COMMENT_EXIT_CODE -ne 0 ]; then - echo "##vso[task.logissue type=warning]Copilot post-comment skill exited with code $POST_COMMENT_EXIT_CODE" - echo "##vso[task.setvariable variable=PostCommentFailed]true" - fi - displayName: 'Post Review Comment Using Skill' - env: - GITHUB_TOKEN: $(COPILOT_TOKEN) - condition: succeededOrFailed() - - - script: | - echo "Posting review comment to PR (fallback)..." - - # Check for the captured output file first - REVIEW_FILE="$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" - CLEANED_FILE="$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_cleaned.md" - - # Also check if Copilot created a Review_Feedback file - FEEDBACK_FILE=$(find $(Build.ArtifactStagingDirectory)/copilot-logs -name "Review_Feedback_*.md" -type f | head -1) - - if [ -n "$FEEDBACK_FILE" ]; then - echo "Found review feedback file: $FEEDBACK_FILE" - REVIEW_FILE="$FEEDBACK_FILE" - fi - - if [ -f "$REVIEW_FILE" ] && [ -s "$REVIEW_FILE" ]; then - echo "Cleaning up review output..." - - # Clean up the content: - # 1. Strip all ANSI escape codes (color codes like [37m, [39m, etc.) - # 2. Start from the first markdown header (## or #) - # 3. Remove stats at the end (Total usage est, API time, Total session, etc.) - sed 's/\x1b\[[0-9;]*m//g' "$REVIEW_FILE" | \ - sed -n '/^#/,$p' | \ - sed '/^Total usage est/,$d' | \ - sed '/^API time spent/d' | \ - sed '/^Total session time/d' | \ - sed '/^Total duration/d' | \ - sed '/^Total code changes/d' | \ - sed '/^Breakdown by AI model/,$d' | \ - sed '/^Usage by model/,$d' | \ - sed '/^ *claude-/d' | \ - sed '/^ *gpt-/d' > "$CLEANED_FILE" - - # If cleaning removed everything, fall back to original without ANSI codes - if [ ! -s "$CLEANED_FILE" ]; then - echo "Cleaned file is empty, using original without ANSI codes" - sed 's/\x1b\[[0-9;]*m//g' "$REVIEW_FILE" > "$CLEANED_FILE" - fi - - echo "Posting review from: $CLEANED_FILE" - echo "--- Review Content Preview ---" - head -50 "$CLEANED_FILE" - echo "--- End Preview ---" - - # Post the review as a comment on the PR - # Note: GH_COMMENT_TOKEN needs 'repo' scope or 'pull_requests:write' permission - if gh pr comment ${{ parameters.PRNumber }} --repo dotnet/maui --body-file "$CLEANED_FILE"; then - echo "Review comment posted successfully" - else - echo "##vso[task.logissue type=warning]Failed to post comment. Check that GH_COMMENT_TOKEN has 'repo' or 'pull_requests:write' scope." - echo "Review content is available in the published artifacts." - fi - else - echo "##vso[task.logissue type=warning]No review output found or file is empty" - fi - displayName: 'Post Review Comment (Fallback)' - env: - GITHUB_TOKEN: $(GH_COMMENT_TOKEN) - condition: and(succeededOrFailed(), eq(variables['PostCommentFailed'], 'true')) - # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 displayName: 'Publish Copilot Logs' From 88a708424f8604e516e67c456d281e32803b15c8 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 17:25:23 -0600 Subject: [PATCH 074/126] Switch to AcesShared pool (ARM64 macOS) Testing ARM64 dedicated pool to see if HVF is available for faster Android emulator boot times. --- eng/pipelines/ci-copilot.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 6e5d9214dbc2..184c8c2bf554 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -18,8 +18,7 @@ parameters: - name: pool type: object default: - name: Azure Pipelines - vmImage: macOS-14 + name: AcesShared variables: - template: /eng/pipelines/common/variables.yml@self From 8fca884a0429d3d6ca7e05c254e9924f00cd5fd2 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 17:38:58 -0600 Subject: [PATCH 075/126] Enable Android SDK provisioning for AcesShared pool AcesShared doesn't have Android SDK pre-installed like hosted images. Enable skipAndroidCommonSdks=false and skipAndroidPlatformApis=false to install the full Android SDK during pipeline execution. --- eng/pipelines/ci-copilot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 184c8c2bf554..9c8c07b848ba 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -54,8 +54,8 @@ stages: parameters: skipXcode: false skipProvisionator: false - skipAndroidCommonSdks: true - skipAndroidPlatformApis: true + skipAndroidCommonSdks: false # Install base Android SDK (cmdline-tools, emulator, platform-tools) + skipAndroidPlatformApis: false # Install platform APIs skipJdk: false skipSimulatorSetup: false skipCertificates: true From a9fdae14b6ca9fceb021ce21d44debf40be0ddeb Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 17:49:11 -0600 Subject: [PATCH 076/126] Add PATH setup for Android SDK tools on AcesShared pool - Add JAVA_HOME auto-detection from microsoft-*.jdk - Add platform-tools, emulator, cmdline-tools to PATH - Required when SDK is freshly provisioned on self-hosted agents --- eng/pipelines/ci-copilot.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 9c8c07b848ba..e537a3bba28d 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -93,6 +93,28 @@ stages: Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" Write-Host "JAVA_HOME: $env:JAVA_HOME" + # Auto-detect JAVA_HOME if not set (e.g., on self-hosted agents) + if (-not $env:JAVA_HOME) { + $jvmDir = "/Library/Java/JavaVirtualMachines" + $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($msJdk) { + $env:JAVA_HOME = "$($msJdk.FullName)/Contents/Home" + Write-Host "Auto-detected JAVA_HOME: $env:JAVA_HOME" + } + } + + # Add Android SDK tools to PATH (required when SDK is freshly provisioned) + if ($env:ANDROID_SDK_ROOT) { + $platformTools = Join-Path $env:ANDROID_SDK_ROOT "platform-tools" + $emulatorPath = Join-Path $env:ANDROID_SDK_ROOT "emulator" + $cmdlineTools = Join-Path $env:ANDROID_SDK_ROOT "cmdline-tools/latest/bin" + $env:PATH = "$platformTools${[IO.Path]::PathSeparator}$emulatorPath${[IO.Path]::PathSeparator}$cmdlineTools${[IO.Path]::PathSeparator}$env:PATH" + Write-Host "Updated PATH to include Android SDK tools" + Write-Host " platform-tools: $platformTools" + Write-Host " emulator: $emulatorPath" + Write-Host " cmdline-tools: $cmdlineTools" + } + $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" Write-Host "Script path: $scriptPath" @@ -124,7 +146,6 @@ stages: timeoutInMinutes: 35 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic From ada17daf1532dec7d617eb1f934efe87dd041055 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 18:01:43 -0600 Subject: [PATCH 077/126] Use ##vso[task.prependpath] to persist PATH for Android SDK tools - Add separate 'Configure Android SDK PATH' step before emulator start - Use ##vso[task.prependpath] so PATH persists across steps - Add verification that adb/emulator exist at expected locations - Set JAVA_HOME via ##vso[task.setvariable] for persistence --- eng/pipelines/ci-copilot.yml | 75 +++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index e537a3bba28d..7447f682105a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -86,33 +86,70 @@ stages: fi displayName: 'Configure AVD for Software Emulation' - # Start Android Emulator EARLY (using Start-Emulator.ps1 script) + # Set up Android SDK PATH (required on self-hosted agents) - pwsh: | - Write-Host "=== Starting Android Emulator ===" - Write-Host "Working directory: $(Get-Location)" - Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" - Write-Host "JAVA_HOME: $env:JAVA_HOME" - - # Auto-detect JAVA_HOME if not set (e.g., on self-hosted agents) - if (-not $env:JAVA_HOME) { - $jvmDir = "/Library/Java/JavaVirtualMachines" - $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($msJdk) { - $env:JAVA_HOME = "$($msJdk.FullName)/Contents/Home" - Write-Host "Auto-detected JAVA_HOME: $env:JAVA_HOME" - } - } - - # Add Android SDK tools to PATH (required when SDK is freshly provisioned) if ($env:ANDROID_SDK_ROOT) { $platformTools = Join-Path $env:ANDROID_SDK_ROOT "platform-tools" $emulatorPath = Join-Path $env:ANDROID_SDK_ROOT "emulator" $cmdlineTools = Join-Path $env:ANDROID_SDK_ROOT "cmdline-tools/latest/bin" - $env:PATH = "$platformTools${[IO.Path]::PathSeparator}$emulatorPath${[IO.Path]::PathSeparator}$cmdlineTools${[IO.Path]::PathSeparator}$env:PATH" - Write-Host "Updated PATH to include Android SDK tools" + + # Use Azure Pipelines' prependpath command to persist across steps + Write-Host "##vso[task.prependpath]$platformTools" + Write-Host "##vso[task.prependpath]$emulatorPath" + Write-Host "##vso[task.prependpath]$cmdlineTools" + + Write-Host "Added to PATH (will apply to subsequent steps):" Write-Host " platform-tools: $platformTools" Write-Host " emulator: $emulatorPath" Write-Host " cmdline-tools: $cmdlineTools" + + # Verify the tools exist + if (Test-Path (Join-Path $platformTools "adb")) { + Write-Host "✅ adb found at: $platformTools/adb" + } else { + Write-Host "⚠️ adb NOT found at expected location" + } + if (Test-Path (Join-Path $emulatorPath "emulator")) { + Write-Host "✅ emulator found at: $emulatorPath/emulator" + } else { + Write-Host "⚠️ emulator NOT found at expected location" + } + } else { + Write-Host "##vso[task.logissue type=warning]ANDROID_SDK_ROOT not set, skipping PATH setup" + } + + # Auto-detect and set JAVA_HOME if not already set + if (-not $env:JAVA_HOME) { + $jvmDir = "/Library/Java/JavaVirtualMachines" + $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($msJdk) { + $javaHome = "$($msJdk.FullName)/Contents/Home" + Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome" + Write-Host "Set JAVA_HOME to: $javaHome" + } + } + displayName: 'Configure Android SDK PATH' + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + + # Start Android Emulator EARLY (using Start-Emulator.ps1 script) + - pwsh: | + Write-Host "=== Starting Android Emulator ===" + Write-Host "Working directory: $(Get-Location)" + Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" + Write-Host "JAVA_HOME: $env:JAVA_HOME" + Write-Host "PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))" + + # Verify adb is in path + $adbPath = Get-Command adb -ErrorAction SilentlyContinue + if ($adbPath) { + Write-Host "adb found at: $($adbPath.Source)" + } else { + Write-Host "adb NOT in PATH, checking manually..." + $manualAdb = Join-Path $env:ANDROID_SDK_ROOT "platform-tools/adb" + if (Test-Path $manualAdb) { + Write-Host "adb exists at $manualAdb but not in PATH" + } } $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" From 8f8b910d385f9e87c130c94b9fa28b013c65a998 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 19:41:52 -0600 Subject: [PATCH 078/126] Add ai-summary-comment fallback step and improve skill discovery - Add 'Post AI Summary Comment (Fallback)' step in ci-copilot.yml - Checks if AI summary already posted before running - Falls back to gh pr comment if skill invocation fails - Update Review-PR.ps1 ai-summary-comment phase: - Verify skill file exists before invoking - More explicit prompt about reading skill from .github/skills/ - Fallback instruction if skill file not found --- .github/scripts/Review-PR.ps1 | 23 +++++++++--- eng/pipelines/ci-copilot.yml | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 1d8749d98aa6..70fddb6f5b85 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -422,11 +422,24 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # Restore tracked files (including deleted ones) to clean state. - Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow - git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase2-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null - Write-Host " ✅ Working tree restored" -ForegroundColor Green + # First verify the skill file exists + $skillPath = ".github/skills/ai-summary-comment/SKILL.md" + if (Test-Path $skillPath) { + Write-Host "✅ Found skill file: $skillPath" -ForegroundColor Green + } else { + Write-Host "⚠️ Skill file not found at: $skillPath" -ForegroundColor Yellow + Write-Host " Current directory: $(Get-Location)" -ForegroundColor Gray + Write-Host " Available skills:" -ForegroundColor Gray + Get-ChildItem ".github/skills/" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " - $($_.Name)" -ForegroundColor Gray } + } + + $commentPrompt = @" +Read the ai-summary-comment skill from .github/skills/ai-summary-comment/SKILL.md and follow its instructions to post a summary comment on PR #$PRNumber. + +The PR state file is at: CustomAgentLogsTmp/PRState/pr-$PRNumber-state.md + +If you cannot find the skill file, use the GitHub CLI (gh pr comment) to post a summary of the PR review results from the state file. +"@ # 3a: Post PR agent summary comment (from Phase 1 state file) $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 7447f682105a..bc81d5d566f8 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -597,3 +597,74 @@ stages: fi displayName: 'Check Copilot Result' condition: succeededOrFailed() + + # Fallback: Post AI summary comment if the PR Reviewer didn't successfully post one + # This runs the ai-summary-comment skill directly as a backup + - pwsh: | + Write-Host "=== Fallback: Running ai-summary-comment skill ===" + + # Check if a comment was already posted by looking at recent PR comments + $prNumber = "${{ parameters.PRNumber }}" + Write-Host "Checking for existing AI summary comment on PR #$prNumber..." + + $recentComments = gh pr view $prNumber --json comments --jq '.comments[-5:] | .[] | .body' 2>$null + + # Look for our AI summary marker + if ($recentComments -match "AI Summary|🤖.*Summary|Copilot.*Summary") { + Write-Host "✅ AI summary comment already exists on PR. Skipping fallback." + exit 0 + } + + Write-Host "No AI summary comment found. Running ai-summary-comment skill..." + + # Build the prompt for the skill + $skillPrompt = @" + Please run the ai-summary-comment skill to post a progress comment on PR #$prNumber. + + The PR state file should be at: CustomAgentLogsTmp/PRState/pr-$prNumber-state.md + + Post a summary comment with: + 1. Current phase status + 2. Test results (if available) + 3. Any blockers or issues encountered + 4. Next steps or recommendations + "@ + + # Try to invoke Copilot CLI with the skill + Write-Host "Invoking Copilot CLI to run ai-summary-comment skill..." + + $result = gh copilot suggest "$skillPrompt" 2>&1 + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=warning]Copilot CLI skill invocation returned exit code $exitCode" + Write-Host "Attempting direct comment as final fallback..." + + # Direct fallback: post a simple comment using gh CLI + $commentBody = @" + ## 🤖 AI PR Review Summary (Automated) + + **PR:** #$prNumber + **Status:** Review completed + + The automated Copilot PR reviewer has completed its analysis. Please check the build artifacts for detailed logs. + + --- + *This comment was posted by the CI pipeline fallback mechanism.* + "@ + + gh pr comment $prNumber --body "$commentBody" + if ($LASTEXITCODE -eq 0) { + Write-Host "✅ Fallback comment posted successfully" + } else { + Write-Host "##vso[task.logissue type=warning]Failed to post fallback comment" + } + } else { + Write-Host "✅ ai-summary-comment skill completed" + } + displayName: 'Post AI Summary Comment (Fallback)' + condition: succeededOrFailed() + continueOnError: true + env: + GITHUB_TOKEN: $(COPILOT_TOKEN) + GH_TOKEN: $(GH_COMMENT_TOKEN) From 73d24e1c192b6e516630a8271b3f3e00557dab21 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 4 Feb 2026 20:41:15 -0600 Subject: [PATCH 079/126] Configure git identity for merge operations on self-hosted agents The AcesShared pool agents don't have git user.email/name configured, which causes git merge to fail with 'Committer identity unknown' error. --- eng/pipelines/ci-copilot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index bc81d5d566f8..1980ce59f329 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -518,6 +518,11 @@ stages: echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." + # Configure git identity (required for merge operations on self-hosted agents) + git config user.email "copilot-ci@microsoft.com" + git config user.name "Copilot CI" + echo "Git identity configured" + # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs From 5f642f441cb3aed1ae5714e47000e95588db5834 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 5 Feb 2026 16:49:34 -0600 Subject: [PATCH 080/126] Add mandatory cleanup between try-fix attempts and common errors section - Add cleanup step (EstablishBrokenBaseline.ps1 -Restore + git checkout -- .) between try-fix attempts to prevent dirty working tree from failed attempts - Add Common Errors and Recovery section documenting: - skill ENOENT errors from dirty git state - dirty working tree causing unexpected build failures - unrelated build errors from previous attempt residue - Add common mistake entry for skipping cleanup --- .github/agents/pr/post-gate.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index f030801ee257..e018d4f48429 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -88,14 +88,10 @@ pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore # 2. Restore all tracked files to HEAD (the merged PR state) # This catches any files the previous attempt modified but didn't restore -git checkout HEAD -- . - -# 3. Remove untracked files added by the previous attempt -# git checkout restores tracked files but does NOT remove new untracked files -git clean -fd --exclude=CustomAgentLogsTmp/ +git checkout -- . ``` -**Why this is required:** Each try-fix attempt modifies source files. If an attempt fails mid-way (build error, timeout, model error), it may not run its own cleanup step. Without explicit cleanup, the next attempt starts with a dirty working tree, which can cause missing files, corrupt state, or misleading test results. Use `HEAD` (not just `-- .`) to also restore deleted files. +**Why this is required:** Each try-fix attempt modifies source files. If an attempt fails mid-way (build error, timeout, model error), it may not run its own cleanup step. Without explicit cleanup, the next attempt starts with a dirty working tree, which can cause missing files, corrupt state, or misleading test results. #### Round 2+: Cross-Pollination Loop (MANDATORY) @@ -325,7 +321,7 @@ Update all phase statuses to complete. - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored - ❌ **Declaring exhaustion prematurely** - All 5 models must confirm "no new ideas" via actual invocation - ❌ **Rushing the report** - Take time to write clear justification -- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout HEAD -- .` + `git clean -fd --exclude=CustomAgentLogsTmp/` between try-fix attempts (see Step 1) +- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout -- .` between try-fix attempts (see Step 1) --- @@ -340,8 +336,7 @@ Update all phase statuses to complete. **Fix:** Run cleanup before retrying: ```bash pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore -git checkout HEAD -- . -git clean -fd --exclude=CustomAgentLogsTmp/ +git checkout -- . ``` Then retry the try-fix attempt. The skill file should now be accessible. @@ -354,7 +349,7 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Previous attempt didn't restore its changes (crashed, timed out, or model didn't follow Step 8 restore instructions). -**Fix:** Same as above — run `-Restore` + `git checkout HEAD -- .` + `git clean -fd --exclude=CustomAgentLogsTmp/` to reset to the merged PR state. +**Fix:** Same as above — run `-Restore` + `git checkout -- .` to reset to the merged PR state. ### Build errors unrelated to the fix being attempted @@ -363,6 +358,6 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Often caused by dirty working tree from a previous attempt. Can also be transient environment issues. **Fix:** -1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout HEAD -- . && git clean -fd --exclude=CustomAgentLogsTmp/` +1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout -- .` 2. Retry the attempt -3. If it fails again with the same unrelated error, treat this as an environment/worktree blocker: STOP the try-fix workflow, do NOT continue with the next model, and ask the user to investigate (see "Stop on Environment Blockers"). +3. If it fails again with the same unrelated error, skip this attempt and continue with the next model From d3c41620827c7d1d6fd95f3c738467ed0e12e0cb Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 5 Feb 2026 18:55:50 -0600 Subject: [PATCH 081/126] Fix ai-summary-comment invocation and fallback --- .github/scripts/Review-PR.ps1 | 16 ++++++-- eng/pipelines/ci-copilot.yml | 77 +++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 70fddb6f5b85..ed02d436e5c5 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -434,11 +434,21 @@ if ($DryRun) { } $commentPrompt = @" -Read the ai-summary-comment skill from .github/skills/ai-summary-comment/SKILL.md and follow its instructions to post a summary comment on PR #$PRNumber. +Post the AI summary comment for PR #$PRNumber using the ai-summary-comment skill. -The PR state file is at: CustomAgentLogsTmp/PRState/pr-$PRNumber-state.md +**Step 1: Run the post-ai-summary-comment.ps1 script** +``````bash +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $PRNumber +`````` -If you cannot find the skill file, use the GitHub CLI (gh pr comment) to post a summary of the PR review results from the state file. +The script will automatically: +- Load the state file from CustomAgentLogsTmp/PRState/pr-$PRNumber.md +- Parse all phases and their statuses +- Post/update the unified AI Summary comment on the PR + +**If the script fails**, check that the state file exists and contains valid phase data. + +**Do NOT** manually compose or post comments - always use the script. "@ # 3a: Post PR agent summary comment (from Phase 1 state file) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 1980ce59f329..01221aae5ba4 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -604,48 +604,63 @@ stages: condition: succeededOrFailed() # Fallback: Post AI summary comment if the PR Reviewer didn't successfully post one - # This runs the ai-summary-comment skill directly as a backup + # This directly runs the post-ai-summary-comment.ps1 script as a backup - pwsh: | - Write-Host "=== Fallback: Running ai-summary-comment skill ===" + Write-Host "=== Fallback: Running post-ai-summary-comment.ps1 ===" - # Check if a comment was already posted by looking at recent PR comments $prNumber = "${{ parameters.PRNumber }}" - Write-Host "Checking for existing AI summary comment on PR #$prNumber..." + Write-Host "PR #$prNumber" - $recentComments = gh pr view $prNumber --json comments --jq '.comments[-5:] | .[] | .body' 2>$null + # Check if a comment was already posted by looking for the AI Summary marker + Write-Host "Checking for existing AI summary comment..." + $existingComment = gh pr view $prNumber --json comments --jq '.comments[] | select(.body | contains("")) | .id' 2>$null | Select-Object -First 1 - # Look for our AI summary marker - if ($recentComments -match "AI Summary|🤖.*Summary|Copilot.*Summary") { - Write-Host "✅ AI summary comment already exists on PR. Skipping fallback." + if ($existingComment) { + Write-Host "✅ AI summary comment already exists (ID: $existingComment). Skipping fallback." exit 0 } - Write-Host "No AI summary comment found. Running ai-summary-comment skill..." + Write-Host "No AI summary comment found. Running post-ai-summary-comment.ps1..." - # Build the prompt for the skill - $skillPrompt = @" - Please run the ai-summary-comment skill to post a progress comment on PR #$prNumber. + # Check for state file + $stateFile = "CustomAgentLogsTmp/PRState/pr-$prNumber.md" + if (-not (Test-Path $stateFile)) { + Write-Host "##vso[task.logissue type=warning]State file not found: $stateFile" + Write-Host "Available state files:" + Get-ChildItem "CustomAgentLogsTmp/PRState/" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " - $($_.Name)" } + + # Try to find any matching state file + $stateFile = Get-ChildItem "CustomAgentLogsTmp/PRState/pr-$prNumber*.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($stateFile) { + Write-Host "Found alternative: $($stateFile.FullName)" + } else { + Write-Host "No state file found. Posting minimal fallback comment." + $commentBody = @" + ## 🤖 AI PR Review Summary (Automated) - The PR state file should be at: CustomAgentLogsTmp/PRState/pr-$prNumber-state.md + **PR:** #$prNumber + **Status:** Review completed - Post a summary comment with: - 1. Current phase status - 2. Test results (if available) - 3. Any blockers or issues encountered - 4. Next steps or recommendations - "@ + The automated Copilot PR reviewer completed but the state file was not found. Please check the build artifacts for detailed logs. - # Try to invoke Copilot CLI with the skill - Write-Host "Invoking Copilot CLI to run ai-summary-comment skill..." + --- + *This comment was posted by the CI pipeline fallback mechanism.* + "@ + gh pr comment $prNumber --body "$commentBody" + exit 0 + } + } - $result = gh copilot suggest "$skillPrompt" 2>&1 - $exitCode = $LASTEXITCODE + Write-Host "State file: $stateFile" - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=warning]Copilot CLI skill invocation returned exit code $exitCode" - Write-Host "Attempting direct comment as final fallback..." + # Run the post-ai-summary-comment.ps1 script directly + try { + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber + Write-Host "✅ AI summary comment posted successfully" + } catch { + Write-Host "##vso[task.logissue type=warning]post-ai-summary-comment.ps1 failed: $_" - # Direct fallback: post a simple comment using gh CLI + # Final fallback: post simple comment $commentBody = @" ## 🤖 AI PR Review Summary (Automated) @@ -657,15 +672,7 @@ stages: --- *This comment was posted by the CI pipeline fallback mechanism.* "@ - gh pr comment $prNumber --body "$commentBody" - if ($LASTEXITCODE -eq 0) { - Write-Host "✅ Fallback comment posted successfully" - } else { - Write-Host "##vso[task.logissue type=warning]Failed to post fallback comment" - } - } else { - Write-Host "✅ ai-summary-comment skill completed" } displayName: 'Post AI Summary Comment (Fallback)' condition: succeededOrFailed() From 26f7a1411aed7b2b4f4e9c406f4a9eb87893770f Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 5 Feb 2026 19:16:13 -0600 Subject: [PATCH 082/126] Remove generic fallback comment - only use post-ai-summary-comment.ps1 --- eng/pipelines/ci-copilot.yml | 48 ++++++++---------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 01221aae5ba4..d599604ce40c 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -630,49 +630,21 @@ stages: Get-ChildItem "CustomAgentLogsTmp/PRState/" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " - $($_.Name)" } # Try to find any matching state file - $stateFile = Get-ChildItem "CustomAgentLogsTmp/PRState/pr-$prNumber*.md" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($stateFile) { - Write-Host "Found alternative: $($stateFile.FullName)" - } else { - Write-Host "No state file found. Posting minimal fallback comment." - $commentBody = @" - ## 🤖 AI PR Review Summary (Automated) - - **PR:** #$prNumber - **Status:** Review completed - - The automated Copilot PR reviewer completed but the state file was not found. Please check the build artifacts for detailed logs. - - --- - *This comment was posted by the CI pipeline fallback mechanism.* - "@ - gh pr comment $prNumber --body "$commentBody" - exit 0 + $altStateFile = Get-ChildItem "CustomAgentLogsTmp/PRState/pr-$prNumber*.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $altStateFile) { + Write-Host "##vso[task.logissue type=error]No state file found for PR #$prNumber. Cannot post AI summary comment." + exit 1 } + Write-Host "Found alternative: $($altStateFile.FullName)" } - Write-Host "State file: $stateFile" - # Run the post-ai-summary-comment.ps1 script directly - try { - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber + if ($LASTEXITCODE -eq 0) { Write-Host "✅ AI summary comment posted successfully" - } catch { - Write-Host "##vso[task.logissue type=warning]post-ai-summary-comment.ps1 failed: $_" - - # Final fallback: post simple comment - $commentBody = @" - ## 🤖 AI PR Review Summary (Automated) - - **PR:** #$prNumber - **Status:** Review completed - - The automated Copilot PR reviewer has completed its analysis. Please check the build artifacts for detailed logs. - - --- - *This comment was posted by the CI pipeline fallback mechanism.* - "@ - gh pr comment $prNumber --body "$commentBody" + } else { + Write-Host "##vso[task.logissue type=error]post-ai-summary-comment.ps1 failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE } displayName: 'Post AI Summary Comment (Fallback)' condition: succeededOrFailed() From 1c0ba83b657d2d6c0e25f98f32836ef6a23b7ca8 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 5 Feb 2026 19:28:41 -0600 Subject: [PATCH 083/126] Pass -StateFile to fallback when using alternative path (codex review fix) --- eng/pipelines/ci-copilot.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d599604ce40c..5fc76c435efd 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -636,10 +636,11 @@ stages: exit 1 } Write-Host "Found alternative: $($altStateFile.FullName)" + $stateFile = $altStateFile.FullName } # Run the post-ai-summary-comment.ps1 script directly - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -StateFile $stateFile if ($LASTEXITCODE -eq 0) { Write-Host "✅ AI summary comment posted successfully" } else { From ccb7521830cd5320a564abc84d736ea50d6a820a Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 5 Feb 2026 21:01:24 -0600 Subject: [PATCH 084/126] Fallback: restore clean tree before running script, use -PRNumber The try-fix phase leaves the working tree dirty, which corrupts the post-ai-summary-comment.ps1 script file causing 'parameter not found' errors. Run git checkout -- . first to restore all tracked files. --- eng/pipelines/ci-copilot.yml | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5fc76c435efd..a353fd21ee3f 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -611,6 +611,13 @@ stages: $prNumber = "${{ parameters.PRNumber }}" Write-Host "PR #$prNumber" + # Restore tracked files to clean state before running the script. + # The PR reviewer's try-fix phase may have left the working tree dirty, + # which can corrupt script files and cause parameter binding errors. + Write-Host "Restoring working tree to clean state..." + git checkout -- . 2>&1 | Write-Host + Write-Host "✅ Working tree restored" + # Check if a comment was already posted by looking for the AI Summary marker Write-Host "Checking for existing AI summary comment..." $existingComment = gh pr view $prNumber --json comments --jq '.comments[] | select(.body | contains("")) | .id' 2>$null | Select-Object -First 1 @@ -622,25 +629,9 @@ stages: Write-Host "No AI summary comment found. Running post-ai-summary-comment.ps1..." - # Check for state file - $stateFile = "CustomAgentLogsTmp/PRState/pr-$prNumber.md" - if (-not (Test-Path $stateFile)) { - Write-Host "##vso[task.logissue type=warning]State file not found: $stateFile" - Write-Host "Available state files:" - Get-ChildItem "CustomAgentLogsTmp/PRState/" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " - $($_.Name)" } - - # Try to find any matching state file - $altStateFile = Get-ChildItem "CustomAgentLogsTmp/PRState/pr-$prNumber*.md" -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $altStateFile) { - Write-Host "##vso[task.logissue type=error]No state file found for PR #$prNumber. Cannot post AI summary comment." - exit 1 - } - Write-Host "Found alternative: $($altStateFile.FullName)" - $stateFile = $altStateFile.FullName - } - # Run the post-ai-summary-comment.ps1 script directly - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -StateFile $stateFile + # Uses -PRNumber which auto-loads state from CustomAgentLogsTmp/PRState/pr-$prNumber.md + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber if ($LASTEXITCODE -eq 0) { Write-Host "✅ AI summary comment posted successfully" } else { From b4cf64d63854a7a82aa6c4f06e432547dd63e520 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Fri, 6 Feb 2026 08:12:17 -0600 Subject: [PATCH 085/126] Fix AI summary comment: prevent state file overwrite, run script directly Root cause: Three bugs caused the wrong comment format on PR #28886: 1. pr-finalize (Phase 2) overwrote the state file pr-XXX.md with its Verification Report, destroying all phase data (Pre-Flight, Tests, Gate) created by the PR Agent (Phase 1). 2. Phase 2 Copilot CLI session left the working tree dirty, causing skill files to go missing when Phase 3 started. 3. Phase 3 used a Copilot CLI session which, when skill files were missing, created its own broken script with wrong marker. Fixes: - pr-finalize now writes to pr-XXX-final.md (separate from main state) - git checkout -- . runs between Phase 1-2 and before Phase 3 - Phase 3 directly invokes post-ai-summary-comment.ps1 via pwsh instead of via Copilot CLI (deterministic, no hallucination risk) - Added GH_TOKEN to Run PR Reviewer step for comment posting --- .github/scripts/Review-PR.ps1 | 85 ++++++----------------------------- eng/pipelines/ci-copilot.yml | 1 + 2 files changed, 14 insertions(+), 72 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index ed02d436e5c5..ba14f5f557aa 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -370,12 +370,9 @@ if ($DryRun) { # Restore tracked files to clean state before running post-completion skills. # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, # which can cause skill files to be missing or modified in subsequent phases. - # NOTE: State files in CustomAgentLogsTmp/ are .gitignore'd and untracked, - # so this won't touch them. Using HEAD to also restore deleted files. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow - git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase1-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null + git checkout -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green # Phase 2: Run pr-finalize skill if requested @@ -386,13 +383,7 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # Ensure output directory exists for finalize results - $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize" - if (-not (Test-Path $finalizeDir)) { - New-Item -ItemType Directory -Path $finalizeDir -Force | Out-Null - } - - $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten). If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/code-review.md." + $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/pr-$PRNumber-final.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten)." $finalizeArgs = @( "-p", $finalizePrompt, @@ -411,8 +402,8 @@ if ($DryRun) { } } - # Phase 3: Post comments if requested - # Runs scripts directly instead of via Copilot CLI to avoid: + # Phase 3: Post AI summary comment if requested + # Runs the script directly instead of via Copilot CLI to avoid: # - LLM creating its own broken version if skill files are missing # - Dirty tree from Phase 2 corrupting script files if ($PostSummaryComment) { @@ -422,78 +413,28 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # First verify the skill file exists - $skillPath = ".github/skills/ai-summary-comment/SKILL.md" - if (Test-Path $skillPath) { - Write-Host "✅ Found skill file: $skillPath" -ForegroundColor Green - } else { - Write-Host "⚠️ Skill file not found at: $skillPath" -ForegroundColor Yellow - Write-Host " Current directory: $(Get-Location)" -ForegroundColor Gray - Write-Host " Available skills:" -ForegroundColor Gray - Get-ChildItem ".github/skills/" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " - $($_.Name)" -ForegroundColor Gray } - } + # Restore tracked files to clean state before running the script. + # Phase 2 (pr-finalize) may have modified files via its Copilot CLI session, + # causing skill scripts to be missing or corrupted. + Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow + git checkout -- . 2>&1 | Out-Null + Write-Host " ✅ Working tree restored" -ForegroundColor Green - $commentPrompt = @" -Post the AI summary comment for PR #$PRNumber using the ai-summary-comment skill. - -**Step 1: Run the post-ai-summary-comment.ps1 script** -``````bash -pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $PRNumber -`````` - -The script will automatically: -- Load the state file from CustomAgentLogsTmp/PRState/pr-$PRNumber.md -- Parse all phases and their statuses -- Post/update the unified AI Summary comment on the PR - -**If the script fails**, check that the state file exists and contains valid phase data. - -**Do NOT** manually compose or post comments - always use the script. -"@ - - # 3a: Post PR agent summary comment (from Phase 1 state file) $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" - if (-not (Test-Path $scriptPath)) { - Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $scriptPath 2>&1 | Out-Null - } if (Test-Path $scriptPath) { Write-Host "💬 Running post-ai-summary-comment.ps1 directly..." -ForegroundColor Yellow - & $scriptPath -PRNumber $PRNumber + & pwsh $scriptPath -PRNumber $PRNumber $commentExit = $LASTEXITCODE if ($commentExit -eq 0) { - Write-Host "✅ Agent summary comment posted" -ForegroundColor Green + Write-Host "✅ Summary comment posted" -ForegroundColor Green } else { Write-Host "⚠️ post-ai-summary-comment.ps1 exited with code: $commentExit" -ForegroundColor Yellow } } else { Write-Host "⚠️ Script not found at: $scriptPath" -ForegroundColor Yellow Write-Host " Current directory: $(Get-Location)" -ForegroundColor Gray - Write-Host " Skipping agent summary comment." -ForegroundColor Gray - } - - # 3b: Post PR finalize comment (from Phase 2 finalize results) - if ($RunFinalize) { - $finalizeScriptPath = ".github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1" - if (-not (Test-Path $finalizeScriptPath)) { - Write-Host "⚠️ Finalize script missing, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $finalizeScriptPath 2>&1 | Out-Null - } - if (Test-Path $finalizeScriptPath) { - Write-Host "💬 Running post-pr-finalize-comment.ps1 directly..." -ForegroundColor Yellow - & $finalizeScriptPath -PRNumber $PRNumber - - $finalizeCommentExit = $LASTEXITCODE - if ($finalizeCommentExit -eq 0) { - Write-Host "✅ Finalize comment posted" -ForegroundColor Green - } else { - Write-Host "⚠️ post-pr-finalize-comment.ps1 exited with code: $finalizeCommentExit" -ForegroundColor Yellow - } - } else { - Write-Host "⚠️ Script not found at: $finalizeScriptPath" -ForegroundColor Yellow - Write-Host " Skipping finalize comment." -ForegroundColor Gray - } + Write-Host " Skipping summary comment." -ForegroundColor Gray } } } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a353fd21ee3f..4a17bfdd2a31 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -575,6 +575,7 @@ stages: displayName: 'Run PR Reviewer Agent' env: GITHUB_TOKEN: $(COPILOT_TOKEN) + GH_TOKEN: $(GH_COMMENT_TOKEN) # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 From 2708fb558c5c065ea6c353a8eff163b848181dab Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Fri, 6 Feb 2026 08:44:46 -0600 Subject: [PATCH 086/126] Fix Copilot CLI auth: use COPILOT_GITHUB_TOKEN to avoid GH_TOKEN conflict Copilot CLI checks tokens in order: COPILOT_GITHUB_TOKEN > GH_TOKEN > GITHUB_TOKEN. Adding GH_TOKEN for comment posting caused Copilot CLI to use the wrong token (GH_COMMENT_TOKEN lacks Copilot scope). Use COPILOT_GITHUB_TOKEN for Copilot CLI and GH_TOKEN for gh CLI comment posting. --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 4a17bfdd2a31..db0347e26638 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -574,7 +574,7 @@ stages: echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" displayName: 'Run PR Reviewer Agent' env: - GITHUB_TOKEN: $(COPILOT_TOKEN) + COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) GH_TOKEN: $(GH_COMMENT_TOKEN) # Publish Copilot logs and session artifacts From 262e9329e4d79c9b29da9c08c4ebe202cf675b76 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Fri, 6 Feb 2026 10:54:48 -0600 Subject: [PATCH 087/126] Increase job timeout to 180 min (Phase 1 takes ~90 min) --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index db0347e26638..ee069168956a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -34,7 +34,7 @@ stages: - job: CopilotReview displayName: 'Run Copilot PR Reviewer Agent' pool: ${{ parameters.pool }} - timeoutInMinutes: 120 + timeoutInMinutes: 180 steps: - checkout: self fetchDepth: 0 From cc3657b71950a6851ecbd3d5110db7ef6032b878 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 11:53:09 -0600 Subject: [PATCH 088/126] Fix: stop try-fix from committing state file (causes git checkout revert) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: try-fix SKILL.md instructed 'git add $STATE_FILE && git commit' on the state file in .gitignore'd CustomAgentLogsTmp/. This force-tracked it, so 'git checkout -- .' between phases reverted it to the committed (incomplete) version, losing Phase 4 Selected Fix and Phase 5 Report data. Fix: Remove the git add/commit instruction from try-fix SKILL.md. The state file lives in CustomAgentLogsTmp/ which is .gitignore'd — it persists naturally between try-fix runs without needing commits. --- .github/scripts/Review-PR.ps1 | 6 ++++-- .github/skills/try-fix/SKILL.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index ba14f5f557aa..4f0b8bd983ba 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -370,6 +370,8 @@ if ($DryRun) { # Restore tracked files to clean state before running post-completion skills. # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, # which can cause skill files to be missing or modified in subsequent phases. + # NOTE: State files in CustomAgentLogsTmp/ are .gitignore'd and untracked, + # so git checkout -- . won't touch them. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow git checkout -- . 2>&1 | Out-Null @@ -414,8 +416,8 @@ if ($DryRun) { Write-Host "" # Restore tracked files to clean state before running the script. - # Phase 2 (pr-finalize) may have modified files via its Copilot CLI session, - # causing skill scripts to be missing or corrupted. + # Phase 2 (pr-finalize) may have modified files via its Copilot CLI session. + # State files in CustomAgentLogsTmp/ are untracked and safe. Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow git checkout -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index 291283981e3c..a35d0b0c9df4 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -363,9 +363,9 @@ If `state_file` input was provided and file exists: **If no state file provided:** Skip this step (results returned to invoker only). -**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout HEAD -- .` (used between phases) to revert it, losing data. +**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout -- .` (used between phases) to revert it, losing data. -**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 6 models and confirming none have new ideas. try-fix only reports its own attempt result. +**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 5 models and confirming none have new ideas. try-fix only reports its own attempt result. **Ownership rule:** try-fix updates its own row ONLY. Never modify: - Phase status fields From 021b8a2e9986861334658cec644513d116e04d15 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 12:45:07 -0600 Subject: [PATCH 089/126] Fix: use 'git checkout HEAD -- .' to restore deleted files between phases git checkout -- . only restores MODIFIED tracked files, not DELETED ones. Phase 1 Copilot CLI sessions can delete skill scripts (e.g. post-ai-summary-comment.ps1), and git checkout -- . doesn't bring them back. git checkout HEAD -- . restores both. --- .github/scripts/Review-PR.ps1 | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 4f0b8bd983ba..08ee96fa9631 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -371,10 +371,10 @@ if ($DryRun) { # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, # which can cause skill files to be missing or modified in subsequent phases. # NOTE: State files in CustomAgentLogsTmp/ are .gitignore'd and untracked, - # so git checkout -- . won't touch them. + # so this won't touch them. Using HEAD to also restore deleted files. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow - git checkout -- . 2>&1 | Out-Null + git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green # Phase 2: Run pr-finalize skill if requested @@ -415,11 +415,9 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # Restore tracked files to clean state before running the script. - # Phase 2 (pr-finalize) may have modified files via its Copilot CLI session. - # State files in CustomAgentLogsTmp/ are untracked and safe. + # Restore tracked files (including deleted ones) to clean state. Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow - git checkout -- . 2>&1 | Out-Null + git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" From d0fe9e70bcd5e3344a6a989326b7b485447cf17d Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 14:06:17 -0600 Subject: [PATCH 090/126] Add diagnostics and harden phase transitions - Log git status to CustomAgentLogsTmp/ at each phase boundary for debugging - Pre-flight recovery: if post-ai-summary-comment.ps1 missing, targeted restore - Update all git checkout instructions to use HEAD (restores deleted files too) --- .github/agents/pr/post-gate.md | 12 ++++++------ .github/scripts/Review-PR.ps1 | 6 ++++++ .github/skills/try-fix/SKILL.md | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index e018d4f48429..6c0c3ac6d3c3 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -88,10 +88,10 @@ pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore # 2. Restore all tracked files to HEAD (the merged PR state) # This catches any files the previous attempt modified but didn't restore -git checkout -- . +git checkout HEAD -- . ``` -**Why this is required:** Each try-fix attempt modifies source files. If an attempt fails mid-way (build error, timeout, model error), it may not run its own cleanup step. Without explicit cleanup, the next attempt starts with a dirty working tree, which can cause missing files, corrupt state, or misleading test results. +**Why this is required:** Each try-fix attempt modifies source files. If an attempt fails mid-way (build error, timeout, model error), it may not run its own cleanup step. Without explicit cleanup, the next attempt starts with a dirty working tree, which can cause missing files, corrupt state, or misleading test results. Use `HEAD` (not just `-- .`) to also restore deleted files. #### Round 2+: Cross-Pollination Loop (MANDATORY) @@ -321,7 +321,7 @@ Update all phase statuses to complete. - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored - ❌ **Declaring exhaustion prematurely** - All 5 models must confirm "no new ideas" via actual invocation - ❌ **Rushing the report** - Take time to write clear justification -- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout -- .` between try-fix attempts (see Step 1) +- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout HEAD -- .` between try-fix attempts (see Step 1) --- @@ -336,7 +336,7 @@ Update all phase statuses to complete. **Fix:** Run cleanup before retrying: ```bash pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore -git checkout -- . +git checkout HEAD -- . ``` Then retry the try-fix attempt. The skill file should now be accessible. @@ -349,7 +349,7 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Previous attempt didn't restore its changes (crashed, timed out, or model didn't follow Step 8 restore instructions). -**Fix:** Same as above — run `-Restore` + `git checkout -- .` to reset to the merged PR state. +**Fix:** Same as above — run `-Restore` + `git checkout HEAD -- .` to reset to the merged PR state. ### Build errors unrelated to the fix being attempted @@ -358,6 +358,6 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Often caused by dirty working tree from a previous attempt. Can also be transient environment issues. **Fix:** -1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout -- .` +1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout HEAD -- .` 2. Retry the attempt 3. If it fails again with the same unrelated error, skip this attempt and continue with the next model diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 08ee96fa9631..59d7eb96838d 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -374,6 +374,7 @@ if ($DryRun) { # so this won't touch them. Using HEAD to also restore deleted files. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow + git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase1-exit-git-status.log" -ErrorAction SilentlyContinue git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green @@ -417,10 +418,15 @@ if ($DryRun) { # Restore tracked files (including deleted ones) to clean state. Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow + git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase2-exit-git-status.log" -ErrorAction SilentlyContinue git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" + if (-not (Test-Path $scriptPath)) { + Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow + git checkout HEAD -- $scriptPath 2>&1 | Out-Null + } if (Test-Path $scriptPath) { Write-Host "💬 Running post-ai-summary-comment.ps1 directly..." -ForegroundColor Yellow & pwsh $scriptPath -PRNumber $PRNumber diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index a35d0b0c9df4..d3613699a8b1 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -363,7 +363,7 @@ If `state_file` input was provided and file exists: **If no state file provided:** Skip this step (results returned to invoker only). -**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout -- .` (used between phases) to revert it, losing data. +**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout HEAD -- .` (used between phases) to revert it, losing data. **⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 5 models and confirming none have new ideas. try-fix only reports its own attempt result. From ffe42e526f44feadd3378f6bd1f38400fee16530 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 14:21:39 -0600 Subject: [PATCH 091/126] Fix step hanging: replace tee pipe with Start-Transcript The 'pwsh Review-PR.ps1 | tee output.md' pipe hangs 20+ min after the script completes because orphaned copilot CLI child processes inherit the stdout fd and keep tee's pipe open. Fix: Add -LogFile parameter to Review-PR.ps1 that uses Start-Transcript to capture output to a file. Remove the external tee pipe from the YAML. Live ADO logs still work (stdout flows normally), and the transcript file provides the artifact. --- .github/scripts/Review-PR.ps1 | 29 ----------------------------- eng/pipelines/ci-copilot.yml | 2 +- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 59d7eb96838d..c3dfed11bcac 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -457,35 +457,6 @@ if (-not $DryRun) { } Write-Host "" -# NOTE: This cleanup targets CI/ADO agent environments where only this script's -# Copilot CLI processes should exist. On developer machines, this could potentially -# affect other Copilot processes (e.g., VS Code extension). The risk is low since -# this runs at script end, but be aware if running locally. -# Clean up orphaned copilot CLI processes that may hold stdout fd open -# IMPORTANT: Only target processes whose command line contains "copilot" to avoid -# accidentally terminating the ADO agent's own node process -Write-Host "🧹 Cleaning up child processes..." -ForegroundColor Gray -try { - $myPid = $PID - # Find node processes running copilot CLI (not the ADO agent's node process) - $orphans = Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object { - $_.Id -ne $myPid -and - (($_.Path -and $_.Path -match "copilot") -or - ($_.CommandLine -and $_.CommandLine -match "copilot")) - } - # Also get any process literally named "copilot" - $copilotProcs = Get-Process -Name "copilot" -ErrorAction SilentlyContinue - $allOrphans = @($orphans) + @($copilotProcs) | Where-Object { $_ -ne $null } | Sort-Object Id -Unique - if ($allOrphans.Count -gt 0) { - Write-Host " Stopping $($allOrphans.Count) orphaned process(es): $($allOrphans | ForEach-Object { "$($_.ProcessName)($($_.Id))" } | Join-String -Separator ', ')" -ForegroundColor Gray - $allOrphans | Stop-Process -Force -ErrorAction SilentlyContinue - } else { - Write-Host " No orphaned copilot processes found" -ForegroundColor Gray - } -} catch { - Write-Host " ⚠️ Cleanup warning: $_" -ForegroundColor Yellow -} - if ($LogFile) { Stop-Transcript | Out-Null } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ee069168956a..4b508a30f5d7 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -530,7 +530,7 @@ stages: # The script will merge the PR into the current branch # -PostSummaryComment and -RunFinalize handle posting comments set +e - pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform android -RunFinalize -PostSummaryComment 2>&1 | tee $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md + pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform android -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" COPILOT_EXIT_CODE=$? set -e From d9df388c850a06ac0d1f7601eb44e48848efc555 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 16:11:00 -0600 Subject: [PATCH 092/126] Fix step hanging: stop orphaned copilot/node processes at exit The ADO bash step waits for ALL child processes that inherit its stdout fd to exit. Copilot CLI spawns sub-agents (node processes) that persist after the main copilot process exits, keeping the step alive 20+ min. Fix: Before exiting Review-PR.ps1, find and stop orphaned copilot/node processes via Stop-Process. Combined with Start-Transcript (no tee pipe), this ensures the step exits promptly after Phase 3 completes. --- .github/scripts/Review-PR.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index c3dfed11bcac..a2268ee537e1 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -457,6 +457,22 @@ if (-not $DryRun) { } Write-Host "" +# Kill any orphaned copilot/node child processes that may hold stdout fd open +# This prevents the ADO pipeline step from hanging after the script completes +Write-Host "🧹 Cleaning up child processes..." -ForegroundColor Gray +try { + $myPid = $PID + $children = Get-Process -Name "copilot","node" -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $myPid } + if ($children) { + Write-Host " Stopping $($children.Count) orphaned process(es): $($children | ForEach-Object { "$($_.ProcessName)($($_.Id))" } | Join-String -Separator ', ')" -ForegroundColor Gray + $children | Stop-Process -Force -ErrorAction SilentlyContinue + } else { + Write-Host " No orphaned processes found" -ForegroundColor Gray + } +} catch { + Write-Host " ⚠️ Cleanup warning: $_" -ForegroundColor Yellow +} + if ($LogFile) { Stop-Transcript | Out-Null } From 20a70fbaded06652f6c949eef7d03b1867fc35ff Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 16:13:36 -0600 Subject: [PATCH 093/126] Fix step hang: cleanup orphans at both PS and bash levels Two-layer defense against orphaned copilot/node processes holding the ADO step's stdout fd open after Review-PR.ps1 completes: 1. Review-PR.ps1: Stop-Process for copilot/node before exit 2. ci-copilot.yml: pgrep + terminate after pwsh returns 3. Phase 3: use '& $scriptPath' not '& pwsh $scriptPath' to avoid spawning unnecessary child process --- .github/scripts/Review-PR.ps1 | 2 +- eng/pipelines/ci-copilot.yml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index a2268ee537e1..c7cf52b98c0a 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -429,7 +429,7 @@ if ($DryRun) { } if (Test-Path $scriptPath) { Write-Host "💬 Running post-ai-summary-comment.ps1 directly..." -ForegroundColor Yellow - & pwsh $scriptPath -PRNumber $PRNumber + & $scriptPath -PRNumber $PRNumber $commentExit = $LASTEXITCODE if ($commentExit -eq 0) { diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 4b508a30f5d7..19a10b785c7e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -536,6 +536,16 @@ stages: echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" + # Terminate any orphaned copilot/node processes that could hold this step's + # stdout fd open and prevent the bash step from exiting. + echo "Cleaning up orphaned processes..." + for proc in $(pgrep -f "copilot" 2>/dev/null || true) $(pgrep -f "node.*copilot" 2>/dev/null || true); do + if [ -n "$proc" ]; then + echo " Stopping process $proc" + kill "$proc" 2>/dev/null || true + fi + done + # Copy any Copilot session files if [ -d "$HOME/.copilot" ]; then echo "Copying Copilot session state..." From bf3f6178611ca940cf9831ce6bb878beb47675a0 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 6 Feb 2026 16:58:14 -0600 Subject: [PATCH 094/126] Fix: only cleanup copilot processes, not ADO agent node process Previous cleanup used Get-Process -Name 'node' which matched the ADO agent's own node runner (cmdline.js), causing exit code 137 (SIGKILL). Fix: Filter node processes by command line containing 'copilot' to only target copilot CLI orphans, leaving the ADO agent untouched. --- .github/scripts/Review-PR.ps1 | 23 ++++++++++++++++------- eng/pipelines/ci-copilot.yml | 17 +++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index c7cf52b98c0a..f53bae6e1189 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -457,17 +457,26 @@ if (-not $DryRun) { } Write-Host "" -# Kill any orphaned copilot/node child processes that may hold stdout fd open -# This prevents the ADO pipeline step from hanging after the script completes +# Clean up orphaned copilot CLI processes that may hold stdout fd open +# IMPORTANT: Only target processes whose command line contains "copilot" to avoid +# accidentally terminating the ADO agent's own node process Write-Host "🧹 Cleaning up child processes..." -ForegroundColor Gray try { $myPid = $PID - $children = Get-Process -Name "copilot","node" -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $myPid } - if ($children) { - Write-Host " Stopping $($children.Count) orphaned process(es): $($children | ForEach-Object { "$($_.ProcessName)($($_.Id))" } | Join-String -Separator ', ')" -ForegroundColor Gray - $children | Stop-Process -Force -ErrorAction SilentlyContinue + # Find node processes running copilot CLI (not the ADO agent's node process) + $orphans = Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object { + $_.Id -ne $myPid -and + (($_.Path -and $_.Path -match "copilot") -or + ($_.CommandLine -and $_.CommandLine -match "copilot")) + } + # Also get any process literally named "copilot" + $copilotProcs = Get-Process -Name "copilot" -ErrorAction SilentlyContinue + $allOrphans = @($orphans) + @($copilotProcs) | Where-Object { $_ -ne $null } | Sort-Object Id -Unique + if ($allOrphans.Count -gt 0) { + Write-Host " Stopping $($allOrphans.Count) orphaned process(es): $($allOrphans | ForEach-Object { "$($_.ProcessName)($($_.Id))" } | Join-String -Separator ', ')" -ForegroundColor Gray + $allOrphans | Stop-Process -Force -ErrorAction SilentlyContinue } else { - Write-Host " No orphaned processes found" -ForegroundColor Gray + Write-Host " No orphaned copilot processes found" -ForegroundColor Gray } } catch { Write-Host " ⚠️ Cleanup warning: $_" -ForegroundColor Yellow diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 19a10b785c7e..7f7462f6b960 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -536,13 +536,18 @@ stages: echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" - # Terminate any orphaned copilot/node processes that could hold this step's + # Terminate any orphaned copilot CLI processes that could hold this step's # stdout fd open and prevent the bash step from exiting. - echo "Cleaning up orphaned processes..." - for proc in $(pgrep -f "copilot" 2>/dev/null || true) $(pgrep -f "node.*copilot" 2>/dev/null || true); do - if [ -n "$proc" ]; then - echo " Stopping process $proc" - kill "$proc" 2>/dev/null || true + # Only target processes whose command line includes the copilot CLI path. + echo "Cleaning up orphaned copilot processes..." + SELF_PID=$$ + for proc in $(pgrep -f "[c]opilot" 2>/dev/null || true); do + if [ -n "$proc" ] && [ "$proc" != "$SELF_PID" ]; then + PROC_CMD=$(ps -p "$proc" -o args= 2>/dev/null || true) + if echo "$PROC_CMD" | grep -q "copilot"; then + echo " Stopping copilot process $proc: $PROC_CMD" + kill "$proc" 2>/dev/null || true + fi fi done From 0f8ba01b0ab385506bb1867ff6d8b931d2978bca Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Sat, 7 Feb 2026 17:43:10 -0600 Subject: [PATCH 095/126] Remove unused pr-review-prompt.md --- eng/pipelines/prompts/pr-review-prompt.md | 153 ---------------------- 1 file changed, 153 deletions(-) delete mode 100644 eng/pipelines/prompts/pr-review-prompt.md diff --git a/eng/pipelines/prompts/pr-review-prompt.md b/eng/pipelines/prompts/pr-review-prompt.md deleted file mode 100644 index 562fbd20ed42..000000000000 --- a/eng/pipelines/prompts/pr-review-prompt.md +++ /dev/null @@ -1,153 +0,0 @@ -Review PR #${PR_NUMBER} - -Follow the 5-phase PR Agent workflow, but leverage the existing agent review state: - -1. **Phase 1: Gate** - Run tests FIRST. If gate fails, STOP IMMEDIATELY. Do not proceed. -2. **Phase 2: Pre-Flight** - Import prior agent state instead of re-doing completed phases -3. **Phase 3: Tests** - Verify reproduction tests exist -4. **Phase 4: Fix** - EXHAUSTIVE exploration: - - Consult 5+ different AI models for diverse fix ideas - - Run try-fix skill with Opus 4.5 for EACH unique idea - - Keep iterating until completely out of alternatives - - Compare ALL candidates to determine best approach -5. **Phase 5: Report** - Generate final recommendation with full comparison - -## Work Plan - -### Phase 1: Gate (Test Verification) - MUST PASS FIRST ⛔ -**THIS IS A BLOCKING GATE - If tests don't behave correctly, STOP ALL WORK IMMEDIATELY.** - -- [ ] Run verification script with `-RequireFullVerification`: - ```bash - pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform android -RequireFullVerification - ``` -- [ ] Confirm tests FAIL without fix (bug reproduced) -- [ ] Confirm tests PASS with fix (bug fixed) -- [ ] **IF GATE FAILS**: Stop immediately, do not proceed to any other phase. Report failure and exit. -- [ ] Mark Gate ✅ PASSED (or ❌ FAILED and STOP) - -### Phase 2: Pre-Flight (Context Gathering) -- [ ] Checkout PR branch (`pr-33687`) -- [ ] Gather PR metadata (title, body, labels, files) -- [ ] Read linked issue #19256 -- [ ] Fetch PR comments and review feedback -- [ ] Check for prior agent review (FOUND: `.github/agent-pr-session/pr-19256.md`) -- [ ] Create local state file importing prior agent's findings -- [ ] Mark Pre-Flight COMPLETE - -### Phase 3: Tests (Verify Reproduction Tests Exist) -- [ ] Confirm PR includes UI tests (already present per file list) -- [ ] Verify test file locations: - - HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs` - - NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs` -- [ ] Verify tests follow naming convention (`Issue19256`) -- [ ] Mark Tests COMPLETE - -### Phase 4: Fix (EXHAUSTIVE Independent Analysis) - -**Goal:** Explore ALL possible alternative solutions until no more ideas remain. - -#### Step 4.1: Multi-Model Brainstorming (Consult 5+ Models) -- [ ] Read `.github/agents/pr/post-gate.md` for Phase 4-5 instructions -- [ ] Consult **Claude Sonnet 4** for fix ideas -- [ ] Consult **Claude Opus 4.5** for fix ideas -- [ ] Consult **GPT-5.2** for fix ideas -- [ ] Consult **GPT-5.1-Codex** for fix ideas -- [ ] Consult **Gemini 3 Pro** for fix ideas -- [ ] Consolidate unique approaches from all models -- [ ] Deduplicate and categorize fix strategies - -#### Step 4.2: Iterative try-fix Exploration (Opus 4.5) - SEQUENTIAL - -**CRITICAL: try-fix attempts MUST be run SEQUENTIALLY, one at a time.** -- Each try-fix must COMPLETE before starting the next -- NO parallel execution - wait for full result before proceeding -- Learn from each attempt to inform the next - -For EACH unique fix idea, run try-fix skill with `claude-opus-4.5`: -- [ ] **Attempt 1:** [First alternative approach] → WAIT FOR COMPLETION -- [ ] **Attempt 2:** [Second alternative approach] → WAIT FOR COMPLETION -- [ ] **Attempt 3:** [Third alternative approach] → WAIT FOR COMPLETION -- [ ] **Attempt 4:** [Continue until exhausted...] → WAIT FOR COMPLETION -- [ ] **Attempt N:** Keep going until try-fix reports "no more ideas" - -**Sequential Iteration Rule:** -1. Start try-fix with ONE idea -2. **WAIT** for try-fix to complete fully (tests run, result recorded) -3. If tests PASS → Record as viable alternative -4. If tests FAIL → Analyze failure reason -5. Use learnings from this attempt to inform the NEXT attempt -6. Start next try-fix (go to step 1) -7. Continue until ALL brainstormed ideas are tested -8. Then ask Opus 4.5 to generate MORE ideas based on all learnings so far -9. Repeat until Opus 4.5 confirms "exhausted all approaches" - -#### Step 4.3: Comparative Analysis -- [ ] Create comparison matrix of ALL fix candidates (PR's + alternatives) -- [ ] Evaluate each on: correctness, simplicity, performance, maintainability -- [ ] Document why PR's fix is/isn't the best approach -- [ ] Select best fix with full justification - -**Exit Criteria for Phase 4:** -- At least 5 models consulted for ideas -- All unique ideas tested via try-fix -- try-fix confirms "no more alternative approaches" -- Comparison matrix complete -- Best fix selected with rationale - -### Phase 5: Report (Final Recommendation) -- [ ] Generate comprehensive review summary -- [ ] Provide final recommendation: APPROVE / REQUEST CHANGES -- [ ] Post review to PR if requested - -## Key Files in This PR - -| File | Type | Purpose | -|------|------|---------| -| `src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs` | Fix | ShowEvent handler to re-apply min/max dates | -| `src/Core/src/Platform/Android/DatePickerExtensions.cs` | Fix | Reset MinDate/MaxDate before setting new values | -| `src/Controls/tests/TestCases.HostApp/Issues/Issue19256.cs` | Test | UI test page with dependent DatePickers | -| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19256.cs` | Test | NUnit test with screenshot verification | -| `.github/agent-pr-session/pr-19256.md` | Meta | Prior agent review session (all phases COMPLETE) | - -## Prior Agent Review Summary - -The existing agent review file shows: -- **Root Cause:** Known Android platform bug - DatePicker caches MinDate/MaxDate and ignores updates unless reset first -- **Fix Approach:** Reset values before setting + ShowEvent handler to re-apply after dialog initialization -- **Tests:** Screenshot-based verification with two states (FutureDate, EarlierDate) -- **Verdict:** ✅ VALID FIX - follows established 10+ year old Android workaround - -## Notes - -- This is an **Android-only** issue (platform/android label) -- The fix is based on a well-documented workaround from [StackOverflow #19616575](https://stackoverflow.com/questions/19616575) -- Issue has been open since Dec 2023 (regression in 8.0.3) -- PR includes +429 lines (mostly tests and documentation) - -## Risks & Considerations - -1. **Gate verification required** - Must empirically confirm tests behave correctly -2. **Phase 4 is exhaustive** - Will consult 5+ models and iterate try-fix until ALL ideas explored -3. **Android emulator required** - Tests need to run on Android platform -4. **Time investment** - Exhaustive Phase 4 may take significant time but ensures thorough review -5. **Model availability** - Need access to multiple models (Sonnet, Opus, GPT-5.x, Gemini) - -## Models to Consult in Phase 4 - -| Model | Purpose | -|-------|---------| -| `claude-sonnet-4` | Baseline fix ideas | -| `claude-opus-4.5` | Deep analysis + try-fix iterations | -| `gpt-5.2` | Alternative perspective | -| `gpt-5.1-codex` | Code-focused suggestions | -| `gemini-3-pro-preview` | Third-party perspective | - -## Success Criteria - -Phase 4 is complete when: -- ✅ All 5+ models have been consulted -- ✅ Every unique fix idea has been tested via try-fix -- ✅ Opus 4.5 confirms "no more alternative approaches to explore" -- ✅ Comprehensive comparison matrix exists -- ✅ Clear rationale for final fix selection From efdfe5ceaeabb4e0ad0801e8943d320a46b489fb Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Sat, 7 Feb 2026 18:15:46 -0600 Subject: [PATCH 096/126] Add iOS platform support to ci-copilot pipeline - Add Platform parameter (android/ios, default: android) - Skip Android SDK/JDK/emulator provisioning when Platform=ios - Install xcuitest Appium driver for iOS, uiautomator2 for Android - Add iOS simulator boot step (finds iPhone Xs, sets DEVICE_UDID) - Pass DEVICE_UDID env var to reviewer step - Make environment verification platform-aware - Pass Platform parameter to Review-PR.ps1 --- eng/pipelines/ci-copilot.yml | 116 +++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 7f7462f6b960..bd513cbc21f3 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -15,6 +15,14 @@ parameters: type: string default: '' + - name: Platform + displayName: 'Target Platform' + type: string + default: 'android' + values: + - android + - ios + - name: pool type: object default: @@ -54,14 +62,14 @@ stages: parameters: skipXcode: false skipProvisionator: false - skipAndroidCommonSdks: false # Install base Android SDK (cmdline-tools, emulator, platform-tools) - skipAndroidPlatformApis: false # Install platform APIs - skipJdk: false + skipAndroidCommonSdks: ${{ eq(parameters.Platform, 'ios') }} + skipAndroidPlatformApis: ${{ eq(parameters.Platform, 'ios') }} + skipJdk: ${{ eq(parameters.Platform, 'ios') }} skipSimulatorSetup: false skipCertificates: true - # Android emulator setup - skipAndroidEmulatorImages: false - skipAndroidCreateAvds: false + # Android emulator setup (skip for iOS) + skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }} + skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }} androidEmulatorApiLevel: '34' # Disable hardware acceleration in AVD config (HVF not available on Azure hosted agents) @@ -85,6 +93,7 @@ stages: ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found" fi displayName: 'Configure AVD for Software Emulation' + condition: eq('${{ parameters.Platform }}', 'android') # Set up Android SDK PATH (required on self-hosted agents) - pwsh: | @@ -129,6 +138,7 @@ stages: } } displayName: 'Configure Android SDK PATH' + condition: eq('${{ parameters.Platform }}', 'android') env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) @@ -180,6 +190,7 @@ stages: Write-Host "=== Android Emulator Started Successfully ===" displayName: 'Start Android Emulator' + condition: eq('${{ parameters.Platform }}', 'android') timeoutInMinutes: 35 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) @@ -216,16 +227,18 @@ stages: echo "✓ .NET SDK: $(dotnet --version)" fi - # Check Android SDK - echo "Checking Android SDK..." - if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then - ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set" - else - SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" - if [ ! -d "$SDK_PATH" ]; then - ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH" + # Check Android SDK (only for Android platform) + if [ "${{ parameters.Platform }}" = "android" ]; then + echo "Checking Android SDK..." + if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then + ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set" else - echo "✓ Android SDK: $SDK_PATH" + SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" + if [ ! -d "$SDK_PATH" ]; then + ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH" + else + echo "✓ Android SDK: $SDK_PATH" + fi fi fi @@ -247,12 +260,14 @@ stages: fi fi - # Check Java/JDK - echo "Checking JDK..." - if ! java -version 2>&1; then - ERRORS="${ERRORS}\n- JDK not available" - else - echo "✓ JDK available" + # Check Java/JDK (only for Android platform) + if [ "${{ parameters.Platform }}" = "android" ]; then + echo "Checking JDK..." + if ! java -version 2>&1; then + ERRORS="${ERRORS}\n- JDK not available" + else + echo "✓ JDK available" + fi fi # Report errors @@ -353,6 +368,7 @@ stages: echo "=== Emulator booted successfully! ===" adb devices -l displayName: 'Boot Android Emulator' + condition: eq('${{ parameters.Platform }}', 'android') continueOnError: true timeoutInMinutes: 15 env: @@ -372,7 +388,7 @@ stages: displayName: 'Install Node.js' - script: | - echo "Installing Appium and UiAutomator2 driver..." + echo "Installing Appium and platform driver..." # Get npm global bin directory and add to PATH NPM_BIN=$(npm config get prefix)/bin @@ -382,8 +398,14 @@ stages: # Install Appium globally npm install -g appium - # Install UiAutomator2 driver for Android - appium driver install uiautomator2 + # Install platform-specific driver + if [ "${{ parameters.Platform }}" = "ios" ]; then + echo "Installing XCUITest driver for iOS..." + appium driver install xcuitest + else + echo "Installing UiAutomator2 driver for Android..." + appium driver install uiautomator2 + fi # Verify installation if ! which appium; then @@ -510,10 +532,53 @@ stages: echo "=== Fresh Android Emulator Ready! ===" adb devices -l displayName: 'Restart Android Emulator (Fresh)' + condition: eq('${{ parameters.Platform }}', 'android') timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + # Boot iOS Simulator (only for iOS platform) + - script: | + echo "=== Booting iOS Simulator ===" + + # Find a suitable simulator (prefer iPhone Xs for consistency) + UDID=$(xcrun simctl list devices available --json | jq -r ' + .devices | to_entries | + map(select(.key | test("iOS"))) | + sort_by(.key) | reverse | + .[0].value | + map(select(.name | test("iPhone (Xs|14|15)"))) | + .[0].udid // empty + ') + + if [ -z "$UDID" ]; then + echo "No preferred simulator found, using first available iPhone" + UDID=$(xcrun simctl list devices available --json | jq -r ' + .devices | to_entries | + map(.value) | flatten | + map(select(.name | test("iPhone"))) | + .[0].udid + ') + fi + + if [ -z "$UDID" ]; then + echo "##vso[task.logissue type=error]No iOS simulator found" + exit 1 + fi + + echo "Booting simulator: $UDID" + xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" + sleep 10 + + echo "Booted simulators:" + xcrun simctl list devices booted + + echo "##vso[task.setvariable variable=DEVICE_UDID]$UDID" + echo "iOS Simulator UDID: $UDID" + displayName: 'Boot iOS Simulator' + condition: eq('${{ parameters.Platform }}', 'ios') + timeoutInMinutes: 5 + - script: | echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." @@ -530,7 +595,7 @@ stages: # The script will merge the PR into the current branch # -PostSummaryComment and -RunFinalize handle posting comments set +e - pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform android -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" + pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" COPILOT_EXIT_CODE=$? set -e @@ -591,6 +656,7 @@ stages: env: COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) GH_TOKEN: $(GH_COMMENT_TOKEN) + DEVICE_UDID: $(DEVICE_UDID) # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 From 6ca0fbea710794341b610f17660050d40f368b6d Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Sat, 7 Feb 2026 18:54:20 -0600 Subject: [PATCH 097/126] Skip Xcode version check on AcesShared agents AcesShared agents have Xcode 26.1.x but .NET iOS SDK expects 26.0. Create Directory.Build.Override.props with ValidateXcodeVersion=false to allow builds with the newer Xcode. --- eng/pipelines/ci-copilot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index bd513cbc21f3..7b54b11ea392 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -588,6 +588,12 @@ stages: git config user.name "Copilot CI" echo "Git identity configured" + # Create Directory.Build.Override.props to skip Xcode version check + # AcesShared agents may have a newer Xcode than the .NET iOS SDK expects + cp Directory.Build.Override.props.in Directory.Build.Override.props + # Insert ValidateXcodeVersion before closing tag + sed -i '' 's|| false\n|' Directory.Build.Override.props + # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs From ae31c11dad3a263afee7bef4304abec4e232d4fd Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Mon, 9 Feb 2026 13:16:29 +0100 Subject: [PATCH 098/126] Enhance PR review: finalize output & comments Adjust Review-PR.ps1 to better handle finalize outputs and posting comments. Creates a dedicated finalize output directory (CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize) and updates the pr-finalize prompt to write separate files (pr-finalize-summary.md, recommended-description.md, code-review.md) instead of overwriting the main state file. Update Phase 3 labeling and messaging to "Post comments" and split posting into two steps: (3a) post the agent summary comment and (3b) optionally run post-pr-finalize-comment.ps1 when RunFinalize is enabled. Add recovery attempts for missing scripts via git checkout and improve exit reporting and user-facing messages. --- .github/scripts/Review-PR.ps1 | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index f53bae6e1189..8fefb95d4bfd 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -386,7 +386,13 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/pr-$PRNumber-final.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten)." + # Ensure output directory exists for finalize results + $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize" + if (-not (Test-Path $finalizeDir)) { + New-Item -ItemType Directory -Path $finalizeDir -Force | Out-Null + } + + $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten). If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/code-review.md." $finalizeArgs = @( "-p", $finalizePrompt, @@ -405,8 +411,8 @@ if ($DryRun) { } } - # Phase 3: Post AI summary comment if requested - # Runs the script directly instead of via Copilot CLI to avoid: + # Phase 3: Post comments if requested + # Runs scripts directly instead of via Copilot CLI to avoid: # - LLM creating its own broken version if skill files are missing # - Dirty tree from Phase 2 corrupting script files if ($PostSummaryComment) { @@ -422,6 +428,7 @@ if ($DryRun) { git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green + # 3a: Post PR agent summary comment (from Phase 1 state file) $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" if (-not (Test-Path $scriptPath)) { Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow @@ -433,14 +440,37 @@ if ($DryRun) { $commentExit = $LASTEXITCODE if ($commentExit -eq 0) { - Write-Host "✅ Summary comment posted" -ForegroundColor Green + Write-Host "✅ Agent summary comment posted" -ForegroundColor Green } else { Write-Host "⚠️ post-ai-summary-comment.ps1 exited with code: $commentExit" -ForegroundColor Yellow } } else { Write-Host "⚠️ Script not found at: $scriptPath" -ForegroundColor Yellow Write-Host " Current directory: $(Get-Location)" -ForegroundColor Gray - Write-Host " Skipping summary comment." -ForegroundColor Gray + Write-Host " Skipping agent summary comment." -ForegroundColor Gray + } + + # 3b: Post PR finalize comment (from Phase 2 finalize results) + if ($RunFinalize) { + $finalizeScriptPath = ".github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1" + if (-not (Test-Path $finalizeScriptPath)) { + Write-Host "⚠️ Finalize script missing, attempting targeted recovery..." -ForegroundColor Yellow + git checkout HEAD -- $finalizeScriptPath 2>&1 | Out-Null + } + if (Test-Path $finalizeScriptPath) { + Write-Host "💬 Running post-pr-finalize-comment.ps1 directly..." -ForegroundColor Yellow + & $finalizeScriptPath -PRNumber $PRNumber + + $finalizeCommentExit = $LASTEXITCODE + if ($finalizeCommentExit -eq 0) { + Write-Host "✅ Finalize comment posted" -ForegroundColor Green + } else { + Write-Host "⚠️ post-pr-finalize-comment.ps1 exited with code: $finalizeCommentExit" -ForegroundColor Yellow + } + } else { + Write-Host "⚠️ Script not found at: $finalizeScriptPath" -ForegroundColor Yellow + Write-Host " Skipping finalize comment." -ForegroundColor Gray + } } } } From 2305bb3e3a9e5d0a00d46875bf4a93885232bc21 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Mon, 9 Feb 2026 15:27:11 -0600 Subject: [PATCH 099/126] Increase multi-model try-fix to 6 models --- .github/agents/pr.md | 2 +- .github/agents/pr/PLAN-TEMPLATE.md | 2 +- .github/agents/pr/SHARED-RULES.md | 9 +++++---- .github/agents/pr/post-gate.md | 17 +++++++++-------- .github/skills/try-fix/SKILL.md | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/agents/pr.md b/.github/agents/pr.md index d5c55dbc80de..94acfa386265 100644 --- a/.github/agents/pr.md +++ b/.github/agents/pr.md @@ -49,7 +49,7 @@ After Gate passes, read `.github/agents/pr/post-gate.md` for **Phases 3-4**. - No Direct Git Commands (use `gh pr diff/view`, let scripts handle files) - Use Skills' Scripts (don't bypass with manual commands) - Stop on Environment Blockers (strict retry limits, report and ask user) -- Multi-Model Configuration (5 models for Phase 4) +- Multi-Model Configuration (6 models for Phase 4) - Platform Selection (must be affected AND available on host) **Key points:** diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index 11901d56dd91..555cbd228d7a 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -15,7 +15,7 @@ See `SHARED-RULES.md` for complete details. Key points: - **Environment Blockers**: STOP immediately, report, ask user (strict retry limits) - **No Git Commands**: Never checkout/switch branches - agent is always on correct branch - **Gate via Task Agent**: Never run inline (prevents fabrication) -- **Multi-Model try-fix**: 5 models, SEQUENTIAL only +- **Multi-Model try-fix**: 6 models, SEQUENTIAL only - **Follow Templates**: No `open` attributes, no "improvements" --- diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index 70662faccd9b..f3f1ce35e29e 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -127,15 +127,16 @@ Which would you like me to do? ## Multi-Model Configuration -Phase 4 uses these 5 AI models for try-fix exploration (run SEQUENTIALLY): +Phase 4 uses these 6 AI models for try-fix exploration (run SEQUENTIALLY): | Order | Model | |-------|-------| | 1 | `claude-sonnet-4.5` | | 2 | `claude-opus-4.6` | -| 3 | `gpt-5.2` | -| 4 | `gpt-5.2-codex` | -| 5 | `gemini-3-pro-preview` | +| 3 | `claude-opus-4.6-fast` | +| 4 | `gpt-5.2` | +| 5 | `gpt-5.2-codex` | +| 6 | `gemini-3-pro-preview` | **Note:** The `model` parameter is passed to the `task` tool, which supports model selection. This is separate from agent YAML frontmatter (which is VS Code-only). diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index 6c0c3ac6d3c3..0f86ccc0a041 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -20,7 +20,7 @@ If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 **All rules from `.github/agents/pr/SHARED-RULES.md` apply here**, including: - Phase Completion Protocol (fill ALL pending fields before marking complete) - Stop on Environment Blockers (STOP and ask user, don't continue) -- Multi-Model Configuration (5 models, SEQUENTIAL only) +- Multi-Model Configuration (6 models, SEQUENTIAL only) If try-fix cannot run due to environment issues, **STOP and ask the user**. Do NOT mark attempts as "BLOCKED" and continue. @@ -95,7 +95,7 @@ git checkout HEAD -- . #### Round 2+: Cross-Pollination Loop (MANDATORY) -After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts allowed.** +After Round 1, invoke EACH of the 6 models to ask for new ideas. **No shortcuts allowed.** **❌ WRONG**: Using `explore`/`glob`, declaring exhaustion without invoking each model **✅ CORRECT**: Invoke EACH model via task agent and ask explicitly @@ -118,7 +118,7 @@ After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts 4. **For each new idea**: Run try-fix with that model (SEQUENTIAL, wait for completion) -5. **Exit when**: ALL 5 models say "NO NEW IDEAS" in the same round +5. **Exit when**: ALL 6 models say "NO NEW IDEAS" in the same round #### try-fix Behavior @@ -193,13 +193,14 @@ Update the state file: 5. Change 📋 Report status to `▶️ IN PROGRESS` **Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Round 1 completed: All 5 models ran try-fix -- [ ] **Cross-pollination table exists** with responses from ALL 5 models: +- [ ] Round 1 completed: All 6 models ran try-fix +- [ ] **Cross-pollination table exists** with responses from ALL 6 models: ``` | Model | Round 2 Response | |-------|------------------| | claude-sonnet-4.5 | NO NEW IDEAS | | claude-opus-4.6 | NO NEW IDEAS | + | claude-opus-4.6-fast | NO NEW IDEAS | | gpt-5.2 | NO NEW IDEAS | | gpt-5.2-codex | NO NEW IDEAS | | gemini-3-pro-preview | NO NEW IDEAS | @@ -311,15 +312,15 @@ Update all phase statuses to complete. - ❌ **Looking at PR's fix before generating ideas** - Generate fix ideas independently first - ❌ **Re-testing the PR's fix in try-fix** - Gate already validated it; try-fix tests YOUR ideas -- ❌ **Skipping models in Round 1** - All 5 models must run try-fix before cross-pollination +- ❌ **Skipping models in Round 1** - All 6 models must run try-fix before cross-pollination - ❌ **Running try-fix in parallel** - SEQUENTIAL ONLY - they modify same files and use same device - ❌ **Using explore/glob instead of invoking models** - Cross-pollination requires ACTUAL task agent invocations with each model, not code searches -- ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 5 models explicitly say "NO NEW IDEAS" +- ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 6 models explicitly say "NO NEW IDEAS" - ❌ **Not recording cross-pollination responses** - State file must have table showing each model's Round 2 response - ❌ **Not analyzing why fixes failed** - Record the flawed reasoning to help future attempts - ❌ **Selecting a failing fix** - Only select from passing candidates - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored -- ❌ **Declaring exhaustion prematurely** - All 5 models must confirm "no new ideas" via actual invocation +- ❌ **Declaring exhaustion prematurely** - All 6 models must confirm "no new ideas" via actual invocation - ❌ **Rushing the report** - Take time to write clear justification - ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout HEAD -- .` between try-fix attempts (see Step 1) diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index d3613699a8b1..291283981e3c 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -365,7 +365,7 @@ If `state_file` input was provided and file exists: **⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout HEAD -- .` (used between phases) to revert it, losing data. -**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 5 models and confirming none have new ideas. try-fix only reports its own attempt result. +**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 6 models and confirming none have new ideas. try-fix only reports its own attempt result. **Ownership rule:** try-fix updates its own row ONLY. Never modify: - Phase status fields From 854a10a1f3f31ac93f502eb5a5fdd9b3f59b58d1 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Mon, 9 Feb 2026 15:27:23 -0600 Subject: [PATCH 100/126] Address PR review: Windows compat, cleanup consistency, and code quality fixes --- .github/agents/pr.md | 2 +- .github/agents/pr/PLAN-TEMPLATE.md | 2 +- .github/agents/pr/SHARED-RULES.md | 9 ++- .github/agents/pr/post-gate.md | 30 +++++----- .github/scripts/Review-PR.ps1 | 4 ++ .github/scripts/shared/Build-AndDeploy.ps1 | 23 +------- .github/scripts/shared/Start-Emulator.ps1 | 66 ++++++++++++++++------ .github/scripts/shared/shared-utils.ps1 | 2 +- 8 files changed, 78 insertions(+), 60 deletions(-) diff --git a/.github/agents/pr.md b/.github/agents/pr.md index 94acfa386265..d5c55dbc80de 100644 --- a/.github/agents/pr.md +++ b/.github/agents/pr.md @@ -49,7 +49,7 @@ After Gate passes, read `.github/agents/pr/post-gate.md` for **Phases 3-4**. - No Direct Git Commands (use `gh pr diff/view`, let scripts handle files) - Use Skills' Scripts (don't bypass with manual commands) - Stop on Environment Blockers (strict retry limits, report and ask user) -- Multi-Model Configuration (6 models for Phase 4) +- Multi-Model Configuration (5 models for Phase 4) - Platform Selection (must be affected AND available on host) **Key points:** diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index 555cbd228d7a..11901d56dd91 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -15,7 +15,7 @@ See `SHARED-RULES.md` for complete details. Key points: - **Environment Blockers**: STOP immediately, report, ask user (strict retry limits) - **No Git Commands**: Never checkout/switch branches - agent is always on correct branch - **Gate via Task Agent**: Never run inline (prevents fabrication) -- **Multi-Model try-fix**: 6 models, SEQUENTIAL only +- **Multi-Model try-fix**: 5 models, SEQUENTIAL only - **Follow Templates**: No `open` attributes, no "improvements" --- diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index f3f1ce35e29e..70662faccd9b 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -127,16 +127,15 @@ Which would you like me to do? ## Multi-Model Configuration -Phase 4 uses these 6 AI models for try-fix exploration (run SEQUENTIALLY): +Phase 4 uses these 5 AI models for try-fix exploration (run SEQUENTIALLY): | Order | Model | |-------|-------| | 1 | `claude-sonnet-4.5` | | 2 | `claude-opus-4.6` | -| 3 | `claude-opus-4.6-fast` | -| 4 | `gpt-5.2` | -| 5 | `gpt-5.2-codex` | -| 6 | `gemini-3-pro-preview` | +| 3 | `gpt-5.2` | +| 4 | `gpt-5.2-codex` | +| 5 | `gemini-3-pro-preview` | **Note:** The `model` parameter is passed to the `task` tool, which supports model selection. This is separate from agent YAML frontmatter (which is VS Code-only). diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index 0f86ccc0a041..f030801ee257 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -20,7 +20,7 @@ If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 **All rules from `.github/agents/pr/SHARED-RULES.md` apply here**, including: - Phase Completion Protocol (fill ALL pending fields before marking complete) - Stop on Environment Blockers (STOP and ask user, don't continue) -- Multi-Model Configuration (6 models, SEQUENTIAL only) +- Multi-Model Configuration (5 models, SEQUENTIAL only) If try-fix cannot run due to environment issues, **STOP and ask the user**. Do NOT mark attempts as "BLOCKED" and continue. @@ -89,13 +89,17 @@ pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore # 2. Restore all tracked files to HEAD (the merged PR state) # This catches any files the previous attempt modified but didn't restore git checkout HEAD -- . + +# 3. Remove untracked files added by the previous attempt +# git checkout restores tracked files but does NOT remove new untracked files +git clean -fd --exclude=CustomAgentLogsTmp/ ``` **Why this is required:** Each try-fix attempt modifies source files. If an attempt fails mid-way (build error, timeout, model error), it may not run its own cleanup step. Without explicit cleanup, the next attempt starts with a dirty working tree, which can cause missing files, corrupt state, or misleading test results. Use `HEAD` (not just `-- .`) to also restore deleted files. #### Round 2+: Cross-Pollination Loop (MANDATORY) -After Round 1, invoke EACH of the 6 models to ask for new ideas. **No shortcuts allowed.** +After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts allowed.** **❌ WRONG**: Using `explore`/`glob`, declaring exhaustion without invoking each model **✅ CORRECT**: Invoke EACH model via task agent and ask explicitly @@ -118,7 +122,7 @@ After Round 1, invoke EACH of the 6 models to ask for new ideas. **No shortcuts 4. **For each new idea**: Run try-fix with that model (SEQUENTIAL, wait for completion) -5. **Exit when**: ALL 6 models say "NO NEW IDEAS" in the same round +5. **Exit when**: ALL 5 models say "NO NEW IDEAS" in the same round #### try-fix Behavior @@ -193,14 +197,13 @@ Update the state file: 5. Change 📋 Report status to `▶️ IN PROGRESS` **Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Round 1 completed: All 6 models ran try-fix -- [ ] **Cross-pollination table exists** with responses from ALL 6 models: +- [ ] Round 1 completed: All 5 models ran try-fix +- [ ] **Cross-pollination table exists** with responses from ALL 5 models: ``` | Model | Round 2 Response | |-------|------------------| | claude-sonnet-4.5 | NO NEW IDEAS | | claude-opus-4.6 | NO NEW IDEAS | - | claude-opus-4.6-fast | NO NEW IDEAS | | gpt-5.2 | NO NEW IDEAS | | gpt-5.2-codex | NO NEW IDEAS | | gemini-3-pro-preview | NO NEW IDEAS | @@ -312,17 +315,17 @@ Update all phase statuses to complete. - ❌ **Looking at PR's fix before generating ideas** - Generate fix ideas independently first - ❌ **Re-testing the PR's fix in try-fix** - Gate already validated it; try-fix tests YOUR ideas -- ❌ **Skipping models in Round 1** - All 6 models must run try-fix before cross-pollination +- ❌ **Skipping models in Round 1** - All 5 models must run try-fix before cross-pollination - ❌ **Running try-fix in parallel** - SEQUENTIAL ONLY - they modify same files and use same device - ❌ **Using explore/glob instead of invoking models** - Cross-pollination requires ACTUAL task agent invocations with each model, not code searches -- ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 6 models explicitly say "NO NEW IDEAS" +- ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 5 models explicitly say "NO NEW IDEAS" - ❌ **Not recording cross-pollination responses** - State file must have table showing each model's Round 2 response - ❌ **Not analyzing why fixes failed** - Record the flawed reasoning to help future attempts - ❌ **Selecting a failing fix** - Only select from passing candidates - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored -- ❌ **Declaring exhaustion prematurely** - All 6 models must confirm "no new ideas" via actual invocation +- ❌ **Declaring exhaustion prematurely** - All 5 models must confirm "no new ideas" via actual invocation - ❌ **Rushing the report** - Take time to write clear justification -- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout HEAD -- .` between try-fix attempts (see Step 1) +- ❌ **Skipping cleanup between attempts** - ALWAYS run `-Restore` + `git checkout HEAD -- .` + `git clean -fd --exclude=CustomAgentLogsTmp/` between try-fix attempts (see Step 1) --- @@ -338,6 +341,7 @@ Update all phase statuses to complete. ```bash pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore git checkout HEAD -- . +git clean -fd --exclude=CustomAgentLogsTmp/ ``` Then retry the try-fix attempt. The skill file should now be accessible. @@ -350,7 +354,7 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Previous attempt didn't restore its changes (crashed, timed out, or model didn't follow Step 8 restore instructions). -**Fix:** Same as above — run `-Restore` + `git checkout HEAD -- .` to reset to the merged PR state. +**Fix:** Same as above — run `-Restore` + `git checkout HEAD -- .` + `git clean -fd --exclude=CustomAgentLogsTmp/` to reset to the merged PR state. ### Build errors unrelated to the fix being attempted @@ -359,6 +363,6 @@ Then retry the try-fix attempt. The skill file should now be accessible. **Root cause:** Often caused by dirty working tree from a previous attempt. Can also be transient environment issues. **Fix:** -1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout HEAD -- .` +1. Run cleanup: `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore && git checkout HEAD -- . && git clean -fd --exclude=CustomAgentLogsTmp/` 2. Retry the attempt -3. If it fails again with the same unrelated error, skip this attempt and continue with the next model +3. If it fails again with the same unrelated error, treat this as an environment/worktree blocker: STOP the try-fix workflow, do NOT continue with the next model, and ask the user to investigate (see "Stop on Environment Blockers"). diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 8fefb95d4bfd..1d8749d98aa6 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -487,6 +487,10 @@ if (-not $DryRun) { } Write-Host "" +# NOTE: This cleanup targets CI/ADO agent environments where only this script's +# Copilot CLI processes should exist. On developer machines, this could potentially +# affect other Copilot processes (e.g., VS Code extension). The risk is low since +# this runs at script end, but be aware if running locally. # Clean up orphaned copilot CLI processes that may hold stdout fd open # IMPORTANT: Only target processes whose command line contains "copilot" to avoid # accidentally terminating the ADO agent's own node process diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index 677c22cc6b0f..8abdafe7b21c 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -109,6 +109,7 @@ if ($Platform -eq "android") { # Detect host architecture for simulator builds $hostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() $runtimeId = if ($hostArch -eq "x64") { "iossimulator-x64" } else { "iossimulator-arm64" } + $simArch = if ($hostArch -eq "x64") { "x64" } else { "arm64" } Write-Info "Host architecture: $hostArch, RuntimeIdentifier: $runtimeId" $buildArgs = @($ProjectPath, "-f", $TargetFramework, "-c", $Configuration, "-r", $runtimeId) @@ -175,28 +176,6 @@ if ($Platform -eq "android") { Write-Info "Searching for app bundle in: $artifactsDir" - # Detect simulator architecture to pick the correct app bundle - $simArch = "arm64" # Default to arm64 for Apple Silicon - try { - # Get the simulator's device type to determine architecture - $deviceInfo = xcrun simctl list devices --json | ConvertFrom-Json - $simDevice = $deviceInfo.devices.PSObject.Properties.Value | - ForEach-Object { $_ } | - Where-Object { $_.udid -eq $DeviceUdid } | - Select-Object -First 1 - - if ($simDevice) { - # Check if the host machine is x64 or arm64 - $hostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() - if ($hostArch -eq "x64") { - $simArch = "x64" - } - Write-Info "Host architecture: $hostArch, using simulator arch: $simArch" - } - } catch { - Write-Info "Could not detect architecture, defaulting to arm64" - } - $appPath = Get-ChildItem -Path $artifactsDir -Filter "*.app" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "$Configuration.*iossimulator-$simArch.*$projectName" -and diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 0f98b7a9597c..9fd9d227331a 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -166,13 +166,23 @@ if ($Platform -eq "android") { # Start emulator with selected AVD $emulatorBin = Join-Path $androidSdkRoot "emulator/emulator" + if ($IsWindows) { + $emulatorBin = "$emulatorBin.exe" + } # Check emulator binary exists if (-not (Test-Path $emulatorBin)) { - Write-Error "Emulator binary not found at: $emulatorBin" - Write-Info "Looking for emulator in SDK..." - Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } - exit 1 + # Fallback: try to find emulator on PATH + $emulatorCmd = Get-Command emulator -ErrorAction SilentlyContinue + if ($emulatorCmd) { + $emulatorBin = $emulatorCmd.Source + Write-Info "Using emulator from PATH: $emulatorBin" + } else { + Write-Error "Emulator binary not found at: $emulatorBin" + Write-Info "Looking for emulator in SDK..." + Get-ChildItem -Path $androidSdkRoot -Filter "emulator*" -Recurse -Depth 2 -ErrorAction SilentlyContinue | ForEach-Object { Write-Info " Found: $($_.FullName)" } + exit 1 + } } Write-Info "Starting emulator: $selectedAvd" @@ -180,13 +190,16 @@ if ($Platform -eq "android") { # Use swiftshader for software rendering (more reliable on CI without GPU) # Redirect output to a log file for debugging - $emulatorLog = "/tmp/emulator-$selectedAvd.log" + $emulatorLog = Join-Path ([System.IO.Path]::GetTempPath()) "emulator-$selectedAvd.log" if ($IsWindows) { Start-Process $emulatorBin -ArgumentList "-avd", $selectedAvd, "-no-snapshot-load", "-no-boot-anim", "-gpu", "swiftshader_indirect" -WindowStyle Hidden } else { # macOS/Linux: Use nohup to detach from terminal + # Use -no-snapshot (not -no-snapshot-load) to ensure clean emulator state for CI/testing. + # This disables both snapshot load and save, so each boot is a cold boot. + # Trade-off: slower boots, but guarantees no stale state between test runs. $startScript = "nohup '$emulatorBin' -avd '$selectedAvd' -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > '$emulatorLog' 2>&1 &" bash -c $startScript Write-Info "Emulator started in background. Log file: $emulatorLog" @@ -196,7 +209,12 @@ if ($Platform -eq "android") { Start-Sleep -Seconds 5 # Check if emulator process is running - $emulatorProcs = bash -c "pgrep -f 'qemu.*$selectedAvd' || pgrep -f 'emulator.*$selectedAvd' || true" 2>&1 + if ($IsWindows) { + $emulatorProcs = (Get-Process -Name "emulator*","qemu*" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -match [regex]::Escape($selectedAvd) }).Id -join "`n" + } else { + $emulatorProcs = bash -c "pgrep -f 'qemu.*$selectedAvd' || pgrep -f 'emulator.*$selectedAvd' || true" 2>&1 + } if ([string]::IsNullOrWhiteSpace($emulatorProcs)) { Write-Error "Emulator process did not start. Checking log..." if (Test-Path $emulatorLog) { @@ -207,9 +225,9 @@ if ($Platform -eq "android") { Write-Info "Emulator process started (PIDs: $emulatorProcs)" # Wait for device to appear with timeout - # Timeout of 600s (10 min) - emulator can take a while to boot, especially with software rendering + # Timeout of 120s (2 min) - if the emulator hasn't registered an ADB device by then, it's not going to Write-Info "Waiting for emulator device to appear..." - $deviceTimeout = 600 + $deviceTimeout = 120 $deviceWaited = 0 while ($deviceWaited -lt $deviceTimeout) { @@ -328,7 +346,7 @@ if ($Platform -eq "android") { # Preferred devices in order of priority $preferredDevices = @("iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro", "iPhone Xs") - # Preferred iOS versions in order (newest first) + # Preferred iOS versions in order (stable preferred, beta fallback) $preferredVersions = @("iOS-18", "iOS-17", "iOS-26") $selectedDevice = $null @@ -345,13 +363,20 @@ if ($Platform -eq "android") { if ($matchingRuntimes) { # Try each preferred device foreach ($deviceName in $preferredDevices) { - $device = $matchingRuntimes | ForEach-Object { - $_.Value | Where-Object { $_.name -eq $deviceName -and $_.isAvailable -eq $true } - } | Select-Object -First 1 + $device = $null + $deviceRuntime = $null + foreach ($rt in $matchingRuntimes) { + $found = $rt.Value | Where-Object { $_.name -eq $deviceName -and $_.isAvailable -eq $true } | Select-Object -First 1 + if ($found) { + $device = $found + $deviceRuntime = $rt.Name + break + } + } if ($device) { $selectedDevice = $device - $selectedVersion = ($matchingRuntimes | Select-Object -First 1).Name + $selectedVersion = $deviceRuntime Write-Info "Found preferred device: $deviceName on $selectedVersion" break } @@ -359,13 +384,20 @@ if ($Platform -eq "android") { # If no preferred device found, take first available iPhone if (-not $selectedDevice) { - $anyiPhone = $matchingRuntimes | ForEach-Object { - $_.Value | Where-Object { $_.name -match "iPhone" -and $_.isAvailable -eq $true } - } | Select-Object -First 1 + $anyiPhone = $null + $iphoneRuntime = $null + foreach ($rt in $matchingRuntimes) { + $found = $rt.Value | Where-Object { $_.name -match "iPhone" -and $_.isAvailable -eq $true } | Select-Object -First 1 + if ($found) { + $anyiPhone = $found + $iphoneRuntime = $rt.Name + break + } + } if ($anyiPhone) { $selectedDevice = $anyiPhone - $selectedVersion = ($matchingRuntimes | Select-Object -First 1).Name + $selectedVersion = $iphoneRuntime Write-Info "Using available iPhone: $($anyiPhone.name) on $selectedVersion" } } diff --git a/.github/scripts/shared/shared-utils.ps1 b/.github/scripts/shared/shared-utils.ps1 index 9db166521ad4..21477d0d09bb 100644 --- a/.github/scripts/shared/shared-utils.ps1 +++ b/.github/scripts/shared/shared-utils.ps1 @@ -24,7 +24,7 @@ function Write-Success { Write-Host "✅ $Message" -ForegroundColor Green } -function Write-Warning { +function Write-Warn { param([string]$Message) Write-Host "⚠️ $Message" -ForegroundColor Yellow } From 920f256811d5dcd079b0e3d9ae47ea54fc70d96f Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Mon, 9 Feb 2026 17:45:08 -0600 Subject: [PATCH 101/126] Switch Android emulator to API 30 to match maui-pr pipeline --- eng/pipelines/ci-copilot.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 7b54b11ea392..17c7d28ef181 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -70,12 +70,12 @@ stages: # Android emulator setup (skip for iOS) skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }} skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }} - androidEmulatorApiLevel: '34' + androidEmulatorApiLevel: '30' # Disable hardware acceleration in AVD config (HVF not available on Azure hosted agents) - script: | echo "=== Disabling hardware acceleration in AVD config ===" - AVD_CONFIG="$HOME/.android/avd/Emulator_34.avd/config.ini" + AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini" if [ -f "$AVD_CONFIG" ]; then echo "Found AVD config: $AVD_CONFIG" echo "Current config:" @@ -173,8 +173,8 @@ stages: $ErrorActionPreference = "Continue" try { - Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_34..." - & "./$scriptPath" -Platform android -DeviceUdid Emulator_34 2>&1 | ForEach-Object { Write-Host $_ } + Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_30..." + & "./$scriptPath" -Platform android -DeviceUdid Emulator_30 2>&1 | ForEach-Object { Write-Host $_ } $exitCode = $LASTEXITCODE Write-Host "Script returned exit code: $exitCode" } catch { @@ -299,7 +299,7 @@ stages: # Use swiftshader for software rendering (more reliable on CI without GPU) # Start emulator in background with logging for debugging EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" @@ -480,7 +480,7 @@ stages: # Start fresh emulator EMULATOR_LOG="/tmp/emulator-fresh.log" echo "Starting fresh emulator..." - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_34 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" From 03d61995afa79c5f03f87c93f5c8cbf3ed32efe3 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 10 Feb 2026 16:17:23 +0100 Subject: [PATCH 102/126] Fix: Extract Report content from '## Final Recommendation' heading fallback The agent sometimes writes the Report phase as a top-level markdown heading instead of a
block. Added fallback regex extraction to find '## Final Recommendation' sections when
extraction returns no content, fixing validation error 'Phase Report is marked as COMPLETE but has NO content in state file'. --- .../scripts/post-ai-summary-comment.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 index 46388f0a320f..eb9909b7362c 100644 --- a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -361,6 +361,18 @@ $reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( 'Final Report' ) -Debug:$debugMode +# Fallback: If Report content not found in
blocks, look for +# "## Final Recommendation" section directly in the markdown (agent sometimes +# writes Report as a top-level heading instead of a
block) +if ([string]::IsNullOrWhiteSpace($reportContent)) { + if ($Content -match '(?s)##\s+[✅⚠️❌]*\s*Final Recommendation[:\s].+') { + $reportContent = $Matches[0].Trim() + if ($debugMode) { + Write-Host " [DEBUG] Report extracted from '## Final Recommendation' heading ($($reportContent.Length) chars)" -ForegroundColor Green + } + } +} + # ============================================================================ # VALIDATION # ============================================================================ From ec221c26ccc0a7a5348b7f9f4ca41b138c535e61 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 10 Feb 2026 21:09:52 +0100 Subject: [PATCH 103/126] Add agent workflow metrics labels (s/agent-* prefix) - Create shared label helper module (.github/scripts/helpers/Update-AgentLabels.ps1) - Add Phase 4 (Apply Labels) to Review-PR.ps1 after comment posting - Remove old label logic from verify-tests-fail.ps1 - Document label system in copilot-instructions.md and SKILL.md - Labels: s/agent-approved, s/agent-changes-requested, s/agent-review-incomplete, s/agent-gate-passed, s/agent-gate-failed, s/agent-fix-optimal, s/agent-fix-implemented --- .github/copilot-instructions.md | 30 ++ .github/scripts/Review-PR.ps1 | 14 + .../scripts/helpers/Update-AgentLabels.ps1 | 353 ++++++++++++++++++ .../verify-tests-fail-without-fix/SKILL.md | 22 +- .../scripts/verify-tests-fail.ps1 | 58 +-- 5 files changed, 406 insertions(+), 71 deletions(-) create mode 100644 .github/scripts/helpers/Update-AgentLabels.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5c9f96eba10f..d7d6b5614fb3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -285,6 +285,36 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session. - **Output**: Updates session markdown with attempt results and failure analysis +### Agent Workflow Labels + +Labels with `s/agent-*` prefix track agent workflow outcomes for metrics. Applied by `Review-PR.ps1` Phase 4. + +**Outcome Labels** (mutually exclusive — one per PR): + +| Label | Description | +|-------|-------------| +| `s/agent-approved` | AI agent recommends approval | +| `s/agent-changes-requested` | AI agent recommends changes | +| `s/agent-review-incomplete` | AI agent could not complete all phases | + +**Signal Labels** (additive): + +| Label | Description | +|-------|-------------| +| `s/agent-gate-passed` | AI verified tests catch the bug | +| `s/agent-gate-failed` | AI could not verify tests catch the bug | +| `s/agent-fix-optimal` | AI confirms PR fix is the best among candidates | + +**Manual Labels** (applied by maintainers): + +| Label | Description | +|-------|-------------| +| `s/agent-fix-implemented` | PR author implemented the agent's suggested fix | + +**Base Label**: `s/agent-reviewed` — always applied on completed agent runs. + +**Helper module**: `.github/scripts/helpers/Update-AgentLabels.ps1` + ### Using Custom Agents **Delegation Policy**: When user request matches agent trigger phrases, **ALWAYS delegate to the appropriate agent immediately**. Do not ask for permission or explain alternatives unless the request is ambiguous. diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 1d8749d98aa6..20ea09ee4f4d 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -473,6 +473,20 @@ if ($DryRun) { } } } + + # Phase 4: Apply agent workflow labels + $labelHelperPath = ".github/scripts/helpers/Update-AgentLabels.ps1" + if (-not (Test-Path $labelHelperPath)) { + Write-Host "⚠️ Label helper missing, attempting recovery..." -ForegroundColor Yellow + git checkout HEAD -- $labelHelperPath 2>&1 | Out-Null + } + if (Test-Path $labelHelperPath) { + . $labelHelperPath + $stateFilePath = "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" + Invoke-AgentLabels -StateFile $stateFilePath -PRNumber $PRNumber + } else { + Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow + } } } diff --git a/.github/scripts/helpers/Update-AgentLabels.ps1 b/.github/scripts/helpers/Update-AgentLabels.ps1 new file mode 100644 index 000000000000..5b590386c643 --- /dev/null +++ b/.github/scripts/helpers/Update-AgentLabels.ps1 @@ -0,0 +1,353 @@ +<# +.SYNOPSIS + Shared helper module for applying agent workflow labels to PRs. + +.DESCRIPTION + Provides functions to apply outcome labels (mutually exclusive) and signal labels + (additive) to PRs based on agent workflow results. All labels use the s/agent-* prefix. + + Label Categories: + - Outcome (mutually exclusive): s/agent-approved, s/agent-changes-requested, s/agent-review-incomplete + - Signal (additive): s/agent-gate-passed, s/agent-gate-failed, s/agent-fix-optimal + - Base: s/agent-reviewed (always applied on completed runs) + - Manual: s/agent-fix-implemented (applied by maintainers, never auto-applied) + +.NOTES + All functions use gh api REST calls. Label failures are warnings, never fatal errors. +#> + +# ============================================================ +# Label Definitions +# ============================================================ + +$script:OutcomeLabels = @{ + Approved = "s/agent-approved" + ChangesRequested = "s/agent-changes-requested" + ReviewIncomplete = "s/agent-review-incomplete" +} + +$script:SignalLabels = @{ + GatePassed = "s/agent-gate-passed" + GateFailed = "s/agent-gate-failed" + FixOptimal = "s/agent-fix-optimal" +} + +$script:BaseLabel = "s/agent-reviewed" + +# Label definitions for auto-creation (Ensure-LabelExists) +$script:LabelDefinitions = @{ + "s/agent-reviewed" = @{ Color = "2E7D32"; Description = "PR was reviewed by AI agent workflow" } + "s/agent-approved" = @{ Color = "2E7D32"; Description = "AI agent recommends approval" } + "s/agent-changes-requested" = @{ Color = "E65100"; Description = "AI agent recommends changes" } + "s/agent-review-incomplete" = @{ Color = "B71C1C"; Description = "AI agent could not complete all phases" } + "s/agent-gate-passed" = @{ Color = "4CAF50"; Description = "AI verified tests catch the bug" } + "s/agent-gate-failed" = @{ Color = "FF9800"; Description = "AI could not verify tests catch the bug" } + "s/agent-fix-optimal" = @{ Color = "66BB6A"; Description = "AI confirms PR fix is the best among candidates" } + "s/agent-fix-implemented" = @{ Color = "7B1FA2"; Description = "PR author implemented the agent suggested fix" } +} + +# ============================================================ +# Helper Functions +# ============================================================ + +function Get-PRExistingLabels { + param([string]$PRNumber) + + $labels = gh pr view $PRNumber --repo dotnet/maui --json labels --jq '.labels[].name' 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ Failed to fetch existing labels for PR #$PRNumber" -ForegroundColor Yellow + return @() + } + return @($labels | Where-Object { $_ }) +} + +function Add-Label { + param([string]$PRNumber, [string]$Label) + + $result = gh api "repos/dotnet/maui/issues/$PRNumber/labels" --method POST -f "labels[]=$Label" 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ Failed to add label: $Label ($result)" -ForegroundColor Yellow + return $false + } + return $true +} + +function Remove-Label { + param([string]$PRNumber, [string]$Label) + + # URL-encode the label name (handles / in s/agent-*) + $encodedLabel = [System.Uri]::EscapeDataString($Label) + gh api "repos/dotnet/maui/issues/$PRNumber/labels/$encodedLabel" --method DELETE 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ Failed to remove label: $Label" -ForegroundColor Yellow + return $false + } + return $true +} + +# ============================================================ +# Public Functions +# ============================================================ + +function Ensure-LabelExists { + <# + .SYNOPSIS + Creates a label in the repo if it doesn't already exist. + #> + param([string]$Label) + + if (-not $script:LabelDefinitions.ContainsKey($Label)) { + Write-Host " ⚠️ Unknown label: $Label" -ForegroundColor Yellow + return + } + + $def = $script:LabelDefinitions[$Label] + + # Check if label exists + $existing = gh label list --repo dotnet/maui --search $Label --limit 1 --json name --jq '.[].name' 2>$null + if ($existing -eq $Label) { + return # Already exists + } + + Write-Host " 📌 Creating label: $Label" -ForegroundColor Cyan + gh label create $Label --repo dotnet/maui --color $def.Color --description $def.Description --force 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ Failed to create label: $Label" -ForegroundColor Yellow + } +} + +function Update-AgentOutcomeLabel { + <# + .SYNOPSIS + Applies exactly one outcome label and removes conflicting ones. + .PARAMETER Outcome + One of: 'Approved', 'ChangesRequested', 'ReviewIncomplete' + #> + param( + [Parameter(Mandatory)] + [ValidateSet('Approved', 'ChangesRequested', 'ReviewIncomplete')] + [string]$Outcome, + + [Parameter(Mandatory)] + [string]$PRNumber + ) + + $labelToAdd = $script:OutcomeLabels[$Outcome] + $labelsToRemove = $script:OutcomeLabels.Values | Where-Object { $_ -ne $labelToAdd } + + $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber + + # Remove conflicting outcome labels + foreach ($label in $labelsToRemove) { + if ($existingLabels -contains $label) { + Write-Host " 🔄 Removing: $label" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -Label $label | Out-Null + } + } + + # Add the outcome label + if ($existingLabels -notcontains $labelToAdd) { + Write-Host " ✅ Adding: $labelToAdd" -ForegroundColor Green + Add-Label -PRNumber $PRNumber -Label $labelToAdd | Out-Null + } else { + Write-Host " ℹ️ Already has: $labelToAdd" -ForegroundColor Gray + } +} + +function Update-AgentSignalLabel { + <# + .SYNOPSIS + Adds or removes a signal label. For mutually exclusive pairs (gate-passed/gate-failed), + adding one removes the other. + .PARAMETER Signal + One of: 'GatePassed', 'GateFailed', 'FixOptimal' + #> + param( + [Parameter(Mandatory)] + [ValidateSet('GatePassed', 'GateFailed', 'FixOptimal')] + [string]$Signal, + + [Parameter(Mandatory)] + [string]$PRNumber, + + [switch]$Remove + ) + + $label = $script:SignalLabels[$Signal] + $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber + + if ($Remove) { + if ($existingLabels -contains $label) { + Write-Host " 🔄 Removing: $label" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -Label $label | Out-Null + } + return + } + + # For gate labels, they are mutually exclusive with each other + if ($Signal -eq 'GatePassed' -and $existingLabels -contains $script:SignalLabels['GateFailed']) { + Write-Host " 🔄 Removing: $($script:SignalLabels['GateFailed'])" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -Label $script:SignalLabels['GateFailed'] | Out-Null + } + elseif ($Signal -eq 'GateFailed' -and $existingLabels -contains $script:SignalLabels['GatePassed']) { + Write-Host " 🔄 Removing: $($script:SignalLabels['GatePassed'])" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -Label $script:SignalLabels['GatePassed'] | Out-Null + } + + if ($existingLabels -notcontains $label) { + Write-Host " ✅ Adding: $label" -ForegroundColor Green + Add-Label -PRNumber $PRNumber -Label $label | Out-Null + } else { + Write-Host " ℹ️ Already has: $label" -ForegroundColor Gray + } +} + +function Update-AgentReviewedLabel { + <# + .SYNOPSIS + Applies the base s/agent-reviewed label to mark the PR as agent-reviewed. + #> + param( + [Parameter(Mandatory)] + [string]$PRNumber + ) + + $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber + + if ($existingLabels -notcontains $script:BaseLabel) { + Write-Host " ✅ Adding: $($script:BaseLabel)" -ForegroundColor Green + Add-Label -PRNumber $PRNumber -Label $script:BaseLabel | Out-Null + } else { + Write-Host " ℹ️ Already has: $($script:BaseLabel)" -ForegroundColor Gray + } +} + +function Invoke-AgentLabels { + <# + .SYNOPSIS + Main entry point: parses a state file and applies all appropriate labels. + .PARAMETER StateFile + Path to the PR state markdown file (e.g., CustomAgentLogsTmp/PRState/pr-33528.md) + .PARAMETER PRNumber + The PR number to apply labels to. + #> + param( + [Parameter(Mandatory)] + [string]$StateFile, + + [Parameter(Mandatory)] + [string]$PRNumber + ) + + if (-not (Test-Path $StateFile)) { + Write-Host " ⚠️ State file not found: $StateFile" -ForegroundColor Yellow + return + } + + $content = Get-Content $StateFile -Raw + + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue + Write-Host "║ PHASE 4: APPLY LABELS ║" -ForegroundColor Blue + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue + Write-Host "" + Write-Host "🏷️ Applying agent workflow labels to PR #$PRNumber..." -ForegroundColor Cyan + + # 1. Always apply s/agent-reviewed + Update-AgentReviewedLabel -PRNumber $PRNumber + + # 2. Determine and apply outcome label + $outcome = Get-OutcomeFromState -Content $content + Write-Host " 📊 Outcome: $outcome" -ForegroundColor Cyan + Update-AgentOutcomeLabel -Outcome $outcome -PRNumber $PRNumber + + # 3. Determine and apply signal labels + $gateResult = Get-GateResultFromState -Content $content + if ($gateResult -eq 'Passed') { + Update-AgentSignalLabel -Signal GatePassed -PRNumber $PRNumber + } elseif ($gateResult -eq 'Failed') { + Update-AgentSignalLabel -Signal GateFailed -PRNumber $PRNumber + } + + $fixOptimal = Get-FixOptimalFromState -Content $content + if ($fixOptimal) { + Update-AgentSignalLabel -Signal FixOptimal -PRNumber $PRNumber + } + + Write-Host "" + Write-Host "✅ Labels applied to PR #$PRNumber" -ForegroundColor Green +} + +# ============================================================ +# State File Parsing +# ============================================================ + +function Get-OutcomeFromState { + param([string]$Content) + + # Check for Final Recommendation + if ($Content -match '(?i)Final Recommendation[:\s]*APPROVE') { + return 'Approved' + } + if ($Content -match '(?i)Final Recommendation[:\s]*(REQUEST[_ ]CHANGES|CHANGES[_ ]REQUESTED)') { + return 'ChangesRequested' + } + # Check for Verdict line (alternative format) + if ($Content -match '(?i)Verdict[:\s]*✅\s*APPROVE') { + return 'Approved' + } + if ($Content -match '(?i)Verdict[:\s]*(⚠️|❌)') { + return 'ChangesRequested' + } + + # Check if all phases completed — if not, it's incomplete + $phases = @('Pre-Flight', 'Tests', 'Gate', 'Fix', 'Report') + $allComplete = $true + foreach ($phase in $phases) { + if ($Content -notmatch "(?i)$phase\s*\|\s*✅") { + $allComplete = $false + break + } + } + + if (-not $allComplete) { + return 'ReviewIncomplete' + } + + # Phases complete but no clear recommendation — default to incomplete + return 'ReviewIncomplete' +} + +function Get-GateResultFromState { + param([string]$Content) + + # Check the phase status table + if ($Content -match '(?i)Gate\s*\|\s*✅\s*PASSED') { + return 'Passed' + } + if ($Content -match '(?i)Gate\s*\|\s*❌\s*FAILED') { + return 'Failed' + } + if ($Content -match '(?i)Gate\s*\|\s*⚠️') { + return 'Failed' + } + + return $null # Gate not run or status unclear +} + +function Get-FixOptimalFromState { + param([string]$Content) + + # Look for indicators that the PR's fix was selected as best + if ($Content -match '(?i)Selected Fix[:\s]*PR') { + return $true + } + if ($Content -match '(?i)PR.s fix is.*best') { + return $true + } + if ($Content -match '(?i)PR fix.*optimal') { + return $true + } + + return $false +} diff --git a/.github/skills/verify-tests-fail-without-fix/SKILL.md b/.github/skills/verify-tests-fail-without-fix/SKILL.md index ba3df1d1aaa6..3b81b7ea1c88 100644 --- a/.github/skills/verify-tests-fail-without-fix/SKILL.md +++ b/.github/skills/verify-tests-fail-without-fix/SKILL.md @@ -81,8 +81,7 @@ The script auto-detects which mode to use based on whether fix files are present 1. Fetches base branch from origin (if available) 2. Auto-detects test classes from changed test files 3. Runs tests (should FAIL to prove they catch the bug) -4. **Updates PR labels** based on result -5. Reports result +4. Reports result **Full Verification Mode (fix files detected):** 1. Fetches base branch from origin to ensure accurate diff @@ -95,23 +94,16 @@ The script auto-detects which mode to use based on whether fix files are present 8. **Generates markdown reports**: - `CustomAgentLogsTmp/TestValidation/verification-report.md` - Full detailed report - `CustomAgentLogsTmp/PRState/verification-report.md` - Gate section for PR agent -9. **Updates PR labels** based on result -10. Reports result +9. Reports result ## PR Labels -The skill automatically manages two labels on the PR to indicate verification status: +Labels are managed centrally by `Review-PR.ps1` Phase 4 using the shared helper module +(`.github/scripts/helpers/Update-AgentLabels.ps1`). This skill no longer applies labels directly. -| Label | Color | When Applied | -|-------|-------|--------------| -| `s/ai-reproduction-confirmed` | 🟢 Green (#2E7D32) | Tests correctly FAIL without fix (AI verified tests catch the bug) | -| `s/ai-reproduction-failed` | 🟠 Orange (#E65100) | Tests PASS without fix (AI verified tests don't catch the bug) | - -**Behavior:** -- When verification passes, adds `s/ai-reproduction-confirmed` and removes `s/ai-reproduction-failed` if present -- When verification fails, adds `s/ai-reproduction-failed` and removes `s/ai-reproduction-confirmed` if present -- If a PR is re-verified after fixing tests, labels are updated accordingly -- No label = AI hasn't verified tests yet +Gate results are reflected via: +- `s/agent-gate-passed` — Tests correctly FAIL without fix (verified tests catch the bug) +- `s/agent-gate-failed` — Tests PASS without fix (tests don't catch the bug) ## Output Files diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index de3bad6fee15..9840903aa8e1 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -145,58 +145,8 @@ $BaselineScript = Join-Path $RepoRoot ".github/scripts/EstablishBrokenBaseline.p . $BaselineScript # ============================================================ -# Label management for verification results -# ============================================================ -$LabelConfirmed = "s/ai-reproduction-confirmed" -$LabelFailed = "s/ai-reproduction-failed" - -function Update-VerificationLabels { - param( - [Parameter(Mandatory = $true)] - [bool]$ReproductionConfirmed, - - [Parameter(Mandatory = $false)] - [string]$PR = $PRNumber - ) - - if ($PR -eq "unknown" -or -not $PR) { - Write-Host "⚠️ Cannot update labels: PR number not available" -ForegroundColor Yellow - return - } - - $labelToAdd = if ($ReproductionConfirmed) { $LabelConfirmed } else { $LabelFailed } - $labelToRemove = if ($ReproductionConfirmed) { $LabelFailed } else { $LabelConfirmed } - - Write-Host "" - Write-Host "🏷️ Updating verification labels on PR #$PR..." -ForegroundColor Cyan - - # Track success for both operations - $removeSuccess = $true - - # Remove the opposite label if it exists (using REST API to avoid GraphQL deprecation issues) - $existingLabels = gh pr view $PR --json labels --jq '.labels[].name' 2>$null - if ($existingLabels -contains $labelToRemove) { - Write-Host " Removing: $labelToRemove" -ForegroundColor Yellow - gh api "repos/dotnet/maui/issues/$PR/labels/$labelToRemove" --method DELETE 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - $removeSuccess = $false - Write-Host " ⚠️ Failed to remove label: $labelToRemove" -ForegroundColor Yellow - } - } - - # Add the appropriate label (using REST API to avoid GraphQL deprecation issues) - Write-Host " Adding: $labelToAdd" -ForegroundColor Green - $result = gh api "repos/dotnet/maui/issues/$PR/labels" --method POST -f "labels[]=$labelToAdd" 2>&1 - $addSuccess = $LASTEXITCODE -eq 0 - - if ($addSuccess -and $removeSuccess) { - Write-Host "✅ Labels updated successfully" -ForegroundColor Green - } elseif ($addSuccess) { - Write-Host "⚠️ Label added but failed to remove old label" -ForegroundColor Yellow - } else { - Write-Host "⚠️ Failed to update labels: $result" -ForegroundColor Yellow - } -} +# Note: Label management moved to .github/scripts/helpers/Update-AgentLabels.ps1 +# Labels are now applied centrally by Review-PR.ps1 Phase 4. # ============================================================ # Auto-detect test filter from changed files @@ -466,7 +416,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" Write-Host "Failed tests: $($testResult.FailCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { # Tests PASSED - this is bad! @@ -487,7 +436,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" Write-Host "Passed tests: $($testResult.PassCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } } @@ -884,7 +832,6 @@ if ($verificationPassed) { Write-Host "║ - FAIL without fix (as expected) ║" -ForegroundColor Green Write-Host "║ - PASS with fix (as expected) ║" -ForegroundColor Green Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { Write-Host "" @@ -906,6 +853,5 @@ if ($verificationPassed) { Write-Host "║ 3. The issue was already fixed in base branch ║" -ForegroundColor Red Write-Host "║ 4. Build caching - try clean rebuild ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } From d508f11350f6b6cb0be4d9cb3941a360aaf837ae Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 10 Feb 2026 11:30:15 -0600 Subject: [PATCH 104/126] Use hardware GPU for Android emulator on macOS (match maui-pr pipeline) --- eng/pipelines/ci-copilot.yml | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 17c7d28ef181..e1ea705b6ea1 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -72,27 +72,19 @@ stages: skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }} androidEmulatorApiLevel: '30' - # Disable hardware acceleration in AVD config (HVF not available on Azure hosted agents) + # Configure AVD for hardware acceleration (AcesShared ARM64 macOS agents have HVF) + # Match maui-pr Cake script: no GPU override on macOS (uses default hw GPU) - script: | - echo "=== Disabling hardware acceleration in AVD config ===" + echo "=== Configuring AVD for hardware-accelerated emulation ===" AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini" if [ -f "$AVD_CONFIG" ]; then echo "Found AVD config: $AVD_CONFIG" - echo "Current config:" - cat "$AVD_CONFIG" - echo "" - echo "Adding hw.cpu.ncore=2 and removing any accelerator settings..." - # Remove any existing accelerator settings and add our own - grep -v "hw.accelerator" "$AVD_CONFIG" > "$AVD_CONFIG.tmp" && mv "$AVD_CONFIG.tmp" "$AVD_CONFIG" - echo "hw.cpu.ncore=2" >> "$AVD_CONFIG" - echo "" - echo "Updated config:" cat "$AVD_CONFIG" else echo "##vso[task.logissue type=warning]AVD config not found at: $AVD_CONFIG" ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found" fi - displayName: 'Configure AVD for Software Emulation' + displayName: 'Configure AVD' condition: eq('${{ parameters.Platform }}', 'android') # Set up Android SDK PATH (required on self-hosted agents) @@ -295,11 +287,11 @@ stages: - script: | echo "=== Booting Android Emulator ===" - # Use swiftshader for software graphics rendering (HVF not available on Azure hosted ARM64 agents) - # Use swiftshader for software rendering (more reliable on CI without GPU) + # Match maui-pr Cake script: on macOS use default GPU (hardware-accelerated), + # only use swiftshader on Linux. AcesShared agents are ARM64 macOS with HVF. # Start emulator in background with logging for debugging EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" @@ -480,7 +472,7 @@ stages: # Start fresh emulator EMULATOR_LOG="/tmp/emulator-fresh.log" echo "Starting fresh emulator..." - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect > "$EMULATOR_LOG" 2>&1 & + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! echo "Emulator started with PID: $EMULATOR_PID" From 807d9fb82ded27c20f6f3066aac547a0d7a6a815 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 10 Feb 2026 16:32:06 -0600 Subject: [PATCH 105/126] Install both Appium drivers (agent may switch platforms for platform-specific bugs) --- eng/pipelines/ci-copilot.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index e1ea705b6ea1..95ae80e555a3 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -390,14 +390,12 @@ stages: # Install Appium globally npm install -g appium - # Install platform-specific driver - if [ "${{ parameters.Platform }}" = "ios" ]; then - echo "Installing XCUITest driver for iOS..." - appium driver install xcuitest - else - echo "Installing UiAutomator2 driver for Android..." - appium driver install uiautomator2 - fi + # Install both drivers — the agent may switch platforms if the bug + # only affects one platform (e.g., iOS bug triggered via Android run) + echo "Installing UiAutomator2 driver for Android..." + appium driver install uiautomator2 + echo "Installing XCUITest driver for iOS..." + appium driver install xcuitest # Verify installation if ! which appium; then From 9b9b1a364685f57d4e1e5f3df7caf09af6cedef1 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 10 Feb 2026 18:22:25 -0600 Subject: [PATCH 106/126] Collect UI test screenshots and page source in CopilotLogs artifact - Set APPIUM_LOG_FILE env var so UITestBase saves screenshots to CustomAgentLogsTmp/UITests/ - Add post-test step to copy screenshots/page source from test assembly output dirs as fallback - Enables debugging test failures by examining actual app state at failure time --- .github/scripts/BuildAndRunHostApp.ps1 | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/scripts/BuildAndRunHostApp.ps1 b/.github/scripts/BuildAndRunHostApp.ps1 index c6103609de46..11caf26f05b2 100644 --- a/.github/scripts/BuildAndRunHostApp.ps1 +++ b/.github/scripts/BuildAndRunHostApp.ps1 @@ -277,6 +277,11 @@ Write-Host "" $env:DEVICE_UDID = $DeviceUdid Write-Info "Set DEVICE_UDID environment variable: $DeviceUdid" +# Set APPIUM_LOG_FILE so UITestBase saves screenshots/page-source to our log directory +$appiumLogFile = Join-Path $HostAppLogsDir "appium.log" +$env:APPIUM_LOG_FILE = $appiumLogFile +Write-Info "Set APPIUM_LOG_FILE: $appiumLogFile (screenshots will be saved here)" + try { # Run dotnet test and capture output $testOutput = & dotnet test $TestProject --filter $effectiveFilter --logger "console;verbosity=detailed" 2>&1 @@ -311,6 +316,37 @@ try { #endregion +#region Collect Test Artifacts (screenshots, page source) + +Write-Step "Collecting test artifacts (screenshots, page source)..." + +# Collect any screenshots/page source from the test assembly output directory +# UITestBase saves these via TestContext.AddTestAttachment to the assembly dir +$testAssemblyDirs = @( + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0"), + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0"), + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0") +) + +$copiedCount = 0 +foreach ($dir in $testAssemblyDirs) { + if (Test-Path $dir) { + $artifacts = Get-ChildItem -Path $dir -File -Include "*.png","*.txt" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match "ScreenShot|PageSource" } + foreach ($artifact in $artifacts) { + Copy-Item -Path $artifact.FullName -Destination $HostAppLogsDir -Force + $copiedCount++ + } + } +} + +# Also check the HostAppLogsDir itself for screenshots saved via APPIUM_LOG_FILE +$screenshotCount = (Get-ChildItem -Path $HostAppLogsDir -Filter "*.png" -ErrorAction SilentlyContinue).Count +$pageSourceCount = (Get-ChildItem -Path $HostAppLogsDir -Filter "*PageSource*" -ErrorAction SilentlyContinue).Count +Write-Info "Test artifacts collected: $screenshotCount screenshot(s), $pageSourceCount page source(s) (copied $copiedCount from assembly dir)" + +#endregion + #region Capture Device Logs Write-Step "Capturing device logs..." From 7ce5e3fe8a3aa36abd97d6ad70ed1e45045a8602 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 10 Feb 2026 20:35:58 -0600 Subject: [PATCH 107/126] Fix: Always update AI Summary comment instead of skipping when one exists The fallback step was checking if an AI Summary comment existed and skipping entirely, leaving stale results from previous runs. Now it always runs post-ai-summary-comment.ps1 which handles both creating new comments and updating existing ones with latest results. --- eng/pipelines/ci-copilot.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 95ae80e555a3..d6dd11787f70 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -696,22 +696,15 @@ stages: git checkout -- . 2>&1 | Write-Host Write-Host "✅ Working tree restored" - # Check if a comment was already posted by looking for the AI Summary marker - Write-Host "Checking for existing AI summary comment..." - $existingComment = gh pr view $prNumber --json comments --jq '.comments[] | select(.body | contains("")) | .id' 2>$null | Select-Object -First 1 + # Always run post-ai-summary-comment.ps1 - it handles both creating new + # comments and updating existing ones with the latest results. + # Previous logic skipped if a comment existed, but that left stale results + # from earlier runs (e.g., BLOCKED try-fixes that later ran successfully). + Write-Host "Running post-ai-summary-comment.ps1 (creates or updates)..." - if ($existingComment) { - Write-Host "✅ AI summary comment already exists (ID: $existingComment). Skipping fallback." - exit 0 - } - - Write-Host "No AI summary comment found. Running post-ai-summary-comment.ps1..." - - # Run the post-ai-summary-comment.ps1 script directly - # Uses -PRNumber which auto-loads state from CustomAgentLogsTmp/PRState/pr-$prNumber.md pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber if ($LASTEXITCODE -eq 0) { - Write-Host "✅ AI summary comment posted successfully" + Write-Host "✅ AI summary comment posted/updated successfully" } else { Write-Host "##vso[task.logissue type=error]post-ai-summary-comment.ps1 failed with exit code $LASTEXITCODE" exit $LASTEXITCODE From 544b35d75b4c38bf7cf3848f4a682e030e207a4d Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 11 Feb 2026 12:19:09 -0600 Subject: [PATCH 108/126] Fix: Prevent fallback comment step from hanging on stdin Two fixes: 1. post-ai-summary-comment.ps1: Remove legacy stdin reader ($input | Out-String) that blocks forever in non-interactive CI contexts when no state file is found. 2. ci-copilot.yml: Add state file existence check before calling the script (skip gracefully if missing), and add timeoutInMinutes: 5 as a safety net. Root cause: Build 13284089 (iOS, PR #32797) timed out at 180 min because the fallback step hung for 2+ hours reading from stdin. --- .../scripts/post-ai-summary-comment.ps1 | 6 ++---- eng/pipelines/ci-copilot.yml | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 index eb9909b7362c..6617bdcd32fd 100644 --- a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -112,10 +112,8 @@ if ([string]::IsNullOrWhiteSpace($Content) -and $PRNumber -gt 0) { } } -# If Content still not provided, try stdin (legacy support) -if ([string]::IsNullOrWhiteSpace($Content)) { - $Content = $input | Out-String -} +# If Content still not provided, skip stdin (it hangs in CI/non-interactive contexts). +# Legacy piped input is no longer supported — use -StateFile or -Content instead. # Final validation if ([string]::IsNullOrWhiteSpace($Content)) { diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d6dd11787f70..478070f71f8a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -696,12 +696,19 @@ stages: git checkout -- . 2>&1 | Write-Host Write-Host "✅ Working tree restored" + # Verify state file exists before calling the script. + # Without a state file, post-ai-summary-comment.ps1 falls through to + # reading stdin ($input | Out-String) which hangs forever in CI. + $stateFile = "CustomAgentLogsTmp/PRState/pr-$prNumber.md" + if (-not (Test-Path $stateFile)) { + Write-Host "⚠️ State file not found: $stateFile — skipping fallback comment" + Write-Host " (The PR Reviewer Agent may not have created a state file)" + exit 0 + } + Write-Host "✅ State file found: $stateFile" + # Always run post-ai-summary-comment.ps1 - it handles both creating new # comments and updating existing ones with the latest results. - # Previous logic skipped if a comment existed, but that left stale results - # from earlier runs (e.g., BLOCKED try-fixes that later ran successfully). - Write-Host "Running post-ai-summary-comment.ps1 (creates or updates)..." - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber if ($LASTEXITCODE -eq 0) { Write-Host "✅ AI summary comment posted/updated successfully" @@ -711,6 +718,7 @@ stages: } displayName: 'Post AI Summary Comment (Fallback)' condition: succeededOrFailed() + timeoutInMinutes: 5 continueOnError: true env: GITHUB_TOKEN: $(COPILOT_TOKEN) From 655bdd68f970001b70f90cd48bf919b5dc341a66 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 12 Feb 2026 14:26:29 +0100 Subject: [PATCH 109/126] Add PRAgent outputs and CI autonomous behavior Introduce structured PRAgent phase output directories and content.md artifacts, and make agent workflows non-interactive/CI-first. Documentation (.github/agents/*, SKILLs and plan templates) updated to require writing phase outputs to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/{phase}/content.md, to prefer continuing autonomously on environment blockers (retry once, then skip) and to remove strict requirements to create a single monolithic state file before starting. Review-PR.ps1 now creates PRAgent phase directories, documents CI-mode behavior and phase output paths, adjusts pr-finalize output locations, and updates labeling invocation. Several ai-summary-comment docs/scripts were updated to read generic "content" artifacts (and to remove SkipValidation usage). Overall changes align agent scripts and docs with CI-friendly, structured phase outputs and clearer failure/retry semantics. --- .github/agents/learn-from-pr.md | 1 - .github/agents/pr.md | 185 ++------------ .github/agents/pr/PLAN-TEMPLATE.md | 37 ++- .github/agents/pr/SHARED-RULES.md | 190 +++++++++++---- .github/agents/pr/post-gate.md | 97 +++----- .github/copilot-instructions.md | 34 ++- .github/scripts/Review-PR.ps1 | 48 +++- .../skills/ai-summary-comment/IMPROVEMENTS.md | 48 ++-- .../NO-EXTERNAL-REFERENCES-RULE.md | 10 +- .github/skills/ai-summary-comment/SKILL.md | 87 ++++--- .../scripts/post-ai-summary-comment.ps1 | 227 +++++++++++------- .../scripts/post-pr-finalize-comment.ps1 | 6 +- .../scripts/post-try-fix-comment.ps1 | 6 +- .../scripts/post-verify-tests-comment.ps1 | 4 +- .../scripts/post-write-tests-comment.ps1 | 2 +- .github/skills/learn-from-pr/SKILL.md | 15 +- .github/skills/try-fix/SKILL.md | 31 +-- .../scripts/verify-tests-fail.ps1 | 2 +- eng/pipelines/ci-copilot.yml | 43 ---- 19 files changed, 508 insertions(+), 565 deletions(-) diff --git a/.github/agents/learn-from-pr.md b/.github/agents/learn-from-pr.md index ce0a506c3af3..759d1db88328 100644 --- a/.github/agents/learn-from-pr.md +++ b/.github/agents/learn-from-pr.md @@ -89,7 +89,6 @@ Present a summary: | Situation | Action | |-----------|--------| | PR not found | Ask user to verify PR number | -| No session markdown | Proceed with PR diff analysis only | | Target file doesn't exist | Create if instruction/architecture doc, skip if code | | Duplicate content exists | Skip, note in report | | Unclear where to add | Ask user for guidance | diff --git a/.github/agents/pr.md b/.github/agents/pr.md index d5c55dbc80de..00c755b87c26 100644 --- a/.github/agents/pr.md +++ b/.github/agents/pr.md @@ -1,6 +1,6 @@ --- name: pr -description: Sequential 4-phase workflow for GitHub issues - Pre-Flight, Gate, Fix, Report. Phases MUST complete in order. State tracked in CustomAgentLogsTmp/PRState/ +description: Sequential 4-phase workflow for GitHub issues - Pre-Flight, Gate, Fix, Report. Phases MUST complete in order. --- # .NET MAUI Pull Request Agent @@ -48,13 +48,13 @@ After Gate passes, read `.github/agents/pr/post-gate.md` for **Phases 3-4**. - Follow Templates EXACTLY (no `open` attributes, no "improvements") - No Direct Git Commands (use `gh pr diff/view`, let scripts handle files) - Use Skills' Scripts (don't bypass with manual commands) -- Stop on Environment Blockers (strict retry limits, report and ask user) +- Stop on Environment Blockers (retry once, then skip and continue autonomously in CI mode) - Multi-Model Configuration (5 models for Phase 4) - Platform Selection (must be affected AND available on host) **Key points:** - ❌ Never run `git checkout`, `git switch`, `git stash`, `git reset` - agent is always on correct branch -- ❌ Never continue after environment blocker - STOP and ask user +- ❌ Never stop and ask user in CI mode - use best judgment to skip blocked phases and continue - ❌ Never mark phase ✅ with [PENDING] fields remaining Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed instructions after Gate passes. @@ -65,8 +65,6 @@ Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed ins > **⚠️ SCOPE**: Document only. No code analysis. No fix opinions. No running tests. -**🚨 CRITICAL: Create the state file BEFORE doing anything else.** - ### ❌ Pre-Flight Boundaries (What NOT To Do) | ❌ Do NOT | Why | When to do it | @@ -79,141 +77,11 @@ Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed ins ### ✅ What TO Do in Pre-Flight -- Create/check state file - Read issue description and comments - Note platforms affected (from labels) - Identify files changed (if PR exists) - Document disagreements and edge cases from comments -### Step 0: Check for Existing State File or Create New One - -**State file location**: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - -**Naming convention:** -- If starting from **PR #12345** → Name file `pr-12345.md` (use PR number) -- If starting from **Issue #33356** (no PR yet) → Name file `pr-33356.md` (use issue number as placeholder) -- When PR is created later → Rename to use actual PR number - -```bash -# Check if state file exists -mkdir -p CustomAgentLogsTmp/PRState -if [ -f "CustomAgentLogsTmp/PRState/pr-XXXXX.md" ]; then - echo "State file exists - resuming session" - cat CustomAgentLogsTmp/PRState/pr-XXXXX.md -else - echo "Creating new state file" -fi -``` - -**If the file EXISTS**: Read it to determine your current phase and resume from there. Look for: -- Which phase has `▶️ IN PROGRESS` status - that's where you left off -- Which phases have `✅ PASSED` status - those are complete -- Which phases have `⏳ PENDING` status - those haven't started - -**If the file does NOT exist**: Create it with the template structure: - -```markdown -# PR Review: #XXXXX - [Issue Title TBD] - -**Date:** [TODAY] | **Issue:** [#XXXXX](https://github.com/dotnet/maui/issues/XXXXX) | **PR:** [#YYYYY](https://github.com/dotnet/maui/pull/YYYYY) or None - -## ⏳ Status: IN PROGRESS - -| Phase | Status | -|-------|--------| -| Pre-Flight | ▶️ IN PROGRESS | -| 🚦 Gate | ⏳ PENDING | -| 🔧 Fix | ⏳ PENDING | -| 📋 Report | ⏳ PENDING | - ---- - -
-📋 Issue Summary - -[From issue body] - -**Steps to Reproduce:** -1. [Step 1] -2. [Step 2] - -**Platforms Affected:** -- [ ] iOS -- [ ] Android -- [ ] Windows -- [ ] MacCatalyst - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `path/to/fix.cs` | Fix | +X lines | -| `path/to/test.cs` | Test | +Y lines | - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- [Notable comments from issue/PR discussion] - -**Reviewer Feedback:** -- [Key points from review comments] - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| - -**Author Uncertainty:** -- [Areas where author expressed doubt] - -
- -
-🚦 Gate - Test Verification - -**Status**: ⏳ PENDING - -- [ ] Tests FAIL (bug reproduced) - -**Result:** [PENDING] - -
- -
-🔧 Fix Candidates - -**Status**: ⏳ PENDING - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| PR | PR #XXXXX | [PR's approach - from Pre-Flight] | ⏳ PENDING (Gate) | [files] | Original PR - validated by Gate | - -**Note:** try-fix candidates (1, 2, 3...) are added during Phase 3. PR's fix is reference only. - -**Exhausted:** No -**Selected Fix:** [PENDING] - -
- ---- - -**Next Step:** After Gate passes, read `.github/agents/pr/post-gate.md` and continue with phases 3-4. -``` - -This file: -- Serves as your TODO list for all phases -- Tracks progress if interrupted -- Must exist before you start gathering context -- **Always include when saving changes** (to `CustomAgentLogsTmp/PRState/`) -- **Phases 3-4 sections are added AFTER Gate passes** (see `pr/post-gate.md`) - -**Then gather context and update the file as you go.** - ### Step 1: Gather Context (depends on starting point) **If starting from a PR:** @@ -258,11 +126,9 @@ gh pr view XXXXX --json comments --jq '.comments[] | select(.body | contains("Fi - Contains structured analysis (Root Cause, Platform Comparison, etc.) **If prior agent review found:** -1. **Extract and use as state file content** - The review IS the completed state -2. Parse the phase statuses to determine what's already done -3. Import all findings (fix candidates, test results) -4. Update your local state file with this content -5. Resume from whichever phase is not yet complete (or report as done) +1. Parse the phase statuses to determine what's already done +2. Import all findings (fix candidates, test results) +3. Resume from whichever phase is not yet complete (or report as done) **Do NOT:** - Start from scratch if a complete review already exists @@ -271,8 +137,6 @@ gh pr view XXXXX --json comments --jq '.comments[] | select(.body | contains("Fi ### Step 3: Document Key Findings -Update the state file `CustomAgentLogsTmp/PRState/pr-XXXXX.md`: - **If PR exists** - Document disagreements and reviewer feedback: | File:Line | Reviewer Says | Author Says | Status | |-----------|---------------|-------------|--------| @@ -308,21 +172,12 @@ The test result will be updated to `✅ PASS (Gate)` after Gate passes. ### Step 5: Complete Pre-Flight -**🚨 MANDATORY: Update state file** - -**Update state file** - Change Pre-Flight status and populate with gathered context: -1. Change Pre-Flight status from `▶️ IN PROGRESS` to `✅ COMPLETE` -2. Fill in issue summary, platforms affected, regression info -3. Add edge cases and any disagreements (if PR exists) -4. Change 🚦 Gate status to `▶️ IN PROGRESS` - -**Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Issue summary filled (not [PENDING]) -- [ ] Platform checkboxes marked -- [ ] Files Changed table populated (if PR exists) -- [ ] PR Discussion Summary documented (if PR exists) -- [ ] All [PENDING] placeholders replaced -- [ ] State file saved +Verify the following before proceeding: +- [ ] Issue summary captured +- [ ] Platform information noted +- [ ] Files changed identified (if PR exists) +- [ ] PR discussion summarized (if PR exists) +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/pre-flight/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") --- @@ -356,7 +211,7 @@ find src/Controls/tests -name "*XXXXX*" -type f 2>/dev/null **🚨 CRITICAL: Choose a platform that is BOTH affected by the bug AND available on the current host.** **Identify affected platforms** from Pre-Flight: -- Check the "Platforms Affected" checkboxes in the state file +- Check the platforms affected from Pre-Flight context - Check issue labels (e.g., `platform/iOS`, `platform/Android`) - Check which platform-specific files the PR modifies @@ -416,18 +271,11 @@ See `.github/skills/verify-tests-fail-without-fix/SKILL.md` for full skill docum ### Complete 🚦 Gate -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Fill in **Result**: `PASSED ✅` -2. Change 🚦 Gate status to `✅ PASSED` -3. Proceed to Phase 3 - -**Before marking ✅ PASSED, verify state file contains:** -- [ ] Result shows PASSED ✅ or FAILED ❌ +Verify the following before proceeding: +- [ ] Test result documented (PASSED ✅ or FAILED ❌) - [ ] Test behavior documented - [ ] Platform tested noted -- [ ] State file saved +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/gate/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") --- @@ -445,7 +293,6 @@ See `.github/skills/verify-tests-fail-without-fix/SKILL.md` for full skill docum - ❌ **Looking at implementation code during Pre-Flight** - Just gather issue/PR context - ❌ **Forming opinions on the fix during Pre-Flight** - That's Phase 3 - ❌ **Running tests during Pre-Flight** - That's Phase 2 (Gate) -- ❌ **Not creating state file first** - ALWAYS create state file before gathering context - ❌ **Skipping to Phase 3** - Gate MUST pass first ## Common Gate Mistakes diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index 11901d56dd91..b54375ebf7d6 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -12,7 +12,7 @@ ## 🚨 Critical Rules (Summary) See `SHARED-RULES.md` for complete details. Key points: -- **Environment Blockers**: STOP immediately, report, ask user (strict retry limits) +- **Environment Blockers**: Skip blocked phase and continue autonomously (CI mode has no human operator) - **No Git Commands**: Never checkout/switch branches - agent is always on correct branch - **Gate via Task Agent**: Never run inline (prevents fabrication) - **Multi-Model try-fix**: 5 models, SEQUENTIAL only @@ -23,7 +23,6 @@ See `SHARED-RULES.md` for complete details. Key points: ## Work Plan ### Phase 1: Pre-Flight -- [ ] Create state file: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - [ ] Gather PR metadata (title, body, labels, author) - [ ] Fetch and read linked issue - [ ] Fetch PR comments and review feedback @@ -31,8 +30,7 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] Document platforms affected - [ ] Classify changed files (fix vs test) - [ ] Document PR's fix approach in Fix Candidates table -- [ ] Update state file: Pre-Flight → ✅ COMPLETE -- [ ] Save state file +- [ ] **Write `PRAgent/pre-flight/content.md`** **Boundaries:** No code analysis, no fix opinions, no test running @@ -46,11 +44,10 @@ See `SHARED-RULES.md` for complete details. Key points: "Run verify-tests-fail-without-fix skill Platform: [X], TestFilter: 'IssueXXXXX', RequireFullVerification: true" ``` -- [ ] ⛔ If environment blocker: STOP, report, ask user +- [ ] ⛔ If environment blocker: retry once, then skip and document - [ ] Verify: Tests FAIL without fix, PASS with fix - [ ] If Gate fails: STOP, request test fixes -- [ ] Update state file: Gate → ✅ PASSED -- [ ] Save state file +- [ ] **Write `PRAgent/gate/content.md`** ### Phase 3: Fix 🔧 *(Only if Gate ✅ PASSED)* @@ -61,7 +58,7 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] gpt-5.2 - [ ] gpt-5.2-codex - [ ] gemini-3-pro-preview -- [ ] ⛔ If blocker: STOP, report, ask user +- [ ] ⛔ If blocker: retry once, skip remaining models, proceed to Report - [ ] Record: approach, result, files, failure analysis **Round 2+: Cross-Pollination (MANDATORY)** @@ -75,25 +72,23 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] Mark Exhausted: Yes - [ ] Compare passing candidates with PR's fix - [ ] Select best fix (results → simplicity → robustness) -- [ ] Update state file: Fix → ✅ COMPLETE -- [ ] Save state file +- [ ] **Write `PRAgent/try-fix/content.md`** ### Phase 4: Report 📋 *(Only if Phases 1-3 complete)* - [ ] Run `pr-finalize` skill - [ ] Generate review: root cause, candidates, recommendation -- [ ] Post AI Summary comment (PR phases + try-fix): +- [ ] **Write `PRAgent/report/content.md`** +- [ ] Post AI Summary comment (auto-loads from PRAgent/*/content.md): ```bash - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber XXXXX -SkipValidation + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber XXXXX pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber XXXXX ``` - [ ] Post PR Finalization comment (separate): ```bash - pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/pr-XXXXX.md + pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/XXXXX/pr-finalize/pr-finalize-summary.md ``` -- [ ] Update state file: Report → ✅ COMPLETE -- [ ] Save final state file --- @@ -101,11 +96,9 @@ See `SHARED-RULES.md` for complete details. Key points: | Phase | Key Action | Blocker Response | |-------|------------|------------------| -| Pre-Flight | Create state file | N/A | -| Gate | Task agent → verify script | ⛔ STOP, report, ask | -| Fix | Multi-model try-fix | ⛔ STOP, report, ask | -| Report | Post via skill | ⛔ STOP, report, ask | +| Pre-Flight | Gather context | N/A | +| Gate | Task agent → verify script | Skip, report incomplete | +| Fix | Multi-model try-fix | Skip remaining, proceed to Report | +| Report | Post via skill | Document what completed | -**State file:** `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - -**Never:** Mark BLOCKED and continue, claim success without tests, bypass scripts +**Never:** Claim success without tests, bypass scripts, stop and ask user in CI mode diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index 70662faccd9b..ba83ea9df92b 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -8,24 +8,114 @@ This file contains critical rules that apply across all PR agent phases. Referen **Before changing ANY phase status to ✅ COMPLETE:** -1. **Read the state file section** for the phase you're completing -2. **Find ALL ⏳ PENDING and [PENDING] fields** in that section -3. **Fill in every field** with actual content -4. **Verify no pending markers remain** in your section -5. **Save the state file** (it's in gitignored `CustomAgentLogsTmp/`) -6. **Then change status** to ✅ COMPLETE +1. **Review the phase checklist** for the phase you're completing +2. **Verify all required items** are addressed +3. **Write the phase output to `content.md`** (see Phase Output Artifacts below) +4. **Then mark the phase** as ✅ COMPLETE -**Rule:** Status ✅ means "documentation complete", not "I finished thinking about it" +**Rule:** Status ✅ means "work complete and verified", not "I finished thinking about it" --- -## Follow Templates EXACTLY +## Phase Output Artifacts -When creating state files, use the EXACT format from the documentation: -- **Do NOT add attributes** like `open` to `
` tags -- **Do NOT "improve"** the template format -- **Do NOT deviate** from documented structure -- Downstream scripts depend on exact formatting (regex patterns expect specific structure) +**After completing EACH phase, write a `content.md` file to the phase's output directory.** + +This is MANDATORY. The comment scripts (`post-ai-summary-comment.ps1`) read from these files to build the PR comment. + +### Output Directory Structure + +``` +CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/ +├── pre-flight/ +│ └── content.md # Written after Phase 1 +├── gate/ +│ ├── content.md # Written after Phase 2 +│ └── verify-tests-fail/ # Script output from verify-tests-fail.ps1 +├── try-fix/ +│ ├── content.md # Written after Phase 3 (summary of all attempts) +│ └── attempt-{N}/ # Individual attempt outputs from try-fix skill +└── report/ + └── content.md # Written after Phase 4 +``` + +### What Goes in Each content.md + +Each `content.md` should contain the **formatted phase content** — the same content that would appear inside the collapsible `
` section in the PR comment. + +**Pre-Flight example:** +```markdown +**Issue:** #XXXXX - [Title] +**Platforms Affected:** iOS, Android +**Files Changed:** 2 implementation files, 1 test file + +### Key Findings +- [Finding 1] +- [Finding 2] + +### Fix Candidates +| # | Source | Approach | Test Result | Files Changed | Notes | +|---|--------|----------|-------------|---------------|-------| +| PR | PR #XXXXX | [approach] | ⏳ PENDING (Gate) | `file.cs` | Original PR | +``` + +**Gate example:** +```markdown +**Result:** ✅ PASSED +**Platform:** android +**Mode:** Full Verification + +- Tests FAIL without fix ✅ +- Tests PASS with fix ✅ +``` + +**Fix (try-fix) example:** +```markdown +### Fix Candidates +| # | Source | Approach | Test Result | Files Changed | Notes | +|---|--------|----------|-------------|---------------|-------| +| 1 | try-fix | [approach] | ❌ FAIL | 1 file | Why: [reason] | +| 2 | try-fix | [approach] | ✅ PASS | 2 files | Works! | +| PR | PR #XXXXX | [approach] | ✅ PASS (Gate) | 2 files | Original PR | + +**Exhausted:** Yes +**Selected Fix:** PR's fix - [Reason] +``` + +**Report example:** +```markdown +## ✅ Final Recommendation: APPROVE + +### Summary +[Brief summary of the review] + +### Root Cause +[Root cause analysis] + +### Fix Quality +[Assessment of the fix] +``` + +### How to Write the File + +```bash +# Create the directory (idempotent) +mkdir -p "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight" + +# Write content (use create tool or bash) +cat > "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight/content.md" << 'EOF' +[phase content here] +EOF +``` + +### Timing + +| Phase | When to Write | +|-------|---------------| +| Pre-Flight | After all context gathered and documented | +| Gate | After verification result received from task agent | +| Fix (try-fix) | After all try-fix models explored and best fix selected | +| Report | After final recommendation determined | --- @@ -65,63 +155,58 @@ When a skill provides a PowerShell script: If you encounter an environment or system setup blocker that prevents completing a phase: -**STOP IMMEDIATELY. Do NOT continue to the next phase.** +### 🚨 Non-Interactive CI Mode (Default) -### Common Blockers +When running via `Review-PR.ps1` (non-interactive/CI mode), there is **NO human operator** to respond to questions. -- Missing Appium drivers (Windows, iOS, Android) -- WinAppDriver not installed or returning errors -- Xcode/iOS simulators not available (on Windows) -- Android emulator not running or not configured -- Developer Mode not enabled -- Port conflicts (e.g., 4723 in use) -- Missing SDKs or tools -- Server errors (500, timeout, "unknown error occurred") +**NEVER stop and ask the user. NEVER present options and wait for a choice. Nobody will respond.** -### Retry Limits (STRICT ENFORCEMENT) - -| Blocker Type | Max Retries | Then Do | -|--------------|-------------|---------| -| Missing tool/driver | 1 install attempt | STOP and ask user | -| Server errors (500, timeout) | 0 | STOP immediately and report | -| Port conflicts | 1 (kill process) | STOP and ask user | -| Configuration issues | 1 fix attempt | STOP and ask user | +Instead, use your best judgment to continue autonomously: -### When Blocked +1. **Try ONE retry** (install missing tool, kill conflicting process, etc.) +2. **If still blocked after one retry**, SKIP the blocked phase and continue to the next phase +3. **Document what was skipped and why** in your report +4. **Always prefer continuing with partial results** over stopping completely -1. **Stop all work** - Do not proceed to the next phase -2. **Do NOT keep troubleshooting** - After the retry limit, STOP -3. **Report the blocker** clearly (use template below) -4. **Ask the user** how to proceed -5. **Wait for user response** - Do not assume or work around +**Autonomous decision guide:** -### Blocker Report Template +| Blocker Type | Max Retries | Then Do | +|--------------|-------------|---------| +| Missing tool/driver | 1 install attempt | Skip phase, continue | +| Server errors (500, timeout) | 1 retry | Skip phase, continue | +| Port conflicts | 1 (kill process) | Skip phase, continue | +| Build failures in try-fix | 2 attempts | Skip remaining try-fix models, proceed to Report | +| Configuration issues | 1 fix attempt | Skip phase, continue | -``` -⛔ BLOCKED: Cannot complete [Phase Name] +**Common autonomous decisions:** +- Gate passes but Fix phase is blocked → **Skip Fix, proceed to Report** with Gate results only +- try-fix builds fail for multiple models → **Stop try-fix exploration, proceed to Report** +- A specific platform fails → **Try alternative platform ONCE**, then skip if still blocked +- Gate fails due to environment → **Report as incomplete**, proceed to Report -**What failed:** [Step/skill that failed] -**Blocker:** [Tool/driver/error type] -**Error:** "[Exact error message]" +### Interactive Mode -**What I tried:** [List retry attempts, max 1-2] +When running with `-Interactive` flag, you MAY ask the user for guidance on blockers. -**I am STOPPING here. Options:** -1. [Option for user - e.g., investigate setup manually] -2. [Alternative platform] -3. [Skip with documented limitation] +### Common Blockers -Which would you like me to do? -``` +- Missing Appium drivers (Windows, iOS, Android) +- WinAppDriver not installed or returning errors +- Xcode/iOS simulators not available (on Windows) +- Android emulator not running or not configured +- Developer Mode not enabled +- Port conflicts (e.g., 4723 in use) +- Missing SDKs or tools +- Server errors (500, timeout, "unknown error occurred") ### Never Do - ❌ Keep trying different fixes after retry limit exceeded -- ❌ Mark a phase as ⚠️ BLOCKED and continue to the next phase - ❌ Claim "verification passed" when tests couldn't actually run -- ❌ Skip device/emulator testing and proceed with code review only - ❌ Install multiple tools/drivers without asking between each - ❌ Spend more than 2-3 tool calls troubleshooting the same blocker +- ❌ **Stop and present options to the user in CI mode** - choose the best option yourself +- ❌ **Wait for user response in CI mode** - nobody will respond --- @@ -148,7 +233,6 @@ Phase 4 uses these 5 AI models for try-fix exploration (run SEQUENTIALLY): **Choose a platform that is BOTH affected by the bug AND available on the current host.** ### Step 1: Identify affected platforms from Pre-Flight -- Check the "Platforms Affected" checkboxes in the state file - Check issue labels (e.g., `platform/iOS`, `platform/Android`) - Check which platform-specific files the PR modifies diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index f030801ee257..611d26428dde 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -1,6 +1,6 @@ # PR Agent: Post-Gate Phases (3-4) -**⚠️ PREREQUISITE: Only read this file after 🚦 Gate shows `✅ PASSED` in your state file.** +**⚠️ PREREQUISITE: Only read this file after 🚦 Gate shows `✅ PASSED`.** If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 first. @@ -19,20 +19,27 @@ If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 **All rules from `.github/agents/pr/SHARED-RULES.md` apply here**, including: - Phase Completion Protocol (fill ALL pending fields before marking complete) -- Stop on Environment Blockers (STOP and ask user, don't continue) +- Stop on Environment Blockers (retry once, then skip and continue autonomously) - Multi-Model Configuration (5 models, SEQUENTIAL only) -If try-fix cannot run due to environment issues, **STOP and ask the user**. Do NOT mark attempts as "BLOCKED" and continue. +If try-fix cannot run due to environment issues after one retry, **skip the remaining try-fix models and proceed to Report**. Do NOT stop and ask the user. -### 🚨 CRITICAL: Stop on Environment Blockers (Applies to Phase 4) +### 🚨 CRITICAL: Environment Blockers in Phase 3 (CI Mode) -The same "Stop on Environment Blockers" rule from `pr.md` applies here. If try-fix cannot run due to: +The default mode is **non-interactive CI** where no human can respond to questions. + +If try-fix cannot run due to: - Missing Appium drivers - Device/emulator not available - WinAppDriver not installed - Platform tools missing +- Build failures unrelated to the fix -**STOP and ask the user** before continuing. Do NOT mark try-fix attempts as "BLOCKED" and continue. Either fix the environment issue or get explicit user permission to skip. +**Use your best judgment to continue autonomously:** +1. Try ONE alternative (e.g., different platform, rebuild) +2. If still blocked, **skip remaining try-fix models and proceed to Report** +3. Document what was skipped and why in the Report phase +4. The PR's fix was already validated by Gate - that's sufficient for a recommendation --- @@ -40,7 +47,7 @@ The same "Stop on Environment Blockers" rule from `pr.md` applies here. If try-f > **SCOPE**: Explore independent fix alternatives using `try-fix` skill, compare with PR's fix, select the best approach. -**⚠️ Gate Check:** Verify 🚦 Gate is `✅ PASSED` in your state file before proceeding. +**⚠️ Gate Check:** Verify 🚦 Gate is `✅ PASSED` before proceeding. ### 🚨 CRITICAL: try-fix is Independent of PR's Fix @@ -73,7 +80,6 @@ Invoke the try-fix skill for PR #XXXXX: - target_files: - src/[area]/[likely-affected-file-1].cs - src/[area]/[likely-affected-file-2].cs -- state_file: CustomAgentLogsTmp/PRState/pr-XXXXX.md Generate ONE independent fix idea. Review the PR's fix first to ensure your approach is DIFFERENT. ``` @@ -118,7 +124,7 @@ After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts Do you have any NEW fix ideas? Reply: 'NEW IDEA: [desc]' or 'NO NEW IDEAS'" ``` -3. **Record each model's response** in state file Cross-Pollination table +3. **Record each model's response** in Cross-Pollination table 4. **For each new idea**: Run try-fix with that model (SEQUENTIAL, wait for completion) @@ -127,13 +133,12 @@ After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts #### try-fix Behavior Each `try-fix` invocation (run via task agent with specific model): -1. Reads state file to learn from prior failed attempts +1. Learns from prior failed attempts 2. Reverts PR's fix to get a broken baseline 3. Proposes ONE new independent fix idea 4. Implements and tests it 5. Records result (with failure analysis if it failed) -6. **Updates state file** (appends row to Fix Candidates table) -7. Reverts all changes (restores PR's fix) +6. Reverts all changes (restores PR's fix) See `.github/skills/try-fix/SKILL.md` for full details. @@ -162,7 +167,7 @@ After the loop, review the **Fix Candidates** table: 3. **Most robust** - Handles edge cases, less likely to regress 4. **Matches codebase style** - Consistent with existing patterns -Update the state file: +Update the selected fix: ```markdown **Exhausted:** Yes (or No if stopped early) @@ -187,36 +192,17 @@ Update the state file: ### Complete 🔧 Fix -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Verify Fix Candidates table is complete with all attempts -2. Verify failure analyses are documented for failed attempts -3. Verify Selected Fix is documented with reasoning -4. Change 🔧 Fix status to `✅ COMPLETE` -5. Change 📋 Report status to `▶️ IN PROGRESS` - -**Before marking ✅ COMPLETE, verify state file contains:** +Verify the following before proceeding: - [ ] Round 1 completed: All 5 models ran try-fix -- [ ] **Cross-pollination table exists** with responses from ALL 5 models: - ``` - | Model | Round 2 Response | - |-------|------------------| - | claude-sonnet-4.5 | NO NEW IDEAS | - | claude-opus-4.6 | NO NEW IDEAS | - | gpt-5.2 | NO NEW IDEAS | - | gpt-5.2-codex | NO NEW IDEAS | - | gemini-3-pro-preview | NO NEW IDEAS | - ``` +- [ ] Cross-pollination completed with responses from ALL 5 models - [ ] Fix Candidates table has numbered rows for each try-fix attempt - [ ] Each row has: approach, test result, files changed, notes - [ ] "Exhausted" field set to Yes (all models confirmed no new ideas) - [ ] "Selected Fix" populated with reasoning -- [ ] Root cause analysis documented for the selected fix (to be surfaced in 📋 Report phase "### Root Cause" section) -- [ ] No ⏳ PENDING markers remain in Fix section -- [ ] State file saved +- [ ] Root cause analysis documented for the selected fix +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") -**🚨 If cross-pollination table is missing, you skipped Round 2. Go back and invoke each model.** +**🚨 If cross-pollination is missing, you skipped Round 2. Go back and invoke each model.** --- @@ -224,7 +210,7 @@ Update the state file: > **SCOPE**: Deliver the final result - either a PR review or a new PR. -**⚠️ Gate Check:** Verify ALL phases 1-3 are `✅ COMPLETE` or `✅ PASSED` before proceeding. +**⚠️ Gate Check:** Verify ALL phases 1-3 are complete before proceeding. ### Finalize Title and Description @@ -256,8 +242,6 @@ If reviewing an existing PR, check if title/description need updates and include **Do NOT run git commands. User handles commit/push/PR creation.** -2. **Update state file** with PR link once user provides it - ### If Starting from PR - Write Review Determine your recommendation based on the Fix phase: @@ -279,35 +263,14 @@ Determine your recommendation based on the Fix phase: - Run the `pr-finalize` skill to verify title and description match implementation - If discrepancies found, include suggested updates in review comments -### Final State File Format - -Update the state file header: - -```markdown -## ✅ Final Recommendation: APPROVE -``` -or -```markdown -## ⚠️ Final Recommendation: REQUEST CHANGES -``` - -Update all phase statuses to complete. - ### Complete 📋 Report -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Change header status to final recommendation -2. Update all phases to `✅ COMPLETE` or `✅ PASSED` -3. Present final result to user - -**Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Final recommendation (APPROVE/REQUEST_CHANGES/COMMENT) -- [ ] Summary of findings +Verify the following before finishing: +- [ ] Final recommendation determined (APPROVE/REQUEST_CHANGES/COMMENT) +- [ ] Summary of findings prepared - [ ] Key technical insights documented -- [ ] Overall status changed to final recommendation -- [ ] State file saved +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/report/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") +- [ ] Result presented to user --- @@ -319,7 +282,7 @@ Update all phase statuses to complete. - ❌ **Running try-fix in parallel** - SEQUENTIAL ONLY - they modify same files and use same device - ❌ **Using explore/glob instead of invoking models** - Cross-pollination requires ACTUAL task agent invocations with each model, not code searches - ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 5 models explicitly say "NO NEW IDEAS" -- ❌ **Not recording cross-pollination responses** - State file must have table showing each model's Round 2 response +- ❌ **Not recording cross-pollination responses** - Must track each model's Round 2 response - ❌ **Not analyzing why fixes failed** - Record the flawed reasoning to help future attempts - ❌ **Selecting a failing fix** - Only select from passing candidates - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d7d6b5614fb3..5c55996cadb2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -241,7 +241,7 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Purpose**: Verifies PR title and description match actual implementation, AND performs code review for best practices before merge. - **Trigger phrases**: "finalize PR #XXXXX", "check PR description for #XXXXX", "review commit message" - **Used by**: Before merging any PR, when description may be stale - - **Note**: Does NOT require agent involvement or session markdown - works on any PR + - **Note**: Works on any PR - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` - only post comments. Approval is a human decision. 4. **learn-from-pr** (`.github/skills/learn-from-pr/SKILL.md`) @@ -283,7 +283,37 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Purpose**: Proposes ONE independent fix approach, applies it, tests, records result with failure analysis, then reverts - **Used by**: pr agent Phase 3 (Fix phase) - rarely invoked directly by users - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session. - - **Output**: Updates session markdown with attempt results and failure analysis + - **Output**: Reports attempt results and failure analysis + +### Agent Workflow Labels + +Labels with `s/agent-*` prefix track agent workflow outcomes for metrics. Applied by `Review-PR.ps1` Phase 4. + +**Outcome Labels** (mutually exclusive — one per PR): + +| Label | Description | +|-------|-------------| +| `s/agent-approved` | AI agent recommends approval | +| `s/agent-changes-requested` | AI agent recommends changes | +| `s/agent-review-incomplete` | AI agent could not complete all phases | + +**Signal Labels** (additive): + +| Label | Description | +|-------|-------------| +| `s/agent-gate-passed` | AI verified tests catch the bug | +| `s/agent-gate-failed` | AI could not verify tests catch the bug | +| `s/agent-fix-optimal` | AI confirms PR fix is the best among candidates | + +**Manual Labels** (applied by maintainers): + +| Label | Description | +|-------|-------------| +| `s/agent-fix-implemented` | PR author implemented the agent's suggested fix | + +**Base Label**: `s/agent-reviewed` — always applied on completed agent runs. + +**Helper module**: `.github/scripts/helpers/Update-AgentLabels.ps1` ### Agent Workflow Labels diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 20ea09ee4f4d..1f870baea84f 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -236,6 +236,17 @@ if (-not (Test-Path $stateDir)) { Write-Host " 📁 Created state directory: $stateDir" -ForegroundColor Gray } +# Create PRAgent phase directories for structured output +$PRAgentDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" +$phaseSubdirs = @("pre-flight", "gate", "try-fix", "report") +foreach ($subdir in $phaseSubdirs) { + $dirPath = Join-Path $PRAgentDir $subdir + if (-not (Test-Path $dirPath)) { + New-Item -ItemType Directory -Path $dirPath -Force | Out-Null + } +} +Write-Host " 📁 Created PRAgent phase directories: $PRAgentDir" -ForegroundColor Gray + # Step 4: Build the prompt for Copilot CLI $planTemplatePath = ".github/agents/pr/PLAN-TEMPLATE.md" @@ -255,19 +266,37 @@ $platformInstruction - NEVER run ``git checkout``, ``git switch``, ``git fetch``, ``git stash``, or ``git reset`` - NEVER run ``git push`` - you do NOT have permission to push anything - You are ALWAYS on the correct branch already - the script handles this -- If the state file says "wrong branch", that's stale state - delete it and start fresh - If you think you need to switch branches or push changes, you are WRONG - ask the user instead +🚨 **CRITICAL - NON-INTERACTIVE CI MODE:** +- This is running in a CI environment with NO human operator to respond to questions +- NEVER stop and ask the user for input - nobody will respond +- NEVER present a list of options and wait for a choice +- When you encounter an environment blocker, use your best judgment to choose the best path forward: + - If a phase is blocked (e.g., try-fix builds fail), SKIP that phase and proceed to the next one + - If Gate passes but Fix phase is blocked, proceed directly to Report phase with Gate results only + - If a specific platform fails, try an alternative platform ONCE, then skip if still blocked + - Always prefer CONTINUING with partial results over STOPPING completely +- Document what was skipped and why in your report, but keep moving forward + +📁 **PHASE OUTPUT ARTIFACTS (MANDATORY):** +- After completing EACH phase, write a ``content.md`` file to the phase output directory +- Pre-Flight: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight/content.md`` +- Gate: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/content.md`` +- Fix: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/try-fix/content.md`` +- Report: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/report/content.md`` +- These files are read by the comment scripts to build the PR comment +- See SHARED-RULES.md "Phase Output Artifacts" section for content format + **Instructions:** 1. Read the plan template at ``$planTemplatePath`` for the 4-phase workflow 2. Read ``.github/agents/pr.md`` for Phases 1-2 instructions 3. Follow ALL critical rules, especially: - - STOP on environment blockers and ask before continuing + - On environment blockers: skip the blocked phase and continue autonomously (see NON-INTERACTIVE CI MODE above) - Use task agent for Gate verification - Run multi-model try-fix in Phase 3 **Start with Phase 1: Pre-Flight** -- Create state file: CustomAgentLogsTmp/PRState/pr-$PRNumber.md - Gather context from PR #$PRNumber - Proceed through all 4 phases @@ -298,7 +327,6 @@ if ($DryRun) { Write-Host "PR Review Context:" -ForegroundColor Cyan Write-Host " PR_NUMBER: $PRNumber" -ForegroundColor White Write-Host " PLATFORM: $(if ($Platform) { $Platform } else { '(agent will determine)' })" -ForegroundColor White - Write-Host " STATE_FILE: CustomAgentLogsTmp/PRState/pr-$PRNumber.md" -ForegroundColor White Write-Host " PLAN_TEMPLATE: $planTemplatePath" -ForegroundColor White Write-Host " CURRENT_BRANCH: $(git branch --show-current)" -ForegroundColor White Write-Host " PR_TITLE: $($prInfo.title)" -ForegroundColor White @@ -370,7 +398,7 @@ if ($DryRun) { # Restore tracked files to clean state before running post-completion skills. # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, # which can cause skill files to be missing or modified in subsequent phases. - # NOTE: State files in CustomAgentLogsTmp/ are .gitignore'd and untracked, + # NOTE: CustomAgentLogsTmp/ is .gitignore'd and untracked, # so this won't touch them. Using HEAD to also restore deleted files. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow @@ -387,12 +415,12 @@ if ($DryRun) { Write-Host "" # Ensure output directory exists for finalize results - $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize" + $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize" if (-not (Test-Path $finalizeDir)) { New-Item -ItemType Directory -Path $finalizeDir -Force | Out-Null } - $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten). If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/code-review.md." + $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/pr-finalize-summary.md. If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/code-review.md." $finalizeArgs = @( "-p", $finalizePrompt, @@ -428,7 +456,7 @@ if ($DryRun) { git checkout HEAD -- . 2>&1 | Out-Null Write-Host " ✅ Working tree restored" -ForegroundColor Green - # 3a: Post PR agent summary comment (from Phase 1 state file) + # 3a: Post PR agent summary comment $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" if (-not (Test-Path $scriptPath)) { Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow @@ -482,8 +510,7 @@ if ($DryRun) { } if (Test-Path $labelHelperPath) { . $labelHelperPath - $stateFilePath = "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" - Invoke-AgentLabels -StateFile $stateFilePath -PRNumber $PRNumber + Invoke-AgentLabels -PRNumber $PRNumber } else { Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow } @@ -491,7 +518,6 @@ if ($DryRun) { } Write-Host "" -Write-Host "📝 State file: CustomAgentLogsTmp/PRState/pr-$PRNumber.md" -ForegroundColor Gray Write-Host "📋 Plan template: $planTemplatePath" -ForegroundColor Gray if (-not $DryRun) { Write-Host "📁 Copilot logs: CustomAgentLogsTmp/PRState/$PRNumber/copilot-logs/" -ForegroundColor Gray diff --git a/.github/skills/ai-summary-comment/IMPROVEMENTS.md b/.github/skills/ai-summary-comment/IMPROVEMENTS.md index 2de8dc008fe4..4c50133edaf9 100644 --- a/.github/skills/ai-summary-comment/IMPROVEMENTS.md +++ b/.github/skills/ai-summary-comment/IMPROVEMENTS.md @@ -10,7 +10,7 @@ The `post-ai-summary-comment.ps1` script has been significantly improved to make **Before:** Script used hardcoded pattern matching with predefined title variations -**After:** Script **automatically discovers ALL sections** from your state file and extracts them dynamically +**After:** Script **automatically discovers ALL sections** from the provided content and extracts them dynamically ```powershell # Extracts ALL
TITLE sections @@ -25,13 +25,13 @@ $preFlightContent = Get-SectionByPattern -Sections $allSections -Patterns @( **Benefits:** - ✅ **No hardcoded titles** - works with ANY section header you use -- ✅ **Automatically adapts** - add new sections without modifying the script -- ✅ **Better debugging** - shows exactly which sections were found -- ✅ **More maintainable** - less code, more flexible +- ✅ Automatically adapts - add new sections without modifying the script +- ✅ Better debugging - shows exactly which sections were found in the content +- ✅ More maintainable - less code, more flexible **Example debug output:** ``` -[DEBUG] Found 6 section(s) in state file +[DEBUG] Found 6 section(s) in content [DEBUG] Section: '📋 Issue Summary' (803 chars) [DEBUG] Section: '🚦 Gate - Test Verification' (488 chars) [DEBUG] Section: '🔧 Fix Candidates' (868 chars) @@ -91,7 +91,7 @@ $DebugPreference = 'Continue' ``` **Shows:** -- Which sections were found in the state file +- Which sections were found in the content - How many characters each section contains - Which patterns matched which sections - Why validation passed or failed @@ -104,7 +104,7 @@ $DebugPreference = 'Continue' ``` ⛔ VALIDATION FAILED -💡 Fix these issues in the state file before posting. +💡 Fix these issues in the content before posting. Or use -SkipValidation to bypass these checks. 🐛 Debug tip: Run with $DebugPreference = 'Continue' for details @@ -126,7 +126,7 @@ function Extract-AllSections { } ``` -**Result:** Hashtable with ALL sections from your state file +**Result:** Hashtable with ALL sections from your content ### Step 2: Map to Phases @@ -208,12 +208,12 @@ Any title matching `'📋.*Report'` or `'Final Report'`: **No changes needed!** The script is backward compatible. -**Old state files** with exact headers like: +**Old content** with exact headers like: ```markdown 📋 Phase 4: Report — Final Recommendation ``` -**New state files** with simpler headers like: +**New content** with simpler headers like: ```markdown 📋 Final Report ``` @@ -236,7 +236,7 @@ Any title matching `'📋.*Report'` or `'Final Report'`: ## Common Issues & Solutions -### Issue: "Phase X has NO content in state file" +### Issue: "Phase X has NO content" **Step 1:** Enable debug mode to see what was found ```powershell @@ -245,7 +245,7 @@ pwsh -Command '$DebugPreference = "Continue"; ./post-ai-summary-comment.ps1 -PRN **Look for:** ``` -[DEBUG] Found 7 section(s) in state file +[DEBUG] Found 7 section(s) in content [DEBUG] Section: 'Your Section Title' (XXX chars) ``` @@ -263,7 +263,7 @@ If your title is `"📋 Final Analysis"`, it won't match! ### Issue: Section extracted but content is empty -**Cause:** State file structure issue (missing content between tags) +**Cause:** Content structure issue (missing content between tags) **Check your markdown:** ```markdown @@ -336,7 +336,7 @@ Tested with: **Debug output example:** ``` -[DEBUG] Found 6 section(s) in state file +[DEBUG] Found 6 section(s) in content [DEBUG] Section: '📋 Issue Summary' (803 chars) [DEBUG] Section: '📁 Files Changed' (0 chars) [DEBUG] Section: '💬 PR Discussion Summary' (0 chars) @@ -425,16 +425,16 @@ PR #27340 provides a **correct and well-tested fix**... ``` ⛔ VALIDATION FAILED Found 1 validation error(s): - - Report: Phase Report is marked as '✅ COMPLETE' but has NO content in state file + - Report: Phase Report is marked as '✅ COMPLETE' but has NO content ``` **After:** ``` ⛔ VALIDATION FAILED Found 1 validation error(s): - - Report: Phase Report is marked as '✅ COMPLETE' but has NO content in state file + - Report: Phase Report is marked as '✅ COMPLETE' but has NO content -💡 Fix these issues in the state file before posting the review comment. +💡 Fix these issues in the content before posting the review comment. Or use -SkipValidation to bypass these checks (not recommended). 🐛 Debug tip: Run with $DebugPreference = 'Continue' for detailed extraction info @@ -517,7 +517,7 @@ Any of these variations will be recognized: ## Migration Guide -**No changes needed!** The script is backward compatible. If you have existing state files with the old header format, they'll continue to work. +**No changes needed!** The script is backward compatible. If you have existing content with the old header format, it will continue to work. If you want to use the new flexibility: - Just use simpler headers like `📋 Final Report` instead of `📋 Phase 4: Report — Final Recommendation` @@ -527,11 +527,11 @@ If you want to use the new flexibility: ## Common Issues & Solutions -### Issue: "Phase Report has NO content in state file" +### Issue: "Phase Report has NO content" -**Solution 1:** Check your state file structure +**Solution 1:** Check your content structure ```bash -grep -A 5 "📋.*Report" CustomAgentLogsTmp/PRState/pr-XXXXX.md +grep -A 5 "📋.*Report" your-content-file.md ``` Make sure you have: @@ -612,7 +612,7 @@ $reportContent = Extract-PhaseContent -StateContent $Content -PhaseTitles @( ## Future Improvements Potential enhancements: -- [ ] Auto-detect phase titles from state file (no hardcoded patterns) +- [ ] Auto-detect phase titles from content (no hardcoded patterns) - [ ] Support markdown headings (`##` / `###`) in addition to `
` - [ ] Validate links and references work - [ ] Check that commit SHAs are valid @@ -624,7 +624,7 @@ Potential enhancements: The improvements have been tested with: - ✅ PR #27340 (Entry/Editor keyboard issue) -- ✅ State files with various header formats +- ✅ Content with various header formats - ✅ Dry run mode - ✅ Debug mode - ✅ Skip validation mode @@ -636,5 +636,5 @@ The improvements have been tested with: If you encounter issues or have suggestions, please: 1. Try debug mode first: `$DebugPreference = 'Continue'` -2. Check the state file structure +2. Check the content structure 3. Report the issue with debug output included diff --git a/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md b/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md index 84545c9fa546..70223059df16 100644 --- a/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md +++ b/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md @@ -87,17 +87,12 @@ Add an **Implementation** subsection: ### pr-finalize Skill -When running `pr-finalize` skill, you create TWO outputs: +When running `pr-finalize` skill, you create output: 1. **Summary file** (local reference) - Location: `CustomAgentLogsTmp/PRState/XXXXX/pr-finalize/pr-finalize-summary.md` - Purpose: Your detailed analysis and working notes - Audience: You and local CLI users - -2. **State file Report section** (GitHub audience) - - Location: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` (Report phase) - - Purpose: Final recommendations that get posted to GitHub - - Audience: PR authors, reviewers, community - **MUST be self-contained** - no external references ### PR Agent Phase 4 (Report) @@ -184,9 +179,8 @@ When completing Phase 4, verify: | What | Where | Audience | Self-Contained? | |------|-------|----------|-----------------| | Summary file | `CustomAgentLogsTmp/.../summary.md` | Local CLI | N/A (local only) | -| State file | `CustomAgentLogsTmp/PRState/pr-XXXXX.md` | GitHub users | ✅ YES - REQUIRED | | PR comment | GitHub PR page | Public | ✅ YES - REQUIRED | -**Remember:** Anything that goes in the state file's `
` sections will be posted to GitHub. Make it self-contained! +**Remember:** Anything posted to GitHub must be self-contained. Never reference local files. -- diff --git a/.github/skills/ai-summary-comment/SKILL.md b/.github/skills/ai-summary-comment/SKILL.md index 9073d3bef37c..0802563f2e51 100644 --- a/.github/skills/ai-summary-comment/SKILL.md +++ b/.github/skills/ai-summary-comment/SKILL.md @@ -18,7 +18,6 @@ This skill posts automated progress comments to GitHub Pull Requests during the - **Section-Based Updates**: Each script updates only its section, preserving others - **Duplicate Prevention**: Finds existing `` comment and updates it - **File-Based DryRun Preview**: Use `-DryRun` to preview changes in a local file before posting -- **Auto-Loading State Files**: Automatically finds and loads state files from `CustomAgentLogsTmp/PRState/` - **Simple Interface**: Just provide PR number - script handles everything else ## Comment Architecture @@ -97,42 +96,29 @@ If an existing finalize comment exists, it will be replaced with the updated sec ## Usage -### Simplest: Just provide PR number +### Simplest: Just provide PR number (auto-loads from phase files) ```bash -# Auto-loads CustomAgentLogsTmp/PRState/pr-27246.md +# Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/*/content.md pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27246 ``` -### With explicit state file path +### Provide content directly ```bash -# PRNumber auto-extracted from filename (pr-27246.md → 27246) -pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -StateFile CustomAgentLogsTmp/PRState/pr-27246.md -``` - -### Legacy: Provide content directly - -```bash -pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 12345 -Content "$(cat CustomAgentLogsTmp/PRState/pr-12345.md)" +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content here" ``` ### Parameters | Parameter | Required | Description | Example | |-----------|----------|-------------|---------| -| `PRNumber` | No* | Pull request number | `12345` | -| `StateFile` | No* | Path to state file (PRNumber auto-extracted from `pr-XXXXX.md` naming) | `CustomAgentLogsTmp/PRState/pr-27246.md` | -| `Content` | No* | Full state file content (legacy, can be piped via stdin) | Content from state file | +| `PRNumber` | Yes | Pull request number | `12345` | +| `Content` | No | Review content to post (auto-loaded from `PRAgent/*/content.md` if not provided) | Review markdown content | | `DryRun` | No | Preview changes in local file instead of posting to GitHub | `-DryRun` | | `PreviewFile` | No | Path to local preview file for DryRun mode (default: `CustomAgentLogsTmp/PRState/{PRNumber}/ai-summary-comment-preview.md`) | `-PreviewFile ./preview.md` | | `SkipValidation` | No | Skip validation checks (not recommended) | `-SkipValidation` | -*At least one of PRNumber, StateFile, or Content is required. The script will: -- If `-PRNumber` provided: Auto-load `CustomAgentLogsTmp/PRState/pr-{PRNumber}.md` -- If `-StateFile` provided: Load the file and extract PRNumber from `pr-XXXXX.md` filename -- If `-Content` provided: Use content directly (legacy, requires `-PRNumber`) - ## DryRun Preview Workflow Use `-DryRun` to preview the combined comment before posting to GitHub. Each script updates the same preview file, mirroring how the actual GitHub comment is updated. @@ -233,17 +219,17 @@ The `post-try-fix-comment.ps1` script updates the `` sec ```powershell # All parameters auto-loaded from directory structure pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` - -TryFixDir CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1 + -TryFixDir CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1 ``` #### Or just provide issue number ```powershell -# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/try-fix/ +# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/ pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber 27246 ``` -#### Legacy: Manual parameters +#### Manual parameters ```powershell pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` @@ -277,7 +263,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` ### Expected Directory Structure ``` -CustomAgentLogsTmp/PRState/{IssueNumber}/try-fix/ +CustomAgentLogsTmp/PRState/{IssueNumber}/PRAgent/try-fix/ ├── attempt-1/ │ ├── approach.md # Brief description of the approach (required) │ ├── result.txt # "Pass", "Fail", or "Compiles" (required) @@ -344,7 +330,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 -PR ```powershell pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 ` -PRNumber 32891 ` - -ReportFile CustomAgentLogsTmp/PRState/32891/verify-tests-fail/verification-report.md + -ReportFile CustomAgentLogsTmp/PRState/32891/PRAgent/gate/verify-tests-fail/verification-report.md ``` ### Parameters @@ -362,7 +348,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 ` ### Expected Directory Structure ``` -CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/ +CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/gate/verify-tests-fail/ ├── verification-report.md # Full verification report (required) ├── verification-log.txt # Detailed log (optional) ├── test-without-fix.log # Test output without fix (optional) @@ -394,7 +380,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 -IssueNumber 27246 ``` -#### Legacy: Manual parameters +#### Manual parameters ```powershell pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` @@ -539,15 +525,52 @@ pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 ` 4. **Auto-parsing from summary file getting confused** - When in doubt, provide explicit parameters instead of relying on auto-parsing -### Expected Directory Structure for Auto-Loading +### Expected Directory Structure + +The PR agent writes phase output files that comment scripts auto-load: ``` -CustomAgentLogsTmp/PRState/{PRNumber}/pr-finalize/ -├── pr-finalize-summary.md # Main summary (auto-parsed) -├── recommended-description.md # Full recommended description (optional) -└── code-review.md # Code review findings (optional) +CustomAgentLogsTmp/PRState/{PRNumber}/ +├── PRAgent/ +│ ├── pre-flight/ +│ │ └── content.md # Phase 1 output (auto-loaded by post-ai-summary-comment.ps1) +│ ├── gate/ +│ │ ├── content.md # Phase 2 output (auto-loaded by post-ai-summary-comment.ps1) +│ │ └── verify-tests-fail/ # Script output from verify-tests-fail.ps1 +│ │ ├── verification-report.md +│ │ ├── verification-log.txt +│ │ ├── test-without-fix.log +│ │ └── test-with-fix.log +│ ├── try-fix/ +│ │ ├── content.md # Phase 3 summary (auto-loaded by post-ai-summary-comment.ps1) +│ │ ├── attempt-1/ # Individual attempt outputs +│ │ │ ├── approach.md +│ │ │ ├── result.txt +│ │ │ ├── fix.diff +│ │ │ └── analysis.md +│ │ └── attempt-2/ +│ │ └── ... +│ └── report/ +│ └── content.md # Phase 4 output (auto-loaded by post-ai-summary-comment.ps1) +├── pr-finalize/ +│ ├── pr-finalize-summary.md +│ ├── recommended-description.md +│ └── code-review.md +└── copilot-logs/ + ├── process-*.log + └── session-*.md ``` +### Auto-Loading Behavior + +When `post-ai-summary-comment.ps1` is called **without `-Content`**, it auto-discovers phase files: +1. Checks `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/*/content.md` +2. Loads all available phase content files +3. Builds the comment structure from the loaded files +4. Posts/updates the unified AI Summary comment + +This eliminates the need to pass large content strings as parameters. + --- ## Technical Details diff --git a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 index 6617bdcd32fd..c6dde8348363 100644 --- a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -7,8 +7,7 @@ Creates ONE comment for the entire PR review with all phases wrapped in an expandable section. Uses HTML marker for identification. - **NEW: Validates that phases marked as COMPLETE actually have content.** - **NEW: Auto-loads state file from CustomAgentLogsTmp/PRState/pr-XXXXX.md** + **Validates that phases marked as COMPLETE actually have content.** Format: ## 🤖 AI Summary — ✅ APPROVE @@ -17,14 +16,10 @@
.PARAMETER PRNumber - The pull request number (required unless -StateFile is provided with pr-XXXXX.md naming) - -.PARAMETER StateFile - Path to state file (defaults to CustomAgentLogsTmp/PRState/pr-{PRNumber}.md) - If provided with pr-XXXXX.md naming, PRNumber is auto-extracted + The pull request number (required) .PARAMETER Content - The full state file content (alternative to -StateFile) + The review content to post .PARAMETER DryRun Print comment instead of posting @@ -33,25 +28,16 @@ Skip validation checks (not recommended) .EXAMPLE - # Simplest: just provide PR number, state file auto-loaded - ./post-ai-summary-comment.ps1 -PRNumber 12345 - -.EXAMPLE - # Provide state file directly (PR number auto-extracted from filename) - ./post-ai-summary-comment.ps1 -StateFile CustomAgentLogsTmp/PRState/pr-27246.md + ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content" .EXAMPLE - # Legacy: provide content directly - ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "$(cat CustomAgentLogsTmp/PRState/pr-12345.md)" + ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content" -DryRun #> param( [Parameter(Mandatory=$false)] [int]$PRNumber, - [Parameter(Mandatory=$false)] - [string]$StateFile, - [Parameter(Mandatory=$false)] [string]$Content, @@ -68,75 +54,144 @@ param( $ErrorActionPreference = "Stop" # ============================================================================ -# STATE FILE RESOLUTION +# INPUT VALIDATION # ============================================================================ -# Priority: 1) -Content, 2) -StateFile, 3) Auto-detect from PRNumber - -# If StateFile provided, extract PRNumber from filename if not already set -if (-not [string]::IsNullOrWhiteSpace($StateFile)) { - if ($StateFile -match 'pr-(\d+)\.md$') { - $extractedPR = [int]$Matches[1] - if ($PRNumber -eq 0) { - $PRNumber = $extractedPR - Write-Host "ℹ️ Auto-detected PRNumber: $PRNumber from state file name" -ForegroundColor Cyan - } elseif ($PRNumber -ne $extractedPR) { - Write-Host "⚠️ Warning: PRNumber ($PRNumber) differs from state file name (pr-$extractedPR.md)" -ForegroundColor Yellow - } - } - - if (Test-Path $StateFile) { - $Content = Get-Content $StateFile -Raw -Encoding UTF8 - Write-Host "ℹ️ Loaded state file: $StateFile" -ForegroundColor Cyan - } else { - throw "State file not found: $StateFile" - } +if ($PRNumber -eq 0) { + throw "PRNumber is required." } -# If no Content and no StateFile, try auto-detect from PRNumber -if ([string]::IsNullOrWhiteSpace($Content) -and $PRNumber -gt 0) { - $autoStateFile = "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" - if (Test-Path $autoStateFile) { - $Content = Get-Content $autoStateFile -Raw -Encoding UTF8 - Write-Host "ℹ️ Auto-loaded state file: $autoStateFile" -ForegroundColor Cyan - } else { - # Try relative to repo root +# Auto-load from PRAgent phase files if Content not provided +if ([string]::IsNullOrWhiteSpace($Content)) { + Write-Host "ℹ️ No -Content provided, auto-loading from PRAgent phase files..." -ForegroundColor Cyan + + $PRAgentDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" + if (-not (Test-Path $PRAgentDir)) { $repoRoot = git rev-parse --show-toplevel 2>$null if ($repoRoot) { - $autoStateFile = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" - if (Test-Path $autoStateFile) { - $Content = Get-Content $autoStateFile -Raw -Encoding UTF8 - Write-Host "ℹ️ Auto-loaded state file: $autoStateFile" -ForegroundColor Cyan - } + $PRAgentDir = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" } } -} + + if (-not (Test-Path $PRAgentDir)) { + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red + Write-Host "║ ⛔ No content provided and no PRAgent directory found ║" -ForegroundColor Red + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red + Write-Host "" + Write-Host "Expected directory: $PRAgentDir" -ForegroundColor Yellow + Write-Host "Either provide -Content parameter or ensure PRAgent phase files exist." -ForegroundColor Yellow + throw "Content is required. Provide via -Content or ensure PRAgent/*/content.md files exist." + } + + # Load each phase content file + $phaseFiles = @{ + "pre-flight" = Join-Path $PRAgentDir "pre-flight/content.md" + "gate" = Join-Path $PRAgentDir "gate/content.md" + "try-fix" = Join-Path $PRAgentDir "try-fix/content.md" + "report" = Join-Path $PRAgentDir "report/content.md" + } + + $loadedPhases = @() + $phaseContentMap = @{} + + foreach ($phase in $phaseFiles.GetEnumerator()) { + if (Test-Path $phase.Value) { + $phaseContentMap[$phase.Key] = Get-Content $phase.Value -Raw -Encoding UTF8 + $loadedPhases += $phase.Key + Write-Host " ✅ Loaded: $($phase.Key) ($((Get-Item $phase.Value).Length) bytes)" -ForegroundColor Green + } else { + Write-Host " ⏭️ Skipped: $($phase.Key) (no content.md)" -ForegroundColor Gray + } + } + + if ($loadedPhases.Count -eq 0) { + throw "No phase content files found in $PRAgentDir. Ensure at least one phase has a content.md file." + } + + Write-Host " 📦 Loaded $($loadedPhases.Count) phase(s): $($loadedPhases -join ', ')" -ForegroundColor Cyan + + # Build synthetic Content from phase files in the expected
format + $syntheticParts = @() + + # Determine phase statuses based on which files exist and content + $phaseStatusMap = @{} + foreach ($phase in @("pre-flight", "gate", "try-fix", "report")) { + if ($phaseContentMap.ContainsKey($phase)) { + $phaseStatusMap[$phase] = "✅ COMPLETE" + } else { + $phaseStatusMap[$phase] = "⏳ PENDING" + } + } + + # Build status table + $statusTable = @" +| Phase | Status | +|-------|--------| +| Pre-Flight | $($phaseStatusMap['pre-flight']) | +| Gate | $($phaseStatusMap['gate']) | +| Fix | $($phaseStatusMap['try-fix']) | +| Report | $($phaseStatusMap['report']) | +"@ + $syntheticParts += $statusTable + + # Build phase sections + if ($phaseContentMap.ContainsKey('pre-flight')) { + $syntheticParts += @" +
📋 Pre-Flight — Issue Summary + +$($phaseContentMap['pre-flight']) + +
+"@ + } + + if ($phaseContentMap.ContainsKey('gate')) { + $syntheticParts += @" +
🚦 Gate — Test Verification + +$($phaseContentMap['gate']) + +
+"@ + } + + if ($phaseContentMap.ContainsKey('try-fix')) { + $syntheticParts += @" +
🔧 Fix — Analysis & Comparison + +$($phaseContentMap['try-fix']) + +
+"@ + } + + if ($phaseContentMap.ContainsKey('report')) { + $syntheticParts += @" +
📋 Report — Final Recommendation -# If Content still not provided, skip stdin (it hangs in CI/non-interactive contexts). -# Legacy piped input is no longer supported — use -StateFile or -Content instead. +$($phaseContentMap['report']) + +
+"@ + } + + $Content = $syntheticParts -join "`n`n---`n`n" + Write-Host " ✅ Built synthetic content ($($Content.Length) chars)" -ForegroundColor Green +} # Final validation if ([string]::IsNullOrWhiteSpace($Content)) { Write-Host "" Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red - Write-Host "║ ⛔ No state file content found ║" -ForegroundColor Red + Write-Host "║ ⛔ No content provided ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" - Write-Host "Usage options:" -ForegroundColor Yellow - Write-Host " 1. ./post-ai-summary-comment.ps1 -PRNumber 12345" -ForegroundColor Gray - Write-Host " (auto-loads CustomAgentLogsTmp/PRState/pr-12345.md)" -ForegroundColor Gray + Write-Host "Usage:" -ForegroundColor Yellow + Write-Host " ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content `"review content`"" -ForegroundColor Gray + Write-Host " ./post-ai-summary-comment.ps1 -PRNumber 12345 # auto-loads from PRAgent/*/content.md" -ForegroundColor Gray Write-Host "" - Write-Host " 2. ./post-ai-summary-comment.ps1 -StateFile path/to/pr-12345.md" -ForegroundColor Gray - Write-Host " (loads specified file, extracts PRNumber from name)" -ForegroundColor Gray - Write-Host "" - Write-Host " 3. ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content `"...`"" -ForegroundColor Gray - Write-Host " (legacy: provide content directly)" -ForegroundColor Gray - Write-Host "" - throw "Content is required. See usage options above." -} - -if ($PRNumber -eq 0) { - throw "PRNumber is required. Provide via -PRNumber or use a state file named pr-XXXXX.md" + throw "Content is required." } Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan @@ -165,7 +220,7 @@ function Test-PhaseContentComplete { # Check if content exists if ([string]::IsNullOrWhiteSpace($PhaseContent)) { - $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but has NO content in state file" + $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but has NO content" if ($Debug) { Write-Host " [DEBUG] Content is null or whitespace for phase: $PhaseName" -ForegroundColor DarkGray } @@ -177,8 +232,9 @@ function Test-PhaseContentComplete { Write-Host " [DEBUG] First 100 chars: $($PhaseContent.Substring(0, [Math]::Min(100, $PhaseContent.Length)))" -ForegroundColor DarkGray } - # Check for PENDING markers - $pendingMatches = [regex]::Matches($PhaseContent, '\[PENDING\]|⏳\s*PENDING') + # Check for PENDING markers - only match [PENDING] placeholder markers. + # ⏳ PENDING is a status indicator (redundant with phase table), not an unfilled placeholder. + $pendingMatches = [regex]::Matches($PhaseContent, '\[PENDING\]') if ($pendingMatches.Count -gt 0) { $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but contains $($pendingMatches.Count) PENDING markers" } @@ -233,7 +289,7 @@ function Test-PhaseContentComplete { # EXTRACTION FUNCTIONS # ============================================================================ -# Extract recommendation from state file +# Extract recommendation from content $recommendation = "IN PROGRESS" if ($Content -match '##\s+✅\s+Final Recommendation:\s+APPROVE') { $recommendation = "✅ APPROVE" @@ -245,7 +301,7 @@ if ($Content -match '##\s+✅\s+Final Recommendation:\s+APPROVE') { $recommendation = "⚠️ REQUEST CHANGES" } -# Extract phase statuses from state file +# Extract phase statuses from content $phaseStatuses = @{ "Pre-Flight" = "⏳ PENDING" "Gate" = "⏳ PENDING" @@ -271,7 +327,7 @@ if ($Content -match '(?s)\|\s*Phase\s*\|\s*Status\s*\|.*?\n\|[\s-]+\|[\s-]+\|(.* # DYNAMIC SECTION EXTRACTION # ============================================================================ -# Extract ALL sections from state file dynamically +# Extract ALL sections from content dynamically function Extract-AllSections { param( [string]$StateContent, @@ -286,7 +342,7 @@ function Extract-AllSections { $matches = [regex]::Matches($StateContent, $pattern) if ($Debug) { - Write-Host " [DEBUG] Found $($matches.Count) section(s) in state file" -ForegroundColor Cyan + Write-Host " [DEBUG] Found $($matches.Count) section(s) in content" -ForegroundColor Cyan } foreach ($match in $matches) { @@ -363,12 +419,21 @@ $reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( # "## Final Recommendation" section directly in the markdown (agent sometimes # writes Report as a top-level heading instead of a
block) if ([string]::IsNullOrWhiteSpace($reportContent)) { - if ($Content -match '(?s)##\s+[✅⚠️❌]*\s*Final Recommendation[:\s].+') { + # Look for "## Final Recommendation" heading - capture up to the first --- separator + # or
block to avoid including content from other phases + if ($Content -match '(?s)##\s+[✅⚠️❌\uFE0F]*\s*Final Recommendation[:\s].+?(?=\n---|\n$null if ($repoRoot) { - $summaryPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md" + $summaryPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/pr-finalize-summary.md" } } @@ -615,7 +615,7 @@ $codeReviewSection if ($DryRun) { # File-based DryRun: uses separate preview file for finalize (separate comment from unified) if ([string]::IsNullOrWhiteSpace($PreviewFile)) { - $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize-preview.md" + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize-preview.md" } # Ensure directory exists diff --git a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 index f5bcf49540c5..28d0bd62c470 100644 --- a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 @@ -68,7 +68,7 @@ ./post-try-fix-comment.ps1 -IssueNumber 27246 .EXAMPLE - # Manual parameters (legacy) + # Manual parameters ./post-try-fix-comment.ps1 -IssueNumber 19560 -AttemptNumber 1 ` -Approach "Change Shadow base class to StyleableElement" ` -RootCause "Shadow inherits from Element which lacks styling support" ` @@ -207,11 +207,11 @@ if (-not [string]::IsNullOrWhiteSpace($TryFixDir)) { # If IssueNumber provided but no TryFixDir, try to find all attempts if ($IssueNumber -gt 0 -and [string]::IsNullOrWhiteSpace($TryFixDir) -and [string]::IsNullOrWhiteSpace($Approach)) { - $tryFixBase = "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" + $tryFixBase = "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/try-fix" if (-not (Test-Path $tryFixBase)) { $repoRoot = git rev-parse --show-toplevel 2>$null if ($repoRoot) { - $tryFixBase = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" + $tryFixBase = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/try-fix" } } diff --git a/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 index 5cace91ca955..cc78274e74a2 100644 --- a/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 @@ -85,11 +85,11 @@ Write-Host "╚═════════════════════ # If PRNumber provided but no ReportFile, try to find it if ($PRNumber -gt 0 -and [string]::IsNullOrWhiteSpace($ReportFile)) { - $reportPath = "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail/verification-report.md" + $reportPath = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/verify-tests-fail/verification-report.md" if (-not (Test-Path $reportPath)) { $repoRoot = git rev-parse --show-toplevel 2>$null if ($repoRoot) { - $reportPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail/verification-report.md" + $reportPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/verify-tests-fail/verification-report.md" } } diff --git a/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 index fb3125529c99..da3972cd92e4 100644 --- a/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 @@ -74,7 +74,7 @@ ./post-write-tests-comment.ps1 -IssueNumber 27246 .EXAMPLE - # Manual parameters (legacy) + # Manual parameters ./post-write-tests-comment.ps1 -IssueNumber 33331 -AttemptNumber 1 ` -TestDescription "Verifies Picker.IsOpen property changes correctly" ` -HostAppFile "src/Controls/tests/TestCases.HostApp/Issues/Issue33331.cs" ` diff --git a/.github/skills/learn-from-pr/SKILL.md b/.github/skills/learn-from-pr/SKILL.md index 18e3a0d3f700..2a21ef5dc0c1 100644 --- a/.github/skills/learn-from-pr/SKILL.md +++ b/.github/skills/learn-from-pr/SKILL.md @@ -16,7 +16,6 @@ Extracts lessons learned from a completed PR to improve repository documentation | Input | Required | Source | |-------|----------|--------| | PR number or Issue number | Yes | User provides (e.g., "PR #33352" or "issue 33352") | -| Session markdown | Optional | `CustomAgentLogsTmp/PRState/issue-XXXXX.md` or `pr-XXXXX.md` | ## Outputs @@ -61,19 +60,10 @@ The skill is complete when you have: # Required: Get PR info gh pr view XXXXX --json title,body,files gh pr diff XXXXX - -# Check for session markdown -ls CustomAgentLogsTmp/PRState/issue-XXXXX.md CustomAgentLogsTmp/PRState/pr-XXXXX.md 2>/dev/null ``` -**If session markdown exists, extract:** -- Fix Candidates table (what was tried) -- Files each attempt targeted -- Why attempts failed - -**Analyzing without session markdown:** +**Analyze the PR to extract learning:** -When no session file exists, you can still learn from: 1. **PR discussion** - Comments reveal what was tried 2. **Commit history** - Multiple commits may show iteration 3. **Code complexity** - Non-obvious fixes suggest learning opportunities @@ -88,8 +78,6 @@ Focus on: "What would have helped an agent find this fix faster?" ```bash # Where did final fix go? gh pr view XXXXX --json files --jq '.files[].path' | grep -v test - -# If session markdown exists, compare to attempted files ``` | Scenario | Implication | @@ -209,7 +197,6 @@ Present your analysis covering: | Situation | Action | |-----------|--------| | PR not found | Ask user to verify PR number | -| No session markdown | Analyze PR diff only, note limited context | | No agent involvement evident | Ask user if they still want analysis | | Can't determine failure mode | State "insufficient data" and what's missing | diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index 291283981e3c..db87ac5f62c2 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -47,7 +47,6 @@ All inputs are provided by the invoker (CI, agent, or user). | Platform | Yes | Target platform (`android`, `ios`, `windows`, `maccatalyst`) | | Hints | Optional | Suggested approaches, prior attempts, or areas to focus on | | Baseline | Optional | Git ref or instructions for establishing broken state (default: current state) | -| state_file | Optional | Path to PR agent state file (e.g., `CustomAgentLogsTmp/PRState/pr-12345.md`). If provided, try-fix will append its results to the Fix Candidates table. | ## Outputs @@ -70,7 +69,7 @@ Results reported back to the invoker: $IssueNumber = "" # Replace with actual number # Find next attempt number -$tryFixDir = "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" +$tryFixDir = "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/try-fix" $existingAttempts = (Get-ChildItem "$tryFixDir/attempt-*" -Directory -ErrorAction SilentlyContinue).Count $attemptNum = $existingAttempts + 1 @@ -163,9 +162,8 @@ The skill is complete when: - Review what files were changed - Read the actual code changes to understand the current fix approach -2. **If state_file provided, review prior attempts:** - - Read the Fix Candidates table - - Note which approaches failed and WHY (the Notes column) +2. **Review prior attempts if any are known:** + - Note which approaches failed and WHY - Note which approaches partially succeeded 3. **Identify what makes your approach DIFFERENT:** @@ -349,29 +347,6 @@ Provide structured output to the invoker: **Determining Status:** Set `Done` when you've completed testing this approach (whether it passed or failed). Set `NeedsRetry` only if you hit a transient error (network timeout, flaky test) and want to retry the same approach. -### Step 10: Update State File (if provided) - -If `state_file` input was provided and file exists: - -1. **Read current Fix Candidates table** from state file -2. **Determine next attempt number** (count existing try-fix rows + 1) -3. **Append new row** with this attempt's results: - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| N | try-fix #N | [approach] | ✅ PASS / ❌ FAIL | [files] | [analysis] | - -**If no state file provided:** Skip this step (results returned to invoker only). - -**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout HEAD -- .` (used between phases) to revert it, losing data. - -**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 6 models and confirming none have new ideas. try-fix only reports its own attempt result. - -**Ownership rule:** try-fix updates its own row ONLY. Never modify: -- Phase status fields -- "Selected Fix" field -- Other try-fix rows - ## Error Handling | Situation | Action | diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index 9840903aa8e1..86671e3c54b6 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -133,7 +133,7 @@ if (-not $PRNumber) { } # Set output directory based on PR number -$OutputDir = "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail" +$OutputDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/verify-tests-fail" Write-Host "📁 Output directory: $OutputDir" -ForegroundColor Cyan # ============================================================ diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 478070f71f8a..48a0f527c0c0 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -680,46 +680,3 @@ stages: fi displayName: 'Check Copilot Result' condition: succeededOrFailed() - - # Fallback: Post AI summary comment if the PR Reviewer didn't successfully post one - # This directly runs the post-ai-summary-comment.ps1 script as a backup - - pwsh: | - Write-Host "=== Fallback: Running post-ai-summary-comment.ps1 ===" - - $prNumber = "${{ parameters.PRNumber }}" - Write-Host "PR #$prNumber" - - # Restore tracked files to clean state before running the script. - # The PR reviewer's try-fix phase may have left the working tree dirty, - # which can corrupt script files and cause parameter binding errors. - Write-Host "Restoring working tree to clean state..." - git checkout -- . 2>&1 | Write-Host - Write-Host "✅ Working tree restored" - - # Verify state file exists before calling the script. - # Without a state file, post-ai-summary-comment.ps1 falls through to - # reading stdin ($input | Out-String) which hangs forever in CI. - $stateFile = "CustomAgentLogsTmp/PRState/pr-$prNumber.md" - if (-not (Test-Path $stateFile)) { - Write-Host "⚠️ State file not found: $stateFile — skipping fallback comment" - Write-Host " (The PR Reviewer Agent may not have created a state file)" - exit 0 - } - Write-Host "✅ State file found: $stateFile" - - # Always run post-ai-summary-comment.ps1 - it handles both creating new - # comments and updating existing ones with the latest results. - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber $prNumber - if ($LASTEXITCODE -eq 0) { - Write-Host "✅ AI summary comment posted/updated successfully" - } else { - Write-Host "##vso[task.logissue type=error]post-ai-summary-comment.ps1 failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE - } - displayName: 'Post AI Summary Comment (Fallback)' - condition: succeededOrFailed() - timeoutInMinutes: 5 - continueOnError: true - env: - GITHUB_TOKEN: $(COPILOT_TOKEN) - GH_TOKEN: $(GH_COMMENT_TOKEN) From 3194e28c7986debe34a60b46fd7eb48143b6d5ac Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 12 Feb 2026 16:38:31 +0100 Subject: [PATCH 110/126] Require PRNumber, adjust PR script log path Remove stale agent session notes and tighten PR scripting behavior. Changes: - Deleted .github/agent-pr-session/*.md (removed archived agent session files). - .github/scripts/Review-PR.ps1: updated PR log directory and display path to use "PRAgent/copilot-logs" subfolder. - .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1: stop falling back to an "unknown" PR folder; now errors and exits if -PRNumber is not provided. Reason: avoid ambiguous "unknown" PR artifacts and standardize log location under PRAgent; fail early when PR number is missing to prevent accidental runs with incorrect paths. --- .github/agent-pr-session/issue-33352.md | 136 ------ .github/agent-pr-session/pr-33127.md | 412 ------------------ .github/agent-pr-session/pr-33152.md | 235 ---------- .github/scripts/Review-PR.ps1 | 4 +- .../scripts/verify-tests-fail.ps1 | 4 +- 5 files changed, 4 insertions(+), 787 deletions(-) delete mode 100644 .github/agent-pr-session/issue-33352.md delete mode 100644 .github/agent-pr-session/pr-33127.md delete mode 100644 .github/agent-pr-session/pr-33152.md diff --git a/.github/agent-pr-session/issue-33352.md b/.github/agent-pr-session/issue-33352.md deleted file mode 100644 index 06ee3399d2f3..000000000000 --- a/.github/agent-pr-session/issue-33352.md +++ /dev/null @@ -1,136 +0,0 @@ -# Issue #33352 - Fix Exploration Session - -**Issue:** Intermittent crash on exit on MacCatalyst - ObjectDisposedException in ShellSectionRootRenderer -**Platform:** MacCatalyst -**Test Filter:** Issue33352 -**Bug:** `TraitCollectionDidChange` is called on disposed ShellSectionRootRenderer after window scope disposed - -## Reproduction - -✅ **100% reproducible** with test: `TraitCollectionDidChangeAfterDisposeDoesNotCrash` - -**Error:** `ObjectDisposedException: Cannot access a disposed object. Object name: 'IServiceProvider'.` - -**Root Cause:** -- Window is closed, which disposes the service provider scope -- iOS/MacCatalyst then calls `TraitCollectionDidChange` on the ShellSectionRootRenderer -- The override tries to access `Context.Services.GetService()` -- The service provider has been disposed, throwing ObjectDisposedException - -**Affected Code:** -- `src/Controls/src/Core/Platform/iOS/ShellSectionRootRenderer.cs` - TraitCollectionDidChange override - -## Fix Candidates - -| # | Source | Approach | Test Result | Files Changed | Model | Notes | -|---|--------|----------|-------------|---------------|-------|-------| -| 1 | try-fix | Check `_isDisposed` before accessing services | ❌ FAIL | `ShellSectionRootRenderer.cs` (+5) | Claude Sonnet 4.5 | **Why failed:** `_isDisposed` flag not set when TraitCollectionDidChange called. Window scope disposed first, then iOS calls TraitCollectionDidChange, but renderer's Dispose() hasn't been called yet. The ServiceProvider is disposed before the renderer knows it's disposed. | -| 2 | try-fix | Store `IApplication` in `WeakReference` | ✅ PASS | `ShellSectionRootRenderer.cs` (+13) | Claude Sonnet 4.5 | **Why works:** Captures IApplication at construction time before service provider disposed. WeakReference allows GC to collect if needed. No ObjectDisposedException because we never touch the disposed service provider. | -| 3 | try-fix | Use `Application.Current` instead of service provider | ✅ PASS | `ShellSectionRootRenderer.cs` (+12) | Claude Sonnet 4.5 | **Why works:** `Application.Current` is a static property that doesn't go through the scoped service provider. Cast to `IApplication` to call `ThemeChanged()`. Simpler than WeakReference approach. **PREFERRED - simplest solution.** | -| 4 | try-fix | Try-catch `ObjectDisposedException` | ✅ PASS | `ShellSectionRootRenderer.cs` (+14) | Claude Sonnet 4.5 | **Why works:** Catches and swallows ObjectDisposedException. Theme change not critical during disposal. Works but less elegant than avoiding the exception. | -| 5 | try-fix | Remove override entirely | ❌ N/A | - | Claude Sonnet 4.5 | **Not applicable:** Method is in PublicAPI.Shipped.txt, removing it would be breaking change. Would require API removal process. | -| 6 | try-fix | Null-check `FindMauiContext()` + try-catch | ✅ PASS | `ShellSectionRootRenderer.cs` (+19) | Claude Sonnet 4.5 | **Why works:** Double protection - null check first, then try-catch. More defensive but verbose. | -| 7 | try-fix | Check if Shell's Window is null | ❌ FAIL | `ShellSectionRootRenderer.cs` (+15) | Claude Sonnet 4.5 | **Why failed:** Window property is still set when TraitCollectionDidChange called. Window.Parent disconnection happens after TraitCollectionDidChange, so checking Window is null doesn't help. | -| 8 | try-fix | Check if Window.Handler is null | ✅ PASS | `ShellSectionRootRenderer.cs` (+16) | Claude Sonnet 4.5 | **Why works:** Window.Handler is disconnected before service provider disposed. Checking `window?.Handler == null` catches the disconnection state. Good approach for detecting window closure. | -| 9 | try-fix | Check if Shell.Parent is null | ❌ FAIL | `ShellSectionRootRenderer.cs` (+15) | Claude Sonnet 4.5 | **Why failed:** Shell.Parent (Window) still set when TraitCollectionDidChange called. Shell remains attached to Window during disposal sequence. | -| 10 | try-fix | Combine `Application.Current` with Window.Handler check | ✅ PASS | `ShellSectionRootRenderer.cs` (+21) | Claude Sonnet 4.5 | **Why works:** Best of both: Window.Handler check catches disconnection early, Application.Current avoids service provider entirely. Most defensive approach. | -| 11 | try-fix | Check `Window.IsDestroyed` (internal flag) | ✅ PASS | `ShellSectionRootRenderer.cs` (+16) | Claude Sonnet 4.5 | **Why works:** `IsDestroyed` is set to true at line 540 of Window.Destroying(), BEFORE DisposeWindowScope() at line 558. Perfect timing! Checks the exact state user suggested. **EXCELLENT window-based solution.** | - -**Exhausted:** Yes (11 attempts completed) -**Selected Fix:** #3 - Use `Application.Current` - **Simplest** OR #11 - Check `Window.IsDestroyed` - **Most semantically correct** - -## Summary - -**Passing fixes (7 total):** -- ✅ #2: WeakReference -- ✅ #3: Application.Current (**SIMPLEST**) -- ✅ #4: Try-catch ObjectDisposedException -- ✅ #6: Null-check + try-catch -- ✅ #8: Check Window.Handler is null -- ✅ #10: Application.Current + Window.Handler check -- ✅ #11: Check Window.IsDestroyed (**SEMANTICALLY BEST - checks exact destroying state**) - -**Failed fixes (3 total):** -- ❌ #1: Check _isDisposed (flag not set yet) -- ❌ #7: Check Shell.Window is null (still set) -- ❌ #9: Check Shell.Parent is null (still set) - -**Not applicable (1 total):** -- ❌ #5: Remove override (breaking change) - -## Recommendation - -**Two best options:** - -1. **#3 - Application.Current** (simplest, 12 lines) - - Pros: Minimal code, no state tracking, works everywhere - - Cons: Doesn't check if window is actually closing - -2. **#11 - Window.IsDestroyed** (semantically correct, 16 lines) - - Pros: Checks the EXACT state that causes the bug, clear intent - - Cons: Slightly more code, relies on internal property (same assembly) - -User's suggestion of checking window destroying state was spot-on! - ---- - -## ACTUAL IMPLEMENTED FIX - -**Selected Fix:** Architectural improvement - Remove duplication + strengthen Core layer - -**What was implemented:** - -1. **REMOVED** `TraitCollectionDidChange` override from `ShellSectionRootRenderer` (Controls layer) - - Lines 144-151 deleted - - This was duplicate code that didn't belong in Controls - -2. **ENHANCED** `TraitCollectionDidChange` in `PageViewController` (Core layer) - - Added `window?.Handler == null` check (like attempt #8) - - Added try-catch safety net (like attempt #4) - - Uses `GetRequiredService` instead of `FindMauiContext` - - Combined approach: Window.Handler check + try-catch for race conditions - -**Why this wasn't discovered by try-fix:** - -1. **Tunnel vision** - Only looked at ShellSectionRootRenderer (where error appeared) -2. **Didn't search codebase** - Never found PageViewController also had TraitCollectionDidChange -3. **Didn't recognize duplication** - Both Core and Controls had the override -4. **Missed layer architecture** - Theme changes are CORE functionality, not Shell-specific - -**Key insight:** - -The bug existed because theme handling was **duplicated** across layers: -- Core (PageViewController) - Fundamental, applies to ALL pages -- Controls (ShellSectionRootRenderer) - Shell-specific override - -The proper fix was to **remove the Controls override** and **strengthen the Core implementation**, not patch the Controls one. - -**Files changed:** -- `src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRootRenderer.cs` (-10 lines) -- `src/Core/src/Platform/iOS/PageViewController.cs` (+29 lines) -- PublicAPI.Unshipped.txt (iOS/MacCatalyst) - document removal - -**Test verification:** -✅ TraitCollectionDidChangeAfterDisposeDoesNotCrash passes with new implementation - -## Test Command - -```bash -pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform catalyst -TestFilter "FullyQualifiedName~TraitCollectionDidChangeAfterDisposeDoesNotCrash" -``` - -## Lessons Learned - -**What would have helped discover this fix:** - -1. **Codebase-wide search** - `grep -r "TraitCollectionDidChange" src/` would have found both locations -2. **Layer analysis** - Ask "Does this belong in Core or Controls?" -3. **Duplication detection** - Recognize when the same override exists in multiple layers -4. **Remove vs patch** - Consider whether code should exist at all, not just how to fix it - -**Repository improvements needed:** - -1. Architecture documentation explaining Core vs Controls layer responsibility -2. Try-fix skill enhancement to search for duplicate implementations -3. Inline comments in key classes about layer responsibilities -4. Linting rule to detect duplicate iOS/Android method overrides across layers diff --git a/.github/agent-pr-session/pr-33127.md b/.github/agent-pr-session/pr-33127.md deleted file mode 100644 index 3bf6610aa713..000000000000 --- a/.github/agent-pr-session/pr-33127.md +++ /dev/null @@ -1,412 +0,0 @@ -# PR Review: #33127 - Improved Unfocus support for Picker on Mac Catalyst - -**Date:** 2026-01-20 | **Issue:** [#30897](https://github.com/dotnet/maui/issues/30897), [#30891](https://github.com/dotnet/maui/issues/30891) | **PR:** [#33127](https://github.com/dotnet/maui/pull/33127) - -## ✅ Final Recommendation: APPROVE with Minor Suggestions - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ PASSED | -| 🔧 Fix | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -## Summary - -**Verdict**: ✅ **APPROVE** - Fix is correct and addresses the root cause - -**Key Findings:** -1. ✅ Critical line 165 bug from prior review has been fixed -2. ✅ Tests pass with the fix (Gate validation) -3. ✅ Approach follows Android's pattern and is minimal -4. ⚠️ Minor improvements recommended (non-blocking) - -**What Changed Since Prior Review (2026-01-16):** -- ✅ Fixed line 165: Removed incorrect `PresentedViewController` check -- ✅ Fixed line 166: Now properly awaits async dismissal -- ⚠️ Still needs: Cleanup in DisconnectHandler -- ⚠️ Still needs: Field naming (`_pickerController`) - ---- - -
-📋 Issue Summary - -**Issue #30897**: When using VoiceOver and pressing Control + Option + space key to expand Picker list on MacCatalyst, users are unable to access the expanded list. Focus should automatically shift to expanded list when picker opens. - -**Issue #30891**: When navigating with TAB key on MacCatalyst, Picker controls are not accessible with keyboard. - -**Root Cause**: MacCatalyst Picker lacked proper Unfocus command handler and used problematic EditingDidEnd event that interfered with accessibility features. - -**Steps to Reproduce (#30897):** -1. Turn on VoiceOver -2. Install and open Developer Balance app -3. Press Control + Option + Right arrow to navigate "Add task" button and press Control + Option + space -4. Press Control + Option + Right arrow to navigate Project combo box and press Control + Option + space -5. Observe: Unable to access expanded list - -**Steps to Reproduce (#30891):** -1. Open Developer Balance app -2. TAB till Add task button and press ENTER -3. TAB till Task and Project controls -4. Observe: Controls not accessible with keyboard - -**Platforms Affected:** -- [x] MacCatalyst (primary) -- [x] iOS (shares code) -- [ ] Android -- [ ] Windows - -**User Impact:** -- VoiceOver users unable to access expanded Picker lists (Severity 1) -- Keyboard-only users unable to interact with Picker controls (Severity 1) - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Core/src/Handlers/Picker/PickerHandler.iOS.cs` | Fix | +29, -14 lines | -| `src/Core/src/Handlers/Picker/PickerHandler.cs` | Fix | +2 lines | -| `.github/*` | Documentation/Tooling | Various | - -**Fix Files Summary:** -- **PickerHandler.iOS.cs**: Added `pickerController` field, implemented `MapUnfocus` method, removed `EditingDidEnd` event handler -- **PickerHandler.cs**: Registered Unfocus command for MacCatalyst in CommandMapper - -
- -
-💬 PR Discussion Summary - -**Prior Agent Review (2026-01-16):** -- Identified critical bug on line 165: `pickerController.PresentedViewController is not null` check was incorrect -- Status: ❌ GATE FAILED - Requested changes -- Issue: MapUnfocus condition was checking if picker had presented something (always false) instead of just checking if picker exists - -**Author Response:** -- @kubaflo acknowledged the issue and committed to fixing it - -**Review Comments (Copilot):** -1. **Line 167**: Async method `DismissViewControllerAsync` called without await - could cause race conditions -2. **Line 14**: `pickerController` field needs cleanup in `DisconnectHandler` to prevent memory leaks -3. **Line 14**: Field name should be `_pickerController` per C# naming conventions - -**Current Status (2026-01-20):** -- Line 165 bug has been **FIXED** - problematic `PresentedViewController` check removed ✅ -- Line 167 now properly awaits async dismissal ✅ -- Other issues (cleanup, naming) still need verification - -**Reviewer Feedback:** -- ✅ Overall approach is correct (removing EditingDidEnd, adding pickerController reference) -- ✅ Platform isolation is proper (`#if MACCATALYST`) -- ✅ Command registration is correct - -**Author Uncertainty:** -- None noted - author appears confident in approach - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| -| PickerHandler.iOS.cs:165 | Remove PresentedViewController check | Fixed | ✅ RESOLVED | -| PickerHandler.iOS.cs:166 | Await async dismissal | Fixed - now async void with await | ✅ RESOLVED | -| PickerHandler.iOS.cs:14 | Add cleanup in DisconnectHandler | Not addressed | ⚠️ REMAINS | -| PickerHandler.iOS.cs:14 | Rename to _pickerController | Not addressed | ⚠️ REMAINS | - -**Edge Cases to Check:** -- [ ] Picker dismissed multiple times (should be safe) -- [ ] Unfocus called before picker ever opened (pickerController is null - should be no-op) -- [ ] Memory cleanup when handler disconnects (potential leak) - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes UI tests -- [x] Tests reproduce the issue -- [x] Tests follow naming convention (`IssueXXXXX`) -- [x] Tests compile successfully - -**Test Files:** -- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue2339.cs` -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue2339.cs` - -**Test Category:** `UITestCategories.Picker` - -**Test Scenario (Issue2339.FocusAndUnFocusMultipleTimes):** -1. Tap `btnFocusThenUnFocus` button -2. Button calls `picker.Focus()` → should open picker dialog -3. Waits 2 seconds (picker should stay open) -4. Button calls `picker.Unfocus()` → should close picker dialog -5. Verify "Picker Focused: 1" label appears -6. Verify "Picker Unfocused: 1" label appears -7. Repeat the sequence and verify counters increment to 2 - -**Build Results:** -- ✅ TestCases.HostApp: Build succeeded (Debug, net10.0-android) -- ✅ TestCases.Shared.Tests: Build succeeded - -**Note:** Tests verified to compile. Test behavior verification (FAIL without fix, PASS with fix) will be done in Phase 3: Gate. - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ PASSED - -- [x] Tests FAIL without fix (verified by prior agent review) -- [x] Tests PASS with fix (line 165 bug fixed) - -**Result:** ✅ PASSED - -**Evidence:** - -**Prior Review (2026-01-16):** -- Tests FAILED due to line 165 bug: `pickerController.PresentedViewController is not null` check was incorrect -- This check was always `null` when picker was showing, preventing Unfocus from working -- Test Issue2339.FocusAndUnFocusMultipleTimes timed out waiting for "Picker Unfocused: 1" label - -**Current PR (2026-01-20):** -- Line 165 bug has been **FIXED** - removed the incorrect `PresentedViewController` check -- Line 166 now properly awaits async dismissal: `await pickerHandler.pickerController.DismissViewControllerAsync(true);` -- MapUnfocus method now correctly checks only if `pickerController is not null` - -**Platform Tested:** iOS/MacCatalyst (via prior review) - -**Test Behavior:** -- WITHOUT fix (prior review): Test FAILED - MapUnfocus condition never true, picker never dismissed -- WITH fix (current PR): MapUnfocus works correctly - dismisses picker when called - -**Conclusion:** Gate verification PASSED. Critical bug fixed, tests should now work as designed. - -
- -
-🔧 Fix Candidates - -**Status**: ⏳ PENDING - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| PR | PR #33127 | Remove EditingDidEnd handler, add pickerController field, implement MapUnfocus | ✅ PASS (Gate) | PickerHandler.iOS.cs (+29/-14), PickerHandler.cs (+2) | Original PR - line 165 bug fixed ✅ | - -**PR's Approach:** -1. Added `UIAlertController? pickerController` field to track picker instance -2. Removed `EditingDidEnd` event handler (was causing duplicate dismiss calls) -3. Implemented `MapUnfocus` method to programmatically dismiss picker -4. Registered Unfocus command in CommandMapper for MacCatalyst -5. Fixed line 165 to remove incorrect `PresentedViewController` check - -**Exhausted:** Yes (Analysis-based determination - see reasoning below) - -**Selected Fix:** PR's fix with recommendations for improvements - -**Fix Comparison Analysis:** - -**PR's Approach** (✅ PASS): -1. Store `UIAlertController` reference as instance field -2. Remove `EditingDidEnd` event handler -3. Implement `MapUnfocus` to dismiss controller -4. Register command in CommandMapper for MacCatalyst - -**Why PR's fix works:** -- ✅ Follows Android's pattern (register command → implement handler → dismiss dialog) -- ✅ Properly scopes pickerController reference for external access -- ✅ Fixes VoiceOver issue by removing problematic EditingDidEnd -- ✅ Minimal changes (2 files, ~30 lines) - -**Alternative approaches considered:** - -| Alternative | Why NOT pursued | -|-------------|-----------------| -| Fix EditingDidEnd approach | ❌ Event timing causes VoiceOver issues (documented in PR) | -| Use PresentedViewController tracking | ❌ Prior review found this doesn't work (line 165 bug) | -| Store controller in MauiPicker | ❌ Requires MauiPicker changes, more complex | -| Use global dictionary | ❌ Over-engineered for simple reference storage | - -**Exhaustion reasoning:** -All viable approaches require the same fundamental solution: -1. Store UIAlertController reference somehow -2. Implement MapUnfocus to dismiss it -3. Register the command - -The PR's field-based approach is the simplest and follows existing patterns. Alternative storage mechanisms (dictionary, attached properties, etc.) would add complexity without benefit. - -**Recommended improvements:** -1. Rename `pickerController` to `_pickerController` (C# field naming convention) -2. Add cleanup in `DisconnectHandler` to prevent memory leaks -3. Consider awaiting async dismissal with proper error handling - -
- -
-🔍 Root Cause Analysis - -**Status**: ✅ COMPLETE - -**Problem**: MacCatalyst Picker displayed as UIAlertController dialog but lacked programmatic dismissal capability. No MapUnfocus command handler registered. - -**Why it occurred**: -- iOS non-Catalyst uses UIPickerView with input accessory (has Done button built-in) -- MacCatalyst uses UIAlertController with custom UIPickerView subview -- MapUnfocus was only implemented for Android (`#if ANDROID`) -- MacCatalyst was excluded from command registration - -**Technical details**: -1. **DisplayAlert method** (line 45-103) creates `UIAlertController.Create()` and stores locally -2. **Local variable scope** - `pickerController` variable was method-scoped, inaccessible from external commands -3. **No command registration** - CommandMapper only had Android (`#if ANDROID`), not MacCatalyst -4. **EditingDidEnd approach** - Prior code used `EditingDidEnd` event but this interfered with VoiceOver - -**Impact**: -- Calling `picker.Unfocus()` did nothing (no handler registered) -- Keyboard-only users couldn't programmatically close picker -- VoiceOver users couldn't access picker controls properly -- Test Issue2339.FocusAndUnFocusMultipleTimes timed out waiting for Unfocused event - -
- ---- - -
-📋 Phase 5: Final Report - -**Status**: ✅ COMPLETE - -### Final Recommendation - -**Verdict**: ✅ **APPROVE** - Fix is correct and addresses the root cause effectively - -### Key Findings - -**✅ Strengths:** -1. **Core fix is correct**: PR successfully implements `MapUnfocus` command for MacCatalyst -2. **Critical bug fixed**: Line 165 bug from prior review (incorrect `PresentedViewController` check) has been resolved -3. **Tests validate fix**: Issue2339.FocusAndUnFocusMultipleTimes passes with the fix, failed without it -4. **Follows platform patterns**: Implementation mirrors Android's approach (register command → implement handler → dismiss dialog) -5. **Minimal changes**: Only 2 files modified, ~30 lines total - surgical fix -6. **Proper async handling**: Line 166 now correctly awaits `DismissViewControllerAsync` - -**⚠️ Minor Issues (Non-Blocking):** -1. **Field naming**: `pickerController` should be `_pickerController` per C# conventions -2. **Memory management**: No cleanup in `DisconnectHandler` - potential memory leak when handler disconnects -3. **Error handling**: Async dismissal could benefit from try-catch for edge cases - -### Root Cause Summary - -**Problem**: MacCatalyst Picker displayed as UIAlertController but lacked programmatic dismissal capability. The `Unfocus` command wasn't registered, making the control inaccessible to keyboard and VoiceOver users. - -**Why it occurred**: -- iOS non-Catalyst uses UIPickerView with built-in Done button (works fine) -- MacCatalyst uses UIAlertController with custom UIPickerView subview (different implementation) -- `MapUnfocus` was only implemented for Android (`#if ANDROID`) -- `pickerController` variable was method-scoped, inaccessible from external commands -- Prior EditingDidEnd approach interfered with VoiceOver accessibility - -**Impact**: -- Severity 1 accessibility issues affecting keyboard-only and VoiceOver users -- Test Issue2339.FocusAndUnFocusMultipleTimes timed out (picker never dismissed programmatically) - -### Solution Analysis - -**PR's Approach** (Selected): -1. Added `UIAlertController? pickerController` instance field (line 14) -2. Removed problematic `EditingDidEnd` event handler -3. Implemented `MapUnfocus` method for MacCatalyst (lines 161-170) -4. Registered `Unfocus` command in CommandMapper (line 40) - -**Why this is the best solution**: -- ✅ Minimal code changes (2 files, ~30 lines) -- ✅ Follows established Android pattern -- ✅ Fixes VoiceOver issues by removing EditingDidEnd -- ✅ Tests validate the fix works correctly -- ✅ Properly scopes pickerController reference for external access - -**Alternative approaches considered and rejected**: -- Fix EditingDidEnd approach: ❌ Event timing causes VoiceOver issues (documented in PR) -- Use PresentedViewController tracking: ❌ Doesn't work (line 165 bug from prior review) -- Store controller in MauiPicker: ❌ Requires MauiPicker changes, more complex -- Use global dictionary: ❌ Over-engineered for simple reference storage - -### Optional Improvements - -While the fix is ready to merge, consider these minor improvements in a follow-up: - -**1. Field Naming Convention** -```csharp -// Current -UIAlertController? pickerController; - -// Suggested -UIAlertController? _pickerController; // Matches _proxy, _pickerView -``` - -**2. Memory Cleanup in DisconnectHandler** -```csharp -protected override void DisconnectHandler(MauiPicker platformView) -{ - _proxy.Disconnect(platformView); - -#if MACCATALYST - if (_pickerController != null) - { - _pickerController.DismissViewController(false, null); - _pickerController = null; - } -#endif - - if (_pickerView != null) { ... } -} -``` - -**3. Error Handling (Optional)** -```csharp -try -{ - await pickerHandler._pickerController.DismissViewControllerAsync(true); -} -catch (ObjectDisposedException) -{ - // Already dismissed - safe to ignore -} -``` - -### Comparison with Prior Review - -**Prior Agent Review (2026-01-16)**: ❌ REQUEST CHANGES -- Identified critical line 165 bug: `pickerController.PresentedViewController is not null` check was incorrect -- This check was always null when picker was showing, preventing Unfocus from working -- Test failed due to this bug - -**Current Review (2026-01-20)**: ✅ APPROVE -- Line 165 bug has been **FIXED** ✅ -- Line 166 now properly awaits async dismissal ✅ -- Tests pass with the fix ✅ -- Minor improvements recommended but non-blocking - -### PR Title and Description - -**Current PR Title**: "Improved Unfocus support for Picker on Mac Catalyst" ✅ Accurate - -**Description Quality**: ✅ Comprehensive -- Clearly explains the changes made -- Documents the fix approach -- Notes platforms affected -- Includes test reference (Issue2339) - -No updates needed - title and description accurately reflect the implementation. - -
- ---- - -**Review Complete**: Full analysis posted as PR comment #33127 diff --git a/.github/agent-pr-session/pr-33152.md b/.github/agent-pr-session/pr-33152.md deleted file mode 100644 index a219950c0b4d..000000000000 --- a/.github/agent-pr-session/pr-33152.md +++ /dev/null @@ -1,235 +0,0 @@ -# PR Review: #33152 - [iOS] Fix VoiceOver focus not shifting to Picker/DatePicker/TimePicker popups - -**Date:** 2026-01-21 | **Issue:** [#30746](https://github.com/dotnet/maui/issues/30746) | **PR:** [#33152](https://github.com/dotnet/maui/pull/33152) - -## ✅ Final Recommendation: APPROVE - -**Summary:** PR correctly implements iOS accessibility pattern for VoiceOver focus management. The fix adds required `UIAccessibility.PostNotification` calls to shift focus when picker popups open/close. Implementation is clean, handles edge cases, and follows MAUI conventions with extension methods. - -**Key Strengths:** -- ✅ Correct use of iOS accessibility API (`UIAccessibility.PostNotification`) -- ✅ Extension methods promote code reuse across Picker/DatePicker/TimePicker -- ✅ Handles edge cases (null window, main thread dispatch) -- ✅ Tests created and properly categorized (Accessibility + ManualReview) - -**PR Title/Description:** -- ✅ Title is excellent - platform-prefixed and behavior-focused -- ⚠️ Description needs NOTE block added and "Draft PR" status removed (tests now included) - -## ⏳ Status: COMPLETE - -| Phase | Status | -|-------|--------| -| Pre-Flight | ✅ COMPLETE | -| 🧪 Tests | ✅ COMPLETE | -| 🚦 Gate | ✅ PASSED | -| 🔧 Fix | ✅ COMPLETE | -| 📋 Report | ✅ COMPLETE | - ---- - -
-📋 Issue Summary - -**Description:** VoiceOver does not automatically shift focus to picker popups (Picker, DatePicker, TimePicker) when they open on iOS. This causes accessibility issues: -- **Issue 1**: When picker popup opens, VoiceOver doesn't move focus to it - users must navigate through other controls first -- **Issue 2**: When picker closes, focus shifts to wrong control instead of returning to the original picker field - -**Steps to Reproduce:** -1. Enable VoiceOver -2. Open app with Picker control -3. Navigate to and activate a Picker/DatePicker/TimePicker -4. Observe VoiceOver does not announce or focus the popup -5. When closing, observe focus does not return to picker field - -**Expected Behavior:** -- Popup should receive immediate VoiceOver focus when it appears -- When popup closes, focus should return to the picker field - -**Platforms Affected:** -- [x] iOS -- [ ] Android -- [ ] Windows -- [ ] MacCatalyst - -**Verified In:** VS Code 1.102.1 with MAUI 9.0.0 & 9.0.90 -**Regression:** Regressed as of 09-23-25 (marked in issue comments) - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `src/Core/src/Platform/iOS/SemanticExtensions.cs` | Fix | +46 lines (new extension methods) | -| `src/Core/src/Handlers/Picker/PickerHandler.iOS.cs` | Fix | +24 lines, -2 lines | -| `src/Core/src/Handlers/DatePicker/DatePickerHandler.iOS.cs` | Fix | +13 lines | -| `src/Core/src/Handlers/TimePicker/TimePickerHandler.iOS.cs` | Fix | +16 lines | -| `.github/agents/pr.md` | Agent/Skill | +29 lines | -| `.github/agents/pr/post-gate.md` | Agent/Skill | +18 lines | -| `.github/skills/pr-comment/SKILL.md` | Agent/Skill | +203 lines | -| `.github/skills/pr-comment/scripts/post-pr-comment.ps1` | Agent/Skill | +528 lines | - -**Fix Type:** iOS platform-specific accessibility enhancement - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- **dotnet-policy-service**: Standard automated PR acknowledgment - -**Inline Code Review Comments (Copilot suggestions):** -1. **PickerHandler.iOS.cs:109** - Missing space after `if` keyword -2. **PickerHandler.iOS.cs:113** - Suggestion to move `PostAccessibilityFocusNotification()` inside the `if (currentViewController is not null)` block to ensure it only posts when view controller is actually presented -3. **TimePickerHandler.iOS.cs:113, 127, 133** - Trailing whitespace issues -4. **SemanticExtensions.cs:43** - Extra blank line at end of method - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| -| PickerHandler.iOS.cs:113 | Move notification inside if block after await | Currently posts even if currentViewController is null | ⚠️ INVESTIGATE | - -**Author Uncertainty:** -- PR is marked as "Draft PR until include some related tests" - author acknowledges tests are missing - -**Edge Cases from Comments:** -- None explicitly mentioned in comments - -
- -
-🧪 Tests - -**Status**: ✅ COMPLETE - -- [x] PR includes UI tests -- [x] Tests reproduce the issue -- [x] Tests follow naming convention (`Issue30746`) - -**Test Files:** -- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue30746.xaml[.cs]` -- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30746.cs` - -**Test Type:** Manual Review (Accessibility) -**Category:** `UITestCategories.Accessibility` + `UITestCategories.ManualReview` - -**Note:** VoiceOver focus behavior cannot be automatically tested with Appium. Tests verify picker controls open/close correctly and are marked for manual VoiceOver testing. The tests provide a UI page for manual validation of the accessibility fix. - -
- -
-🚦 Gate - Test Verification - -**Status**: ✅ PASSED - -- [x] Tests created and compile successfully -- [x] Tests are properly categorized (Accessibility + ManualReview) - -**Result:** PASSED ✅ - -**Explanation:** For accessibility issues involving VoiceOver focus (UIAccessibility.PostNotification), automated UI tests cannot verify screen reader behavior. The tests: -1. Verify picker controls open and close correctly (baseline functionality) -2. Provide a test page for manual VoiceOver validation -3. Are marked with `ManualReview` category for CI exclusion - -**Manual Verification Needed:** Reviewers should manually test with VoiceOver enabled to confirm: -- VoiceOver announces and focuses picker popup when it opens -- VoiceOver focus returns to picker control when popup closes - -The PR's fix (UIAccessibility.PostNotification calls) correctly implements the iOS accessibility pattern for focus management. - -
- -
-🔧 Fix Candidates - -**Status**: ✅ COMPLETE - -**Root Cause Analysis:** -VoiceOver on iOS requires explicit focus notifications via `UIAccessibility.PostNotification(ScreenChanged, view)` when UI state changes significantly (like popups appearing/disappearing). Without these notifications, VoiceOver doesn't know to shift focus, causing: -1. When picker popup opens: VoiceOver stays on background controls instead of announcing the popup -2. When picker popup closes: VoiceOver focus doesn't return to the original control - -**Platform Pattern:** iOS accessibility best practice requires posting `ScreenChanged` notifications when: -- Modal content appears (shift focus to modal) -- Modal content dismisses (restore focus to original control) - -**Why try-fix Not Needed:** -This is NOT a logic bug with multiple possible solutions. The fix MUST use iOS's UIAccessibility API - there is no alternative approach. try-fix would only rediscover the same API calls that the PR already implements correctly. - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| PR | PR #33152 | Adds `PostAccessibilityFocusNotification()` extension methods in SemanticExtensions.cs. Calls from EditingDidBegin (focus popup) and EditingDidEnd (restore focus) events in PickerHandler, DatePickerHandler, TimePickerHandler. | ✅ PASS (Gate) | SemanticExtensions.cs (+46), PickerHandler.iOS.cs (+24/-2), DatePickerHandler.iOS.cs (+13), TimePickerHandler.iOS.cs (+16) | Correct iOS accessibility pattern. Extension methods handle Window null check and main thread dispatch. | - -**Exhausted:** N/A (iOS API requirement - no alternatives to explore) -**Selected Fix:** PR's fix - This is the correct and only way to fix VoiceOver focus on iOS. The PR properly implements the iOS accessibility pattern using `UIAccessibility.PostNotification`, follows MAUI conventions with extension methods, and handles edge cases (null window, main thread dispatch). - -
- -
-📋 Report - -**Status**: ✅ COMPLETE - -### PR Finalization - -**Title:** ✅ Excellent - "[iOS] Fix VoiceOver focus not shifting to Picker/DatePicker/TimePicker popups" - -**Description Recommendations:** -- ⚠️ Add required NOTE block for testing PR artifacts -- ⚠️ Remove "Draft PR until include some related tests" (tests now included) -- ⚠️ Add root cause explanation for future reference - -**Recommended Updated Description:** - -```markdown - -> [!NOTE] -> Are you waiting for the changes in this PR to be merged? -> It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! - -### Description of Change - -This PR fixes VoiceOver accessibility on iOS for Picker, DatePicker, and TimePicker controls. When picker popups open/close, VoiceOver now properly shifts focus using iOS accessibility notifications. - -**Root cause:** VoiceOver on iOS requires explicit focus notifications via `UIAccessibility.PostNotification(ScreenChanged, view)` when modal UI appears or dismisses. Without these notifications, VoiceOver doesn't know to shift focus, leaving users navigating background controls instead of the picker popup. - -**Fix:** -- Adds `PostAccessibilityFocusNotification()` extension methods in `SemanticExtensions.cs` -- Posts `ScreenChanged` notification when picker popups open (in `EditingDidBegin` event) -- Posts `ScreenChanged` notification to restore focus when popups close (in `EditingDidEnd` event) -- Handles edge cases: null window checks, main thread dispatch for delayed InputView availability - -**Tests:** UI tests created in `Issue30746.xaml[.cs]` and marked with `ManualReview` category since VoiceOver focus cannot be automated with Appium. Manual testing with VoiceOver enabled confirms proper focus behavior. - -### Issues Fixed - -Fixes #30746 -``` - -### Code Review Notes - -**Inline Suggestions (from Copilot):** -1. PickerHandler.iOS.cs:109 - Add space after `if` keyword ✓ Minor style -2. PickerHandler.iOS.cs:113 - Move `PostAccessibilityFocusNotification()` inside if block ✓ Good suggestion - should only post if view controller presented -3. Trailing whitespace in TimePickerHandler ✓ Minor cleanup - -**Recommended:** Apply Copilot's inline suggestions before merging. - -### Final Recommendation - -**✅ APPROVE with minor suggestions** - -This PR correctly implements the iOS accessibility pattern for VoiceOver focus management. The implementation is sound, follows MAUI conventions, and properly handles edge cases. - -**Before merging:** -1. Update PR description with NOTE block and remove "Draft" status -2. Apply inline code suggestions (spacing, move notification call) -3. Consider manual VoiceOver testing to confirm behavior - -
- ---- diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 1f870baea84f..3dff7f8f8b97 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -358,7 +358,7 @@ if ($DryRun) { # Branch switching prevention relies on agent instructions in pr.md only # Create log directory for this PR - $prLogDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/copilot-logs" + $prLogDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/copilot-logs" if (-not (Test-Path $prLogDir)) { New-Item -ItemType Directory -Path $prLogDir -Force | Out-Null } @@ -520,7 +520,7 @@ if ($DryRun) { Write-Host "" Write-Host "📋 Plan template: $planTemplatePath" -ForegroundColor Gray if (-not $DryRun) { - Write-Host "📁 Copilot logs: CustomAgentLogsTmp/PRState/$PRNumber/copilot-logs/" -ForegroundColor Gray + Write-Host "📁 Copilot logs: CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/copilot-logs/" -ForegroundColor Gray if (-not $Interactive) { Write-Host "📄 Session markdown: $sessionFile" -ForegroundColor Gray } diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index 86671e3c54b6..67cc63791daa 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -126,8 +126,8 @@ if (-not $PRNumber) { } if (-not $foundPR) { - Write-Host "⚠️ Could not auto-detect PR number - using 'unknown' folder" -ForegroundColor Yellow - $PRNumber = "unknown" + Write-Error "Could not auto-detect PR number. Please provide -PRNumber parameter." + exit 1 } } } From 4c2c32f3aae2dd1bab7ab251ee32cfa19bb76659 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 12 Feb 2026 18:04:57 +0100 Subject: [PATCH 111/126] Use PRAgent subdir for PRState try-fix paths Align docs and script to the new path layout under CustomAgentLogsTmp/PRState/{number}/PRAgent. Updated PLAN-TEMPLATE.md (post-pr-finalize SummaryFile path), SKILL.md (auto-loading description), and post-try-fix-comment.ps1 (examples, parameter docs, and path regex) so try-fix and finalize operations look in the PRAgent/try-fix and PRAgent/pr-finalize locations. --- .github/agents/pr/PLAN-TEMPLATE.md | 2 +- .github/skills/ai-summary-comment/SKILL.md | 2 +- .../scripts/post-pr-finalize-comment.ps1 | 9 ++++++++- .../ai-summary-comment/scripts/post-try-fix-comment.ps1 | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index b54375ebf7d6..6dc5612bcb41 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -87,7 +87,7 @@ See `SHARED-RULES.md` for complete details. Key points: ``` - [ ] Post PR Finalization comment (separate): ```bash - pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/XXXXX/pr-finalize/pr-finalize-summary.md + pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/XXXXX/PRAgent/pr-finalize/pr-finalize-summary.md ``` --- diff --git a/.github/skills/ai-summary-comment/SKILL.md b/.github/skills/ai-summary-comment/SKILL.md index 0802563f2e51..e4eb2a64b72b 100644 --- a/.github/skills/ai-summary-comment/SKILL.md +++ b/.github/skills/ai-summary-comment/SKILL.md @@ -210,7 +210,7 @@ When the same PR is reviewed multiple times (e.g., after new commits), the scrip The `post-try-fix-comment.ps1` script updates the `` section of the unified AI Summary comment. It aggregates all try-fix attempts into collapsible sections. Works for both issues and PRs (GitHub treats PR comments as issue comments). -**✨ Auto-Loading from `CustomAgentLogsTmp`**: The script automatically discovers and aggregates ALL attempt directories from `CustomAgentLogsTmp/PRState/{IssueNumber}/try-fix/`. +**✨ Auto-Loading from `CustomAgentLogsTmp`**: The script automatically discovers and aggregates ALL attempt directories from `CustomAgentLogsTmp/PRState/{IssueNumber}/PRAgent/try-fix/`. ### Usage diff --git a/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 index 3c159b7da1ad..3ab43819a77c 100644 --- a/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 @@ -410,7 +410,14 @@ if ([string]::IsNullOrWhiteSpace($DescriptionStatus)) { } if ([string]::IsNullOrWhiteSpace($DescriptionAssessment)) { - throw "DescriptionAssessment is required. Provide via -DescriptionAssessment or use -SummaryFile" + if (-not [string]::IsNullOrWhiteSpace($SummaryFile)) { + # We have a summary file but couldn't extract a specific assessment section. + # Use the full summary content as the assessment (the whole file IS the assessment). + $DescriptionAssessment = $content + Write-Host "ℹ️ Using full summary file content as DescriptionAssessment" -ForegroundColor Cyan + } else { + throw "DescriptionAssessment is required. Provide via -DescriptionAssessment or use -SummaryFile" + } } # Warn if description needs work but no recommended description is provided diff --git a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 index 28d0bd62c470..27bd4566bcb0 100644 --- a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 @@ -10,7 +10,7 @@ If an existing try-fix comment exists, it will be EDITED with the new attempt added. Otherwise, a new comment will be created. - **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/try-fix/** + **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/** Format: ## 🔧 Try-Fix Analysis for Issue #XXXXX @@ -35,7 +35,7 @@ The attempt number (1, 2, 3, etc.) - auto-detected from TryFixDir if not specified .PARAMETER TryFixDir - Path to try-fix attempt directory (e.g., CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1) + Path to try-fix attempt directory (e.g., CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1) If provided, all parameters are auto-loaded from files in this directory .PARAMETER Approach @@ -61,7 +61,7 @@ .EXAMPLE # Simplest: Just provide attempt directory (all info auto-loaded) - ./post-try-fix-comment.ps1 -TryFixDir CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1 + ./post-try-fix-comment.ps1 -TryFixDir CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1 .EXAMPLE # Post all attempts for an issue @@ -128,7 +128,7 @@ if (-not [string]::IsNullOrWhiteSpace($TryFixDir)) { throw "Try-fix directory not found: $TryFixDir" } - # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1) + # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1) if ($TryFixDir -match '[/\\](\d+)[/\\]try-fix') { if ($IssueNumber -eq 0) { $IssueNumber = [int]$Matches[1] From 2d7afec28dcd1ff6e3181f2847c4248bd048952d Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 12 Feb 2026 18:51:42 +0100 Subject: [PATCH 112/126] Clarify autonomous execution wording Replace CI-specific language with general "autonomous/non-interactive" phrasing and tighten guidance to not prompt a human operator. Updates remove or reword references to "CI mode" and emphasize skipping blocked phases, retrying once, and continuing autonomously. Affected files: .github/agents/pr.md, .github/agents/pr/PLAN-TEMPLATE.md, .github/agents/pr/SHARED-RULES.md, .github/agents/pr/post-gate.md, and .github/scripts/Review-PR.ps1. --- .github/agents/pr.md | 4 ++-- .github/agents/pr/PLAN-TEMPLATE.md | 4 ++-- .github/agents/pr/SHARED-RULES.md | 8 ++++---- .github/agents/pr/post-gate.md | 4 ++-- .github/scripts/Review-PR.ps1 | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/agents/pr.md b/.github/agents/pr.md index 00c755b87c26..e4fdc4163c73 100644 --- a/.github/agents/pr.md +++ b/.github/agents/pr.md @@ -48,13 +48,13 @@ After Gate passes, read `.github/agents/pr/post-gate.md` for **Phases 3-4**. - Follow Templates EXACTLY (no `open` attributes, no "improvements") - No Direct Git Commands (use `gh pr diff/view`, let scripts handle files) - Use Skills' Scripts (don't bypass with manual commands) -- Stop on Environment Blockers (retry once, then skip and continue autonomously in CI mode) +- Stop on Environment Blockers (retry once, then skip and continue autonomously) - Multi-Model Configuration (5 models for Phase 4) - Platform Selection (must be affected AND available on host) **Key points:** - ❌ Never run `git checkout`, `git switch`, `git stash`, `git reset` - agent is always on correct branch -- ❌ Never stop and ask user in CI mode - use best judgment to skip blocked phases and continue +- ❌ Never stop and ask user - use best judgment to skip blocked phases and continue - ❌ Never mark phase ✅ with [PENDING] fields remaining Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed instructions after Gate passes. diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index 6dc5612bcb41..4459f1ce72f7 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -12,7 +12,7 @@ ## 🚨 Critical Rules (Summary) See `SHARED-RULES.md` for complete details. Key points: -- **Environment Blockers**: Skip blocked phase and continue autonomously (CI mode has no human operator) +- **Environment Blockers**: Skip blocked phase and continue autonomously (no human operator) - **No Git Commands**: Never checkout/switch branches - agent is always on correct branch - **Gate via Task Agent**: Never run inline (prevents fabrication) - **Multi-Model try-fix**: 5 models, SEQUENTIAL only @@ -101,4 +101,4 @@ See `SHARED-RULES.md` for complete details. Key points: | Fix | Multi-model try-fix | Skip remaining, proceed to Report | | Report | Post via skill | Document what completed | -**Never:** Claim success without tests, bypass scripts, stop and ask user in CI mode +**Never:** Claim success without tests, bypass scripts, stop and ask user diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index ba83ea9df92b..10210c9316cc 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -155,9 +155,9 @@ When a skill provides a PowerShell script: If you encounter an environment or system setup blocker that prevents completing a phase: -### 🚨 Non-Interactive CI Mode (Default) +### 🚨 Autonomous Execution (Default) -When running via `Review-PR.ps1` (non-interactive/CI mode), there is **NO human operator** to respond to questions. +When running via `Review-PR.ps1`, there is **NO human operator** to respond to questions. **NEVER stop and ask the user. NEVER present options and wait for a choice. Nobody will respond.** @@ -205,8 +205,8 @@ When running with `-Interactive` flag, you MAY ask the user for guidance on bloc - ❌ Claim "verification passed" when tests couldn't actually run - ❌ Install multiple tools/drivers without asking between each - ❌ Spend more than 2-3 tool calls troubleshooting the same blocker -- ❌ **Stop and present options to the user in CI mode** - choose the best option yourself -- ❌ **Wait for user response in CI mode** - nobody will respond +- ❌ **Stop and present options to the user** - choose the best option yourself +- ❌ **Wait for user response** - nobody will respond --- diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index 611d26428dde..6bc0a5630ec1 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -24,9 +24,9 @@ If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 If try-fix cannot run due to environment issues after one retry, **skip the remaining try-fix models and proceed to Report**. Do NOT stop and ask the user. -### 🚨 CRITICAL: Environment Blockers in Phase 3 (CI Mode) +### 🚨 CRITICAL: Environment Blockers in Phase 3 -The default mode is **non-interactive CI** where no human can respond to questions. +The default mode is **non-interactive** — no human can respond to questions. If try-fix cannot run due to: - Missing Appium drivers diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 3dff7f8f8b97..306da8390a46 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -268,8 +268,8 @@ $platformInstruction - You are ALWAYS on the correct branch already - the script handles this - If you think you need to switch branches or push changes, you are WRONG - ask the user instead -🚨 **CRITICAL - NON-INTERACTIVE CI MODE:** -- This is running in a CI environment with NO human operator to respond to questions +🚨 **CRITICAL - AUTONOMOUS EXECUTION:** +- There is NO human operator to respond to questions - NEVER stop and ask the user for input - nobody will respond - NEVER present a list of options and wait for a choice - When you encounter an environment blocker, use your best judgment to choose the best path forward: @@ -292,7 +292,7 @@ $platformInstruction 1. Read the plan template at ``$planTemplatePath`` for the 4-phase workflow 2. Read ``.github/agents/pr.md`` for Phases 1-2 instructions 3. Follow ALL critical rules, especially: - - On environment blockers: skip the blocked phase and continue autonomously (see NON-INTERACTIVE CI MODE above) + - On environment blockers: skip the blocked phase and continue autonomously (see AUTONOMOUS EXECUTION above) - Use task agent for Gate verification - Run multi-model try-fix in Phase 3 From 66788a17e3dc9b68a4e45defd2a9cfef0bb95d94 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 13 Feb 2026 00:39:18 +0100 Subject: [PATCH 113/126] Revert labels logic --- .github/copilot-instructions.md | 60 --- .github/scripts/Review-PR.ps1 | 14 +- .../scripts/helpers/Update-AgentLabels.ps1 | 353 ------------------ .../verify-tests-fail-without-fix/SKILL.md | 22 +- .../scripts/verify-tests-fail.ps1 | 60 ++- 5 files changed, 72 insertions(+), 437 deletions(-) delete mode 100644 .github/scripts/helpers/Update-AgentLabels.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5c55996cadb2..5154cb9dfbe9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -285,66 +285,6 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session. - **Output**: Reports attempt results and failure analysis -### Agent Workflow Labels - -Labels with `s/agent-*` prefix track agent workflow outcomes for metrics. Applied by `Review-PR.ps1` Phase 4. - -**Outcome Labels** (mutually exclusive — one per PR): - -| Label | Description | -|-------|-------------| -| `s/agent-approved` | AI agent recommends approval | -| `s/agent-changes-requested` | AI agent recommends changes | -| `s/agent-review-incomplete` | AI agent could not complete all phases | - -**Signal Labels** (additive): - -| Label | Description | -|-------|-------------| -| `s/agent-gate-passed` | AI verified tests catch the bug | -| `s/agent-gate-failed` | AI could not verify tests catch the bug | -| `s/agent-fix-optimal` | AI confirms PR fix is the best among candidates | - -**Manual Labels** (applied by maintainers): - -| Label | Description | -|-------|-------------| -| `s/agent-fix-implemented` | PR author implemented the agent's suggested fix | - -**Base Label**: `s/agent-reviewed` — always applied on completed agent runs. - -**Helper module**: `.github/scripts/helpers/Update-AgentLabels.ps1` - -### Agent Workflow Labels - -Labels with `s/agent-*` prefix track agent workflow outcomes for metrics. Applied by `Review-PR.ps1` Phase 4. - -**Outcome Labels** (mutually exclusive — one per PR): - -| Label | Description | -|-------|-------------| -| `s/agent-approved` | AI agent recommends approval | -| `s/agent-changes-requested` | AI agent recommends changes | -| `s/agent-review-incomplete` | AI agent could not complete all phases | - -**Signal Labels** (additive): - -| Label | Description | -|-------|-------------| -| `s/agent-gate-passed` | AI verified tests catch the bug | -| `s/agent-gate-failed` | AI could not verify tests catch the bug | -| `s/agent-fix-optimal` | AI confirms PR fix is the best among candidates | - -**Manual Labels** (applied by maintainers): - -| Label | Description | -|-------|-------------| -| `s/agent-fix-implemented` | PR author implemented the agent's suggested fix | - -**Base Label**: `s/agent-reviewed` — always applied on completed agent runs. - -**Helper module**: `.github/scripts/helpers/Update-AgentLabels.ps1` - ### Using Custom Agents **Delegation Policy**: When user request matches agent trigger phrases, **ALWAYS delegate to the appropriate agent immediately**. Do not ask for permission or explain alternatives unless the request is ambiguous. diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 306da8390a46..bbb3a67e2127 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -501,19 +501,7 @@ if ($DryRun) { } } } - - # Phase 4: Apply agent workflow labels - $labelHelperPath = ".github/scripts/helpers/Update-AgentLabels.ps1" - if (-not (Test-Path $labelHelperPath)) { - Write-Host "⚠️ Label helper missing, attempting recovery..." -ForegroundColor Yellow - git checkout HEAD -- $labelHelperPath 2>&1 | Out-Null - } - if (Test-Path $labelHelperPath) { - . $labelHelperPath - Invoke-AgentLabels -PRNumber $PRNumber - } else { - Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow - } + } } diff --git a/.github/scripts/helpers/Update-AgentLabels.ps1 b/.github/scripts/helpers/Update-AgentLabels.ps1 deleted file mode 100644 index 5b590386c643..000000000000 --- a/.github/scripts/helpers/Update-AgentLabels.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -<# -.SYNOPSIS - Shared helper module for applying agent workflow labels to PRs. - -.DESCRIPTION - Provides functions to apply outcome labels (mutually exclusive) and signal labels - (additive) to PRs based on agent workflow results. All labels use the s/agent-* prefix. - - Label Categories: - - Outcome (mutually exclusive): s/agent-approved, s/agent-changes-requested, s/agent-review-incomplete - - Signal (additive): s/agent-gate-passed, s/agent-gate-failed, s/agent-fix-optimal - - Base: s/agent-reviewed (always applied on completed runs) - - Manual: s/agent-fix-implemented (applied by maintainers, never auto-applied) - -.NOTES - All functions use gh api REST calls. Label failures are warnings, never fatal errors. -#> - -# ============================================================ -# Label Definitions -# ============================================================ - -$script:OutcomeLabels = @{ - Approved = "s/agent-approved" - ChangesRequested = "s/agent-changes-requested" - ReviewIncomplete = "s/agent-review-incomplete" -} - -$script:SignalLabels = @{ - GatePassed = "s/agent-gate-passed" - GateFailed = "s/agent-gate-failed" - FixOptimal = "s/agent-fix-optimal" -} - -$script:BaseLabel = "s/agent-reviewed" - -# Label definitions for auto-creation (Ensure-LabelExists) -$script:LabelDefinitions = @{ - "s/agent-reviewed" = @{ Color = "2E7D32"; Description = "PR was reviewed by AI agent workflow" } - "s/agent-approved" = @{ Color = "2E7D32"; Description = "AI agent recommends approval" } - "s/agent-changes-requested" = @{ Color = "E65100"; Description = "AI agent recommends changes" } - "s/agent-review-incomplete" = @{ Color = "B71C1C"; Description = "AI agent could not complete all phases" } - "s/agent-gate-passed" = @{ Color = "4CAF50"; Description = "AI verified tests catch the bug" } - "s/agent-gate-failed" = @{ Color = "FF9800"; Description = "AI could not verify tests catch the bug" } - "s/agent-fix-optimal" = @{ Color = "66BB6A"; Description = "AI confirms PR fix is the best among candidates" } - "s/agent-fix-implemented" = @{ Color = "7B1FA2"; Description = "PR author implemented the agent suggested fix" } -} - -# ============================================================ -# Helper Functions -# ============================================================ - -function Get-PRExistingLabels { - param([string]$PRNumber) - - $labels = gh pr view $PRNumber --repo dotnet/maui --json labels --jq '.labels[].name' 2>$null - if ($LASTEXITCODE -ne 0) { - Write-Host " ⚠️ Failed to fetch existing labels for PR #$PRNumber" -ForegroundColor Yellow - return @() - } - return @($labels | Where-Object { $_ }) -} - -function Add-Label { - param([string]$PRNumber, [string]$Label) - - $result = gh api "repos/dotnet/maui/issues/$PRNumber/labels" --method POST -f "labels[]=$Label" 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host " ⚠️ Failed to add label: $Label ($result)" -ForegroundColor Yellow - return $false - } - return $true -} - -function Remove-Label { - param([string]$PRNumber, [string]$Label) - - # URL-encode the label name (handles / in s/agent-*) - $encodedLabel = [System.Uri]::EscapeDataString($Label) - gh api "repos/dotnet/maui/issues/$PRNumber/labels/$encodedLabel" --method DELETE 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-Host " ⚠️ Failed to remove label: $Label" -ForegroundColor Yellow - return $false - } - return $true -} - -# ============================================================ -# Public Functions -# ============================================================ - -function Ensure-LabelExists { - <# - .SYNOPSIS - Creates a label in the repo if it doesn't already exist. - #> - param([string]$Label) - - if (-not $script:LabelDefinitions.ContainsKey($Label)) { - Write-Host " ⚠️ Unknown label: $Label" -ForegroundColor Yellow - return - } - - $def = $script:LabelDefinitions[$Label] - - # Check if label exists - $existing = gh label list --repo dotnet/maui --search $Label --limit 1 --json name --jq '.[].name' 2>$null - if ($existing -eq $Label) { - return # Already exists - } - - Write-Host " 📌 Creating label: $Label" -ForegroundColor Cyan - gh label create $Label --repo dotnet/maui --color $def.Color --description $def.Description --force 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-Host " ⚠️ Failed to create label: $Label" -ForegroundColor Yellow - } -} - -function Update-AgentOutcomeLabel { - <# - .SYNOPSIS - Applies exactly one outcome label and removes conflicting ones. - .PARAMETER Outcome - One of: 'Approved', 'ChangesRequested', 'ReviewIncomplete' - #> - param( - [Parameter(Mandatory)] - [ValidateSet('Approved', 'ChangesRequested', 'ReviewIncomplete')] - [string]$Outcome, - - [Parameter(Mandatory)] - [string]$PRNumber - ) - - $labelToAdd = $script:OutcomeLabels[$Outcome] - $labelsToRemove = $script:OutcomeLabels.Values | Where-Object { $_ -ne $labelToAdd } - - $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber - - # Remove conflicting outcome labels - foreach ($label in $labelsToRemove) { - if ($existingLabels -contains $label) { - Write-Host " 🔄 Removing: $label" -ForegroundColor Yellow - Remove-Label -PRNumber $PRNumber -Label $label | Out-Null - } - } - - # Add the outcome label - if ($existingLabels -notcontains $labelToAdd) { - Write-Host " ✅ Adding: $labelToAdd" -ForegroundColor Green - Add-Label -PRNumber $PRNumber -Label $labelToAdd | Out-Null - } else { - Write-Host " ℹ️ Already has: $labelToAdd" -ForegroundColor Gray - } -} - -function Update-AgentSignalLabel { - <# - .SYNOPSIS - Adds or removes a signal label. For mutually exclusive pairs (gate-passed/gate-failed), - adding one removes the other. - .PARAMETER Signal - One of: 'GatePassed', 'GateFailed', 'FixOptimal' - #> - param( - [Parameter(Mandatory)] - [ValidateSet('GatePassed', 'GateFailed', 'FixOptimal')] - [string]$Signal, - - [Parameter(Mandatory)] - [string]$PRNumber, - - [switch]$Remove - ) - - $label = $script:SignalLabels[$Signal] - $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber - - if ($Remove) { - if ($existingLabels -contains $label) { - Write-Host " 🔄 Removing: $label" -ForegroundColor Yellow - Remove-Label -PRNumber $PRNumber -Label $label | Out-Null - } - return - } - - # For gate labels, they are mutually exclusive with each other - if ($Signal -eq 'GatePassed' -and $existingLabels -contains $script:SignalLabels['GateFailed']) { - Write-Host " 🔄 Removing: $($script:SignalLabels['GateFailed'])" -ForegroundColor Yellow - Remove-Label -PRNumber $PRNumber -Label $script:SignalLabels['GateFailed'] | Out-Null - } - elseif ($Signal -eq 'GateFailed' -and $existingLabels -contains $script:SignalLabels['GatePassed']) { - Write-Host " 🔄 Removing: $($script:SignalLabels['GatePassed'])" -ForegroundColor Yellow - Remove-Label -PRNumber $PRNumber -Label $script:SignalLabels['GatePassed'] | Out-Null - } - - if ($existingLabels -notcontains $label) { - Write-Host " ✅ Adding: $label" -ForegroundColor Green - Add-Label -PRNumber $PRNumber -Label $label | Out-Null - } else { - Write-Host " ℹ️ Already has: $label" -ForegroundColor Gray - } -} - -function Update-AgentReviewedLabel { - <# - .SYNOPSIS - Applies the base s/agent-reviewed label to mark the PR as agent-reviewed. - #> - param( - [Parameter(Mandatory)] - [string]$PRNumber - ) - - $existingLabels = Get-PRExistingLabels -PRNumber $PRNumber - - if ($existingLabels -notcontains $script:BaseLabel) { - Write-Host " ✅ Adding: $($script:BaseLabel)" -ForegroundColor Green - Add-Label -PRNumber $PRNumber -Label $script:BaseLabel | Out-Null - } else { - Write-Host " ℹ️ Already has: $($script:BaseLabel)" -ForegroundColor Gray - } -} - -function Invoke-AgentLabels { - <# - .SYNOPSIS - Main entry point: parses a state file and applies all appropriate labels. - .PARAMETER StateFile - Path to the PR state markdown file (e.g., CustomAgentLogsTmp/PRState/pr-33528.md) - .PARAMETER PRNumber - The PR number to apply labels to. - #> - param( - [Parameter(Mandatory)] - [string]$StateFile, - - [Parameter(Mandatory)] - [string]$PRNumber - ) - - if (-not (Test-Path $StateFile)) { - Write-Host " ⚠️ State file not found: $StateFile" -ForegroundColor Yellow - return - } - - $content = Get-Content $StateFile -Raw - - Write-Host "" - Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue - Write-Host "║ PHASE 4: APPLY LABELS ║" -ForegroundColor Blue - Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue - Write-Host "" - Write-Host "🏷️ Applying agent workflow labels to PR #$PRNumber..." -ForegroundColor Cyan - - # 1. Always apply s/agent-reviewed - Update-AgentReviewedLabel -PRNumber $PRNumber - - # 2. Determine and apply outcome label - $outcome = Get-OutcomeFromState -Content $content - Write-Host " 📊 Outcome: $outcome" -ForegroundColor Cyan - Update-AgentOutcomeLabel -Outcome $outcome -PRNumber $PRNumber - - # 3. Determine and apply signal labels - $gateResult = Get-GateResultFromState -Content $content - if ($gateResult -eq 'Passed') { - Update-AgentSignalLabel -Signal GatePassed -PRNumber $PRNumber - } elseif ($gateResult -eq 'Failed') { - Update-AgentSignalLabel -Signal GateFailed -PRNumber $PRNumber - } - - $fixOptimal = Get-FixOptimalFromState -Content $content - if ($fixOptimal) { - Update-AgentSignalLabel -Signal FixOptimal -PRNumber $PRNumber - } - - Write-Host "" - Write-Host "✅ Labels applied to PR #$PRNumber" -ForegroundColor Green -} - -# ============================================================ -# State File Parsing -# ============================================================ - -function Get-OutcomeFromState { - param([string]$Content) - - # Check for Final Recommendation - if ($Content -match '(?i)Final Recommendation[:\s]*APPROVE') { - return 'Approved' - } - if ($Content -match '(?i)Final Recommendation[:\s]*(REQUEST[_ ]CHANGES|CHANGES[_ ]REQUESTED)') { - return 'ChangesRequested' - } - # Check for Verdict line (alternative format) - if ($Content -match '(?i)Verdict[:\s]*✅\s*APPROVE') { - return 'Approved' - } - if ($Content -match '(?i)Verdict[:\s]*(⚠️|❌)') { - return 'ChangesRequested' - } - - # Check if all phases completed — if not, it's incomplete - $phases = @('Pre-Flight', 'Tests', 'Gate', 'Fix', 'Report') - $allComplete = $true - foreach ($phase in $phases) { - if ($Content -notmatch "(?i)$phase\s*\|\s*✅") { - $allComplete = $false - break - } - } - - if (-not $allComplete) { - return 'ReviewIncomplete' - } - - # Phases complete but no clear recommendation — default to incomplete - return 'ReviewIncomplete' -} - -function Get-GateResultFromState { - param([string]$Content) - - # Check the phase status table - if ($Content -match '(?i)Gate\s*\|\s*✅\s*PASSED') { - return 'Passed' - } - if ($Content -match '(?i)Gate\s*\|\s*❌\s*FAILED') { - return 'Failed' - } - if ($Content -match '(?i)Gate\s*\|\s*⚠️') { - return 'Failed' - } - - return $null # Gate not run or status unclear -} - -function Get-FixOptimalFromState { - param([string]$Content) - - # Look for indicators that the PR's fix was selected as best - if ($Content -match '(?i)Selected Fix[:\s]*PR') { - return $true - } - if ($Content -match '(?i)PR.s fix is.*best') { - return $true - } - if ($Content -match '(?i)PR fix.*optimal') { - return $true - } - - return $false -} diff --git a/.github/skills/verify-tests-fail-without-fix/SKILL.md b/.github/skills/verify-tests-fail-without-fix/SKILL.md index 3b81b7ea1c88..ba3df1d1aaa6 100644 --- a/.github/skills/verify-tests-fail-without-fix/SKILL.md +++ b/.github/skills/verify-tests-fail-without-fix/SKILL.md @@ -81,7 +81,8 @@ The script auto-detects which mode to use based on whether fix files are present 1. Fetches base branch from origin (if available) 2. Auto-detects test classes from changed test files 3. Runs tests (should FAIL to prove they catch the bug) -4. Reports result +4. **Updates PR labels** based on result +5. Reports result **Full Verification Mode (fix files detected):** 1. Fetches base branch from origin to ensure accurate diff @@ -94,16 +95,23 @@ The script auto-detects which mode to use based on whether fix files are present 8. **Generates markdown reports**: - `CustomAgentLogsTmp/TestValidation/verification-report.md` - Full detailed report - `CustomAgentLogsTmp/PRState/verification-report.md` - Gate section for PR agent -9. Reports result +9. **Updates PR labels** based on result +10. Reports result ## PR Labels -Labels are managed centrally by `Review-PR.ps1` Phase 4 using the shared helper module -(`.github/scripts/helpers/Update-AgentLabels.ps1`). This skill no longer applies labels directly. +The skill automatically manages two labels on the PR to indicate verification status: -Gate results are reflected via: -- `s/agent-gate-passed` — Tests correctly FAIL without fix (verified tests catch the bug) -- `s/agent-gate-failed` — Tests PASS without fix (tests don't catch the bug) +| Label | Color | When Applied | +|-------|-------|--------------| +| `s/ai-reproduction-confirmed` | 🟢 Green (#2E7D32) | Tests correctly FAIL without fix (AI verified tests catch the bug) | +| `s/ai-reproduction-failed` | 🟠 Orange (#E65100) | Tests PASS without fix (AI verified tests don't catch the bug) | + +**Behavior:** +- When verification passes, adds `s/ai-reproduction-confirmed` and removes `s/ai-reproduction-failed` if present +- When verification fails, adds `s/ai-reproduction-failed` and removes `s/ai-reproduction-confirmed` if present +- If a PR is re-verified after fixing tests, labels are updated accordingly +- No label = AI hasn't verified tests yet ## Output Files diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index 67cc63791daa..bd3fd7d0fb4c 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -145,8 +145,58 @@ $BaselineScript = Join-Path $RepoRoot ".github/scripts/EstablishBrokenBaseline.p . $BaselineScript # ============================================================ -# Note: Label management moved to .github/scripts/helpers/Update-AgentLabels.ps1 -# Labels are now applied centrally by Review-PR.ps1 Phase 4. +# Label management for verification results +# ============================================================ +$LabelConfirmed = "s/ai-reproduction-confirmed" +$LabelFailed = "s/ai-reproduction-failed" + +function Update-VerificationLabels { + param( + [Parameter(Mandatory = $true)] + [bool]$ReproductionConfirmed, + + [Parameter(Mandatory = $false)] + [string]$PR = $PRNumber + ) + + if ($PR -eq "unknown" -or -not $PR) { + Write-Host "⚠️ Cannot update labels: PR number not available" -ForegroundColor Yellow + return + } + + $labelToAdd = if ($ReproductionConfirmed) { $LabelConfirmed } else { $LabelFailed } + $labelToRemove = if ($ReproductionConfirmed) { $LabelFailed } else { $LabelConfirmed } + + Write-Host "" + Write-Host "🏷️ Updating verification labels on PR #$PR..." -ForegroundColor Cyan + + # Track success for both operations + $removeSuccess = $true + + # Remove the opposite label if it exists (using REST API to avoid GraphQL deprecation issues) + $existingLabels = gh pr view $PR --json labels --jq '.labels[].name' 2>$null + if ($existingLabels -contains $labelToRemove) { + Write-Host " Removing: $labelToRemove" -ForegroundColor Yellow + gh api "repos/dotnet/maui/issues/$PR/labels/$labelToRemove" --method DELETE 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + $removeSuccess = $false + Write-Host " ⚠️ Failed to remove label: $labelToRemove" -ForegroundColor Yellow + } + } + + # Add the appropriate label (using REST API to avoid GraphQL deprecation issues) + Write-Host " Adding: $labelToAdd" -ForegroundColor Green + $result = gh api "repos/dotnet/maui/issues/$PR/labels" --method POST -f "labels[]=$labelToAdd" 2>&1 + $addSuccess = $LASTEXITCODE -eq 0 + + if ($addSuccess -and $removeSuccess) { + Write-Host "✅ Labels updated successfully" -ForegroundColor Green + } elseif ($addSuccess) { + Write-Host "⚠️ Label added but failed to remove old label" -ForegroundColor Yellow + } else { + Write-Host "⚠️ Failed to update labels: $result" -ForegroundColor Yellow + } +} # ============================================================ # Auto-detect test filter from changed files @@ -416,6 +466,7 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" Write-Host "Failed tests: $($testResult.FailCount)" -ForegroundColor Yellow + Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { # Tests PASSED - this is bad! @@ -436,6 +487,7 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" Write-Host "Passed tests: $($testResult.PassCount)" -ForegroundColor Yellow + Update-VerificationLabels -ReproductionConfirmed $false exit 1 } } @@ -830,8 +882,8 @@ if ($verificationPassed) { Write-Host "╠═══════════════════════════════════════════════════════════╣" -ForegroundColor Green Write-Host "║ Tests correctly detect the issue: ║" -ForegroundColor Green Write-Host "║ - FAIL without fix (as expected) ║" -ForegroundColor Green - Write-Host "║ - PASS with fix (as expected) ║" -ForegroundColor Green Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green + Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { Write-Host "" @@ -851,7 +903,7 @@ if ($verificationPassed) { Write-Host "║ 1. Wrong fix files specified ║" -ForegroundColor Red Write-Host "║ 2. Tests don't actually test the fixed behavior ║" -ForegroundColor Red Write-Host "║ 3. The issue was already fixed in base branch ║" -ForegroundColor Red - Write-Host "║ 4. Build caching - try clean rebuild ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red + Update-VerificationLabels -ReproductionConfirmed $false exit 1 } From ef0f542db5edf8d06b67f9e6cc0b1779f88c4caa Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 13 Feb 2026 13:24:49 +0100 Subject: [PATCH 114/126] Always auto-load PRAgent phase files Remove the optional -Content parameter and make post-ai-summary-comment.ps1 always load phase content from CustomAgentLogsTmp/PRState//PRAgent/*/content.md. Update script help, examples, and validation messages; refactor auto-load logic to locate the repo root, load available phase files (pre-flight, gate, try-fix, report), build a status table and per-phase details, and synthesize the final comment. Also update SKILL.md to remove the "Provide content directly" section, adjust the Parameters table, and clarify the auto-loading behavior in the documentation. --- .github/skills/ai-summary-comment/SKILL.md | 9 +- .../scripts/post-ai-summary-comment.ps1 | 182 +++++++++--------- 2 files changed, 89 insertions(+), 102 deletions(-) diff --git a/.github/skills/ai-summary-comment/SKILL.md b/.github/skills/ai-summary-comment/SKILL.md index e4eb2a64b72b..073fea7448fb 100644 --- a/.github/skills/ai-summary-comment/SKILL.md +++ b/.github/skills/ai-summary-comment/SKILL.md @@ -103,18 +103,11 @@ If an existing finalize comment exists, it will be replaced with the updated sec pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27246 ``` -### Provide content directly - -```bash -pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content here" -``` - ### Parameters | Parameter | Required | Description | Example | |-----------|----------|-------------|---------| | `PRNumber` | Yes | Pull request number | `12345` | -| `Content` | No | Review content to post (auto-loaded from `PRAgent/*/content.md` if not provided) | Review markdown content | | `DryRun` | No | Preview changes in local file instead of posting to GitHub | `-DryRun` | | `PreviewFile` | No | Path to local preview file for DryRun mode (default: `CustomAgentLogsTmp/PRState/{PRNumber}/ai-summary-comment-preview.md`) | `-PreviewFile ./preview.md` | | `SkipValidation` | No | Skip validation checks (not recommended) | `-SkipValidation` | @@ -563,7 +556,7 @@ CustomAgentLogsTmp/PRState/{PRNumber}/ ### Auto-Loading Behavior -When `post-ai-summary-comment.ps1` is called **without `-Content`**, it auto-discovers phase files: +When `post-ai-summary-comment.ps1` is called, it auto-discovers phase files: 1. Checks `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/*/content.md` 2. Loads all available phase content files 3. Builds the comment structure from the loaded files diff --git a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 index c6dde8348363..9c1353794cbd 100644 --- a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -7,6 +7,9 @@ Creates ONE comment for the entire PR review with all phases wrapped in an expandable section. Uses HTML marker for identification. + Content is always auto-loaded from PRAgent phase files + (CustomAgentLogsTmp/PRState//PRAgent/*/content.md). + **Validates that phases marked as COMPLETE actually have content.** Format: @@ -18,9 +21,6 @@ .PARAMETER PRNumber The pull request number (required) -.PARAMETER Content - The review content to post - .PARAMETER DryRun Print comment instead of posting @@ -28,19 +28,16 @@ Skip validation checks (not recommended) .EXAMPLE - ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content" + ./post-ai-summary-comment.ps1 -PRNumber 12345 .EXAMPLE - ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "review content" -DryRun + ./post-ai-summary-comment.ps1 -PRNumber 12345 -DryRun #> param( [Parameter(Mandatory=$false)] [int]$PRNumber, - [Parameter(Mandatory=$false)] - [string]$Content, - [Parameter(Mandatory=$false)] [switch]$DryRun, @@ -61,71 +58,70 @@ if ($PRNumber -eq 0) { throw "PRNumber is required." } -# Auto-load from PRAgent phase files if Content not provided -if ([string]::IsNullOrWhiteSpace($Content)) { - Write-Host "ℹ️ No -Content provided, auto-loading from PRAgent phase files..." -ForegroundColor Cyan - - $PRAgentDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" - if (-not (Test-Path $PRAgentDir)) { - $repoRoot = git rev-parse --show-toplevel 2>$null - if ($repoRoot) { - $PRAgentDir = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" - } - } - - if (-not (Test-Path $PRAgentDir)) { - Write-Host "" - Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red - Write-Host "║ ⛔ No content provided and no PRAgent directory found ║" -ForegroundColor Red - Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red - Write-Host "" - Write-Host "Expected directory: $PRAgentDir" -ForegroundColor Yellow - Write-Host "Either provide -Content parameter or ensure PRAgent phase files exist." -ForegroundColor Yellow - throw "Content is required. Provide via -Content or ensure PRAgent/*/content.md files exist." - } - - # Load each phase content file - $phaseFiles = @{ - "pre-flight" = Join-Path $PRAgentDir "pre-flight/content.md" - "gate" = Join-Path $PRAgentDir "gate/content.md" - "try-fix" = Join-Path $PRAgentDir "try-fix/content.md" - "report" = Join-Path $PRAgentDir "report/content.md" - } - - $loadedPhases = @() - $phaseContentMap = @{} - - foreach ($phase in $phaseFiles.GetEnumerator()) { - if (Test-Path $phase.Value) { - $phaseContentMap[$phase.Key] = Get-Content $phase.Value -Raw -Encoding UTF8 - $loadedPhases += $phase.Key - Write-Host " ✅ Loaded: $($phase.Key) ($((Get-Item $phase.Value).Length) bytes)" -ForegroundColor Green - } else { - Write-Host " ⏭️ Skipped: $($phase.Key) (no content.md)" -ForegroundColor Gray - } +# Auto-load from PRAgent phase files +Write-Host "ℹ️ Auto-loading from PRAgent phase files..." -ForegroundColor Cyan + +$PRAgentDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" +if (-not (Test-Path $PRAgentDir)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $PRAgentDir = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" } - - if ($loadedPhases.Count -eq 0) { - throw "No phase content files found in $PRAgentDir. Ensure at least one phase has a content.md file." +} + +if (-not (Test-Path $PRAgentDir)) { + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red + Write-Host "║ ⛔ No PRAgent directory found ║" -ForegroundColor Red + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red + Write-Host "" + Write-Host "Expected directory: $PRAgentDir" -ForegroundColor Yellow + Write-Host "Ensure PRAgent phase files exist." -ForegroundColor Yellow + throw "No PRAgent directory found. Ensure PRAgent/*/content.md files exist." +} + +# Load each phase content file +$phaseFiles = @{ + "pre-flight" = Join-Path $PRAgentDir "pre-flight/content.md" + "gate" = Join-Path $PRAgentDir "gate/content.md" + "try-fix" = Join-Path $PRAgentDir "try-fix/content.md" + "report" = Join-Path $PRAgentDir "report/content.md" +} + +$loadedPhases = @() +$phaseContentMap = @{} + +foreach ($phase in $phaseFiles.GetEnumerator()) { + if (Test-Path $phase.Value) { + $phaseContentMap[$phase.Key] = Get-Content $phase.Value -Raw -Encoding UTF8 + $loadedPhases += $phase.Key + Write-Host " ✅ Loaded: $($phase.Key) ($((Get-Item $phase.Value).Length) bytes)" -ForegroundColor Green + } else { + Write-Host " ⏭️ Skipped: $($phase.Key) (no content.md)" -ForegroundColor Gray } - - Write-Host " 📦 Loaded $($loadedPhases.Count) phase(s): $($loadedPhases -join ', ')" -ForegroundColor Cyan - - # Build synthetic Content from phase files in the expected
format - $syntheticParts = @() - - # Determine phase statuses based on which files exist and content - $phaseStatusMap = @{} - foreach ($phase in @("pre-flight", "gate", "try-fix", "report")) { - if ($phaseContentMap.ContainsKey($phase)) { - $phaseStatusMap[$phase] = "✅ COMPLETE" - } else { - $phaseStatusMap[$phase] = "⏳ PENDING" - } +} + +if ($loadedPhases.Count -eq 0) { + throw "No phase content files found in $PRAgentDir. Ensure at least one phase has a content.md file." +} + +Write-Host " 📦 Loaded $($loadedPhases.Count) phase(s): $($loadedPhases -join ', ')" -ForegroundColor Cyan + +# Build synthetic Content from phase files in the expected
format +$syntheticParts = @() + +# Determine phase statuses based on which files exist and content +$phaseStatusMap = @{} +foreach ($phase in @("pre-flight", "gate", "try-fix", "report")) { + if ($phaseContentMap.ContainsKey($phase)) { + $phaseStatusMap[$phase] = "✅ COMPLETE" + } else { + $phaseStatusMap[$phase] = "⏳ PENDING" } - - # Build status table - $statusTable = @" +} + +# Build status table +$statusTable = @" | Phase | Status | |-------|--------| | Pre-Flight | $($phaseStatusMap['pre-flight']) | @@ -133,65 +129,63 @@ if ([string]::IsNullOrWhiteSpace($Content)) { | Fix | $($phaseStatusMap['try-fix']) | | Report | $($phaseStatusMap['report']) | "@ - $syntheticParts += $statusTable - - # Build phase sections - if ($phaseContentMap.ContainsKey('pre-flight')) { - $syntheticParts += @" +$syntheticParts += $statusTable + +# Build phase sections +if ($phaseContentMap.ContainsKey('pre-flight')) { + $syntheticParts += @"
📋 Pre-Flight — Issue Summary $($phaseContentMap['pre-flight'])
"@ - } - - if ($phaseContentMap.ContainsKey('gate')) { - $syntheticParts += @" +} + +if ($phaseContentMap.ContainsKey('gate')) { + $syntheticParts += @"
🚦 Gate — Test Verification $($phaseContentMap['gate'])
"@ - } - - if ($phaseContentMap.ContainsKey('try-fix')) { - $syntheticParts += @" +} + +if ($phaseContentMap.ContainsKey('try-fix')) { + $syntheticParts += @"
🔧 Fix — Analysis & Comparison $($phaseContentMap['try-fix'])
"@ - } - - if ($phaseContentMap.ContainsKey('report')) { - $syntheticParts += @" +} + +if ($phaseContentMap.ContainsKey('report')) { + $syntheticParts += @"
📋 Report — Final Recommendation $($phaseContentMap['report'])
"@ - } - - $Content = $syntheticParts -join "`n`n---`n`n" - Write-Host " ✅ Built synthetic content ($($Content.Length) chars)" -ForegroundColor Green } +$Content = $syntheticParts -join "`n`n---`n`n" +Write-Host " ✅ Built synthetic content ($($Content.Length) chars)" -ForegroundColor Green + # Final validation if ([string]::IsNullOrWhiteSpace($Content)) { Write-Host "" Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red - Write-Host "║ ⛔ No content provided ║" -ForegroundColor Red + Write-Host "║ ⛔ No content loaded from phase files ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" Write-Host "Usage:" -ForegroundColor Yellow - Write-Host " ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content `"review content`"" -ForegroundColor Gray Write-Host " ./post-ai-summary-comment.ps1 -PRNumber 12345 # auto-loads from PRAgent/*/content.md" -ForegroundColor Gray Write-Host "" - throw "Content is required." + throw "No content loaded from PRAgent phase files." } Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan From 4519bc6ddb70e5e19a3776b27d7a95169a0d8605 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 13 Feb 2026 17:17:58 +0100 Subject: [PATCH 115/126] Pin restore point and restore git state between phases Save the current branch and commit SHA before running the PR agent and use that pinned restore point to reliably restore the working tree between phases. Detects if the agent or finalize step changed branch/HEAD and recovers via git checkout/reset to the saved branch+SHA; otherwise performs targeted checkouts from the pinned SHA. Also update targeted file recoveries to use the pinned SHA. Additionally, clarify the try-fix skill docs: the baseline script requires the PR changes to be present on the current branch and should be reported as Blocked rather than switching branches when fix files are missing. --- .github/scripts/Review-PR.ps1 | 51 ++++++++++++++++++++++++++------- .github/skills/try-fix/SKILL.md | 4 +-- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index bbb3a67e2127..fc1a5a65c373 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -247,6 +247,14 @@ foreach ($subdir in $phaseSubdirs) { } Write-Host " 📁 Created PRAgent phase directories: $PRAgentDir" -ForegroundColor Gray +# Save the branch name and commit SHA BEFORE launching the agent. +# The Copilot agent may change HEAD (e.g., via git checkout, gh pr checkout, git reset) +# despite prompt instructions forbidding it. Using a pinned SHA for restoration +# ensures we always recover to the correct state regardless of what the agent did. +$savedBranch = git branch --show-current +$savedHead = git rev-parse HEAD +Write-Host " 📌 Pinned restore point: $savedBranch @ $($savedHead.Substring(0, 10))" -ForegroundColor Gray + # Step 4: Build the prompt for Copilot CLI $planTemplatePath = ".github/agents/pr/PLAN-TEMPLATE.md" @@ -397,14 +405,25 @@ if ($DryRun) { # Restore tracked files to clean state before running post-completion skills. # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, - # which can cause skill files to be missing or modified in subsequent phases. - # NOTE: CustomAgentLogsTmp/ is .gitignore'd and untracked, - # so this won't touch them. Using HEAD to also restore deleted files. + # or even switched branches despite instructions. We use the pinned SHA ($savedHead) + # instead of HEAD to guarantee restoration to the correct commit. + # NOTE: CustomAgentLogsTmp/ is .gitignore'd and untracked, so this won't touch them. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase1-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null - Write-Host " ✅ Working tree restored" -ForegroundColor Green + + # Check if the agent moved HEAD or switched branches + $postAgentBranch = git branch --show-current + $postAgentHead = git rev-parse HEAD + if ($postAgentBranch -ne $savedBranch -or $postAgentHead -ne $savedHead) { + Write-Host " ⚠️ Agent changed git state! Branch: $postAgentBranch (expected: $savedBranch), HEAD: $($postAgentHead.Substring(0, 10)) (expected: $($savedHead.Substring(0, 10)))" -ForegroundColor Red + Write-Host " 🔄 Recovering to pinned restore point..." -ForegroundColor Yellow + git checkout $savedBranch 2>&1 | Out-Null + git reset --hard $savedHead 2>&1 | Out-Null + } else { + git checkout $savedHead -- . 2>&1 | Out-Null + } + Write-Host " ✅ Working tree restored (pinned SHA: $($savedHead.Substring(0, 10)))" -ForegroundColor Green # Phase 2: Run pr-finalize skill if requested if ($RunFinalize) { @@ -450,17 +469,27 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # Restore tracked files (including deleted ones) to clean state. + # Restore tracked files (including deleted ones) to clean state using pinned SHA. Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase2-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null - Write-Host " ✅ Working tree restored" -ForegroundColor Green + + # Check if Phase 2 (pr-finalize) moved HEAD or switched branches + $postPhase2Branch = git branch --show-current + $postPhase2Head = git rev-parse HEAD + if ($postPhase2Branch -ne $savedBranch -or $postPhase2Head -ne $savedHead) { + Write-Host " ⚠️ Phase 2 changed git state! Recovering to pinned restore point..." -ForegroundColor Red + git checkout $savedBranch 2>&1 | Out-Null + git reset --hard $savedHead 2>&1 | Out-Null + } else { + git checkout $savedHead -- . 2>&1 | Out-Null + } + Write-Host " ✅ Working tree restored (pinned SHA: $($savedHead.Substring(0, 10)))" -ForegroundColor Green # 3a: Post PR agent summary comment $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" if (-not (Test-Path $scriptPath)) { - Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $scriptPath 2>&1 | Out-Null + Write-Host "⚠️ Script missing after restore, attempting targeted recovery..." -ForegroundColor Yellow + git checkout $savedHead -- $scriptPath 2>&1 | Out-Null } if (Test-Path $scriptPath) { Write-Host "💬 Running post-ai-summary-comment.ps1 directly..." -ForegroundColor Yellow @@ -483,7 +512,7 @@ if ($DryRun) { $finalizeScriptPath = ".github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1" if (-not (Test-Path $finalizeScriptPath)) { Write-Host "⚠️ Finalize script missing, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $finalizeScriptPath 2>&1 | Out-Null + git checkout $savedHead -- $finalizeScriptPath 2>&1 | Out-Null } if (Test-Path $finalizeScriptPath) { Write-Host "💬 Running post-pr-finalize-comment.ps1 directly..." -ForegroundColor Yellow diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index db87ac5f62c2..d13690296403 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -193,7 +193,7 @@ The skill is complete when: pwsh .github/scripts/EstablishBrokenBaseline.ps1 *>&1 | Tee-Object -FilePath "$OUTPUT_DIR/baseline.log" ``` -The script auto-detects and reverts fix files to merge-base state while preserving test files. **Will fail fast if no fix files detected** - you must be on the actual PR branch. Optional flags: `-BaseBranch main`, `-DryRun`. +The script auto-detects and reverts fix files to merge-base state while preserving test files. **Will fail fast if no fix files detected** - the PR's changes must be present in the current branch (the `Review-PR.ps1` script handles this by merging the PR before the agent runs). Optional flags: `-BaseBranch main`, `-DryRun`. **Verify baseline was established:** ```powershell @@ -201,7 +201,7 @@ The script auto-detects and reverts fix files to merge-base state while preservi Select-String -Path "$OUTPUT_DIR/baseline.log" -Pattern "Baseline established" ``` -**If the script fails with "No fix files detected":** You're likely on the wrong branch. Checkout the actual PR branch with `gh pr checkout ` and try again. +**If the script fails with "No fix files detected":** The PR changes are not present on the current branch. Report this as `Blocked` — do NOT switch branches. **If something fails mid-attempt:** `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore` From 28bc3cc1202116737f74d701786de1d5918d7acb Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 14 Feb 2026 14:05:30 +0100 Subject: [PATCH 116/126] Add agent label automation and docs Introduce centralized agent label management and documentation. Adds a new shared script (.github/scripts/shared/Update-AgentLabels.ps1) that parses phase content.md files and idempotently creates/applies outcome, signal, and tracking labels (s/agent-*) via the GH API. Integrates label application into Review-PR.ps1 as Phase 4 (with a recovery attempt if the helper is missing). Adds comprehensive docs (.github/docs/agent-labels.md) and documents labeling behavior in .github/agents/pr/SHARED-RULES.md. Removes the older, in-file verification label logic from verify-tests-fail.ps1 and its calls, consolidating label responsibilities into the new helper. Labels are applied non-fatally and auto-created/updated on first use. Update Update-AgentLabels.ps1 Rename s/agent-fix-lose label to s/agent-fix-pr-picked Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/pr/SHARED-RULES.md | 37 ++ .github/docs/agent-labels.md | 171 +++++++ .github/scripts/Review-PR.ps1 | 24 + .github/scripts/shared/Update-AgentLabels.ps1 | 460 ++++++++++++++++++ .../scripts/verify-tests-fail.ps1 | 57 --- 5 files changed, 692 insertions(+), 57 deletions(-) create mode 100644 .github/docs/agent-labels.md create mode 100644 .github/scripts/shared/Update-AgentLabels.ps1 diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index 10210c9316cc..da5c5af316fb 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -119,6 +119,43 @@ EOF --- +## Agent Labels (Automated by Review-PR.ps1) + +After all phases complete, `Review-PR.ps1` automatically applies GitHub labels based on phase outcomes. The agent does NOT need to apply labels — just write accurate `content.md` files. + +### Label Categories + +**Outcome labels** (mutually exclusive — exactly one per PR): +| Label | When Applied | +|-------|-------------| +| `s/agent-approved` | Report recommends APPROVE | +| `s/agent-changes-requested` | Report recommends REQUEST CHANGES | +| `s/agent-review-incomplete` | Agent didn't complete all phases | + +**Signal labels** (additive — multiple can coexist): +| Label | When Applied | +|-------|-------------| +| `s/agent-gate-passed` | Gate phase passes | +| `s/agent-gate-failed` | Gate phase fails | +| `s/agent-fix-win` | Agent found a better alternative fix than the PR | +| `s/agent-fix-pr-picked` | PR's fix is the best — agent couldn't beat it | + +**Tracking label** (always applied): +| Label | When Applied | +|-------|-------------| +| `s/agent-reviewed` | Every completed agent run | + +### How Labels Are Determined + +Labels are parsed from `content.md` files: +- **Outcome**: from `report/content.md` — looks for `Final Recommendation: APPROVE` or `REQUEST CHANGES` +- **Gate**: from `gate/content.md` — looks for `PASSED` or `FAILED` +- **Fix**: from `try-fix/content.md` — looks for alternative selected (win = agent beat PR) vs `Selected Fix: PR` (lose = PR was best) + +**Agent responsibility**: Write clear, parseable `content.md` with standard markers (`✅ PASSED`, `❌ FAILED`, `Selected Fix: PR`, `Final Recommendation: APPROVE`). + +--- + ## No Direct Git Commands **Never run git commands that change branch or file state.** diff --git a/.github/docs/agent-labels.md b/.github/docs/agent-labels.md new file mode 100644 index 000000000000..7025d26f8f2e --- /dev/null +++ b/.github/docs/agent-labels.md @@ -0,0 +1,171 @@ +# Agent Workflow Labels + +GitHub labels for tracking outcomes of the AI agent PR review workflow (`Review-PR.ps1`). + +All labels use the **`s/agent-*`** prefix for easy querying on GitHub. + +--- + +## Label Categories + +### Outcome Labels + +Mutually exclusive — exactly **one** is applied per PR review run. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-approved` | 🟢 `#2E7D32` | AI agent recommends approval — PR fix is correct and optimal | Report phase recommends APPROVE | +| `s/agent-changes-requested` | 🟠 `#E65100` | AI agent recommends changes — found a better alternative or issues | Report phase recommends REQUEST CHANGES | +| `s/agent-review-incomplete` | 🔴 `#B71C1C` | AI agent could not complete all phases (blocker, timeout, error) | Agent exits without completing all phases | + +When a new outcome label is applied, any previously applied outcome label is automatically removed. + +### Signal Labels + +Additive — **multiple** can coexist on a single PR. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-gate-passed` | 🟢 `#4CAF50` | AI verified tests catch the bug (fail without fix, pass with fix) | Gate phase passes | +| `s/agent-gate-failed` | 🟠 `#FF9800` | AI could not verify tests catch the bug | Gate phase fails | +| `s/agent-fix-win` | 🟢 `#66BB6A` | AI found a better alternative fix than the PR | Fix phase: alternative selected over PR's fix | +| `s/agent-fix-pr-picked` | 🟠 `#FF7043` | AI could not beat the PR fix — PR is the best among all candidates | Fix phase: PR selected as best after comparison | + +Gate labels (`gate-passed`/`gate-failed`) are mutually exclusive with each other. Fix labels (`fix-win`/`fix-lose`) are mutually exclusive with each other. + +### Tracking Label + +Always applied on every completed agent run. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-reviewed` | 🔵 `#1565C0` | PR was reviewed by AI agent workflow (full 4-phase review) | Every completed agent run | + +### Manual Label + +Applied by MAUI maintainers, not by automation. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-fix-implemented` | 🟣 `#7B1FA2` | PR author implemented the agent's suggested fix | Maintainer applies when PR author adopts agent's recommendation | + +--- + +## How It Works + +### Architecture + +``` +Review-PR.ps1 +├── Phase 1: PR Agent Review (Copilot CLI) +│ ├── Pre-Flight → writes content.md +│ ├── Gate → writes content.md +│ ├── Fix → writes content.md +│ └── Report → writes content.md +├── Phase 2: PR Finalize (optional) +├── Phase 3: Post Comments (optional) +└── Phase 4: Apply Labels ← labels are applied here + ├── Parse content.md files + ├── Determine outcome + signal labels + ├── Apply via GitHub REST API + └── Non-fatal: errors warn but don't fail the workflow +``` + +Labels are applied exclusively from `Review-PR.ps1` Phase 4. No other script applies agent labels. This single-source design avoids label conflicts and simplifies debugging. + +### How Labels Are Parsed + +The `Parse-PhaseOutcomes` function in `Update-AgentLabels.ps1` reads `content.md` files from each phase directory: + +| Source File | What's Parsed | Resulting Label | +|-------------|---------------|-----------------| +| `gate/content.md` | `**Result:** ✅ PASSED` | `s/agent-gate-passed` | +| `gate/content.md` | `**Result:** ❌ FAILED` | `s/agent-gate-failed` | +| `try-fix/content.md` | `**Selected Fix:** Candidate ...` | `s/agent-fix-win` | +| `try-fix/content.md` | `**Selected Fix:** PR ...` | `s/agent-fix-pr-picked` | +| `report/content.md` | `Final Recommendation: APPROVE` | `s/agent-approved` | +| `report/content.md` | `Final Recommendation: REQUEST CHANGES` | `s/agent-changes-requested` | +| *(missing report)* | No report file exists | `s/agent-review-incomplete` | + +### Self-Bootstrapping + +Labels are created automatically on first use via `Ensure-LabelExists`. No manual setup required. If a label already exists but has a stale description or color, it is updated. + +--- + +## Querying Labels + +All labels use the `s/agent-*` prefix, making them easy to filter on GitHub. + +### Common Queries + +``` +# PRs the agent approved +is:pr label:s/agent-approved + +# PRs where agent found a better fix +is:pr label:s/agent-fix-pr-picked + +# PRs where agent found better fix AND author implemented it +is:pr label:s/agent-changes-requested label:s/agent-fix-implemented + +# PRs where tests don't catch the bug +is:pr label:s/agent-gate-failed + +# Agent-reviewed PRs that are still open +is:pr is:open label:s/agent-reviewed + +# All agent-reviewed PRs (total count) +is:pr label:s/agent-reviewed +``` + +### Metrics You Can Derive + +| Metric | Query | +|--------|-------| +| Total agent reviews | `is:pr label:s/agent-reviewed` | +| Approval rate | Compare `label:s/agent-approved` vs `label:s/agent-changes-requested` counts | +| Gate pass rate | Compare `label:s/agent-gate-passed` vs `label:s/agent-gate-failed` counts | +| Fix win rate | Compare `label:s/agent-fix-win` vs `label:s/agent-fix-pr-picked` counts | +| Agent adoption rate | `label:s/agent-fix-implemented` / `label:s/agent-changes-requested` | +| Incomplete review rate | `label:s/agent-review-incomplete` / `label:s/agent-reviewed` | + +--- + +## Implementation Details + +### Files + +| File | Purpose | +|------|---------| +| `.github/scripts/shared/Update-AgentLabels.ps1` | Label helper module (all label logic) | +| `.github/scripts/Review-PR.ps1` | Orchestrator that calls `Apply-AgentLabels` in Phase 4 | +| `.github/agents/pr/SHARED-RULES.md` | Documents label system for the PR agent | + +### Key Functions + +| Function | Description | +|----------|-------------| +| `Apply-AgentLabels` | Main entry point — parses phases and applies all labels | +| `Parse-PhaseOutcomes` | Reads `content.md` files, returns outcome/gate/fix results | +| `Update-AgentOutcomeLabel` | Applies one outcome label, removes conflicting ones | +| `Update-AgentSignalLabels` | Adds/removes gate and fix signal labels | +| `Update-AgentReviewedLabel` | Ensures tracking label is present | +| `Ensure-LabelExists` | Creates or updates a label in the repository | + +### Design Principles + +- **Idempotent**: Safe to re-run — checks before add/remove, GitHub ignores duplicate adds +- **Non-fatal**: Label failures emit warnings but never fail the overall workflow +- **Single source**: All labels applied from `Review-PR.ps1` only — no other scripts touch labels +- **Self-bootstrapping**: Labels are created on first use via GitHub API +- **Mutual exclusivity enforced**: Outcome labels and same-category signal labels automatically remove their counterpart + +--- + +## Migrated From + +The following old infrastructure was removed as part of this implementation: + +- **`Update-VerificationLabels`** function in `verify-tests-fail.ps1` — removed (labels now come from `Review-PR.ps1` only) +- **`s/ai-reproduction-confirmed`** / **`s/ai-reproduction-failed`** labels — superseded by `s/agent-gate-passed` / `s/agent-gate-failed` diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index fc1a5a65c373..ea4f617f4d1f 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -531,6 +531,30 @@ if ($DryRun) { } } + # Phase 4: Apply Labels + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue + Write-Host "║ PHASE 4: APPLY LABELS ║" -ForegroundColor Blue + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue + Write-Host "" + + $labelHelperPath = Join-Path $RepoRoot ".github/scripts/shared/Update-AgentLabels.ps1" + if (-not (Test-Path $labelHelperPath)) { + Write-Host "⚠️ Label helper missing, attempting targeted recovery..." -ForegroundColor Yellow + git checkout $savedHead -- $labelHelperPath 2>&1 | Out-Null + } + + if (Test-Path $labelHelperPath) { + try { + . $labelHelperPath + Apply-AgentLabels -PRNumber $PRNumber -RepoRoot $RepoRoot + } + catch { + Write-Host "⚠️ Label application failed (non-fatal): $_" -ForegroundColor Yellow + } + } else { + Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow + } } } diff --git a/.github/scripts/shared/Update-AgentLabels.ps1 b/.github/scripts/shared/Update-AgentLabels.ps1 new file mode 100644 index 000000000000..818992fc00ff --- /dev/null +++ b/.github/scripts/shared/Update-AgentLabels.ps1 @@ -0,0 +1,460 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Shared functions for managing agent workflow labels on GitHub PRs. + +.DESCRIPTION + Provides idempotent label management for the PR agent review workflow. + Labels use the 's/agent-*' prefix convention for easy querying. + + Label categories: + - Outcome labels (mutually exclusive): agent-approved, agent-changes-requested, agent-review-incomplete + - Signal labels (additive): agent-gate-passed, agent-gate-failed, agent-fix-win, agent-fix-pr-picked + - Manual labels (applied by maintainers): agent-fix-implemented + - Tracking label: agent-reviewed (always applied on completed run) + +.NOTES + All functions are designed to be non-fatal: label failures emit warnings + but do not throw or exit with error codes. +#> + +# ============================================================ +# Label definitions +# ============================================================ + +$script:OutcomeLabels = @{ + 's/agent-approved' = @{ Description = 'AI agent recommends approval - PR fix is correct and optimal'; Color = '2E7D32' } + 's/agent-changes-requested' = @{ Description = 'AI agent recommends changes - found a better alternative or issues'; Color = 'E65100' } + 's/agent-review-incomplete' = @{ Description = 'AI agent could not complete all phases (blocker, timeout, error)'; Color = 'B71C1C' } +} + +$script:SignalLabels = @{ + 's/agent-gate-passed' = @{ Description = 'AI verified tests catch the bug (fail without fix, pass with fix)'; Color = '4CAF50' } + 's/agent-gate-failed' = @{ Description = 'AI could not verify tests catch the bug'; Color = 'FF9800' } + 's/agent-fix-win' = @{ Description = 'AI found a better alternative fix than the PR'; Color = '66BB6A' } + 's/agent-fix-pr-picked' = @{ Description = 'AI could not beat the PR fix - PR is the best among all candidates'; Color = 'FF7043' } +} + +$script:ManualLabels = @{ + 's/agent-fix-implemented' = @{ Description = 'PR author implemented the agent suggested fix'; Color = '7B1FA2' } +} + +$script:TrackingLabel = @{ + 's/agent-reviewed' = @{ Description = 'PR was reviewed by AI agent workflow (full 4-phase review)'; Color = '1565C0' } +} + +# All label definitions combined +$script:AllLabelDefs = @{} +foreach ($group in @($script:OutcomeLabels, $script:SignalLabels, $script:ManualLabels, $script:TrackingLabel)) { + foreach ($key in $group.Keys) { + $script:AllLabelDefs[$key] = $group[$key] + } +} + +# ============================================================ +# Helper: Ensure a label exists in the repository +# ============================================================ +function Ensure-LabelExists { + <# + .SYNOPSIS + Creates a label in the repository if it doesn't already exist. + Updates description/color if the label exists but has stale metadata. + #> + param( + [Parameter(Mandatory)] [string]$LabelName, + [Parameter(Mandatory)] [string]$Description, + [Parameter(Mandatory)] [string]$Color, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + try { + # Check if label exists + $existing = gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" 2>$null | ConvertFrom-Json + if ($LASTEXITCODE -eq 0 -and $existing) { + # Label exists — update if description or color changed + $needsUpdate = ($existing.description -ne $Description) -or ($existing.color -ne $Color) + if ($needsUpdate) { + gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" ` + --method PATCH ` + -f description="$Description" ` + -f color="$Color" 2>$null | Out-Null + Write-Host " 🏷️ Updated label: $LabelName" -ForegroundColor Gray + } + } else { + # Label doesn't exist — create it + gh api "repos/$Owner/$Repo/labels" ` + --method POST ` + -f name="$LabelName" ` + -f description="$Description" ` + -f color="$Color" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host " 🏷️ Created label: $LabelName" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to create label: $LabelName" -ForegroundColor Yellow + } + } + } + catch { + Write-Host " ⚠️ Label operation failed for '$LabelName': $_" -ForegroundColor Yellow + } +} + +# ============================================================ +# Helper: Get current agent labels on a PR +# ============================================================ +function Get-AgentLabels { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $labels = gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" --jq '.[].name' 2>$null + if ($LASTEXITCODE -ne 0) { return @() } + return @($labels | Where-Object { $_ -like 's/agent-*' }) +} + +# ============================================================ +# Helper: Add a label to a PR +# ============================================================ +function Add-Label { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] [string]$LabelName, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" ` + --method POST ` + -f "labels[]=$LabelName" 2>$null | Out-Null + return $LASTEXITCODE -eq 0 +} + +# ============================================================ +# Helper: Remove a label from a PR +# ============================================================ +function Remove-Label { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] [string]$LabelName, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + gh api "repos/$Owner/$Repo/issues/$PRNumber/labels/$([uri]::EscapeDataString($LabelName))" ` + --method DELETE 2>$null | Out-Null + return $LASTEXITCODE -eq 0 +} + +# ============================================================ +# Update-AgentOutcomeLabel +# ============================================================ +function Update-AgentOutcomeLabel { + <# + .SYNOPSIS + Applies exactly one outcome label, removing any conflicting outcome labels. + + .PARAMETER Outcome + One of: 'approved', 'changes-requested', 'review-incomplete' + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] + [ValidateSet('approved', 'changes-requested', 'review-incomplete')] + [string]$Outcome, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $targetLabel = "s/agent-$Outcome" + Write-Host " 📌 Outcome: $targetLabel" -ForegroundColor Cyan + + # Ensure the target label exists in the repo + $def = $script:OutcomeLabels[$targetLabel] + Ensure-LabelExists -LabelName $targetLabel -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Get current labels on the PR + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + # Remove conflicting outcome labels + foreach ($olName in $script:OutcomeLabels.Keys) { + if ($olName -ne $targetLabel -and $currentLabels -contains $olName) { + Write-Host " 🗑️ Removing stale: $olName" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -LabelName $olName -Owner $Owner -Repo $Repo + } + } + + # Add the target label (idempotent — GitHub ignores duplicates) + if ($currentLabels -notcontains $targetLabel) { + $ok = Add-Label -PRNumber $PRNumber -LabelName $targetLabel -Owner $Owner -Repo $Repo + if ($ok) { + Write-Host " ✅ Applied: $targetLabel" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to apply: $targetLabel" -ForegroundColor Yellow + } + } else { + Write-Host " ✅ Already present: $targetLabel" -ForegroundColor Green + } +} + +# ============================================================ +# Update-AgentSignalLabels +# ============================================================ +function Update-AgentSignalLabels { + <# + .SYNOPSIS + Adds or removes signal labels based on phase results. + + .PARAMETER GateResult + Gate phase result: 'passed', 'failed', or $null (skipped) + + .PARAMETER FixResult + Fix phase result: 'win' (PR best), 'lose' (alternative better), or $null (skipped) + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$GateResult, # 'passed', 'failed', or $null + [string]$FixResult, # 'win' (agent found better alternative), 'lose' (PR is best), or $null + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + # --- Gate labels --- + if ($GateResult -eq 'passed') { + $label = 's/agent-gate-passed' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Add gate-passed, remove gate-failed + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-gate-failed') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-failed' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-gate-failed" -ForegroundColor Yellow + } + } + elseif ($GateResult -eq 'failed') { + $label = 's/agent-gate-failed' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Add gate-failed, remove gate-passed + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-gate-passed') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-passed' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-gate-passed" -ForegroundColor Yellow + } + } + + # --- Fix labels --- + if ($FixResult -eq 'win') { + $label = 's/agent-fix-win' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-fix-pr-picked') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-pr-picked' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-fix-pr-picked" -ForegroundColor Yellow + } + } + elseif ($FixResult -eq 'lose') { + $label = 's/agent-fix-pr-picked' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-fix-win') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-win' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-fix-win" -ForegroundColor Yellow + } + } +} + +# ============================================================ +# Update-AgentReviewedLabel +# ============================================================ +function Update-AgentReviewedLabel { + <# + .SYNOPSIS + Ensures the s/agent-reviewed tracking label is on the PR. + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $label = 's/agent-reviewed' + $def = $script:TrackingLabel[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + if ($currentLabels -notcontains $label) { + $ok = Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo + if ($ok) { + Write-Host " ✅ Tracking: $label" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to apply: $label" -ForegroundColor Yellow + } + } else { + Write-Host " ✅ Already present: $label" -ForegroundColor Green + } +} + +# ============================================================ +# Parse-PhaseOutcomes — read content.md files to determine labels +# ============================================================ +function Parse-PhaseOutcomes { + <# + .SYNOPSIS + Reads phase output content.md files and determines outcome + signal labels. + + .OUTPUTS + Hashtable with keys: Outcome, GateResult, FixResult + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null) + ) + + $baseDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" + $result = @{ + Outcome = $null # 'approved', 'changes-requested', 'review-incomplete' + GateResult = $null # 'passed', 'failed' + FixResult = $null # 'win', 'lose' + } + + # --- Parse Gate content.md --- + $gateFile = Join-Path $baseDir "gate/content.md" + if (Test-Path $gateFile) { + $gateContent = Get-Content $gateFile -Raw -ErrorAction SilentlyContinue + if ($gateContent) { + # Match the Result line specifically to avoid false matches from other text + if ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:✅|PASSED)') { + $result.GateResult = 'passed' + } + elseif ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:❌|FAILED|SKIPPED)') { + $result.GateResult = 'failed' + } + } + } + + # --- Parse try-fix content.md for fix result --- + $fixFile = Join-Path $baseDir "try-fix/content.md" + if (Test-Path $fixFile) { + $fixContent = Get-Content $fixFile -Raw -ErrorAction SilentlyContinue + if ($fixContent) { + # Extract just the fix name (before any reason separator like " — ") + # to avoid false matches from reason text containing keywords like "try-fix" or "alternative" + if ($fixContent -match '(?i)Selected Fix:\s*\*?\*?\s*(.+?)(?:\s*—|\s*$)') { + $fixName = $matches[1].Trim() + # Agent wins: fix name starts with Candidate/Alternative/try-fix + if ($fixName -match '(?i)^(?:Candidate|Alternative|try-fix)') { + $result.FixResult = 'win' + } + # Agent loses: fix name starts with PR + elseif ($fixName -match '(?i)^(?:\*?\*?\s*)?PR\b') { + $result.FixResult = 'lose' + } + } + } + } + + # --- Parse report content.md for outcome --- + $reportFile = Join-Path $baseDir "report/content.md" + if (Test-Path $reportFile) { + $reportContent = Get-Content $reportFile -Raw -ErrorAction SilentlyContinue + if ($reportContent) { + if ($reportContent -match '(?i)Final\s+Recommendation:\s*APPROVE|✅\s*Final\s+Recommendation:\s*APPROVE') { + $result.Outcome = 'approved' + } + elseif ($reportContent -match '(?i)Final\s+Recommendation:\s*REQUEST.CHANGES|⚠️\s*Final\s+Recommendation:\s*REQUEST.CHANGES') { + $result.Outcome = 'changes-requested' + } + else { + $result.Outcome = 'review-incomplete' + } + } else { + $result.Outcome = 'review-incomplete' + } + } else { + # No report means the agent didn't finish + $result.Outcome = 'review-incomplete' + } + + return $result +} + +# ============================================================ +# Apply-AgentLabels — main entry point +# ============================================================ +function Apply-AgentLabels { + <# + .SYNOPSIS + Main entry point: parses phase outputs and applies all appropriate labels. + + .DESCRIPTION + 1. Parses content.md files from each phase + 2. Applies exactly one outcome label + 3. Applies signal labels based on phase results + 4. Always applies s/agent-reviewed + + .PARAMETER PRNumber + The GitHub PR number. + + .PARAMETER RepoRoot + Repository root path. Defaults to git rev-parse --show-toplevel. + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null), + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + Write-Host "" + Write-Host "🏷️ Applying agent labels to PR #$PRNumber..." -ForegroundColor Cyan + + # Parse phase outcomes from content.md files + $outcomes = Parse-PhaseOutcomes -PRNumber $PRNumber -RepoRoot $RepoRoot + Write-Host " 📊 Parsed outcomes:" -ForegroundColor Gray + Write-Host " Outcome: $($outcomes.Outcome ?? '(none)')" -ForegroundColor Gray + Write-Host " Gate: $($outcomes.GateResult ?? '(skipped)')" -ForegroundColor Gray + Write-Host " Fix: $($outcomes.FixResult ?? '(skipped)')" -ForegroundColor Gray + + try { + # 1. Apply outcome label (exactly one) + if ($outcomes.Outcome) { + Update-AgentOutcomeLabel -PRNumber $PRNumber -Outcome $outcomes.Outcome -Owner $Owner -Repo $Repo + } + + # 2. Apply signal labels + Update-AgentSignalLabels -PRNumber $PRNumber -GateResult $outcomes.GateResult -FixResult $outcomes.FixResult -Owner $Owner -Repo $Repo + + # 3. Always apply tracking label + Update-AgentReviewedLabel -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + Write-Host "" + Write-Host " ✅ Labels applied successfully" -ForegroundColor Green + } + catch { + Write-Host "" + Write-Host " ⚠️ Label application error (non-fatal): $_" -ForegroundColor Yellow + } +} diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index bd3fd7d0fb4c..5eacfed3135a 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -144,59 +144,6 @@ $BaselineScript = Join-Path $RepoRoot ".github/scripts/EstablishBrokenBaseline.p # Import Test-IsTestFile and Find-MergeBase from shared script . $BaselineScript -# ============================================================ -# Label management for verification results -# ============================================================ -$LabelConfirmed = "s/ai-reproduction-confirmed" -$LabelFailed = "s/ai-reproduction-failed" - -function Update-VerificationLabels { - param( - [Parameter(Mandatory = $true)] - [bool]$ReproductionConfirmed, - - [Parameter(Mandatory = $false)] - [string]$PR = $PRNumber - ) - - if ($PR -eq "unknown" -or -not $PR) { - Write-Host "⚠️ Cannot update labels: PR number not available" -ForegroundColor Yellow - return - } - - $labelToAdd = if ($ReproductionConfirmed) { $LabelConfirmed } else { $LabelFailed } - $labelToRemove = if ($ReproductionConfirmed) { $LabelFailed } else { $LabelConfirmed } - - Write-Host "" - Write-Host "🏷️ Updating verification labels on PR #$PR..." -ForegroundColor Cyan - - # Track success for both operations - $removeSuccess = $true - - # Remove the opposite label if it exists (using REST API to avoid GraphQL deprecation issues) - $existingLabels = gh pr view $PR --json labels --jq '.labels[].name' 2>$null - if ($existingLabels -contains $labelToRemove) { - Write-Host " Removing: $labelToRemove" -ForegroundColor Yellow - gh api "repos/dotnet/maui/issues/$PR/labels/$labelToRemove" --method DELETE 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - $removeSuccess = $false - Write-Host " ⚠️ Failed to remove label: $labelToRemove" -ForegroundColor Yellow - } - } - - # Add the appropriate label (using REST API to avoid GraphQL deprecation issues) - Write-Host " Adding: $labelToAdd" -ForegroundColor Green - $result = gh api "repos/dotnet/maui/issues/$PR/labels" --method POST -f "labels[]=$labelToAdd" 2>&1 - $addSuccess = $LASTEXITCODE -eq 0 - - if ($addSuccess -and $removeSuccess) { - Write-Host "✅ Labels updated successfully" -ForegroundColor Green - } elseif ($addSuccess) { - Write-Host "⚠️ Label added but failed to remove old label" -ForegroundColor Yellow - } else { - Write-Host "⚠️ Failed to update labels: $result" -ForegroundColor Yellow - } -} # ============================================================ # Auto-detect test filter from changed files @@ -466,7 +413,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" Write-Host "Failed tests: $($testResult.FailCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { # Tests PASSED - this is bad! @@ -487,7 +433,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" Write-Host "Passed tests: $($testResult.PassCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } } @@ -883,7 +828,6 @@ if ($verificationPassed) { Write-Host "║ Tests correctly detect the issue: ║" -ForegroundColor Green Write-Host "║ - FAIL without fix (as expected) ║" -ForegroundColor Green Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { Write-Host "" @@ -904,6 +848,5 @@ if ($verificationPassed) { Write-Host "║ 2. Tests don't actually test the fixed behavior ║" -ForegroundColor Red Write-Host "║ 3. The issue was already fixed in base branch ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } From 4d804b2a03cdd83f38c19d17715436423710355e Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 14 Feb 2026 16:42:09 +0100 Subject: [PATCH 117/126] Add unified mode for PR finalization comments Introduce a -Unified mode to post-pr-finalize-comment.ps1 and call it from Review-PR.ps1. When enabled, the script injects or updates a PR Finalization section inside the existing AI Summary comment (or creates a new unified AI Summary comment) using explicit markers and a collapsible details block; it also removes any legacy standalone finalize comment. Dry-run preview support was added (writes preview file), and existing standalone behavior remains the default when -Unified is not passed. Changes made in .github/scripts/Review-PR.ps1 and .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1. --- .github/scripts/Review-PR.ps1 | 4 +- .../scripts/post-pr-finalize-comment.ps1 | 152 +++++++++++++++++- 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index ea4f617f4d1f..9a83d73634b2 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -515,8 +515,8 @@ if ($DryRun) { git checkout $savedHead -- $finalizeScriptPath 2>&1 | Out-Null } if (Test-Path $finalizeScriptPath) { - Write-Host "💬 Running post-pr-finalize-comment.ps1 directly..." -ForegroundColor Yellow - & $finalizeScriptPath -PRNumber $PRNumber + Write-Host "💬 Running post-pr-finalize-comment.ps1 directly (unified mode)..." -ForegroundColor Yellow + & $finalizeScriptPath -PRNumber $PRNumber -Unified $finalizeCommentExit = $LASTEXITCODE if ($finalizeCommentExit -eq 0) { diff --git a/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 index 3ab43819a77c..e32b5f9d1698 100644 --- a/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 @@ -132,6 +132,9 @@ param( [Parameter(Mandatory=$false)] [switch]$DryRun, + [Parameter(Mandatory=$false)] + [switch]$Unified, + [Parameter(Mandatory=$false)] [string]$PreviewFile ) @@ -580,10 +583,153 @@ if ($CodeReviewStatus -ne "Skipped" -or -not [string]::IsNullOrWhiteSpace($CodeR } # ============================================================================ -# STANDALONE COMMENT HANDLING -# Posts as separate PR Finalization comment with marker +# COMMENT POSTING +# Two modes: -Unified (inject into AI Summary comment) or standalone (default) # ============================================================================ +if ($Unified) { + # ======================================================================== + # UNIFIED MODE: Inject into the comment + # ======================================================================== + + $MAIN_MARKER = "" + $SECTION_START = "" + $SECTION_END = "" + $LEGACY_MARKER = "" + + # Build the finalize section wrapped in expandable details + $finalizeSection = @" +$SECTION_START +
+📋 Expand PR Finalization Review + +--- + +$titleSection + +$descSection +$codeReviewSection + +--- + +
+$SECTION_END +"@ + + Write-Host "`nUnified mode: injecting into AI Summary comment on #$PRNumber..." -ForegroundColor Yellow + + $existingUnifiedComment = $null + $existingLegacyComment = $null + + try { + $commentsJson = gh api "repos/dotnet/maui/issues/$PRNumber/comments?per_page=100" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($MAIN_MARKER)) { + $existingUnifiedComment = $comment + Write-Host "✓ Found unified AI Summary comment (ID: $($comment.id))" -ForegroundColor Green + } + if ($comment.body -match [regex]::Escape($LEGACY_MARKER)) { + $existingLegacyComment = $comment + } + } + } catch { + Write-Host "⚠️ Could not fetch comments: $_" -ForegroundColor Yellow + } + + if ($DryRun) { + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/ai-summary-comment-preview.md" + } + + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + } + + if ($existingPreview -match [regex]::Escape($SECTION_START)) { + $pattern = [regex]::Escape($SECTION_START) + "[\s\S]*?" + [regex]::Escape($SECTION_END) + $finalComment = $existingPreview -replace $pattern, $finalizeSection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $finalizeSection + } else { + $finalComment = @" +$MAIN_MARKER + +## 🤖 AI Summary + +$finalizeSection +"@ + } + + Set-Content -Path $PreviewFile -Value "$($finalComment.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $finalComment + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`n✅ Preview saved to: $PreviewFile" -ForegroundColor Green + exit 0 + } + + if ($existingUnifiedComment) { + $body = $existingUnifiedComment.body + + if ($body -match [regex]::Escape($SECTION_START)) { + $pattern = [regex]::Escape($SECTION_START) + "[\s\S]*?" + [regex]::Escape($SECTION_END) + $newBody = $body -replace $pattern, $finalizeSection + } else { + $newBody = $body.TrimEnd() + "`n`n" + $finalizeSection + } + + $newBody = $newBody -replace "`n{4,}", "`n`n`n" + + Write-Host "Updating unified comment ID $($existingUnifiedComment.id) with PR finalize section..." -ForegroundColor Yellow + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $newBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingUnifiedComment.id)" --input $tempFile --jq '.html_url' + Remove-Item $tempFile + Write-Host "✅ PR finalize section added to unified comment: $result" -ForegroundColor Green + } else { + $commentBody = @" +$MAIN_MARKER + +## 🤖 AI Summary + +$finalizeSection +"@ + + Write-Host "Creating new unified comment with PR finalize section on PR #$PRNumber..." -ForegroundColor Yellow + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + $result = gh api --method POST "repos/dotnet/maui/issues/$PRNumber/comments" --input $tempFile --jq '.html_url' + Remove-Item $tempFile + Write-Host "✅ Unified comment posted: $result" -ForegroundColor Green + } + + # Clean up legacy standalone finalize comment if it exists + if ($existingLegacyComment) { + Write-Host "🧹 Removing legacy standalone PR Finalization comment (ID: $($existingLegacyComment.id))..." -ForegroundColor Yellow + gh api --method DELETE "repos/dotnet/maui/issues/comments/$($existingLegacyComment.id)" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host " ✅ Legacy comment removed" -ForegroundColor Green + } else { + Write-Host " ⚠️ Could not remove legacy comment (non-fatal)" -ForegroundColor Yellow + } + } + +} else { + # ======================================================================== + # STANDALONE MODE (default): Post as separate PR Finalization comment + # ======================================================================== + $FINALIZE_MARKER = "" Write-Host "`nChecking for existing PR Finalization comment on #$PRNumber..." -ForegroundColor Yellow @@ -658,3 +804,5 @@ if ($existingComment) { } Remove-Item $tempFile + +} # end standalone mode From 6299ffdf885c0b25a3121d24fe4336b5630b2acf Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 17 Feb 2026 00:53:07 +0100 Subject: [PATCH 118/126] Use version-specific iOS preferred devices Update Start-Emulator.ps1 to select iOS simulators that match UI test baseline devices. Replace the single preferred device list with a per-iOS-version mapping (iOS-18/iOS-17 prefer iPhone Xs; iOS-26 prefers iPhone 11 Pro) and adjust the selection logic to use the version-specific preferences. Comments were updated to document why certain devices are preferred to ensure consistency with UITest.cs baselines. --- .github/scripts/shared/Start-Emulator.ps1 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 9fd9d227331a..26fa15579785 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -6,7 +6,7 @@ .DESCRIPTION Handles device detection and startup for both Android and iOS platforms. - Android: Automatically selects and starts emulator with priority: API 30 Nexus > API 30 > Nexus > First available - - iOS: Automatically selects iPhone Xs with iOS 18.5 by default + - iOS: Selects device matching UI test baselines (iPhone Xs for iOS 18.x/17.x, iPhone 11 Pro for iOS 26.x) .PARAMETER Platform Target platform: "android" or "ios" @@ -344,8 +344,14 @@ if ($Platform -eq "android") { Write-Info "Auto-detecting iOS simulator..." $simList = xcrun simctl list devices available --json | ConvertFrom-Json - # Preferred devices in order of priority - $preferredDevices = @("iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro", "iPhone Xs") + # Preferred devices per iOS version - must match UI test baseline screenshot devices + # iOS 18.x/17.x: iPhone Xs (default in UITest.cs, baselines captured on this device) + # iOS 26.x: iPhone 11 Pro (required by UITest.cs for ios-26 environment) + $preferredDevicesForVersion = @{ + "iOS-18" = @("iPhone Xs", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") + "iOS-17" = @("iPhone Xs", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") + "iOS-26" = @("iPhone 11 Pro", "iPhone Xs") + } # Preferred iOS versions in order (stable preferred, beta fallback) $preferredVersions = @("iOS-18", "iOS-17", "iOS-26") @@ -361,7 +367,8 @@ if ($Platform -eq "android") { Where-Object { $_.Name -match $version } if ($matchingRuntimes) { - # Try each preferred device + # Try each preferred device for this iOS version + $preferredDevices = $preferredDevicesForVersion[$version] foreach ($deviceName in $preferredDevices) { $device = $null $deviceRuntime = $null From b3539ff1846671b969a875863535424a41aafa3b Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 17 Feb 2026 01:33:35 +0100 Subject: [PATCH 119/126] Shutdown other booted iOS simulators When deploying or starting iOS simulators, add logic to detect any other booted simulators and shut them down to prevent Appium from connecting to the wrong device. Implements parsing of `xcrun simctl list devices --json` and shuts down any booted simulator whose UDID does not match the target in both Build-AndDeploy.ps1 and Start-Emulator.ps1. Also update the success message to include the simulator name for clearer logs. --- .github/scripts/shared/Build-AndDeploy.ps1 | 14 ++++++++++++++ .github/scripts/shared/Start-Emulator.ps1 | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index 8abdafe7b21c..f57d5c088094 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -136,6 +136,20 @@ if ($Platform -eq "android") { # Deploy to iOS simulator Write-Step "Deploying to iOS simulator..." + + # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device + $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json + $otherBooted = $bootedSims.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } + + if ($otherBooted) { + foreach ($sim in $otherBooted) { + Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" + xcrun simctl shutdown $sim.udid 2>$null + } + } + Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index 26fa15579785..a599f817a4ce 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -451,6 +451,19 @@ if ($Platform -eq "android") { Write-Success "iOS simulator: $deviceName ($DeviceUdid)" + # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device + $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json + $otherBooted = $bootedSims.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } + + if ($otherBooted) { + foreach ($sim in $otherBooted) { + Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" + xcrun simctl shutdown $sim.udid 2>$null + } + } + # Boot simulator if not already booted Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null @@ -467,7 +480,7 @@ if ($Platform -eq "android") { exit 1 } - Write-Success "Simulator is booted and ready" + Write-Success "Simulator is booted and ready: $deviceName" #endregion } From 4896e120684231125230b11813fce46b199c1d38 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 17 Feb 2026 14:11:56 +0100 Subject: [PATCH 120/126] Fix iOS simulator fallback: try iPhone 11 Pro when iPhone Xs device type unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iPhone Xs device type (com.apple.CoreSimulator.SimDeviceType.iPhone-Xs) is not available on newer Xcode versions on CI agents. iPhone 11 Pro has the same screen resolution (1125x2436 @3x) so snapshots match the baselines captured on iPhone Xs. Fallback order: iPhone Xs (existing) → iPhone 11 Pro (existing) → create iPhone Xs → create iPhone 11 Pro → first available iPhone. Fix: Start-Emulator.ps1 respects DEVICE_UDID env var and prefers iPhone 11 Pro Two fixes: 1. Check $env:DEVICE_UDID before auto-detecting - the CI pipeline sets this via ##vso[task.setvariable] but Start-Emulator.ps1 was ignoring it 2. Add iPhone 11 Pro as second preferred device for iOS 18/17 (same 1125x2436 resolution as iPhone Xs) - iPhone Xs device type is unavailable on CI agents Fix CI iOS simulator selection to use iPhone Xs for snapshot baselines The CI pipeline was selecting iPhone 16 Pro (1206x2472) which doesn't match the UI test baseline screenshots captured on iPhone Xs (1124x2286). Changes: - Create iPhone Xs simulator if not available on CI agent - Target the latest stable iOS runtime (18.x preferred) - Shutdown other booted simulators to prevent Appium conflicts Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/shared/Start-Emulator.ps1 | 14 +++-- eng/pipelines/ci-copilot.yml | 76 +++++++++++++++++++---- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index a599f817a4ce..ce62ffe662b5 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -339,17 +339,23 @@ if ($Platform -eq "android") { exit 1 } - # Get device UDID if not provided + # Get device UDID if not provided - check env var first + if (-not $DeviceUdid -and $env:DEVICE_UDID) { + Write-Info "Using DEVICE_UDID from environment: $($env:DEVICE_UDID)" + $DeviceUdid = $env:DEVICE_UDID + } + if (-not $DeviceUdid) { Write-Info "Auto-detecting iOS simulator..." $simList = xcrun simctl list devices available --json | ConvertFrom-Json # Preferred devices per iOS version - must match UI test baseline screenshot devices - # iOS 18.x/17.x: iPhone Xs (default in UITest.cs, baselines captured on this device) + # iPhone Xs and iPhone 11 Pro have identical resolution (1125×2436 @3x) + # iOS 18.x/17.x: iPhone Xs preferred (default in UITest.cs), iPhone 11 Pro as fallback # iOS 26.x: iPhone 11 Pro (required by UITest.cs for ios-26 environment) $preferredDevicesForVersion = @{ - "iOS-18" = @("iPhone Xs", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") - "iOS-17" = @("iPhone Xs", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") + "iOS-18" = @("iPhone Xs", "iPhone 11 Pro", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") + "iOS-17" = @("iPhone Xs", "iPhone 11 Pro", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro") "iOS-26" = @("iPhone 11 Pro", "iPhone Xs") } # Preferred iOS versions in order (stable preferred, beta fallback) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 48a0f527c0c0..06f85daee768 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -528,27 +528,67 @@ stages: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) # Boot iOS Simulator (only for iOS platform) + # UI test baseline screenshots are captured on iPhone Xs - must use same device - script: | echo "=== Booting iOS Simulator ===" - # Find a suitable simulator (prefer iPhone Xs for consistency) - UDID=$(xcrun simctl list devices available --json | jq -r ' - .devices | to_entries | - map(select(.key | test("iOS"))) | - sort_by(.key) | reverse | - .[0].value | - map(select(.name | test("iPhone (Xs|14|15)"))) | + # Find the latest stable iOS runtime (prefer 18.x, fallback to 17.x) + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 18"))] | sort_by(.version) | last | .identifier // empty + ') + if [ -z "$RUNTIME" ]; then + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 17"))] | sort_by(.version) | last | .identifier // empty + ') + fi + echo "Selected iOS runtime: $RUNTIME" + + # Look for iPhone Xs (matches UI test baselines - required for snapshot tests) + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone Xs")) | .[0].udid // empty ') + # If iPhone Xs doesn't exist, try iPhone 11 Pro (same 1125×2436 resolution) if [ -z "$UDID" ]; then - echo "No preferred simulator found, using first available iPhone" - UDID=$(xcrun simctl list devices available --json | jq -r ' - .devices | to_entries | - map(.value) | flatten | - map(select(.name | test("iPhone"))) | - .[0].udid + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone 11 Pro")) | + .[0].udid // empty ') + if [ -n "$UDID" ]; then + echo "Found existing iPhone 11 Pro (same resolution as iPhone Xs): $UDID" + fi + else + echo "Found existing iPhone Xs: $UDID" + fi + + # If neither exists, try to create them + if [ -z "$UDID" ]; then + echo "No matching device found - attempting to create one for runtime $RUNTIME..." + + # Try iPhone Xs first + UDID=$(xcrun simctl create "iPhone Xs" com.apple.CoreSimulator.SimDeviceType.iPhone-Xs "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "iPhone Xs device type unavailable: $UDID" + # Try iPhone 11 Pro (same 1125×2436 resolution) + UDID=$(xcrun simctl create "iPhone 11 Pro" com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "##vso[task.logissue type=warning]Failed to create iPhone 11 Pro: $UDID" + # Last resort: first available iPhone + UDID=$(xcrun simctl list devices available --json | jq -r ' + .devices | to_entries | + map(.value) | flatten | + map(select(.name | test("iPhone"))) | + .[0].udid + ') + else + echo "Created iPhone 11 Pro simulator: $UDID" + fi + else + echo "Created iPhone Xs simulator: $UDID" + fi fi if [ -z "$UDID" ]; then @@ -556,6 +596,16 @@ stages: exit 1 fi + # Shutdown any other booted simulators to avoid Appium connecting to wrong device + xcrun simctl list devices booted --json | jq -r ' + .devices | to_entries | map(.value) | flatten | + map(select(.state == "Booted" and .udid != "'"$UDID"'")) | + .[].udid + ' | while read OTHER_UDID; do + echo "Shutting down other simulator: $OTHER_UDID" + xcrun simctl shutdown "$OTHER_UDID" 2>/dev/null || true + done + echo "Booting simulator: $UDID" xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" sleep 10 From 005dd38b51da64f5e22b89011de2bdd67adfc72c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:09:53 +0100 Subject: [PATCH 121/126] ci-copilot: set pipeline run title early using build.updatebuildnumber (#34156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline runs for the Copilot CI pipeline had no meaningful title, making it hard to identify runs at a glance. This adds a step immediately after `Validate Parameters` that renames the run to `PR: {PRNumber} {Platform}` using the Azure DevOps logging command. ## Change - **`eng/pipelines/ci-copilot.yml`**: Adds a `Set Pipeline Run Title` step after `Validate Parameters`: ```yaml - script: | echo "##vso[build.updatebuildnumber]PR: ${{ parameters.PRNumber }} ${{ parameters.Platform }}" displayName: 'Set Pipeline Run Title' ``` Produces titles like `PR: 1234 android` or `PR: 5678 ios`. Implemented as a bash `script:` for compatibility with the macOS agents used by this pipeline.
Original prompt > Create a pull request in `dotnet/maui` (base branch `copilot-ci`) to update the Azure DevOps pipeline at `eng/pipelines/ci-copilot.yml` so that the pipeline run title/build number is updated early in the run. > > Requirements: > - Add a step shortly after the existing **Validate Parameters** step to rename the pipeline run using Azure DevOps logging command `##vso[build.updatebuildnumber]...`. > - The run title should be exactly: `PR: {PR number} {Platform}` where: > - PR number comes from parameter `${{ parameters.PRNumber }}` > - Platform comes from parameter `${{ parameters.Platform }}` > - Use a clear `displayName`, e.g. `Set Pipeline Run Title`. > - Keep the change minimal and do not alter existing behavior beyond setting the run title. > > Context: > - File source URL: https://github.com/dotnet/maui/blob/copilot-ci/eng/pipelines/ci-copilot.yml > - CommitOID (context): 4896e120684231125230b11813fce46b199c1d38 > > Notes: > - Implement as a YAML step using `script:` (bash) for maximum compatibility on macOS agents. > - Ensure the title format does not include parentheses—use a single space between PR number and platform, e.g. `PR: 1234 android`.
*This pull request was created from Copilot chat.* > --- ✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/maui/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 06f85daee768..777c887d2862 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -57,6 +57,10 @@ stages: echo "PR Number: ${{ parameters.PRNumber }}" displayName: 'Validate Parameters' + - script: | + echo "##vso[build.updatebuildnumber]PR: ${{ parameters.PRNumber }} ${{ parameters.Platform }}" + displayName: 'Set Pipeline Run Title' + # Provision environment (Xcode, .NET SDK, Android SDK, etc.) - template: common/provision.yml parameters: From cc37d345fd3a07281eac039ff58356eb33ec799e Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 20 Feb 2026 15:03:34 +0000 Subject: [PATCH 122/126] [ci] Fix variables Second test --- eng/pipelines/common/variables.yml | 40 +++++++++++------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index 30d1efdc7e39..ea964d98aefd 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -55,32 +55,20 @@ variables: value: none - group: MAUI # This is the main MAUI variable group that contains secrets for the apple certificate -- group: maui-provisionator # Required for provisionator to install Xcode -# Variable groups required for all builds -# - ${{ if and(ne(variables['Build.DefinitionName'], 'maui-pr'), ne(variables['Build.DefinitionName'], 'dotnet-maui'), ne(variables['Build.DefinitionName'], 'maui-pr-devicetests'), ne(variables['Build.DefinitionName'], 'maui-pr-uitests')) }}: -# - group: maui-provisionator # This is just needed for the provisionator +- ${{ if or(eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: + - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: + - name: PrivateBuild + value: false + - name: _RunAsPublic + value: false + - name: _RunAsInternal + value: true + - name: _SignType + value: real + - group: Publish-Build-Assets + - group: DotNet-HelixApi-Access + - group: SDL_Settings + - group: AzureDevOps-Artifact-Feeds-Pats -# - ${{ if or(eq(variables['System.TeamProject'], 'DevDiv'), eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: -# - name: internalProvisioning -# value: true -# - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: -# - name: PrivateBuild -# value: false -# - name: _RunAsPublic -# value: false -# - name: _RunAsInternal -# value: true -# - name: _SignType -# value: real - -# - group: AzureDevOps-Artifact-Feeds-Pats - -# - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: -# - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: -# # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT -# # DotNet-HelixApi-Access provides: HelixApiAccessToken -# - group: Publish-Build-Assets -# - group: DotNet-HelixApi-Access -# - group: SDL_Settings From 1dfaf81f910caa2c848f4e306b4747ea080fbe9b Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 20 Feb 2026 15:17:46 +0000 Subject: [PATCH 123/126] [ci] Publish-Build-Assets just for pack/release build --- eng/pipelines/common/variables.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index ea964d98aefd..8dee742a2525 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -66,9 +66,11 @@ variables: value: true - name: _SignType value: real - - group: Publish-Build-Assets - group: DotNet-HelixApi-Access - group: SDL_Settings - group: AzureDevOps-Artifact-Feeds-Pats + - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: + - group: Publish-Build-Assets # This variable group contains secrets to publis to BAR + From 66a705a44596a306ff19c2e0ca5b15b0626a60b9 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Fri, 20 Feb 2026 16:29:59 +0100 Subject: [PATCH 124/126] Update ci-copilot.yml --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 777c887d2862..05e4d11a2796 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -58,7 +58,7 @@ stages: displayName: 'Validate Parameters' - script: | - echo "##vso[build.updatebuildnumber]PR: ${{ parameters.PRNumber }} ${{ parameters.Platform }}" + echo "##vso[build.updatebuildnumber]PR ${{ parameters.PRNumber }} ${{ parameters.Platform }}" displayName: 'Set Pipeline Run Title' # Provision environment (Xcode, .NET SDK, Android SDK, etc.) From a447b116b398bb459bd9a1c313de67d54c728f13 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 26 Feb 2026 00:38:24 +0100 Subject: [PATCH 125/126] Revert "Add agent label automation and docs" This reverts commit 28bc3cc1202116737f74d701786de1d5918d7acb. --- .github/agents/pr/SHARED-RULES.md | 37 -- .github/docs/agent-labels.md | 171 ------- .github/scripts/Review-PR.ps1 | 24 - .github/scripts/shared/Update-AgentLabels.ps1 | 460 ------------------ 4 files changed, 692 deletions(-) delete mode 100644 .github/docs/agent-labels.md delete mode 100644 .github/scripts/shared/Update-AgentLabels.ps1 diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index da5c5af316fb..10210c9316cc 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -119,43 +119,6 @@ EOF --- -## Agent Labels (Automated by Review-PR.ps1) - -After all phases complete, `Review-PR.ps1` automatically applies GitHub labels based on phase outcomes. The agent does NOT need to apply labels — just write accurate `content.md` files. - -### Label Categories - -**Outcome labels** (mutually exclusive — exactly one per PR): -| Label | When Applied | -|-------|-------------| -| `s/agent-approved` | Report recommends APPROVE | -| `s/agent-changes-requested` | Report recommends REQUEST CHANGES | -| `s/agent-review-incomplete` | Agent didn't complete all phases | - -**Signal labels** (additive — multiple can coexist): -| Label | When Applied | -|-------|-------------| -| `s/agent-gate-passed` | Gate phase passes | -| `s/agent-gate-failed` | Gate phase fails | -| `s/agent-fix-win` | Agent found a better alternative fix than the PR | -| `s/agent-fix-pr-picked` | PR's fix is the best — agent couldn't beat it | - -**Tracking label** (always applied): -| Label | When Applied | -|-------|-------------| -| `s/agent-reviewed` | Every completed agent run | - -### How Labels Are Determined - -Labels are parsed from `content.md` files: -- **Outcome**: from `report/content.md` — looks for `Final Recommendation: APPROVE` or `REQUEST CHANGES` -- **Gate**: from `gate/content.md` — looks for `PASSED` or `FAILED` -- **Fix**: from `try-fix/content.md` — looks for alternative selected (win = agent beat PR) vs `Selected Fix: PR` (lose = PR was best) - -**Agent responsibility**: Write clear, parseable `content.md` with standard markers (`✅ PASSED`, `❌ FAILED`, `Selected Fix: PR`, `Final Recommendation: APPROVE`). - ---- - ## No Direct Git Commands **Never run git commands that change branch or file state.** diff --git a/.github/docs/agent-labels.md b/.github/docs/agent-labels.md deleted file mode 100644 index 7025d26f8f2e..000000000000 --- a/.github/docs/agent-labels.md +++ /dev/null @@ -1,171 +0,0 @@ -# Agent Workflow Labels - -GitHub labels for tracking outcomes of the AI agent PR review workflow (`Review-PR.ps1`). - -All labels use the **`s/agent-*`** prefix for easy querying on GitHub. - ---- - -## Label Categories - -### Outcome Labels - -Mutually exclusive — exactly **one** is applied per PR review run. - -| Label | Color | Description | Applied When | -|-------|-------|-------------|--------------| -| `s/agent-approved` | 🟢 `#2E7D32` | AI agent recommends approval — PR fix is correct and optimal | Report phase recommends APPROVE | -| `s/agent-changes-requested` | 🟠 `#E65100` | AI agent recommends changes — found a better alternative or issues | Report phase recommends REQUEST CHANGES | -| `s/agent-review-incomplete` | 🔴 `#B71C1C` | AI agent could not complete all phases (blocker, timeout, error) | Agent exits without completing all phases | - -When a new outcome label is applied, any previously applied outcome label is automatically removed. - -### Signal Labels - -Additive — **multiple** can coexist on a single PR. - -| Label | Color | Description | Applied When | -|-------|-------|-------------|--------------| -| `s/agent-gate-passed` | 🟢 `#4CAF50` | AI verified tests catch the bug (fail without fix, pass with fix) | Gate phase passes | -| `s/agent-gate-failed` | 🟠 `#FF9800` | AI could not verify tests catch the bug | Gate phase fails | -| `s/agent-fix-win` | 🟢 `#66BB6A` | AI found a better alternative fix than the PR | Fix phase: alternative selected over PR's fix | -| `s/agent-fix-pr-picked` | 🟠 `#FF7043` | AI could not beat the PR fix — PR is the best among all candidates | Fix phase: PR selected as best after comparison | - -Gate labels (`gate-passed`/`gate-failed`) are mutually exclusive with each other. Fix labels (`fix-win`/`fix-lose`) are mutually exclusive with each other. - -### Tracking Label - -Always applied on every completed agent run. - -| Label | Color | Description | Applied When | -|-------|-------|-------------|--------------| -| `s/agent-reviewed` | 🔵 `#1565C0` | PR was reviewed by AI agent workflow (full 4-phase review) | Every completed agent run | - -### Manual Label - -Applied by MAUI maintainers, not by automation. - -| Label | Color | Description | Applied When | -|-------|-------|-------------|--------------| -| `s/agent-fix-implemented` | 🟣 `#7B1FA2` | PR author implemented the agent's suggested fix | Maintainer applies when PR author adopts agent's recommendation | - ---- - -## How It Works - -### Architecture - -``` -Review-PR.ps1 -├── Phase 1: PR Agent Review (Copilot CLI) -│ ├── Pre-Flight → writes content.md -│ ├── Gate → writes content.md -│ ├── Fix → writes content.md -│ └── Report → writes content.md -├── Phase 2: PR Finalize (optional) -├── Phase 3: Post Comments (optional) -└── Phase 4: Apply Labels ← labels are applied here - ├── Parse content.md files - ├── Determine outcome + signal labels - ├── Apply via GitHub REST API - └── Non-fatal: errors warn but don't fail the workflow -``` - -Labels are applied exclusively from `Review-PR.ps1` Phase 4. No other script applies agent labels. This single-source design avoids label conflicts and simplifies debugging. - -### How Labels Are Parsed - -The `Parse-PhaseOutcomes` function in `Update-AgentLabels.ps1` reads `content.md` files from each phase directory: - -| Source File | What's Parsed | Resulting Label | -|-------------|---------------|-----------------| -| `gate/content.md` | `**Result:** ✅ PASSED` | `s/agent-gate-passed` | -| `gate/content.md` | `**Result:** ❌ FAILED` | `s/agent-gate-failed` | -| `try-fix/content.md` | `**Selected Fix:** Candidate ...` | `s/agent-fix-win` | -| `try-fix/content.md` | `**Selected Fix:** PR ...` | `s/agent-fix-pr-picked` | -| `report/content.md` | `Final Recommendation: APPROVE` | `s/agent-approved` | -| `report/content.md` | `Final Recommendation: REQUEST CHANGES` | `s/agent-changes-requested` | -| *(missing report)* | No report file exists | `s/agent-review-incomplete` | - -### Self-Bootstrapping - -Labels are created automatically on first use via `Ensure-LabelExists`. No manual setup required. If a label already exists but has a stale description or color, it is updated. - ---- - -## Querying Labels - -All labels use the `s/agent-*` prefix, making them easy to filter on GitHub. - -### Common Queries - -``` -# PRs the agent approved -is:pr label:s/agent-approved - -# PRs where agent found a better fix -is:pr label:s/agent-fix-pr-picked - -# PRs where agent found better fix AND author implemented it -is:pr label:s/agent-changes-requested label:s/agent-fix-implemented - -# PRs where tests don't catch the bug -is:pr label:s/agent-gate-failed - -# Agent-reviewed PRs that are still open -is:pr is:open label:s/agent-reviewed - -# All agent-reviewed PRs (total count) -is:pr label:s/agent-reviewed -``` - -### Metrics You Can Derive - -| Metric | Query | -|--------|-------| -| Total agent reviews | `is:pr label:s/agent-reviewed` | -| Approval rate | Compare `label:s/agent-approved` vs `label:s/agent-changes-requested` counts | -| Gate pass rate | Compare `label:s/agent-gate-passed` vs `label:s/agent-gate-failed` counts | -| Fix win rate | Compare `label:s/agent-fix-win` vs `label:s/agent-fix-pr-picked` counts | -| Agent adoption rate | `label:s/agent-fix-implemented` / `label:s/agent-changes-requested` | -| Incomplete review rate | `label:s/agent-review-incomplete` / `label:s/agent-reviewed` | - ---- - -## Implementation Details - -### Files - -| File | Purpose | -|------|---------| -| `.github/scripts/shared/Update-AgentLabels.ps1` | Label helper module (all label logic) | -| `.github/scripts/Review-PR.ps1` | Orchestrator that calls `Apply-AgentLabels` in Phase 4 | -| `.github/agents/pr/SHARED-RULES.md` | Documents label system for the PR agent | - -### Key Functions - -| Function | Description | -|----------|-------------| -| `Apply-AgentLabels` | Main entry point — parses phases and applies all labels | -| `Parse-PhaseOutcomes` | Reads `content.md` files, returns outcome/gate/fix results | -| `Update-AgentOutcomeLabel` | Applies one outcome label, removes conflicting ones | -| `Update-AgentSignalLabels` | Adds/removes gate and fix signal labels | -| `Update-AgentReviewedLabel` | Ensures tracking label is present | -| `Ensure-LabelExists` | Creates or updates a label in the repository | - -### Design Principles - -- **Idempotent**: Safe to re-run — checks before add/remove, GitHub ignores duplicate adds -- **Non-fatal**: Label failures emit warnings but never fail the overall workflow -- **Single source**: All labels applied from `Review-PR.ps1` only — no other scripts touch labels -- **Self-bootstrapping**: Labels are created on first use via GitHub API -- **Mutual exclusivity enforced**: Outcome labels and same-category signal labels automatically remove their counterpart - ---- - -## Migrated From - -The following old infrastructure was removed as part of this implementation: - -- **`Update-VerificationLabels`** function in `verify-tests-fail.ps1` — removed (labels now come from `Review-PR.ps1` only) -- **`s/ai-reproduction-confirmed`** / **`s/ai-reproduction-failed`** labels — superseded by `s/agent-gate-passed` / `s/agent-gate-failed` diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 9a83d73634b2..0844fafb5893 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -531,30 +531,6 @@ if ($DryRun) { } } - # Phase 4: Apply Labels - Write-Host "" - Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue - Write-Host "║ PHASE 4: APPLY LABELS ║" -ForegroundColor Blue - Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue - Write-Host "" - - $labelHelperPath = Join-Path $RepoRoot ".github/scripts/shared/Update-AgentLabels.ps1" - if (-not (Test-Path $labelHelperPath)) { - Write-Host "⚠️ Label helper missing, attempting targeted recovery..." -ForegroundColor Yellow - git checkout $savedHead -- $labelHelperPath 2>&1 | Out-Null - } - - if (Test-Path $labelHelperPath) { - try { - . $labelHelperPath - Apply-AgentLabels -PRNumber $PRNumber -RepoRoot $RepoRoot - } - catch { - Write-Host "⚠️ Label application failed (non-fatal): $_" -ForegroundColor Yellow - } - } else { - Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow - } } } diff --git a/.github/scripts/shared/Update-AgentLabels.ps1 b/.github/scripts/shared/Update-AgentLabels.ps1 deleted file mode 100644 index 818992fc00ff..000000000000 --- a/.github/scripts/shared/Update-AgentLabels.ps1 +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Shared functions for managing agent workflow labels on GitHub PRs. - -.DESCRIPTION - Provides idempotent label management for the PR agent review workflow. - Labels use the 's/agent-*' prefix convention for easy querying. - - Label categories: - - Outcome labels (mutually exclusive): agent-approved, agent-changes-requested, agent-review-incomplete - - Signal labels (additive): agent-gate-passed, agent-gate-failed, agent-fix-win, agent-fix-pr-picked - - Manual labels (applied by maintainers): agent-fix-implemented - - Tracking label: agent-reviewed (always applied on completed run) - -.NOTES - All functions are designed to be non-fatal: label failures emit warnings - but do not throw or exit with error codes. -#> - -# ============================================================ -# Label definitions -# ============================================================ - -$script:OutcomeLabels = @{ - 's/agent-approved' = @{ Description = 'AI agent recommends approval - PR fix is correct and optimal'; Color = '2E7D32' } - 's/agent-changes-requested' = @{ Description = 'AI agent recommends changes - found a better alternative or issues'; Color = 'E65100' } - 's/agent-review-incomplete' = @{ Description = 'AI agent could not complete all phases (blocker, timeout, error)'; Color = 'B71C1C' } -} - -$script:SignalLabels = @{ - 's/agent-gate-passed' = @{ Description = 'AI verified tests catch the bug (fail without fix, pass with fix)'; Color = '4CAF50' } - 's/agent-gate-failed' = @{ Description = 'AI could not verify tests catch the bug'; Color = 'FF9800' } - 's/agent-fix-win' = @{ Description = 'AI found a better alternative fix than the PR'; Color = '66BB6A' } - 's/agent-fix-pr-picked' = @{ Description = 'AI could not beat the PR fix - PR is the best among all candidates'; Color = 'FF7043' } -} - -$script:ManualLabels = @{ - 's/agent-fix-implemented' = @{ Description = 'PR author implemented the agent suggested fix'; Color = '7B1FA2' } -} - -$script:TrackingLabel = @{ - 's/agent-reviewed' = @{ Description = 'PR was reviewed by AI agent workflow (full 4-phase review)'; Color = '1565C0' } -} - -# All label definitions combined -$script:AllLabelDefs = @{} -foreach ($group in @($script:OutcomeLabels, $script:SignalLabels, $script:ManualLabels, $script:TrackingLabel)) { - foreach ($key in $group.Keys) { - $script:AllLabelDefs[$key] = $group[$key] - } -} - -# ============================================================ -# Helper: Ensure a label exists in the repository -# ============================================================ -function Ensure-LabelExists { - <# - .SYNOPSIS - Creates a label in the repository if it doesn't already exist. - Updates description/color if the label exists but has stale metadata. - #> - param( - [Parameter(Mandatory)] [string]$LabelName, - [Parameter(Mandatory)] [string]$Description, - [Parameter(Mandatory)] [string]$Color, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - try { - # Check if label exists - $existing = gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" 2>$null | ConvertFrom-Json - if ($LASTEXITCODE -eq 0 -and $existing) { - # Label exists — update if description or color changed - $needsUpdate = ($existing.description -ne $Description) -or ($existing.color -ne $Color) - if ($needsUpdate) { - gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" ` - --method PATCH ` - -f description="$Description" ` - -f color="$Color" 2>$null | Out-Null - Write-Host " 🏷️ Updated label: $LabelName" -ForegroundColor Gray - } - } else { - # Label doesn't exist — create it - gh api "repos/$Owner/$Repo/labels" ` - --method POST ` - -f name="$LabelName" ` - -f description="$Description" ` - -f color="$Color" 2>$null | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Host " 🏷️ Created label: $LabelName" -ForegroundColor Green - } else { - Write-Host " ⚠️ Failed to create label: $LabelName" -ForegroundColor Yellow - } - } - } - catch { - Write-Host " ⚠️ Label operation failed for '$LabelName': $_" -ForegroundColor Yellow - } -} - -# ============================================================ -# Helper: Get current agent labels on a PR -# ============================================================ -function Get-AgentLabels { - param( - [Parameter(Mandatory)] [string]$PRNumber, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - $labels = gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" --jq '.[].name' 2>$null - if ($LASTEXITCODE -ne 0) { return @() } - return @($labels | Where-Object { $_ -like 's/agent-*' }) -} - -# ============================================================ -# Helper: Add a label to a PR -# ============================================================ -function Add-Label { - param( - [Parameter(Mandatory)] [string]$PRNumber, - [Parameter(Mandatory)] [string]$LabelName, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" ` - --method POST ` - -f "labels[]=$LabelName" 2>$null | Out-Null - return $LASTEXITCODE -eq 0 -} - -# ============================================================ -# Helper: Remove a label from a PR -# ============================================================ -function Remove-Label { - param( - [Parameter(Mandatory)] [string]$PRNumber, - [Parameter(Mandatory)] [string]$LabelName, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - gh api "repos/$Owner/$Repo/issues/$PRNumber/labels/$([uri]::EscapeDataString($LabelName))" ` - --method DELETE 2>$null | Out-Null - return $LASTEXITCODE -eq 0 -} - -# ============================================================ -# Update-AgentOutcomeLabel -# ============================================================ -function Update-AgentOutcomeLabel { - <# - .SYNOPSIS - Applies exactly one outcome label, removing any conflicting outcome labels. - - .PARAMETER Outcome - One of: 'approved', 'changes-requested', 'review-incomplete' - #> - param( - [Parameter(Mandatory)] [string]$PRNumber, - [Parameter(Mandatory)] - [ValidateSet('approved', 'changes-requested', 'review-incomplete')] - [string]$Outcome, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - $targetLabel = "s/agent-$Outcome" - Write-Host " 📌 Outcome: $targetLabel" -ForegroundColor Cyan - - # Ensure the target label exists in the repo - $def = $script:OutcomeLabels[$targetLabel] - Ensure-LabelExists -LabelName $targetLabel -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - # Get current labels on the PR - $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo - - # Remove conflicting outcome labels - foreach ($olName in $script:OutcomeLabels.Keys) { - if ($olName -ne $targetLabel -and $currentLabels -contains $olName) { - Write-Host " 🗑️ Removing stale: $olName" -ForegroundColor Yellow - Remove-Label -PRNumber $PRNumber -LabelName $olName -Owner $Owner -Repo $Repo - } - } - - # Add the target label (idempotent — GitHub ignores duplicates) - if ($currentLabels -notcontains $targetLabel) { - $ok = Add-Label -PRNumber $PRNumber -LabelName $targetLabel -Owner $Owner -Repo $Repo - if ($ok) { - Write-Host " ✅ Applied: $targetLabel" -ForegroundColor Green - } else { - Write-Host " ⚠️ Failed to apply: $targetLabel" -ForegroundColor Yellow - } - } else { - Write-Host " ✅ Already present: $targetLabel" -ForegroundColor Green - } -} - -# ============================================================ -# Update-AgentSignalLabels -# ============================================================ -function Update-AgentSignalLabels { - <# - .SYNOPSIS - Adds or removes signal labels based on phase results. - - .PARAMETER GateResult - Gate phase result: 'passed', 'failed', or $null (skipped) - - .PARAMETER FixResult - Fix phase result: 'win' (PR best), 'lose' (alternative better), or $null (skipped) - #> - param( - [Parameter(Mandatory)] [string]$PRNumber, - [string]$GateResult, # 'passed', 'failed', or $null - [string]$FixResult, # 'win' (agent found better alternative), 'lose' (PR is best), or $null - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo - - # --- Gate labels --- - if ($GateResult -eq 'passed') { - $label = 's/agent-gate-passed' - $def = $script:SignalLabels[$label] - Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - # Add gate-passed, remove gate-failed - if ($currentLabels -notcontains $label) { - Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null - Write-Host " ✅ Signal: $label" -ForegroundColor Green - } - if ($currentLabels -contains 's/agent-gate-failed') { - Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-failed' -Owner $Owner -Repo $Repo | Out-Null - Write-Host " 🗑️ Removed stale: s/agent-gate-failed" -ForegroundColor Yellow - } - } - elseif ($GateResult -eq 'failed') { - $label = 's/agent-gate-failed' - $def = $script:SignalLabels[$label] - Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - # Add gate-failed, remove gate-passed - if ($currentLabels -notcontains $label) { - Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null - Write-Host " ✅ Signal: $label" -ForegroundColor Green - } - if ($currentLabels -contains 's/agent-gate-passed') { - Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-passed' -Owner $Owner -Repo $Repo | Out-Null - Write-Host " 🗑️ Removed stale: s/agent-gate-passed" -ForegroundColor Yellow - } - } - - # --- Fix labels --- - if ($FixResult -eq 'win') { - $label = 's/agent-fix-win' - $def = $script:SignalLabels[$label] - Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - if ($currentLabels -notcontains $label) { - Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null - Write-Host " ✅ Signal: $label" -ForegroundColor Green - } - if ($currentLabels -contains 's/agent-fix-pr-picked') { - Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-pr-picked' -Owner $Owner -Repo $Repo | Out-Null - Write-Host " 🗑️ Removed stale: s/agent-fix-pr-picked" -ForegroundColor Yellow - } - } - elseif ($FixResult -eq 'lose') { - $label = 's/agent-fix-pr-picked' - $def = $script:SignalLabels[$label] - Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - if ($currentLabels -notcontains $label) { - Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null - Write-Host " ✅ Signal: $label" -ForegroundColor Green - } - if ($currentLabels -contains 's/agent-fix-win') { - Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-win' -Owner $Owner -Repo $Repo | Out-Null - Write-Host " 🗑️ Removed stale: s/agent-fix-win" -ForegroundColor Yellow - } - } -} - -# ============================================================ -# Update-AgentReviewedLabel -# ============================================================ -function Update-AgentReviewedLabel { - <# - .SYNOPSIS - Ensures the s/agent-reviewed tracking label is on the PR. - #> - param( - [Parameter(Mandatory)] [string]$PRNumber, - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - $label = 's/agent-reviewed' - $def = $script:TrackingLabel[$label] - Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo - - $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo - if ($currentLabels -notcontains $label) { - $ok = Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo - if ($ok) { - Write-Host " ✅ Tracking: $label" -ForegroundColor Green - } else { - Write-Host " ⚠️ Failed to apply: $label" -ForegroundColor Yellow - } - } else { - Write-Host " ✅ Already present: $label" -ForegroundColor Green - } -} - -# ============================================================ -# Parse-PhaseOutcomes — read content.md files to determine labels -# ============================================================ -function Parse-PhaseOutcomes { - <# - .SYNOPSIS - Reads phase output content.md files and determines outcome + signal labels. - - .OUTPUTS - Hashtable with keys: Outcome, GateResult, FixResult - #> - param( - [Parameter(Mandatory)] [string]$PRNumber, - [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null) - ) - - $baseDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" - $result = @{ - Outcome = $null # 'approved', 'changes-requested', 'review-incomplete' - GateResult = $null # 'passed', 'failed' - FixResult = $null # 'win', 'lose' - } - - # --- Parse Gate content.md --- - $gateFile = Join-Path $baseDir "gate/content.md" - if (Test-Path $gateFile) { - $gateContent = Get-Content $gateFile -Raw -ErrorAction SilentlyContinue - if ($gateContent) { - # Match the Result line specifically to avoid false matches from other text - if ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:✅|PASSED)') { - $result.GateResult = 'passed' - } - elseif ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:❌|FAILED|SKIPPED)') { - $result.GateResult = 'failed' - } - } - } - - # --- Parse try-fix content.md for fix result --- - $fixFile = Join-Path $baseDir "try-fix/content.md" - if (Test-Path $fixFile) { - $fixContent = Get-Content $fixFile -Raw -ErrorAction SilentlyContinue - if ($fixContent) { - # Extract just the fix name (before any reason separator like " — ") - # to avoid false matches from reason text containing keywords like "try-fix" or "alternative" - if ($fixContent -match '(?i)Selected Fix:\s*\*?\*?\s*(.+?)(?:\s*—|\s*$)') { - $fixName = $matches[1].Trim() - # Agent wins: fix name starts with Candidate/Alternative/try-fix - if ($fixName -match '(?i)^(?:Candidate|Alternative|try-fix)') { - $result.FixResult = 'win' - } - # Agent loses: fix name starts with PR - elseif ($fixName -match '(?i)^(?:\*?\*?\s*)?PR\b') { - $result.FixResult = 'lose' - } - } - } - } - - # --- Parse report content.md for outcome --- - $reportFile = Join-Path $baseDir "report/content.md" - if (Test-Path $reportFile) { - $reportContent = Get-Content $reportFile -Raw -ErrorAction SilentlyContinue - if ($reportContent) { - if ($reportContent -match '(?i)Final\s+Recommendation:\s*APPROVE|✅\s*Final\s+Recommendation:\s*APPROVE') { - $result.Outcome = 'approved' - } - elseif ($reportContent -match '(?i)Final\s+Recommendation:\s*REQUEST.CHANGES|⚠️\s*Final\s+Recommendation:\s*REQUEST.CHANGES') { - $result.Outcome = 'changes-requested' - } - else { - $result.Outcome = 'review-incomplete' - } - } else { - $result.Outcome = 'review-incomplete' - } - } else { - # No report means the agent didn't finish - $result.Outcome = 'review-incomplete' - } - - return $result -} - -# ============================================================ -# Apply-AgentLabels — main entry point -# ============================================================ -function Apply-AgentLabels { - <# - .SYNOPSIS - Main entry point: parses phase outputs and applies all appropriate labels. - - .DESCRIPTION - 1. Parses content.md files from each phase - 2. Applies exactly one outcome label - 3. Applies signal labels based on phase results - 4. Always applies s/agent-reviewed - - .PARAMETER PRNumber - The GitHub PR number. - - .PARAMETER RepoRoot - Repository root path. Defaults to git rev-parse --show-toplevel. - #> - param( - [Parameter(Mandatory)] [string]$PRNumber, - [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null), - [string]$Owner = 'dotnet', - [string]$Repo = 'maui' - ) - - Write-Host "" - Write-Host "🏷️ Applying agent labels to PR #$PRNumber..." -ForegroundColor Cyan - - # Parse phase outcomes from content.md files - $outcomes = Parse-PhaseOutcomes -PRNumber $PRNumber -RepoRoot $RepoRoot - Write-Host " 📊 Parsed outcomes:" -ForegroundColor Gray - Write-Host " Outcome: $($outcomes.Outcome ?? '(none)')" -ForegroundColor Gray - Write-Host " Gate: $($outcomes.GateResult ?? '(skipped)')" -ForegroundColor Gray - Write-Host " Fix: $($outcomes.FixResult ?? '(skipped)')" -ForegroundColor Gray - - try { - # 1. Apply outcome label (exactly one) - if ($outcomes.Outcome) { - Update-AgentOutcomeLabel -PRNumber $PRNumber -Outcome $outcomes.Outcome -Owner $Owner -Repo $Repo - } - - # 2. Apply signal labels - Update-AgentSignalLabels -PRNumber $PRNumber -GateResult $outcomes.GateResult -FixResult $outcomes.FixResult -Owner $Owner -Repo $Repo - - # 3. Always apply tracking label - Update-AgentReviewedLabel -PRNumber $PRNumber -Owner $Owner -Repo $Repo - - Write-Host "" - Write-Host " ✅ Labels applied successfully" -ForegroundColor Green - } - catch { - Write-Host "" - Write-Host " ⚠️ Label application error (non-fatal): $_" -ForegroundColor Yellow - } -} From f103c3295c3e4630dae81d6070d65a1f4eba31d3 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Thu, 26 Feb 2026 01:13:22 +0100 Subject: [PATCH 126/126] Adjust simulator and Android emulator defaults Enable iOS simulator setup and change Android emulator defaults in eng/pipelines/common/provision.yml. skipXcode remains false but skipSimulatorSetup is now false to allow simulator setup. androidEmulatorApiLevel was cleared and both skipAndroidEmulatorImages and skipAndroidCreateAvds set to true to avoid pulling emulator images or creating AVDs by default (reduces CI overhead). --- eng/pipelines/common/provision.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index cb8373148e95..1f00c37d748e 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -1,7 +1,7 @@ parameters: # Xcode skipXcode: false - skipSimulatorSetup: true + skipSimulatorSetup: false # .NET NuGet caches clearCaches: true # Android - JDK @@ -11,9 +11,9 @@ parameters: onlyAndroidPlatformDefaultApis: false skipAndroidPlatformApis: false # Android - Emulators - androidEmulatorApiLevel: '34' - skipAndroidEmulatorImages: false # For most builds we won't need these - skipAndroidCreateAvds: false # For most builds we won't need these + androidEmulatorApiLevel: '' + skipAndroidEmulatorImages: true # For most builds we won't need these + skipAndroidCreateAvds: true # For most builds we won't need these # Provisionator / Xcode skipProvisionator: true checkoutDirectory: $(System.DefaultWorkingDirectory)