Skip to content

Add review triggering workflow#35250

Open
JanKrivanek wants to merge 13 commits intomainfrom
dev/jankrivanek/review-trigger
Open

Add review triggering workflow#35250
JanKrivanek wants to merge 13 commits intomainfrom
dev/jankrivanek/review-trigger

Conversation

@JanKrivanek
Copy link
Copy Markdown
Member

@JanKrivanek JanKrivanek commented Apr 30, 2026

Context

Add ability for maintainers to trigger the AzDO PR review pipeline via /review comment on PR

Notes

  • The workflow allways runs from main - so users cannot chage behavior in their PRs
  • Unprivileged users slash command is ignored

Tested execution:

Copilot AI review requested due to automatic review settings April 30, 2026 11:41
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35250

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35250"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a GitHub Actions entrypoint to let maintainers trigger the DevDiv “maui-copilot” AzDO pipeline for a PR via a /review comment (or manual workflow dispatch), using OIDC token exchange instead of PATs.

Changes:

  • Introduces a new review-trigger.yml workflow that listens to issue_comment and workflow_dispatch and queues AzDO pipeline 27723.
  • Adds a setup/troubleshooting guide for configuring Azure managed identity + GitHub OIDC federated credentials to call the AzDO REST API.
Show a summary per file
File Description
.github/workflows/review-trigger.yml New workflow that gates /review on actor permissions and triggers the DevDiv AzDO pipeline run via OIDC→Entra token exchange.
.github/docs/trigger-azdo-pipeline-setup.md New documentation describing the OIDC-to-AzDO token flow and one-time identity setup steps.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment thread .github/workflows/review-trigger.yml
Comment thread .github/docs/trigger-azdo-pipeline-setup.md Outdated
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 30, 2026

🤖 AI Summary

👋 @JanKrivanek — new AI review results are available. Please review the latest session below.

📊 Review Session51cc27f · Update .github/docs/trigger-azdo-pipeline-setup.md · 2026-04-30 12:23 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: android

Reason: Gate was run externally before this skill invocation. Tests did NOT behave as expected. This PR adds a GitHub Actions workflow (no MAUI app code changes), so no Android device tests exist for this change.


🔍 Pre-Flight — Context & Validation

Issue: No linked issue — this PR adds new infrastructure
PR: #35250 - Add review triggering workflow
Platforms Affected: GitHub Actions / CI infrastructure (not a MAUI platform fix)
Files Changed: 0 implementation (MAUI code), 2 infrastructure (GitHub Actions workflow + docs)

Key Findings

  • New GitHub Actions workflow (review-trigger.yml) enables maintainers to trigger AzDO review pipeline via /review comment, using OIDC (no PAT)
  • Companion setup documentation added in .github/docs/trigger-azdo-pipeline-setup.md
  • Gate FAILED because there are no Android MAUI device tests applicable to a GitHub Actions workflow change
  • Prior Copilot review comment (resolved): permission gate should include maintain and use exit 0 for unauthorized users instead of exit 1
  • Prior Copilot review comment (resolved): docs referenced wrong workflow filename (now fixed in latest commit)
  • No linked GitHub issue — this is an additive infrastructure change

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: high
Errors: 0 | Warnings: 3 | Suggestions: 2

Key code review findings:

  • ⚠️ .github/workflows/review-trigger.yml:51${{ inputs.pr_number }} directly interpolated into bash (GitHub Actions injection anti-pattern; fix: move to env var)
  • ⚠️ .github/workflows/review-trigger.yml:69,87 — OIDC and AzDO tokens stored as step outputs (hygiene: chain into single step to avoid surfacing tokens in runner filesystem)
  • ⚠️ .github/workflows/review-trigger.yml:38 — Permission gate misses maintain role; exit 1 for unauthorized users creates noisy failed workflow runs (should be exit 0 / no-op)
  • 💡 .github/workflows/review-trigger.yml:63 — Dead pr_title output set but never used
  • 💡 .github/workflows/review-trigger.yml — No concurrency group; parallel /review comments could queue multiple AzDO runs

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35250 Add OIDC-based review trigger workflow with maintainer permission gate ❌ FAILED (Gate) .github/workflows/review-trigger.yml, .github/docs/trigger-azdo-pipeline-setup.md Original PR — no MAUI device tests applicable

