Skip to content

I9_Incremental_loading_on_scroll - Jumping back to the top of the list as you scroll down. #15433

I9_Incremental_loading_on_scroll - Jumping back to the top of the list as you scroll down.

I9_Incremental_loading_on_scroll - Jumping back to the top of the list as you scroll down. #15433

# 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