diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
index 4a1a1a9ed381..8c9971391500 100644
--- a/.github/aw/actions-lock.json
+++ b/.github/aw/actions-lock.json
@@ -15,6 +15,11 @@
"version": "v0.62.2",
"sha": "20045bbd5ad2632b9809856c389708eab1bd16ef"
},
+ "github/gh-aw-actions/setup@v0.62.5": {
+ "repo": "github/gh-aw-actions/setup",
+ "version": "v0.62.5",
+ "sha": "dc50be57c94373431b49d3d0927f318ac2bb5c4c"
+ },
"github/gh-aw/actions/setup@v0.43.19": {
"repo": "github/gh-aw/actions/setup",
"version": "v0.43.19",
diff --git a/.github/workflows/copilot-detect-categories.lock.yml b/.github/workflows/copilot-detect-categories.lock.yml
new file mode 100644
index 000000000000..3978d41cc2bf
--- /dev/null
+++ b/.github/workflows/copilot-detect-categories.lock.yml
@@ -0,0 +1,1092 @@
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw (v0.62.5). DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# Not all edits will cause changes to this file.
+#
+# For more information: https://github.github.com/gh-aw/introduction/overview/
+#
+# Detects UI test and device test categories in PR diffs and posts a comment listing which test categories should run on CI pipelines
+#
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"381c5e8c8f0215824fc39005cc9383d9324c20c57766dadeef9eea2bdb5cf72a","compiler_version":"v0.62.5","strict":true,"agent_id":"copilot","agent_model":"claude-sonnet-4.6"}
+
+name: "Detect Test Categories for Regression Detection"
+"on":
+ issue_comment:
+ types:
+ - created
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: PR number to detect categories for
+ required: true
+ type: number
+
+permissions: {}
+
+concurrency:
+ cancel-in-progress: true
+ group: detect-categories-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number || github.run_id }}
+
+run-name: "Detect Test Categories for Regression Detection"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: >
+ needs.pre_activation.outputs.activated == 'true' && (((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request &&
+ startsWith(github.event.comment.body, '/detect-categories'))) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id))
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ body: ${{ steps.sanitized.outputs.body }}
+ comment_id: ""
+ comment_repo: ""
+ lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ text: ${{ steps.sanitized.outputs.text }}
+ title: ${{ steps.sanitized.outputs.title }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw-actions/setup@dc50be57c94373431b49d3d0927f318ac2bb5c4c # v0.62.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ - name: Generate agentic run info
+ id: generate_aw_info
+ env:
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
+ GH_AW_INFO_MODEL: "claude-sonnet-4.6"
+ GH_AW_INFO_VERSION: ""
+ GH_AW_INFO_AGENT_VERSION: "latest"
+ GH_AW_INFO_CLI_VERSION: "v0.62.5"
+ GH_AW_INFO_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.24.5"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_COMPILED_STRICT: "true"
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .github
+ .agents
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Check workflow file timestamps
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_WORKFLOW_FILE: "copilot-detect-categories.lock.yml"
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+ - name: Compute current body text
+ id: sanitized
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs');
+ await main();
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_EXPR_93C755A4: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ run: |
+ bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh
+ {
+ cat << 'GH_AW_PROMPT_EOF'
+
+ GH_AW_PROMPT_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
+ cat << 'GH_AW_PROMPT_EOF'
+
+ Tools: add_comment, missing_tool, missing_data, noop
+
+
+ The following GitHub context information is available for this workflow:
+ {{#if __GH_AW_GITHUB_ACTOR__ }}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_REPOSITORY__ }}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_WORKSPACE__ }}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
+ - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
+ - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
+ - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
+ - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_RUN_ID__ }}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ GH_AW_PROMPT_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
+ if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
+ cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md"
+ fi
+ cat << 'GH_AW_PROMPT_EOF'
+
+ GH_AW_PROMPT_EOF
+ cat << 'GH_AW_PROMPT_EOF'
+ {{#runtime-import .github/workflows/copilot-detect-categories.md}}
+ GH_AW_PROMPT_EOF
+ } > "$GH_AW_PROMPT"
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_EXPR_93C755A4: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Substitute placeholders
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_EXPR_93C755A4: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+
+ const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_EXPR_93C755A4: process.env.GH_AW_EXPR_93C755A4,
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT,
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED
+ }
+ });
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh
+ - name: Upload activation artifact
+ if: success()
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: activation
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ env:
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ GH_AW_ASSETS_ALLOWED_EXTS: ""
+ GH_AW_ASSETS_BRANCH: ""
+ GH_AW_ASSETS_MAX_SIZE_KB: 0
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ GH_AW_WORKFLOW_ID_SANITIZED: copilotdetectcategories
+ outputs:
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw-actions/setup@dc50be57c94373431b49d3d0927f318ac2bb5c4c # v0.62.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ - name: Set runtime paths
+ run: |
+ echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_ENV"
+ echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_ENV"
+ echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_ENV"
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ - name: Configure gh CLI for GitHub Enterprise
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh
+ env:
+ GH_TOKEN: ${{ github.token }}
+ - env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ if: github.event_name == 'pull_request'
+ name: Gate — skip if no relevant source files in diff
+ run: "RELEVANT_FILES=$(gh pr diff \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --name-only \\\n | grep -E '\\.(cs|xaml)$' \\\n | grep -iE '(src/Controls/|src/Core/|src/Essentials/|src/Graphics/|src/BlazorWebView/|tests/)' \\\n || true)\nif [ -z \"$RELEVANT_FILES\" ]; then\n echo \"⏭️ No relevant source or test files found in PR diff. Skipping category detection.\"\n exit 1\nfi\necho \"✅ Found relevant files to analyze:\"\necho \"$RELEVANT_FILES\" | head -30\n"
+ - env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+ name: Gather PR diff analysis
+ run: "echo \"Gathering PR analysis for PR #$PR_NUMBER...\"\n\nDIFF=$(gh pr diff \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" 2>/dev/null || true)\nif [ -z \"$DIFF\" ]; then\n echo \"NO_DIFF=true\" > \"$GITHUB_WORKSPACE/category-results.txt\"\n echo \"⚠️ Could not fetch PR diff\"\n exit 0\nfi\n\n# ── UI Test Categories ───────────────────────────────────────────\n# Extract [Category(UITestCategories.X)] from added lines\nUI_CATEGORIES=$(echo \"$DIFF\" \\\n | grep -E '^\\+.*\\[Category\\(UITestCategories\\.' \\\n | grep -oE 'UITestCategories\\.[A-Za-z0-9_]+' \\\n | sed 's/UITestCategories\\.//' \\\n | sort -u \\\n || true)\n\nUI_NAMEOF=$(echo \"$DIFF\" \\\n | grep -E '^\\+.*\\[Category\\(' \\\n | grep -oE 'nameof\\(UITestCategories\\.[A-Za-z0-9_]+\\)' \\\n | sed 's/nameof(UITestCategories\\.//;s/)//' \\\n | sort -u \\\n || true)\n\nUI_QUOTED=$(echo \"$DIFF\" \\\n | grep -E '^\\+.*\\[Category\\(\"' \\\n | grep -oE '\\[Category\\(\"[A-Za-z0-9_]+\"\\)' \\\n | sed 's/\\[Category(\"//;s/\")//' \\\n | sort -u \\\n || true)\n\nALL_UI_CATEGORIES=$(echo -e \"${UI_CATEGORIES}\\n${UI_NAMEOF}\\n${UI_QUOTED}\" \\\n | grep -v '^$' | sort -u || true)\n\n# ── Device Test Categories ───────────────────────────────────────\n# Extract [Category(TestCategory.X)] from added lines\nDEVICE_CATEGORIES=$(echo \"$DIFF\" \\\n | grep -E '^\\+.*\\[Category\\(TestCategory\\.' \\\n | grep -oE 'TestCategory\\.[A-Za-z0-9_]+' \\\n | sed 's/TestCategory\\.//' \\\n | sort -u \\\n || true)\n\nDEVICE_NAMEOF=$(echo \"$DIFF\" \\\n | grep -E '^\\+.*\\[Category\\(' \\\n | grep -oE 'nameof\\(TestCategory\\.[A-Za-z0-9_]+\\)' \\\n | sed 's/nameof(TestCategory\\.//;s/)//' \\\n | sort -u \\\n || true)\n\nALL_DEVICE_CATEGORIES=$(echo -e \"${DEVICE_CATEGORIES}\\n${DEVICE_NAMEOF}\" \\\n | grep -v '^$' | sort -u || true)\n\n# ── Changed file paths ──────────────────────────────────────────\nCHANGED_FILES=$(gh pr diff \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --name-only 2>/dev/null || true)\n\nTEST_FILES=$(echo \"$CHANGED_FILES\" \\\n | grep -iE '(tests?/|TestCases|UnitTests|DeviceTests)' \\\n | grep -E '\\.(cs|xaml)$' \\\n || true)\n\nSOURCE_FILES=$(echo \"$CHANGED_FILES\" \\\n | grep -E '\\.(cs|xaml)$' \\\n | grep -v -iE '(tests?/|TestCases|UnitTests|DeviceTests)' \\\n || true)\n\n# ── Write results ───────────────────────────────────────────────\n{\n echo \"PR_NUMBER=$PR_NUMBER\"\n\n if [ -n \"$ALL_UI_CATEGORIES\" ]; then\n echo \"UI_CATEGORIES< \"$GITHUB_WORKSPACE/category-results.txt\"\n\necho \"=== Summary ===\"\n[ -n \"$ALL_UI_CATEGORIES\" ] && echo \"UI categories: $ALL_UI_CATEGORIES\" || echo \"UI categories: (none detected)\"\n[ -n \"$ALL_DEVICE_CATEGORIES\" ] && echo \"Device categories: $ALL_DEVICE_CATEGORIES\" || echo \"Device categories: (none detected)\"\necho \"Test files changed: $(echo \"$TEST_FILES\" | grep -c '.' || echo 0)\"\necho \"Source files changed: $(echo \"$SOURCE_FILES\" | grep -c '.' || echo 0)\"\n"
+ - env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ inputs.pr_number }}
+ if: github.event_name == 'workflow_dispatch'
+ name: Checkout PR and restore agent infrastructure
+ run: pwsh .github/scripts/Checkout-GhAwPr.ps1
+
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request || github.event.issue.pull_request
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.24.5
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ with:
+ script: |
+ const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download container images
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.5 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.5 ghcr.io/github/gh-aw-firewall/squid:0.24.5 ghcr.io/github/gh-aw-mcpg:v0.1.20 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine
+ - name: Write Safe Outputs Config
+ run: |
+ mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
+ cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
+ {"add_comment":{"max":1,"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_EOF
+ - name: Write Safe Outputs Tools
+ run: |
+ cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_EOF'
+ {
+ "description_suffixes": {
+ "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: *."
+ },
+ "repo_params": {},
+ "dynamic_tools": []
+ }
+ GH_AW_SAFE_OUTPUTS_TOOLS_META_EOF
+ cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF'
+ {
+ "add_comment": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "item_number": {
+ "issueOrPRNumber": true
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ }
+ }
+ GH_AW_SAFE_OUTPUTS_VALIDATION_EOF
+ node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs
+ - name: Generate Safe Outputs MCP Server Config
+ id: safe-outputs-config
+ run: |
+ # Generate a secure random API key (360 bits of entropy, 40+ chars)
+ # Mask immediately to prevent timing vulnerabilities
+ API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${API_KEY}"
+
+ PORT=3001
+
+ # Set outputs for next steps
+ {
+ echo "safe_outputs_api_key=${API_KEY}"
+ echo "safe_outputs_port=${PORT}"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Safe Outputs MCP server will run on port ${PORT}"
+
+ - name: Start Safe Outputs MCP HTTP Server
+ id: safe-outputs-start
+ env:
+ DEBUG: '*'
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ run: |
+ # Environment variables are set above to prevent template injection
+ export DEBUG
+ export GH_AW_SAFE_OUTPUTS_PORT
+ export GH_AW_SAFE_OUTPUTS_API_KEY
+ export GH_AW_SAFE_OUTPUTS_TOOLS_PATH
+ export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
+ export GH_AW_MCP_LOG_DIR
+
+ bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh
+
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
+ GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }}
+ GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p /tmp/gh-aw/mcp-config
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="80"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.20'
+
+ mkdir -p /home/runner/.copilot
+ cat << GH_AW_MCP_CONFIG_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v0.32.0",
+ "env": {
+ "GITHUB_HOST": "\${GITHUB_SERVER_URL}",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ },
+ "guard-policies": {
+ "allow-only": {
+ "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
+ "repos": "$GITHUB_MCP_GUARD_REPOS"
+ }
+ }
+ },
+ "safeoutputs": {
+ "type": "http",
+ "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT",
+ "headers": {
+ "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}"
+ },
+ "guard-policies": {
+ "write-sink": {
+ "accept": [
+ "*"
+ ]
+ }
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_EOF
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Clean git credentials
+ continue-on-error: true
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 15
+ run: |
+ set -o pipefail
+ touch /tmp/gh-aw/agent-step-summary.md
+ # shellcheck disable=SC1003
+ sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.5 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: claude-sonnet-4.6
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_PHASE: agent
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_VERSION: v0.62.5
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ XDG_CONFIG_HOME: /home/runner
+ - name: Detect inference access error
+ id: detect-inference-error
+ if: always()
+ continue-on-error: true
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: |
+ # Copy Copilot session state files to logs folder for artifact collection
+ # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
+ SESSION_STATE_DIR="$HOME/.copilot/session-state"
+ LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
+
+ if [ -d "$SESSION_STATE_DIR" ]; then
+ echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
+ mkdir -p "$LOGS_DIR"
+ cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
+ echo "Session state files copied successfully"
+ else
+ echo "No session-state directory found at $SESSION_STATE_DIR"
+ fi
+ - name: Stop MCP Gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh
+ - name: Copy Safe Outputs
+ if: always()
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs');
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
+ # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)
+ if command -v awf &> /dev/null; then
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo 'AWF binary not installed, skipping firewall log summary'
+ fi
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ if-no-files-found: ignore
+ # --- Threat Detection (inline) ---
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }}
+ HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP configuration for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f /tmp/gh-aw/mcp-config/mcp-servers.json
+ rm -f /home/runner/.copilot/mcp-config.json
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/aw-prompts
+ cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true
+ cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true
+ for f in /tmp/gh-aw/aw-*.patch; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ echo "Prepared threat detection files:"
+ ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ WORKFLOW_DESCRIPTION: "Detects UI test and device test categories in PR diffs and posts a comment listing which test categories should run on CI pipelines"
+ HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs');
+ await main();
+ - name: Ensure threat-detection directory and log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection
+ touch /tmp/gh-aw/threat-detection/detection.log
+ - name: Execute GitHub Copilot CLI
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ id: detection_agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ # --allow-tool shell(cat)
+ # --allow-tool shell(grep)
+ # --allow-tool shell(head)
+ # --allow-tool shell(jq)
+ # --allow-tool shell(ls)
+ # --allow-tool shell(tail)
+ # --allow-tool shell(wc)
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ touch /tmp/gh-aw/agent-step-summary.md
+ # shellcheck disable=SC1003
+ sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.5 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: claude-sonnet-4.6
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: v0.62.5
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ XDG_CONFIG_HOME: /home/runner
+ - name: Parse threat detection results
+ id: parse_detection_results
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs');
+ await main();
+ - name: Upload threat detection log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: detection
+ path: /tmp/gh-aw/threat-detection/detection.log
+ if-no-files-found: ignore
+ - name: Set detection conclusion
+ id: detection_conclusion
+ if: always()
+ env:
+ RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
+ DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }}
+ run: |
+ if [[ "$RUN_DETECTION" != "true" ]]; then
+ echo "conclusion=skipped" >> "$GITHUB_OUTPUT"
+ echo "success=true" >> "$GITHUB_OUTPUT"
+ echo "Detection was not needed, marking as skipped"
+ elif [[ "$DETECTION_SUCCESS" == "true" ]]; then
+ echo "conclusion=success" >> "$GITHUB_OUTPUT"
+ echo "success=true" >> "$GITHUB_OUTPUT"
+ echo "Detection passed successfully"
+ else
+ echo "conclusion=failure" >> "$GITHUB_OUTPUT"
+ echo "success=false" >> "$GITHUB_OUTPUT"
+ echo "Detection found issues"
+ fi
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - safe_outputs
+ if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-copilot-detect-categories"
+ cancel-in-progress: false
+ outputs:
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw-actions/setup@dc50be57c94373431b49d3d0927f318ac2bb5c4c # v0.62.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
+ - name: Process No-Op Messages
+ id: noop
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs');
+ await main();
+ - name: Record Missing Tool
+ id: missing_tool
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs');
+ await main();
+ - name: Handle Agent Failure
+ id: handle_agent_failure
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_WORKFLOW_ID: "copilot-detect-categories"
+ GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
+ GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🏷️ *Category detection by [{workflow_name}]({run_url})*\",\"runStarted\":\"🔍 Detecting test categories… [{workflow_name}]({run_url})\",\"runSuccess\":\"✅ Category detection complete! [{workflow_name}]({run_url})\",\"runFailure\":\"❌ Category detection failed. [{workflow_name}]({run_url}) {status}\"}"
+ GH_AW_GROUP_REPORTS: "false"
+ GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
+ GH_AW_TIMEOUT_MINUTES: "15"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs');
+ await main();
+ - name: Handle No-Op Message
+ id: handle_noop_message
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }}
+ GH_AW_NOOP_REPORT_AS_ISSUE: "true"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs');
+ await main();
+
+ pre_activation:
+ if: >
+ ((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request &&
+ startsWith(github.event.comment.body, '/detect-categories'))) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id)
+ runs-on: ubuntu-slim
+ outputs:
+ activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ matched_command: ''
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw-actions/setup@dc50be57c94373431b49d3d0927f318ac2bb5c4c # v0.62.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_REQUIRED_ROLES: admin,maintainer,write
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs');
+ await main();
+
+ safe_outputs:
+ needs: agent
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.agent.outputs.detection_success == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/copilot-detect-categories"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: "claude-sonnet-4.6"
+ GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🏷️ *Category detection by [{workflow_name}]({run_url})*\",\"runStarted\":\"🔍 Detecting test categories… [{workflow_name}]({run_url})\",\"runSuccess\":\"✅ Category detection complete! [{workflow_name}]({run_url})\",\"runFailure\":\"❌ Category detection failed. [{workflow_name}]({run_url}) {status}\"}"
+ GH_AW_WORKFLOW_ID: "copilot-detect-categories"
+ GH_AW_WORKFLOW_NAME: "Detect Test Categories for Regression Detection"
+ outputs:
+ code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
+ code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
+ comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }}
+ comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw-actions/setup@dc50be57c94373431b49d3d0927f318ac2bb5c4c # v0.62.5
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
+ - name: Configure GH_HOST for enterprise compatibility
+ shell: bash
+ run: |
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"}}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs');
+ await main();
+ - name: Upload safe output items
+ if: always()
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: safe-output-items
+ path: /tmp/gh-aw/safe-output-items.jsonl
+ if-no-files-found: ignore
+
diff --git a/.github/workflows/copilot-detect-categories.md b/.github/workflows/copilot-detect-categories.md
new file mode 100644
index 000000000000..302b0a576804
--- /dev/null
+++ b/.github/workflows/copilot-detect-categories.md
@@ -0,0 +1,345 @@
+---
+description: Detects UI test and device test categories in PR diffs and posts a comment listing which test categories should run on CI pipelines
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ issue_comment:
+ types: [created]
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'PR number to detect categories for'
+ required: true
+ type: number
+
+if: >-
+ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
+ github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request &&
+ startsWith(github.event.comment.body, '/detect-categories'))
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+
+engine:
+ id: copilot
+ model: claude-sonnet-4.6
+
+safe-outputs:
+ add-comment:
+ max: 1
+ target: "*"
+ noop:
+ messages:
+ footer: "> 🏷️ *Category detection by [{workflow_name}]({run_url})*"
+ run-started: "🔍 Detecting test categories… [{workflow_name}]({run_url})"
+ run-success: "✅ Category detection complete! [{workflow_name}]({run_url})"
+ run-failure: "❌ Category detection failed. [{workflow_name}]({run_url}) {status}"
+
+tools:
+ github:
+ toolsets: [default]
+
+network: defaults
+
+concurrency:
+ group: "detect-categories-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number || github.run_id }}"
+ cancel-in-progress: true
+
+timeout-minutes: 15
+
+steps:
+ - name: Gate — skip if no relevant source files in diff
+ if: github.event_name == 'pull_request'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ RELEVANT_FILES=$(gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --name-only \
+ | grep -E '\.(cs|xaml)$' \
+ | grep -iE '(src/Controls/|src/Core/|src/Essentials/|src/Graphics/|src/BlazorWebView/|tests/)' \
+ || true)
+ if [ -z "$RELEVANT_FILES" ]; then
+ echo "⏭️ No relevant source or test files found in PR diff. Skipping category detection."
+ exit 1
+ fi
+ echo "✅ Found relevant files to analyze:"
+ echo "$RELEVANT_FILES" | head -30
+
+ - name: Gather PR diff analysis
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+ run: |
+ echo "Gathering PR analysis for PR #$PR_NUMBER..."
+
+ DIFF=$(gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true)
+ if [ -z "$DIFF" ]; then
+ echo "NO_DIFF=true" > "$GITHUB_WORKSPACE/category-results.txt"
+ echo "⚠️ Could not fetch PR diff"
+ exit 0
+ fi
+
+ # ── UI Test Categories ───────────────────────────────────────────
+ # Extract [Category(UITestCategories.X)] from added lines
+ UI_CATEGORIES=$(echo "$DIFF" \
+ | grep -E '^\+.*\[Category\(UITestCategories\.' \
+ | grep -oE 'UITestCategories\.[A-Za-z0-9_]+' \
+ | sed 's/UITestCategories\.//' \
+ | sort -u \
+ || true)
+
+ UI_NAMEOF=$(echo "$DIFF" \
+ | grep -E '^\+.*\[Category\(' \
+ | grep -oE 'nameof\(UITestCategories\.[A-Za-z0-9_]+\)' \
+ | sed 's/nameof(UITestCategories\.//;s/)//' \
+ | sort -u \
+ || true)
+
+ UI_QUOTED=$(echo "$DIFF" \
+ | grep -E '^\+.*\[Category\("' \
+ | grep -oE '\[Category\("[A-Za-z0-9_]+"\)' \
+ | sed 's/\[Category("//;s/")//' \
+ | sort -u \
+ || true)
+
+ ALL_UI_CATEGORIES=$(echo -e "${UI_CATEGORIES}\n${UI_NAMEOF}\n${UI_QUOTED}" \
+ | grep -v '^$' | sort -u || true)
+
+ # ── Device Test Categories ───────────────────────────────────────
+ # Extract [Category(TestCategory.X)] from added lines
+ DEVICE_CATEGORIES=$(echo "$DIFF" \
+ | grep -E '^\+.*\[Category\(TestCategory\.' \
+ | grep -oE 'TestCategory\.[A-Za-z0-9_]+' \
+ | sed 's/TestCategory\.//' \
+ | sort -u \
+ || true)
+
+ DEVICE_NAMEOF=$(echo "$DIFF" \
+ | grep -E '^\+.*\[Category\(' \
+ | grep -oE 'nameof\(TestCategory\.[A-Za-z0-9_]+\)' \
+ | sed 's/nameof(TestCategory\.//;s/)//' \
+ | sort -u \
+ || true)
+
+ ALL_DEVICE_CATEGORIES=$(echo -e "${DEVICE_CATEGORIES}\n${DEVICE_NAMEOF}" \
+ | grep -v '^$' | sort -u || true)
+
+ # ── Changed file paths ──────────────────────────────────────────
+ CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --name-only 2>/dev/null || true)
+
+ TEST_FILES=$(echo "$CHANGED_FILES" \
+ | grep -iE '(tests?/|TestCases|UnitTests|DeviceTests)' \
+ | grep -E '\.(cs|xaml)$' \
+ || true)
+
+ SOURCE_FILES=$(echo "$CHANGED_FILES" \
+ | grep -E '\.(cs|xaml)$' \
+ | grep -v -iE '(tests?/|TestCases|UnitTests|DeviceTests)' \
+ || true)
+
+ # ── Write results ───────────────────────────────────────────────
+ {
+ echo "PR_NUMBER=$PR_NUMBER"
+
+ if [ -n "$ALL_UI_CATEGORIES" ]; then
+ echo "UI_CATEGORIES< "$GITHUB_WORKSPACE/category-results.txt"
+
+ echo "=== Summary ==="
+ [ -n "$ALL_UI_CATEGORIES" ] && echo "UI categories: $ALL_UI_CATEGORIES" || echo "UI categories: (none detected)"
+ [ -n "$ALL_DEVICE_CATEGORIES" ] && echo "Device categories: $ALL_DEVICE_CATEGORIES" || echo "Device categories: (none detected)"
+ echo "Test files changed: $(echo "$TEST_FILES" | grep -c '.' || echo 0)"
+ echo "Source files changed: $(echo "$SOURCE_FILES" | grep -c '.' || echo 0)"
+
+ - name: Checkout PR and restore agent infrastructure
+ if: github.event_name == 'workflow_dispatch'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ inputs.pr_number }}
+ run: pwsh .github/scripts/Checkout-GhAwPr.ps1
+---
+
+# Detect Test Categories for Regression Detection
+
+You are a CI assistant that analyzes PR diffs to determine which UI test and device test categories should run on the existing `maui-pr-uitests` and `maui-pr-devicetests` pipelines. You MUST post a comment with your findings.
+
+## Context
+
+- **Repository**: ${{ github.repository }}
+- **PR Number**: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
+
+## Instructions
+
+### Step 1: Read detection results
+
+Read `category-results.txt` from the workspace root. It contains:
+- `PR_NUMBER=`
+- `UI_CATEGORIES` block — UI test categories detected from `[Category(UITestCategories.X)]` in added lines
+- `DEVICE_CATEGORIES` block — device test categories detected from `[Category(TestCategory.X)]` in added lines
+- `---TEST_FILES---` section — changed test file paths
+- `---SOURCE_FILES---` section — changed source (non-test) file paths
+- `---ALL_FILES---` section — all changed files
+
+### Step 2: Intelligent category inference
+
+If categories were detected directly from test annotations, use those.
+
+If **NO categories** were detected (no test files changed, or test files don't have new Category attributes), you MUST **infer** which categories are likely affected based on the changed **source files**. Use this mapping:
+
+#### Source Path → UI Test Category mapping
+
+| Source path pattern | UI Test Categories | Device Test Categories |
+|--------------------|--------------------|----------------------|
+| `src/Controls/src/Core/Button/` or `*Button*.cs` | Button | Button |
+| `src/Controls/src/Core/Label/` or `*Label*.cs` | Label | Label |
+| `src/Controls/src/Core/Entry/` or `*Entry*.cs` | Entry | Entry |
+| `src/Controls/src/Core/Editor/` or `*Editor*.cs` | Editor | Editor |
+| `src/Controls/src/Core/CollectionView/` | CollectionView | CollectionView |
+| `src/Controls/src/Core/CarouselView/` | CarouselView | CarouselView |
+| `src/Controls/src/Core/ListView/` or `*ListView*.cs` | ListView | ListView |
+| `src/Controls/src/Core/Shell/` or `*Shell*.cs` | Shell | Shell |
+| `src/Controls/src/Core/NavigationPage/` or `*Navigation*.cs` | Navigation | NavigationPage |
+| `src/Controls/src/Core/Layout/` or `*Layout*.cs` | Layout | Layout |
+| `src/Controls/src/Core/ScrollView/` | ScrollView | ScrollView |
+| `src/Controls/src/Core/WebView/` | WebView | WebView |
+| `src/Controls/src/Core/Image/` or `*Image.cs` | Image | Image |
+| `src/Controls/src/Core/ImageButton/` | ImageButton | ImageButton |
+| `src/Controls/src/Core/SearchBar/` | SearchBar | SearchBar |
+| `src/Controls/src/Core/Picker/` | Picker | Picker |
+| `src/Controls/src/Core/DatePicker/` | DatePicker | DatePicker |
+| `src/Controls/src/Core/TimePicker/` | TimePicker | TimePicker |
+| `src/Controls/src/Core/Switch/` | Switch | - |
+| `src/Controls/src/Core/Slider/` | Slider | Slider |
+| `src/Controls/src/Core/Stepper/` | Stepper | Stepper |
+| `src/Controls/src/Core/CheckBox/` | CheckBox | CheckBox |
+| `src/Controls/src/Core/RadioButton/` | RadioButton | RadioButton |
+| `src/Controls/src/Core/ProgressBar/` | ProgressBar | ProgressBar |
+| `src/Controls/src/Core/ActivityIndicator/` | ActivityIndicator | ActivityIndicator |
+| `src/Controls/src/Core/Border/` or `*Border*.cs` | Border | Border |
+| `src/Controls/src/Core/Frame/` | Frame | - |
+| `src/Controls/src/Core/BoxView/` | BoxView | - |
+| `src/Controls/src/Core/RefreshView/` | RefreshView | RefreshView |
+| `src/Controls/src/Core/SwipeView/` | SwipeView | SwipeView |
+| `src/Controls/src/Core/IndicatorView/` | IndicatorView | IndicatorView |
+| `src/Controls/src/Core/FlyoutPage/` | FlyoutPage | FlyoutPage |
+| `src/Controls/src/Core/TabbedPage/` | TabbedPage | TabbedPage |
+| `src/Controls/src/Core/Page/` or `*Page.cs` (base) | Page | Page |
+| `src/Controls/src/Core/Window/` | Window | Window |
+| `src/Controls/src/Core/Shapes/` or `*Shape*.cs` | Shape | - |
+| `src/Controls/src/Core/Shadow/` | Shadow | - |
+| `src/Controls/src/Core/Brush/` | Brush | - |
+| `src/Controls/src/Core/Gestures/` or `*Gesture*.cs` | Gestures | Gesture |
+| `src/Controls/src/Core/DragDrop/` | DragAndDrop | - |
+| `src/Controls/src/Core/Accessibility/` | Accessibility | Accessibility |
+| `src/Controls/src/Core/Handlers/Items/` | CollectionView, CarouselView | CollectionView, CarouselView |
+| `src/Controls/src/Core/Handlers/Items2/` | CollectionView, CarouselView | CollectionView, CarouselView |
+| `src/Core/src/Handlers/` | (match handler name) | (match handler name) |
+| `src/Core/src/Platform/` | Layout, Page, Window | Layout, Page, Window |
+| `src/Core/src/Fonts/` | Fonts | Fonts |
+| `src/Essentials/` | - | (Essentials device tests) |
+| `src/Graphics/` | GraphicsView | Graphics |
+| `src/BlazorWebView/` | WebView | BlazorWebView |
+| `src/Controls/src/Core/Layout/Grid*` | Layout | Layout, FlexLayout |
+| `src/Controls/src/Core/Toolbar/` | ToolbarItem | Toolbar |
+
+**Important inference rules:**
+1. If a handler file changes (e.g., `ButtonHandler.Android.cs`), always include that control's category
+2. If platform-specific code changes (e.g., files in `Platform/Android/`), consider broader categories
+3. If core infrastructure changes (e.g., `VisualElement.cs`, `Element.cs`), recommend running ALL categories
+4. If only build/pipeline files change, recommend NO test categories
+5. When in doubt, include the category — it's better to test more than miss a regression
+
+### Step 3: Determine device test recommendation
+
+Based on the categories and source files:
+- If **Controls handler/platform code** changed → recommend device tests
+- If **Core handler/platform code** changed → recommend device tests
+- If **only UI test files** changed → device tests optional
+- If **Essentials/Graphics/BlazorWebView** code changed → recommend device tests for those projects
+
+### Step 4: Post the comment
+
+Use `add_comment` with `item_number` set to the PR number. Use this format:
+
+```markdown
+## 🏷️ Test Categories for Regression Detection
+
+{1-2 sentence summary of what was detected/inferred}
+
+### UI Test Categories
+
+| Category | Source |
+|----------|--------|
+| {Category} | {Detected from test / Inferred from source change} |
+
+**Pipeline filter:** `{comma-separated UI categories}`
+
+### Device Test Categories
+
+| Category | Project | Source |
+|----------|---------|--------|
+| {Category} | {Controls/Core/Essentials/Graphics/BlazorWebView} | {Detected/Inferred} |
+
+**Recommendation:** {Run device tests: Yes/No} — {reason}
+
+### 🚀 Run Targeted Tests on Existing Pipelines
+
+Both `maui-pr-uitests` and `maui-pr-devicetests` now support category filtering parameters. To run only the relevant tests for this PR:
+
+**UI Tests** — trigger `maui-pr-uitests` with:
+> Parameter: `uiTestCategories` = `{comma-separated UI categories}`
+
+**Device Tests** — trigger `maui-pr-devicetests` with:
+> Parameter: `deviceTestCategories` = `{semicolon-separated device categories}`
+
+When triggered without parameters (e.g., by normal PR push), all categories run as usual.
+
+
+📁 Changed files ({total count})
+
+**Test files ({count}):**
+{list}
+
+**Source files ({count}):**
+{list}
+
+
+
+> ℹ️ Categories are detected from `[Category()]` attributes in the diff and inferred from changed source file paths.
+```
+
+If NO categories could be detected or inferred (e.g., only docs/build files changed):
+
+```markdown
+## 🏷️ Test Categories for Regression Detection
+
+No test categories could be determined for this PR. The changes don't appear to affect any testable control or platform code.
+
+
+📁 Changed files ({count})
+
+{list}
+
+
+```
+
diff --git a/eng/helix_xharness.proj b/eng/helix_xharness.proj
index ffba39148bb1..206a93f3d9d0 100644
--- a/eng/helix_xharness.proj
+++ b/eng/helix_xharness.proj
@@ -32,6 +32,17 @@
CollectionView;Shell;HybridWebView
+
+
+
+
+
+
AppleIntelligenceChatClient
@@ -252,6 +263,26 @@
+
+
+
+
+ xharness apple test --target "$target" --app "$app" --output-directory "$output_directory" --timeout "$timeout" --launch-timeout "$launch_timeout" --set-env="TestFilter=Category=$(DeviceTestCategoryFilter)"
+
+
+
+
+ TestFilter=Category=$(DeviceTestCategoryFilter)
+
+
+
+
diff --git a/eng/pipelines/arcade/stage-device-tests.yml b/eng/pipelines/arcade/stage-device-tests.yml
index e4c84a19d9e0..9c903cab8422 100644
--- a/eng/pipelines/arcade/stage-device-tests.yml
+++ b/eng/pipelines/arcade/stage-device-tests.yml
@@ -182,9 +182,9 @@ stages:
parameters:
HelixProjectPath: ${{ parameters.helixProject }}
${{ if eq(parameters.runAsPublic, true) }}:
- HelixProjectArguments: /p:TargetOS=ios /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsIOS_ /p:HelixInternal=False
+ HelixProjectArguments: /p:TargetOS=ios /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsIOS_ /p:HelixInternal=False ${{ parameters.extraHelixArguments }}
${{ else }}:
- HelixProjectArguments: /p:TargetOS=ios /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsIOS_ /p:HelixInternal=True
+ HelixProjectArguments: /p:TargetOS=ios /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsIOS_ /p:HelixInternal=True ${{ parameters.extraHelixArguments }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixConfiguration: $(_BuildConfig)
IncludeDotNetCli: true
@@ -236,9 +236,9 @@ stages:
parameters:
HelixProjectPath: ${{ parameters.helixProject }}
${{ if eq(parameters.runAsPublic, true) }}:
- HelixProjectArguments: /p:TargetOS=maccatalyst /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsMacCatalyst_ /p:HelixInternal=False
+ HelixProjectArguments: /p:TargetOS=maccatalyst /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsMacCatalyst_ /p:HelixInternal=False ${{ parameters.extraHelixArguments }}
${{ else }}:
- HelixProjectArguments: /p:TargetOS=maccatalyst /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsMacCatalyst_ /p:HelixInternal=True
+ HelixProjectArguments: /p:TargetOS=maccatalyst /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsMacCatalyst_ /p:HelixInternal=True ${{ parameters.extraHelixArguments }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixConfiguration: $(_BuildConfig)
IncludeDotNetCli: true
@@ -288,9 +288,9 @@ stages:
parameters:
HelixProjectPath: ${{ parameters.helixProject }}
${{ if eq(parameters.runAsPublic, true) }}:
- HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsAndroid_ /p:HelixInternal=False
+ HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsAndroid_ /p:HelixInternal=False ${{ parameters.extraHelixArguments }}
${{ else }}:
- HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsAndroid_ /p:HelixInternal=True
+ HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsAndroid_ /p:HelixInternal=True ${{ parameters.extraHelixArguments }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixConfiguration: $(_BuildConfig)
IncludeDotNetCli: true
@@ -395,9 +395,9 @@ stages:
parameters:
HelixProjectPath: ${{ parameters.helixProject }}
${{ if eq(parameters.runAsPublic, true) }}:
- HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig)_CoreCLR /p:TestRunNamePrefix=DeviceTestsAndroid_CoreCLR_ /p:HelixInternal=False
+ HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig)_CoreCLR /p:TestRunNamePrefix=DeviceTestsAndroid_CoreCLR_ /p:HelixInternal=False ${{ parameters.extraHelixArguments }}
${{ else }}:
- HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig)_CoreCLR /p:TestRunNamePrefix=DeviceTestsAndroid_CoreCLR_ /p:HelixInternal=True
+ HelixProjectArguments: /p:TargetOS=android /p:TestRunNameSuffix=_$(_BuildConfig)_CoreCLR /p:TestRunNamePrefix=DeviceTestsAndroid_CoreCLR_ /p:HelixInternal=True ${{ parameters.extraHelixArguments }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixConfiguration: $(_BuildConfig)
IncludeDotNetCli: true
@@ -625,9 +625,9 @@ stages:
parameters:
HelixProjectPath: ${{ parameters.helixProject }}
${{ if eq(parameters.runAsPublic, true) }}:
- HelixProjectArguments: /p:TargetOS=windows /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsWindows_ /p:HelixInternal=False
+ HelixProjectArguments: /p:TargetOS=windows /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsWindows_ /p:HelixInternal=False ${{ parameters.extraHelixArguments }}
${{ else }}:
- HelixProjectArguments: /p:TargetOS=windows /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsWindows_ /p:HelixInternal=True
+ HelixProjectArguments: /p:TargetOS=windows /p:TestRunNameSuffix=_$(_BuildConfig) /p:TestRunNamePrefix=DeviceTestsWindows_ /p:HelixInternal=True ${{ parameters.extraHelixArguments }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixConfiguration: $(_BuildConfig)
IncludeDotNetCli: true
diff --git a/eng/pipelines/ci-device-tests.yml b/eng/pipelines/ci-device-tests.yml
index 6dd405b3f56a..d830f35620ea 100644
--- a/eng/pipelines/ci-device-tests.yml
+++ b/eng/pipelines/ci-device-tests.yml
@@ -55,6 +55,16 @@ variables:
parameters:
+- name: deviceTestCategories
+ displayName: 'Device Test Categories to run (semicolon-separated, e.g. Button;Label;Shell). Empty = all categories.'
+ type: string
+ default: ''
+
+- name: prNumber
+ displayName: 'PR number to test (checks out the PR branch). Leave empty for current branch.'
+ type: string
+ default: ''
+
# Internal pools (dnceng)
- name: windowsPoolInternal
type: object
@@ -129,7 +139,19 @@ stages:
runAsPublic: true
runWindowsTests: true
TargetFrameworkVersion: ${{ targetFrameworkVersion.tfm }}
+ # When deviceTestCategories is specified, pass it to helix_xharness.proj
+ # which will apply it as a TestFilter on all work items.
+ ${{ if ne(parameters.deviceTestCategories, '') }}:
+ extraHelixArguments: '/p:DeviceTestCategoryFilter=${{ parameters.deviceTestCategories }}'
prepareSteps:
+ - ${{ if ne(parameters.prNumber, '') }}:
+ - script: |
+ echo "Checking out PR #${{ parameters.prNumber }}..."
+ git fetch origin pull/${{ parameters.prNumber }}/head:pr-${{ parameters.prNumber }}
+ git checkout pr-${{ parameters.prNumber }}
+ echo "Checked out PR #${{ parameters.prNumber }}"
+ git log --oneline -1
+ displayName: 'Checkout PR #${{ parameters.prNumber }}'
- template: /eng/pipelines/common/provision.yml@self
parameters:
checkoutDirectory: '$(System.DefaultWorkingDirectory)'
diff --git a/eng/pipelines/ci-uitests.yml b/eng/pipelines/ci-uitests.yml
index ff4d6125f69d..9e0467c62909 100644
--- a/eng/pipelines/ci-uitests.yml
+++ b/eng/pipelines/ci-uitests.yml
@@ -64,6 +64,16 @@ parameters:
type: boolean
default: false
+ - name: uiTestCategories
+ displayName: 'UI Test Categories to run (comma-separated, e.g. Button,Label,Shell). Empty = all categories.'
+ type: string
+ default: ''
+
+ - name: prNumber
+ displayName: 'PR number to test (checks out the PR branch). Leave empty for current branch.'
+ type: string
+ default: ''
+
# Internal pools (dnceng)
- name: androidPoolInternal
type: object
@@ -150,6 +160,13 @@ stages:
- template: common/ui-tests.yml
parameters:
+ # When uiTestCategories is specified, run only those categories (single matrix entry).
+ # When empty (default), the full categoryGroupsToTest list from ui-tests.yml is used.
+ ${{ if ne(parameters.uiTestCategories, '') }}:
+ categoryGroupsToTest:
+ - '${{ parameters.uiTestCategories }}'
+ # When prNumber is specified, checkout that PR branch before building.
+ prNumber: ${{ parameters.prNumber }}
# Select pools based on pipeline - internal vs public
${{ if eq(variables['System.TeamProject'], 'internal') }}:
androidPool: ${{ parameters.androidPoolInternal }}
diff --git a/eng/pipelines/common/ui-tests.yml b/eng/pipelines/common/ui-tests.yml
index 544b45bc7c57..5f78c79da8bf 100644
--- a/eng/pipelines/common/ui-tests.yml
+++ b/eng/pipelines/common/ui-tests.yml
@@ -12,6 +12,7 @@ parameters:
defaultiOSVersion: '26.0'
timeoutInMinutes: 180
skipProvisioning: true
+ prNumber: '' # When set, checks out the PR branch before building
BuildNativeAOT: false # Parameter to control whether NativeAOT artifacts should be built
RunNativeAOT: false # Parameter to control whether NativeAOT UI tests should run
categoryGroupsToTest:
@@ -63,6 +64,14 @@ stages:
REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
APPIUM_HOME: $(System.DefaultWorkingDirectory)/.appium/
steps:
+ - ${{ if ne(parameters.prNumber, '') }}:
+ - script: |
+ echo "Checking out PR #${{ parameters.prNumber }}..."
+ git fetch origin pull/${{ parameters.prNumber }}/head:pr-${{ parameters.prNumber }}
+ git checkout pr-${{ parameters.prNumber }}
+ echo "Checked out PR #${{ parameters.prNumber }}"
+ git log --oneline -1
+ displayName: 'Checkout PR #${{ parameters.prNumber }}'
- template: ui-tests-build-sample.yml
parameters:
runtimeVariant: "Mono"
@@ -79,6 +88,14 @@ stages:
REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
APPIUM_HOME: $(System.DefaultWorkingDirectory)/.appium/
steps:
+ - ${{ if ne(parameters.prNumber, '') }}:
+ - script: |
+ echo "Checking out PR #${{ parameters.prNumber }}..."
+ git fetch origin pull/${{ parameters.prNumber }}/head:pr-${{ parameters.prNumber }}
+ git checkout pr-${{ parameters.prNumber }}
+ echo "Checked out PR #${{ parameters.prNumber }}"
+ git log --oneline -1
+ displayName: 'Checkout PR #${{ parameters.prNumber }}'
- template: ui-tests-build-sample.yml
parameters:
runtimeVariant: "CoreCLR"