🔬 Code Review — Deep Analysis

Code Review — PR #35250

Independent Assessment

What this changes: Adds two new files: (1) a GitHub Actions workflow (review-trigger.yml) that allows maintainers to trigger the maui-copilot AzDO pipeline by commenting /review on a PR, using OIDC federated credentials instead of a PAT; (2) a detailed setup guide documenting the OIDC identity configuration required to make it work.

Inferred motivation: Maintainers want a low-friction /review command that invokes the AzDO Copilot pipeline without requiring stored secrets (PAT rotation risk) or manual pipeline runs in the AzDO portal.


Reconciliation with PR Narrative

Author claims: Workflow always runs from main (so untrusted PR code can't alter it), and unprivileged users' commands are ignored via explicit permission check.

Agreement/disagreement: Both claims are accurate. The issue_comment trigger fires the default-branch workflow YAML, and the Check actor permission step gates on admin/write. However, two security hygiene issues exist in the implementation that the PR description doesn't address.


Findings

⚠️ Warning — ${{ inputs.pr_number }} directly interpolated into bash

File: .github/workflows/review-trigger.yml, Resolve PR number step (~line 51)

run: |
  if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
    PR_NUMBER="${{ inputs.pr_number }}"

${{ inputs.pr_number }} is expanded into the bash script before the runner executes it. If the input contains shell metacharacters (e.g., "; curl evil.com; echo "), they execute in the workflow runner context — with id-token: write permissions. This is the canonical GitHub Actions injection anti-pattern (GitHub Security Lab).

In practice, only users who can trigger workflow_dispatch (write-privileged maintainers) can supply this input, so the practical blast radius is limited to those who already have broad repo access. Still, the fix is a one-liner and eliminates the pattern entirely:

- name: Resolve PR number
  id: pr
  env:
    GH_TOKEN: ${{ github.token }}
    INPUT_PR_NUMBER: ${{ inputs.pr_number }}   # ← move to env, not inline
  run: |
    if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
      PR_NUMBER="${INPUT_PR_NUMBER}"
    else
      PR_NUMBER="${{ github.event.issue.number }}"
    fi

(github.event.issue.number is an integer assigned by GitHub — no injection risk there.)


⚠️ Warning — Sensitive tokens stored as step outputs

File: .github/workflows/review-trigger.yml, Get OIDC Token (~line 69) and Exchange for AzDO Token (~line 87) steps

echo "oidc_token=${OIDC_TOKEN}" >> "$GITHUB_OUTPUT"
# ...
echo "azdo_token=${AZDO_TOKEN}" >> "$GITHUB_OUTPUT"

::add-mask:: correctly masks both values in logs. However, writing them to $GITHUB_OUTPUT means the raw token values exist in the runner's file system for the duration of the job (in the workflow's output store file). The AzDO bearer token (azdo_token) is especially sensitive — it's a short-lived but fully functional identity token.

The cleaner pattern is to chain the three network calls (OIDC → AzDO token → AzDO pipeline trigger) into a single step, eliminating the need to surface tokens as step outputs at all.


💡 Suggestion — pr_title output is set but never used

File: .github/workflows/review-trigger.yml, Resolve PR number step (~line 63)

