I9_Incremental_loading_on_scroll - Jumping back to the top of the list as you scroll down. #15433
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Trigger the maui-copilot DevDiv pipeline when a maintainer comments '/review' on a PR. | |
| # Uses OIDC (no PAT) — see .github/docs/trigger-azdo-pipeline-setup.md for identity setup. | |
| name: Review Trigger | |
| on: | |
| issue_comment: | |
| types: [created] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to review' | |
| required: true | |
| platform: | |
| description: 'Target platform (android, ios, catalyst, windows, or empty for pipeline default)' | |
| required: false | |
| type: choice | |
| options: | |
| - '' | |
| - android | |
| - ios | |
| - catalyst | |
| - windows | |
| pipeline_ref: | |
| description: 'AzDO pipeline branch (default: main)' | |
| required: false | |
| default: 'main' | |
| jobs: | |
| # Lightweight pre-flight gate that matches the command and checks permission | |
| # before provisioning the write-capable trigger-review job. | |
| match: | |
| if: github.event_name == 'workflow_dispatch' || github.event.issue.pull_request | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| matched: ${{ steps.check.outputs.matched }} | |
| proceed: ${{ steps.gate.outputs.proceed }} | |
| steps: | |
| - name: Match /review command | |
| id: check | |
| env: | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| echo "matched=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| TRIMMED_BODY=$(printf '%s' "${COMMENT_BODY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') | |
| # `/review rerun` is not a supported command. Reject it explicitly so | |
| # it cannot fall through to the generic `/review` matcher below. `sed` | |
| # strips leading whitespace per line, so a leading BLANK line survives in | |
| # TRIMMED_BODY; tolerate leading whitespace/newlines here (consistent with | |
| # the generic matcher) so a pasted `\n/review rerun` can't evade the guard. | |
| if [[ "${TRIMMED_BODY}" =~ ^[[:space:]]*/review[[:space:]]+rerun([[:space:]]|$) ]]; then | |
| echo "matched=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Match `/review` as the first non-whitespace token, optionally followed by args. | |
| # Allows arbitrary leading whitespace (spaces, tabs, newlines). | |
| # `/review tests` is reserved for the gh-aw test-failure review workflow. | |
| if [[ "${COMMENT_BODY}" =~ ^[[:space:]]*/review[[:space:]]+tests([[:space:]]|$) ]]; then | |
| echo "matched=false" >> "$GITHUB_OUTPUT" | |
| elif [[ "${COMMENT_BODY}" =~ ^[[:space:]]*/review([[:space:]]|$) ]]; then | |
| echo "matched=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "matched=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Check actor permission | |
| id: gate | |
| if: steps.check.outputs.matched == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ACTOR: ${{ github.actor }} | |
| REPO: ${{ github.repository }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| echo "workflow_dispatch — skipping collaborator check" | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.permission' 2>/dev/null); then | |
| echo "::warning::Permission lookup failed for ${ACTOR}; treating the caller as unauthorized." | |
| PERMISSION="none" | |
| fi | |
| echo "User ${ACTOR} has permission: ${PERMISSION}" | |
| # write, maintain, and admin can all trigger /review | |
| if [[ "${PERMISSION}" != "admin" && "${PERMISSION}" != "maintain" && "${PERMISSION}" != "write" ]]; then | |
| echo "::notice::User ${ACTOR} does not have sufficient access (${PERMISSION}). Only write/maintain/admin can trigger /review. Skipping." | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| trigger-review: | |
| needs: match | |
| if: needs.match.outputs.proceed == 'true' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: review-trigger-${{ github.event.issue.number || inputs.pr_number }} | |
| cancel-in-progress: false | |
| timeout-minutes: 10 | |
| permissions: | |
| id-token: write | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| env: | |
| REPO: ${{ github.repository }} | |
| steps: | |
| - name: Parse parameters | |
| id: params | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| INPUT_PR_NUMBER: ${{ inputs.pr_number }} | |
| INPUT_PLATFORM: ${{ inputs.platform }} | |
| INPUT_PIPELINE_REF: ${{ inputs.pipeline_ref }} | |
| run: | | |
| # Valid platforms (from AzDO pipeline definition) | |
| VALID_PLATFORMS="android ios catalyst windows" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| PR_NUMBER="${INPUT_PR_NUMBER}" | |
| PLATFORM="${INPUT_PLATFORM}" | |
| PIPELINE_REF="${INPUT_PIPELINE_REF:-main}" | |
| else | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| # Trim any leading whitespace (spaces/tabs/newlines) the user may have | |
| # accidentally typed before the slash command, then strip the '/review' | |
| # prefix and parse remaining args. | |
| TRIMMED_BODY=$(printf '%s' "${COMMENT_BODY}" | sed -e 's/^[[:space:]]*//') | |
| ARGS=$(echo "${TRIMMED_BODY}" | sed -n 's|^/review[[:space:]]*||p' | tr -s ' ') | |
| PLATFORM="" | |
| PIPELINE_REF="main" | |
| # Parse args: positional platform, --branch <ref>, --platform <name> | |
| # Disable globbing so user input like '*.cs' doesn't expand | |
| set -f | |
| set -- ${ARGS} | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --branch=*|-b=*) | |
| PIPELINE_REF="${1#*=}" | |
| ;; | |
| --branch|-b) | |
| shift | |
| if [ $# -gt 0 ] && [[ "$1" != --* ]]; then | |
| PIPELINE_REF="$1" | |
| fi | |
| ;; | |
| --platform=*|-p=*) | |
| CANDIDATE=$(echo "${1#*=}" | tr '[:upper:]' '[:lower:]') | |
| for p in ${VALID_PLATFORMS}; do | |
| if [ "${CANDIDATE}" = "${p}" ]; then | |
| PLATFORM="${p}" | |
| break | |
| fi | |
| done | |
| ;; | |
| --platform|-p) | |
| shift | |
| if [ $# -gt 0 ] && [[ "$1" != --* ]]; then | |
| CANDIDATE=$(echo "$1" | tr '[:upper:]' '[:lower:]') | |
| for p in ${VALID_PLATFORMS}; do | |
| if [ "${CANDIDATE}" = "${p}" ]; then | |
| PLATFORM="${p}" | |
| break | |
| fi | |
| done | |
| fi | |
| ;; | |
| *) | |
| # Check if it's a valid platform name | |
| for p in ${VALID_PLATFORMS}; do | |
| if [ "$(echo "$1" | tr '[:upper:]' '[:lower:]')" = "${p}" ]; then | |
| PLATFORM="${p}" | |
| break | |
| fi | |
| done | |
| ;; | |
| esac | |
| shift || true | |
| done | |
| fi | |
| # The AzDO payload adds refs/heads/, so accept either branch names or | |
| # fully-qualified refs/heads/<branch> input without double-prefixing. | |
| PIPELINE_REF=$(echo "${PIPELINE_REF}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's|^refs/heads/||') | |
| # Sanitize ref to valid git ref characters only | |
| PIPELINE_REF=$(echo "${PIPELINE_REF}" | sed 's/[^a-zA-Z0-9/_.\-]//g') | |
| # Reject path traversal, empty segments, and leading / | |
| case "${PIPELINE_REF}" in | |
| *..*|//*|*//*|*/|/*) PIPELINE_REF="main" ;; | |
| esac | |
| if [ -z "${PIPELINE_REF}" ]; then | |
| PIPELINE_REF="main" | |
| fi | |
| # Validate PR number is a positive integer | |
| if ! [[ "${PR_NUMBER}" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::pr_number must be a positive integer, got: '${PR_NUMBER}'" | |
| exit 1 | |
| fi | |
| echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" | |
| echo "platform=${PLATFORM}" >> "$GITHUB_OUTPUT" | |
| echo "pipeline_ref=${PIPELINE_REF}" >> "$GITHUB_OUTPUT" | |
| echo "Parsed — PR: #${PR_NUMBER}, Platform: '${PLATFORM:-<not specified>}', Ref: ${PIPELINE_REF}" | |
| - name: Validate PR | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.params.outputs.pr_number }} | |
| run: | | |
| PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}") | |
| PR_STATE=$(echo "${PR_JSON}" | jq -r '.state') | |
| if [ "${PR_STATE}" != "open" ]; then | |
| echo "::error::PR #${PR_NUMBER} is not open (state: ${PR_STATE})" | |
| exit 1 | |
| fi | |
| PR_TITLE=$(echo "${PR_JSON}" | jq -r '.title') | |
| echo "PR #${PR_NUMBER}: ${PR_TITLE}" | |
| echo "### Reviewing PR #${PR_NUMBER}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "${PR_TITLE}" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Checkout repository scripts | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Set review in-progress lock | |
| id: review_lock | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.params.outputs.pr_number }} | |
| run: | | |
| . .github/scripts/shared/Update-AgentLabels.ps1 | |
| $labels = Get-AgentLabels -PRNumber $env:PR_NUMBER -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}' | |
| if ($labels -contains 's/agent-review-in-progress') { | |
| if (Test-AgentReviewInProgressIsStale -PRNumber $env:PR_NUMBER -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}') { | |
| Clear-AgentReviewInProgress -PRNumber $env:PR_NUMBER -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}' | Out-Null | |
| } else { | |
| "locked=true" >> $env:GITHUB_OUTPUT | |
| "### /review skipped" >> $env:GITHUB_STEP_SUMMARY | |
| "PR #$($env:PR_NUMBER) already has ``s/agent-review-in-progress``." >> $env:GITHUB_STEP_SUMMARY | |
| exit 0 | |
| } | |
| } | |
| $locked = Set-AgentReviewInProgress -PRNumber $env:PR_NUMBER -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}' | |
| if (-not $locked) { | |
| throw "Failed to apply s/agent-review-in-progress to PR #$($env:PR_NUMBER)." | |
| } | |
| if ($labels -contains 's/agent-ready-for-rerun') { | |
| if (Remove-Label -PRNumber $env:PR_NUMBER -LabelName 's/agent-ready-for-rerun' -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}') { | |
| Write-Host " ✅ Removed stale queue label: s/agent-ready-for-rerun" | |
| } else { | |
| Write-Host " ⚠️ Could not remove stale queue label: s/agent-ready-for-rerun" -ForegroundColor Yellow | |
| } | |
| } | |
| "locked=false" >> $env:GITHUB_OUTPUT | |
| - name: Infer platform | |
| if: steps.review_lock.outputs.locked != 'true' | |
| id: infer | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PLATFORM: ${{ steps.params.outputs.platform }} | |
| PR_NUMBER: ${{ steps.params.outputs.pr_number }} | |
| run: | | |
| # If platform was explicitly set, use it as-is | |
| if [ -n "${PLATFORM}" ]; then | |
| echo "Platform explicitly set to: ${PLATFORM}" | |
| echo "platform=${PLATFORM}" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "No platform specified — inferring from PR #${PR_NUMBER} labels..." | |
| echo "(File-based detection is handled by the agentic-labeler.md workflow on PR open/reopen.)" | |
| # Check PR labels applied by agentic-labeler.md or manually | |
| LABELS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.labels[].name' 2>/dev/null || true) | |
| LABELS_LOWER=$(echo "${LABELS}" | tr '[:upper:]' '[:lower:]') | |
| echo "PR labels: ${LABELS_LOWER:-<none>}" | |
| if echo "${LABELS_LOWER}" | grep -qE '^platform/ios$'; then | |
| PLATFORM="ios" | |
| elif echo "${LABELS_LOWER}" | grep -qE '^(platform/macos|platform/maccatalyst)$'; then | |
| PLATFORM="catalyst" | |
| elif echo "${LABELS_LOWER}" | grep -qE '^platform/android$'; then | |
| PLATFORM="android" | |
| elif echo "${LABELS_LOWER}" | grep -qE '^platform/windows$'; then | |
| PLATFORM="windows" | |
| fi | |
| # Default to android when labels are inconclusive | |
| if [ -z "${PLATFORM}" ]; then | |
| echo "No platform label found — defaulting to android. Use --platform to specify explicitly." | |
| PLATFORM="android" | |
| fi | |
| echo "Inferred platform: ${PLATFORM}" | |
| echo "platform=${PLATFORM}" >> "$GITHUB_OUTPUT" | |
| # OIDC -> AzDO token exchange -> pipeline trigger combined into a single | |
| # step. Keeping the tokens in shell-local variables avoids persisting them | |
| # through `$GITHUB_OUTPUT` between steps (where they are masked but still | |
| # serialized to disk on the runner). | |
| - name: Trigger maui-copilot pipeline | |
| id: trigger_azdo | |
| if: steps.review_lock.outputs.locked != 'true' | |
| env: | |
| PR_NUMBER: ${{ steps.params.outputs.pr_number }} | |
| PIPELINE_REF: ${{ steps.params.outputs.pipeline_ref }} | |
| PLATFORM: ${{ steps.infer.outputs.platform }} | |
| AZDO_TENANT_ID: ${{ secrets.AZDO_TRIGGER_TENANT_ID }} | |
| AZDO_CLIENT_ID: ${{ secrets.AZDO_TRIGGER_CLIENT_ID }} | |
| run: | | |
| # 1. Get GitHub OIDC token | |
| OIDC_TOKEN=$(curl -s -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=api://AzureADTokenExchange" \ | |
| | jq -r '.value') | |
| if [ -z "$OIDC_TOKEN" ] || [ "$OIDC_TOKEN" = "null" ]; then | |
| echo "::error::Failed to get OIDC token" | |
| exit 1 | |
| fi | |
| echo "::add-mask::${OIDC_TOKEN}" | |
| # 2. Exchange OIDC token for AzDO access token | |
| AZURE_RESPONSE=$(curl -s -X POST \ | |
| "https://login.microsoftonline.com/${AZDO_TENANT_ID}/oauth2/v2.0/token" \ | |
| -d "grant_type=client_credentials" \ | |
| -d "client_id=${AZDO_CLIENT_ID}" \ | |
| -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ | |
| -d "client_assertion=${OIDC_TOKEN}" \ | |
| -d "scope=499b84ac-1321-427f-aa17-267ca6975798/.default") | |
| # Drop OIDC token from memory once we have the AzDO token | |
| unset OIDC_TOKEN | |
| AZDO_TOKEN=$(echo "$AZURE_RESPONSE" | jq -r '.access_token') | |
| if [ -z "$AZDO_TOKEN" ] || [ "$AZDO_TOKEN" = "null" ]; then | |
| echo "::error::Failed to get Azure AD token" | |
| echo "$AZURE_RESPONSE" | jq '{error, error_description, error_codes, timestamp, trace_id}' 2>/dev/null \ | |
| || echo "(failed to parse AAD response — check job permissions)" | |
| exit 1 | |
| fi | |
| echo "::add-mask::${AZDO_TOKEN}" | |
| unset AZURE_RESPONSE | |
| # 3. Trigger the pipeline | |
| echo "Triggering maui-copilot pipeline for PR #${PR_NUMBER} (platform: ${PLATFORM}, ref: ${PIPELINE_REF})..." | |
| # Platform is always resolved at this point (inferred or explicit). | |
| # Build JSON payload safely with jq to avoid injection. | |
| PAYLOAD=$(jq -n \ | |
| --arg pr "${PR_NUMBER}" \ | |
| --arg plat "${PLATFORM}" \ | |
| --arg ref "refs/heads/${PIPELINE_REF}" \ | |
| '{ | |
| templateParameters: { PRNumber: $pr, Platform: $plat }, | |
| resources: { repositories: { self: { refName: $ref } } } | |
| }') | |
| RESPONSE=$(curl -s -w "\n%{http_code}" \ | |
| -X POST "https://dev.azure.com/DevDiv/DevDiv/_apis/pipelines/27723/runs?api-version=7.1" \ | |
| -H "Authorization: Bearer ${AZDO_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "${PAYLOAD}") | |
| unset AZDO_TOKEN | |
| HTTP_CODE=$(echo "${RESPONSE}" | tail -1) | |
| RESPONSE_BODY=$(echo "${RESPONSE}" | head -n -1) | |
| echo "HTTP Status: ${HTTP_CODE}" | |
| if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then | |
| RUN_ID=$(echo "${RESPONSE_BODY}" | jq -r '.id') | |
| PIPELINE_NAME=$(echo "${RESPONSE_BODY}" | jq -r '.pipeline.name') | |
| echo "Pipeline '${PIPELINE_NAME}' triggered! Run ID: ${RUN_ID}" | |
| echo "View: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=${RUN_ID}" | |
| else | |
| echo "::error::Failed to trigger pipeline. HTTP ${HTTP_CODE}" | |
| echo "${RESPONSE_BODY}" | jq . 2>/dev/null || echo "${RESPONSE_BODY}" | |
| exit 1 | |
| fi | |
| - name: Clear review lock on trigger failure | |
| if: always() && steps.review_lock.outputs.locked == 'false' && steps.trigger_azdo.outcome != 'success' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.params.outputs.pr_number }} | |
| run: | | |
| . .github/scripts/shared/Update-AgentLabels.ps1 | |
| Clear-AgentReviewInProgress -PRNumber $env:PR_NUMBER -Owner '${{ github.repository_owner }}' -Repo '${{ github.event.repository.name }}' | Out-Null | |
| - name: Hide the /review command comment as resolved | |
| if: ${{ !cancelled() && github.event_name == 'issue_comment' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| COMMENT_NODE_ID: ${{ github.event.comment.node_id }} | |
| run: | | |
| # This job only runs after the pre-flight authorization gate succeeds, so | |
| # collapse the recognized command no matter the trigger outcome, including | |
| # a lock-skip (locked == 'true') or a failed AzDO trigger. Unauthorized | |
| # commenters never enter this job, leaving their comments fully visible. | |
| # | |
| # We MINIMIZE (hide as resolved) rather than delete so the command — and its | |
| # --branch/--platform options — survives in the REST comment history that the rerun | |
| # scanner replays. Minimized comments are still returned by the REST list endpoint; | |
| # only collapsed in the web UI. A failed hide must never fail the review trigger. | |
| if gh api graphql -f query='mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:RESOLVED}){minimizedComment{isMinimized}}}' -f id="$COMMENT_NODE_ID" --silent; then | |
| echo "Hid /review command comment ${COMMENT_NODE_ID} as resolved" | |
| else | |
| echo "::warning::Could not hide /review command comment ${COMMENT_NODE_ID}" | |
| fi |