PR_TITLE=$(echo "${PR_DATA}\" | jq -r '.title')
echo "pr_title=${PR_TITLE}" >> "$GITHUB_OUTPUT"

steps.pr.outputs.pr_title is not referenced in any subsequent step. Remove both lines to eliminate dead code.


💡 Suggestion — No concurrency group defined

File: .github/workflows/review-trigger.yml

If a maintainer comments /review multiple times in quick succession, multiple AzDO pipeline runs will queue in parallel. A concurrency group would cancel stale runs.


⚠️ Warning — Permission gate misses maintain role (from prior review)

File: .github/workflows/review-trigger.yml, Check actor permission step (~line 38)

The gate only allows admin and write. GitHub also has a maintain access level for maintainers who should be able to trigger the review pipeline. Additionally, the current implementation uses exit 1 for unauthorized users, creating noisy failed workflow runs. Should use exit 0 (no-op) for unauthorized users.


Devil's Advocate

On the injection finding: Am I overstating the risk? Yes — the practical exploitability requires write access, and a write-privileged maintainer already has the ability to do far more damage via direct commits or PRs. The risk is real but the blast radius is self-limited. I'm flagging it because the fix is trivial and sets a good hygiene example for future workflows in this repo.

On the token-in-output finding: Is ::add-mask:: actually sufficient? GitHub's own documentation says masked values are redacted from logs and from step output echo in the runner UI. The underlying $GITHUB_OUTPUT file on the runner VM does contain the raw value, but only accessible to processes running on that same runner job. For ubuntu-latest GitHub-hosted runners (ephemeral VMs), this is not a meaningful attack surface. I'm flagging it as a hygiene concern, not a real vulnerability in this deployment context.

On the overall design: The OIDC approach (no PAT, managed identity, federated credential, ::add-mask:: on sensitive values, explicit permission check before acting) is well-thought-out. The documentation file is thorough and the troubleshooting table covers the real failure modes. The approach is sound.


Blast Radius Assessment

The workflow triggers only on:

  1. PR comments starting with /review (gated on admin/write permission)
  2. workflow_dispatch (repo-level access required)

No untrusted code is executed. The workflow reads only GitHub API data (PR metadata). The OIDC token exchange is limited to a specific pipeline (DevDiv/27723). Blast radius is contained — only affects AzDO pipeline triggering.

Failure Mode Probes

Failure Mode Outcome
Non-maintainer comments /review Currently: exit 1 (noisy failed run). Should be exit 0 (silent skip).
workflow_dispatch with malicious pr_number Shell injection risk due to direct ${{ inputs.pr_number }} interpolation
OIDC token leak via step outputs Low risk on ephemeral runners, but hygiene concern
Parallel /review comments Multiple AzDO pipeline runs queued

Verdict: NEEDS_CHANGES

Confidence: high
Errors: 0 | Warnings: 3 | Suggestions: 2

Summary: The design is solid — OIDC without a PAT is the right approach, and the maintainer permission gate is correct. Three issues should be addressed before merge: (1) missing maintain permission level + noisy exit 1 for unauthorized users, (2) the ${{ inputs.pr_number }} direct interpolation (injection anti-pattern), (3) sensitive tokens passed through step outputs. Two suggestions (dead pr_title output, no concurrency group) are minor cleanup items.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Permission gate (+maintain, exit 0) + injection fix (env var) + token hygiene (chained OIDC step) ⚠️ BLOCKED 1 file EstablishBrokenBaseline fails — .github/-only PR
2 try-fix Concurrency group + dead pr_title removal + permission comment ⚠️ BLOCKED 1 file Complementary to attempt 1
3 try-fix Job-level if: with author_association (removes explicit permission step entirely) ⚠️ BLOCKED 1 file Different authorization model
4 try-fix Comprehensive (model unavailable) ⚠️ BLOCKED gemini-3-pro-preview not available
PR PR #35250 Add OIDC-based review trigger workflow ❌ FAILED (Gate) 2 files Original PR — gate has no applicable MAUI tests

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 Yes Use azure/login@v2 instead of manual curl OIDC dance — but NOT viable (explicitly blocked in dotnet org per PR docs; manual curl is the correct approach here)

Exhausted: Yes (4 models queried; new idea from cross-pollination rejected as infeasible due to org policy)

Selected Fix: No passing candidates — all attempts BLOCKED. The PR's approach (manual OIDC curl) is correct for the dotnet org context. The issues are security hygiene improvements, not fundamental design flaws.

Recommendation: Apply improvements from attempts 1+2 to the PR: add "maintain" to permission gate, change exit 1 → exit 0 for unauthorized users, move ${{ inputs.pr_number }} to env var, chain token steps, add concurrency group, remove dead pr_title code.


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE GitHub Actions workflow PR; no linked issue
Code Review NEEDS_CHANGES (high) 0 errors, 3 warnings, 2 suggestions
Gate ❌ FAILED android — no applicable MAUI device tests for workflow-only PR
Try-Fix ✅ COMPLETE 4 attempts, 0 passing (all BLOCKED — no MAUI test harness for .github/ files)
Report ✅ COMPLETE

Code Review Impact on Try-Fix

Code review identified 3 warnings: (1) permission gate missing maintain role + noisy exit 1 for unauthorized users, (2) ${{ inputs.pr_number }} direct bash interpolation (injection anti-pattern), (3) OIDC/AzDO tokens in step outputs. These directly shaped all 4 try-fix approaches: attempt 1 addressed issues 1+2+3, attempt 2 added concurrency group + dead code removal, attempt 3 explored a fundamentally different authorization model (job-level if: condition). Cross-pollination surfaced a azure/login idea which was rejected as infeasible (blocked by dotnet org policy per PR documentation).

Summary

PR #35250 adds a well-designed OIDC-based /review slash command that triggers the AzDO Copilot pipeline when a maintainer comments on a PR. The core design (OIDC federated credentials, no PAT, explicit permission gate) is sound. However, three security hygiene issues need to be addressed before merge:

  1. Permission gate misses maintain role and fails the job (exit 1) for unauthorized users, creating noisy failed workflow runs. Should use exit 0 (silent no-op) and include maintain.
  2. ${{ inputs.pr_number }} is directly interpolated into bash — the canonical GitHub Actions script injection anti-pattern. Fix: move to env: variable.
  3. OIDC and AzDO tokens are written to $GITHUB_OUTPUT — hygiene concern. Fix: chain the three network calls into one step so tokens never leave local shell scope.

Two additional minor cleanups: remove unused pr_title output and add a concurrency group.

The Gate FAILED because this is a workflow-only PR — no Android MAUI device tests exist for .github/ changes. All try-fix attempts were Blocked for the same reason. The failure is expected and does not reflect a functional regression.

Root Cause

Not a bug fix PR — this is new feature infrastructure. The code review found security hygiene issues in the initial implementation that should be corrected before merge. The most impactful is the permission gate design (noisy failures + missing maintain role) which would degrade day-to-day maintainer experience.

Fix Quality

The PR's fix is functionally correct but has three hygiene issues flagged by code review. The prior Copilot PR review thread about maintain + exit 1 was marked resolved with author comment "It is better to have explicit info about why the command was ignored" — but the conversation appears to have concluded without the code actually being changed (the current workflow code still uses exit 1 and only checks admin/write). These issues should be addressed in the PR before merge.

Recommended changes to the PR:

  • Add "maintain" to the permission allowlist; change exit 1exit 0 (with a log message for diagnostics)
  • Move ${{ inputs.pr_number }}env: INPUT_PR_NUMBER: ${{ inputs.pr_number }} and reference ${INPUT_PR_NUMBER} in the script
  • Chain OIDC token fetch + AzDO token exchange + pipeline trigger into a single step (no step outputs for sensitive tokens)
  • Remove unused pr_title output
  • Add concurrency: group (e.g., review-trigger-pr-${{ github.event.issue.number || inputs.pr_number }})

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 30, 2026
@JanKrivanek JanKrivanek enabled auto-merge April 30, 2026 14:09
@github-actions
Copy link
Copy Markdown
Contributor

🤖 AI Review — Demo

⚠️ This is a demo comment posted by the review-post-demo gh-aw workflow to validate the safe-outputs posting pipeline.

📋 Recommendation

Verdict: ✅ This PR looks good (demo placeholder)

The PR introduces changes that appear reasonable. This is sample content demonstrating that the gh-aw workflow can:

  1. Run pre-agent steps: to prepare data
  2. Hand off files to the sandboxed agent
  3. Post the content verbatim as a PR comment via safe-outputs

🛡️ Gate — Test Verification

Check Result
Tests pass on main ✅ Passed
Tests fail without fix ✅ Confirmed
No regressions ✅ Clean

🔍 Pre-Flight

  • PR targets main branch
  • No public API changes detected
  • No breaking changes to existing handlers

🔬 Code Review

Code follows MAUI conventions. Handler lifecycle is correctly implemented.
Platform-specific files use proper naming conventions.

📋 Final Report

This demo proves the end-to-end gh-aw → safe-outputs pipeline works correctly.
When wired to the real DevDiv maui-copilot pipeline, this same mechanism will
post actual review results.

🔍 Review posted by Write metadata

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented May 2, 2026

Can we extend /review to support optional parameters like /review [platform] [branch]?

If not provided:

  • platform would be auto-detected
  • branch would default to main

Usage:
  /review           -> triggers pipeline from main
  /review my-branch -> triggers pipeline from refs/heads/my-branch

Also available as pipeline_ref input in workflow_dispatch.
- Add --platform/-p and --branch/-b flags to /review command parser
- Support positional platform argument (e.g., /review 12345 android)
- Pipeline default changed to 'auto' with runtime inference:
  1. Deterministic: PR labels (a/ios, a/android, etc.)
  2. Deterministic: Changed file paths (single-platform dominance)
  3. Copilot CLI fallback for ambiguous cases
- Inference step moved after Copilot CLI install for availability
- Compile-time expressions treat 'auto' same as 'android' (pool, provisioning, emulator)
…fix Copilot output parsing

- Use GH_COMMENT_TOKEN (authenticated) instead of COPILOT_GITHUB_TOKEN for gh api calls
- Add platform/ios and platform/macos to label detection patterns
- Extract last valid platform word from Copilot CLI verbose MCP output
- Add debug logging for fetched labels
@JanKrivanek
Copy link
Copy Markdown
Member Author

Can we extend /review to support optional parameters like /review [platform] [branch]?

If not provided:

  • platform would be auto-detected
  • branch would default to main

@kubaflo this is now supported. Either via positional or keyed args (--branch|-b; --platform|-p)

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented May 6, 2026

Multimodal review — PR #35250 (Add review triggering workflow)

Reviewed all three changed files (review-trigger.yml, ci-copilot.yml, trigger-azdo-pipeline-setup.md), the previously-completed validation runs (Actions run 25163585137, DevDiv build 13980704), and the Apr 30 Copilot-bot threads.

Overall this is a great addition — OIDC instead of PATs is the right call, the setup guide captures hard-won tribal knowledge (case-sensitive subjects, Basic vs Stakeholder, enterprise claim), and the slash-command UX is clean. Below are issues I think are worth addressing before merge.


🔴 High — auto platform routes to the wrong pool

This is the issue I'm most concerned about. In eng/pipelines/ci-copilot.yml, the platform-inference step runs at runtime and only sets the InferredPlatform variable used by Review-PR.ps1. But pool selection, provisioning, simulator boot, and device setup all use the compile-time ${{ parameters.Platform }} expression — which for auto resolves to the android branch via or(eq(..., 'android'), eq(..., 'auto')). The author already documents this in the inline comment:

Compile-time ${{ parameters.Platform }} expressions (pool, provisioning, device setup) use the parameter value directly — when 'auto' is the parameter, those resolve to the android defaults.

Consequence: if /review (no args) is fired against an iOS-only PR, the inference step correctly resolves InferredPlatform=ios and Review-PR.ps1 is told -Platform ios — but the job is sitting on an ubuntu-22.04 pool with no Mac, no Xcode, no simulator. Any iOS test/build the reviewer attempts will fail in confusing ways. Same for catalyst and windows.

This effectively means auto is "android, with the reviewer pretending it's something else". Either:

  1. Move inference to the GitHub Actions workflow (review-trigger.yml) and pass the resolved platform as the templateParameters.Platform value in the AzDO request payload. Then the AzDO pipeline only ever sees concrete platforms — pool selection becomes correct.
  2. Or, make auto strictly mean "auto on android pool" and reject inference results other than android with a comment back on the PR asking the user to re-trigger with --platform.

Option 1 is cleaner — the GitHub Actions workflow already has gh available and the same label/file-path heuristic ports trivially. The Copilot-CLI fallback is overkill there too (see next item).


🟠 Medium — Copilot CLI fallback for inference is overkill

Infer Platform from PR invokes the Copilot CLI when label + file-path checks are inconclusive:

COPILOT_PROMPT="Analyze PR #${PR_NUMBER} ... Respond with EXACTLY one word: android, ios, catalyst, or windows. If unsure, respond with: android"
COPILOT_RAW=$(copilot -p "${COPILOT_PROMPT}" 2>/dev/null || true)

Concerns:

  • Latencycopilot -p typically takes 30s–2min, which is non-trivial overhead per /review invocation.
  • Non-determinism — output parsing relies on grep -oE '\b(android|ios|catalyst|windows)\b' | tail -1. If the model writes "this affects both ios and android" in reasoning text, tail -1 picks the last mentioned platform somewhat arbitrarily.
  • Failure mode is silent2>/dev/null || true swallows errors; we then default to android. If Copilot is rate-limited, every "auto" PR that hits the fallback path reviews as android without any signal that the inference failed.
  • The fallback IS android anyway — so the LLM call only differs from the no-LLM path when it returns ios/catalyst/windows, which (per the previous finding) routes to the wrong pool.

Recommendation: drop the Copilot fallback. When deterministic checks are inconclusive, default to android and post a heads-up comment pointing to --platform for explicit selection. Faster, deterministic, observable.

Related: the "single platform dominates" check (HAS_IOS > 0 && HAS_ANDROID == 0 && ...) treats any cross-platform PR as ambiguous. Many real PRs are layout/handler fixes that touch both Android and iOS handlers — those will all hit the slow Copilot path. Consider weighted scoring (most-changed platform wins), or just acknowledge the limitation.


🟠 Medium — Validation evidence is stale

The PR description points to:

  • GH Actions run 25163585137 — built from commit 3eb0bd7b (the temporary push-trigger commit), which predates the auto-detection logic added in f7811104. So the new parser, auto parameter, and --branch/--platform handling are not exercised by that run.
  • DevDiv build 13980704 — sourced from refs/heads/main at e20401ce, not the PR's ci-copilot.yml changes.

So the integration test demonstrates "old workflow can trigger old pipeline", not "new workflow + new pipeline work end-to-end". A fresh test run after the most recent commits (fe9091f9, 773c46c4) — including at least one /review --platform ios invocation — would significantly de-risk merge. The temporary push: trigger could be re-added on this branch for that.


🟠 Medium — Word splitting + glob expansion on user comment text

review-trigger.yml line ~71-78:

ARGS=$(echo "${COMMENT_BODY}" | sed -n 's|^/review[[:space:]]*||p' | tr -s ' ')
PLATFORM=""
PIPELINE_REF="main"
set -- ${ARGS}                       # <-- unquoted, no set -f
while [ $# -gt 0 ]; do
  case "$1" in ...

set -- with unquoted ${ARGS} performs both word splitting (intended) and pathname expansion (not intended). A maintainer comment /review *.cs would have the shell try to glob *.cs against the runner's cwd. There's no actions/checkout step so cwd is /home/runner/work/maui/maui (empty) and the glob would expand to nothing — but defense in depth, and this also bites if the workflow is ever extended to checkout or cd somewhere with files.

Fix: set -f immediately before set -- to disable globbing for that scope, or read into an array:

set -f
set -- $ARGS

🟡 Low — /review prefix matcher is too lax

startsWith(github.event.comment.body, '/review') matches /reviewing today, /reviewer-comment, /review-this-later, etc. The downstream sed strips /review and tries to parse the rest as args, failing silently and queueing an AzDO build for unrelated comments.

Fix: anchor on word boundary:

if: >-
  github.event_name == 'workflow_dispatch' ||
  (github.event.issue.pull_request &&
   (github.event.comment.body == '/review' ||
    startsWith(github.event.comment.body, '/review ') ||
    startsWith(github.event.comment.body, '/review--') ||
    startsWith(github.event.comment.body, '/review-')))

(The last two seem ugly because GitHub Actions expressions don't support regex — alternative: do the strict check inside the bash step and exit 0 if it doesn't match, so the run still happens but is a quick no-op. Tradeoff: maintains reachability but spends 5–10s of runner time per false-positive comment.)


🟡 Low — maintain permission level excluded from allowlist

Already discussed in the resolved Copilot thread. Documenting your decision in a code comment ("write/admin only — maintain excluded because X") would help future maintainers who notice the gap. Per GitHub permission levels, the values are admin/maintain/write/triage/read/none — leaving maintain out is a real omission, since maintain-level collaborators are typically area-owners who would be the natural users of /review.


🟡 Low — PR_TITLE fetched but never surfaced

Validate PR step grabs the title and prints it locally, but it's not used elsewhere. If the intent was to surface it in the run summary, add to $GITHUB_STEP_SUMMARY. Otherwise drop the line.


🟡 Low — PIPELINE_REF sanitization is permissive

PIPELINE_REF=$(echo "${PIPELINE_REF}" | sed 's/[^a-zA-Z0-9/_.\-]//g')

Allows .., multiple consecutive //, leading /, trailing /. AzDO will reject most of these, but tightening locally gives clearer error messages and avoids quirky AzDO API responses leaking through.

# Reject path traversal and empty segments
case "${PIPELINE_REF}" in
  *..*|//*|*//*|*/) PIPELINE_REF="main" ;;
esac

🟡 Low — Error logging in Exchange for AzDO Token

echo "$AZURE_RESPONSE" | jq 'del(.access_token)' 2>/dev/null || echo "$AZURE_RESPONSE"

If Azure ever includes id_token/refresh_token/assertion in error responses, those leak. Also the fallback echo "$AZURE_RESPONSE" on jq failure prints the raw response uncensored. Safer:

echo "$AZURE_RESPONSE" | jq '{error, error_description, error_codes, timestamp, trace_id}' 2>/dev/null \
  || echo "(failed to parse AAD response — check job permissions, redacted)"

📘 Setup doc — minor polish suggestions

trigger-azdo-pipeline-setup.md is unusually high quality; the case-sensitivity, Stakeholder-vs-Basic, enterprise-claim, and "azure/login is blocked" notes are exactly the things a future maintainer wouldn't otherwise discover for hours. Two small additions:

  • Step 2 mentions repo:dotnet/maui:pull_request as a possible subject, but the PR uses issue_comment triggering and the actual federated subject for that event is repo:dotnet/maui:ref:refs/heads/main (since issue_comment runs from the default branch). Worth a sentence clarifying which subject corresponds to which GitHub event.
  • The token-flow diagram could note that the OIDC token's sub claim is what's matched against the federated credential --subject (the case-sensitivity warning would land harder right next to that explanation).

Summary

Severity Count Items
🔴 High 1 auto platform → wrong pool
🟠 Medium 3 Copilot fallback overkill · stale validation · glob expansion in arg parser
🟡 Low 5 /review prefix laxness · maintain excluded · unused PR_TITLE · permissive ref sanitization · error logging
📘 Doc 1 Subject-claim clarification

Strong direction overall — happy to chat through the auto-platform routing if option 1 (inference in GH Actions) feels like too much surface change.

…y issues

Fixes from PR #35250 review comment:

🔴 High - auto platform routes to wrong pool:
  Move platform inference from AzDO pipeline to GH Actions workflow.
  Pipeline now always receives a concrete platform value, ensuring
  correct pool selection, provisioning, and device setup.

🟠 Medium - Drop Copilot CLI fallback:
  Removed non-deterministic LLM inference. Default to android when
  deterministic checks (labels + file paths) are inconclusive.

🟠 Medium - Glob expansion on user comment text:
  Added 'set -f' before arg parsing to disable pathname expansion.

🟡 Low - /review prefix too lax:
  Changed condition to match exact '/review' or '/review ' prefix,
  preventing false positives like '/reviewing'.

🟡 Low - maintain permission excluded:
  Added 'maintain' to allowed permission levels alongside write/admin.

🟡 Low - Unused PR_TITLE:
  Now surfaces PR title in GITHUB_STEP_SUMMARY.

🟡 Low - PIPELINE_REF sanitization:
  Added path traversal (..) and empty segment (//) rejection.

🟡 Low - Azure error response logging:
  Now extracts only safe fields (error, error_description, trace_id)
  instead of echoing raw response that could contain tokens.

📘 Doc - Setup doc subject-claim clarification:
  Added note explaining OIDC sub claim mapping for issue_comment
  vs pull_request events.
Copy link
Copy Markdown
Member

@PureWeen PureWeen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Code Review — 3 Independent Reviewers

Methodology: 3 independent reviewers with adversarial consensus
CI Status: All checks passing

Summary

# Severity Category Consensus Issue
1 Error Command Injection 2/3 Shell injection via ${{ inputs.pr_number }} direct interpolation (line 67)
2 Warning JSON Injection 2/3 Hand-built JSON with unsanitized input (line 235)
3 Warning Command Injection 2/3 Step outputs re-interpolated via ${{ }} in downstream steps (lines 134, 151)
4 Suggestion Documentation 2/3 Input description says branch/tag but only branches work (line 25)
5 Suggestion Logic 2/3 Arg parser swallows next flag as option value (line 82)
6 Suggestion Logic 2/3 PIPELINE_REF sanitization misses leading / (line 117)

Key recommendation

Finding 1 is the blocking issue. Pass inputs.pr_number through env: instead of ${{ }} and validate it is numeric. This also neutralizes Findings 2-3.

The removal of file-based platform inference in the latest commit is a good simplification.

See inline comments for details and fix suggestions.

Comment thread .github/workflows/review-trigger.yml
@PureWeen
Copy link
Copy Markdown
Member

PureWeen commented May 6, 2026

Inline Findings (GitHub API blocked inline placement — posting as comment)

Finding 1 — Line 67Command Injection
${{ inputs.pr_number }} is interpolated directly into shell. Pass through env: and validate numeric:

env:
  INPUT_PR_NUMBER: ${{ inputs.pr_number }}
run: |
  PR_NUMBER="${INPUT_PR_NUMBER}"
  if ! [[ "${PR_NUMBER}" =~ ^[1-9][0-9]*$ ]]; then
    echo "::error::pr_number must be a positive integer"; exit 1
  fi

Finding 2 — Line 235 ⚠️ JSON Injection
Hand-built JSON with unsanitized PR_NUMBER. Use jq for safe construction instead of string interpolation.


Finding 3 — Line 134 ⚠️ Command Injection
Step outputs re-interpolated via ${{ steps.params.outputs.pr_number }} in downstream steps. Resolved by fixing Finding 1.


Finding 4 — Line 25 💡 Documentation
Input says "branch/tag" but line 248 hardcodes refs/heads/. Update to AzDO pipeline branch (default: main).


Finding 5 — Line 82 💡 Logic
/review --branch --platform ios swallows --platform as branch value. Reject values starting with --.


Finding 6 — Line 117 💡 Logic
(Already posted inline) PIPELINE_REF sanitization misses leading /. Add /* to the case pattern.

@JanKrivanek
Copy link
Copy Markdown
Member Author

/azp run maui-pr-uitests, maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants