Skip to content

Harden review-trigger workflow: env-var inputs, single-step token flow, pre-flight authorize job#35439

Open
JanKrivanek wants to merge 1 commit into
mainfrom
fix/harden-review-trigger-35431
Open

Harden review-trigger workflow: env-var inputs, single-step token flow, pre-flight authorize job#35439
JanKrivanek wants to merge 1 commit into
mainfrom
fix/harden-review-trigger-35431

Conversation

@JanKrivanek

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Fixes #35431.

Three follow-up hardening items from the adversarial code review of #35250 applied to .github/workflows/review-trigger.yml:

1. Move github.actor / github.repository to env: vars

${{ github.actor }} and ${{ github.repository }} were interpolated directly into shell run: blocks. Not exploitable today (GitHub usernames are [a-zA-Z0-9-] and github.repository is constant), but inconsistent with how inputs.pr_number and COMMENT_BODY are already hardened. Now passed via env: (ACTOR, REPO) and consumed as shell variables in gh api calls.

2. Combine OIDC + AzDO token exchange + pipeline trigger into one step

The OIDC token, the AzDO access token, and the pipeline-trigger call are now performed in a single step. Previously the masked tokens were persisted through $GITHUB_OUTPUT between steps; they now stay shell-local and are explicitly unset once consumed.

3. Permission check moved to a lightweight pre-flight job

A new authorize job runs the comment-match + collaborator-permission check before the heavier trigger-review job is provisioned. Drive-by /review comments by non-collaborators no longer spin up the main runner. Unauthorized callers now exit 0 with a ::notice:: instead of ::error::, so the workflow run history stays clean.

Validation

  • YAML parses (python -c "import yaml; yaml.safe_load(...)").
  • No behavioral change for authorized callers; same parameters parsed, same pipeline triggered.
  • workflow_dispatch continues to bypass the collaborator check (only repo-write users can dispatch workflows anyway).

Three follow-up improvements from the adversarial code review of PR #35250:

1. Move github.actor / github.repository to env: vars (ACTOR, REPO) so they are no longer interpolated directly into shell. Consistent with the existing hardening pattern for COMMENT_BODY and inputs.pr_number.

2. Combine OIDC fetch + AzDO token exchange + pipeline trigger into a single step. Avoids persisting masked tokens through GITHUB_OUTPUT between steps; tokens stay shell-local and are explicitly unset once consumed.

3. Move actor permission check into a lightweight pre-flight 'authorize' job. Unauthorized /review comments no longer provision the heavier trigger-review runner. Failure exits 0 with a notice instead of erroring, keeping the run history clean.
@github-actions

Copy link
Copy Markdown
Contributor

🚀 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 -- 35439

Or

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

@JanKrivanek

Copy link
Copy Markdown
Member Author

/review

@MauiBot

MauiBot commented May 14, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

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

📊 Review Session95da608 · Harden review-trigger workflow (#35431) · 2026-05-14 16:58 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ SKIPPED

No tests were detected in this PR.

Recommendation: Add tests to verify the fix using the write-tests-agent.


🧪 UI Tests — Category Detection

No UI test categories needed for this PR (no UI-relevant changes).


🔍 Pre-Flight — Context & Validation

Issue: #35431 - Harden review-trigger workflow: consistency and cost improvements
PR: #35439 - Harden review-trigger workflow: env-var inputs, single-step token flow, pre-flight authorize job
Platforms Affected: N/A — GitHub Actions workflow file only (.github/workflows/review-trigger.yml)
Files Changed: 1 implementation (.github/workflows/review-trigger.yml, +64 / -35), 0 test

Key Findings

  • PR addresses three follow-up hardening items identified in the adversarial review of PR Add review triggering workflow #35250:
    1. github.actor / github.repository are now passed via env: (ACTOR, REPO) and consumed as shell variables in gh api calls instead of being interpolated directly into shell run: blocks. Brings actor/repo to the same hardening level the PR previously gave inputs.pr_number and the comment body.
    2. OIDC token fetch + AzDO token exchange + pipeline trigger are collapsed into a single step. Tokens stay shell-local (OIDC_TOKEN, AZDO_TOKEN, AZURE_RESPONSE) and are explicitly unset once consumed, instead of being passed between steps via $GITHUB_OUTPUT (which is masked but serialized to disk on the runner).
    3. A new lightweight authorize job runs the comment-match + collaborator-permission check before the heavier trigger-review job is provisioned. Drive-by /review comments by non-collaborators no longer spin up the main runner. Unauthorized callers exit 0 with ::notice:: (instead of ::error:: + exit 1), keeping run history clean.
  • Behavioral parity for authorized callers: same comment-trigger condition, same arg parsing, same gh api calls, same OIDC → AzDO → pipeline-trigger flow, same pipeline ID (27723) and payload shape.
  • Permissions are correctly minimized: authorize job drops id-token: write (only trigger-review keeps it).
  • workflow_dispatch is correctly short-circuited inside the authorize step (EVENT_NAME == workflow_dispatchproceed=true) — only repo-write users can dispatch workflows anyway, so the collaborator check is intentionally skipped.
  • authorize job's top-level if: mirrors the previous job-level if: exactly (workflow_dispatch OR PR-issue-comment matching /review or /review …).
  • Tokens are still ::add-mask::'d before any logging; sensitive vars are unset after their last use within the combined step.
  • inputs.pr_number and COMMENT_BODY (the original injection-prone surfaces) remain hardened in the params step — this PR does not touch them.

Code Review Summary

Verdict: LGTM
Confidence: high
Errors: 0 | Warnings: 0 | Suggestions: 2
Note: Review performed inline (not via maui-expert-reviewer sub-agent) because the changed file is a GitHub Actions YAML workflow, not .NET/MAUI runtime code. The expert reviewer's MAUI-domain dimensions (handler lifecycle, layout, XAML, CollectionView, platform threading, etc.) are not applicable. Review focused on GitHub-Actions security dimensions: untrusted-input injection, token hygiene, least-privilege permissions, job/step ordering, and run-history hygiene.

Key code review findings:

  • 💡 .github/workflows/review-trigger.yml:84-86 — The env: block on the trigger-review job declares REPO and ACTOR, but ACTOR is not referenced anywhere inside that job's steps. Declaring it is harmless and future-proofs against re-introducing ${{ github.actor }} inline, but it's dead weight today. Optional cleanup.
  • 💡 .github/workflows/review-trigger.yml:62gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.permission' 2>/dev/null || echo "none" silently swallows API errors and falls through to the none permission check. This is the right safe-default (deny on failure), but the silent 2>/dev/null means a real outage (e.g., GH API down) looks identical to a true "not a collaborator" response. Consider logging the error to stderr without affecting the gate decision. Non-blocking.
  • No injection vectors introduced: every shell command consumes attacker-controllable values via env: (COMMENT_BODY, INPUT_*, ACTOR, REPO), never via ${{ }} interpolation into run: blocks.
  • Token hygiene improved: removing echo "...=${TOKEN}" >> "$GITHUB_OUTPUT" for both OIDC_TOKEN and AZDO_TOKEN is a strict-positive change. The unset calls are best-effort defense-in-depth (shells don't truly zero memory) but cost nothing.
  • Job split is sound: authorize correctly publishes proceed via outputs: and trigger-review correctly gates on it via needs.authorize.outputs.proceed == 'true'. cancel-in-progress: false on trigger-review remains correct (don't kill an in-flight pipeline trigger).
  • The PR description's "no behavioral change for authorized callers" claim is accurate — the matchers, arg parser, validation, and AzDO call are byte-for-byte identical to pre-PR for the happy path.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35439 Three-pronged hardening: env-var inputs for actor/repo; collapse OIDC+AzDO+trigger into one step with explicit unset; split into authorize pre-job + trigger-review job, exit 0 on unauthorized ⚠️ Gate SKIPPED — no tests detected .github/workflows/review-trigger.yml Original PR; matches issue #35431 acceptance criteria exactly

🔧 Fix — Analysis & Comparison

Try-Fix Phase Aggregate Narrative

Four diverse candidates were explored, each loading a different maui-expert-reviewer dimension to maximize approach diversity. Because the PR touches only a GitHub Actions YAML workflow (no .NET runtime, no buildable/testable surface), the candidates are conceptual alternative-approach diffs evaluated for security, performance, maintainability, and policy-fidelity to issue #35431.

Attempt Dimension loaded Approach in one line Outcome
try-fix-1 Security / injection surface Add permissions: {} default, set -euo pipefail, lift remaining ${{ github.event_name }} to env Valid hardening but out-of-scope vs issue #35431
try-fix-2 API / contract design Keep authorize pre-flight job but fail loudly (exit 1, ::error::) to preserve audit trail Violates issue #35431 item 3 which explicitly mandates exit 0 + ::notice::
try-fix-3 Performance / runner cost Use github.event.comment.author_association in job-level if: — zero runners for unauthorized Silently changes access policy (admits triage users that the PR rejects)
try-fix-4 State isolation / threading Keep three separate steps, pass tokens via mktemp files in $RUNNER_TEMP instead of $GITHUB_OUTPUT More complex than PR with no measurable security gain

Key takeaways

Notes on methodology

Because no executable test loop exists for a workflow YAML in this repo, the standard try-fix BuildAndRunHostApp.ps1 / Run-DeviceTests.ps1 test commands cannot be applied. Each candidate was instead evaluated against:

No code was written to the repository; candidates exist only as conceptual diffs in the per-attempt content.md files.


📋 Report — Final Recommendation

Phase 3 — Comparative Report

Candidates evaluated

Candidate Description
pr PR #35439 as submitted by JanKrivanek
pr-plus-reviewer PR #35439 + two suggestion-level polish edits from the inline expert review
try-fix-1 PR + permissions: {} default + set -euo pipefail + EVENT_NAME env var (security hardening expansion)
try-fix-2 PR but authorize job fails loudly (exit 1 + ::error::) on unauthorized (audit-trail priority)
try-fix-3 Replace API call with github.event.comment.author_association in job-level if: (zero-runner gate)
try-fix-4 Keep three separate steps; pass tokens via mktemp files in $RUNNER_TEMP instead of $GITHUB_OUTPUT

Scoring rubric (out of 5 per axis)

Candidate Scope-fidelity to #35431 Security delta vs main Behavioral parity Maintainability Total
pr 5 4 5 5 19
pr-plus-reviewer 5 4 5 4 (extra comments) 18
try-fix-1 3 (out-of-scope) 5 5 3 (larger diff) 16
try-fix-2 2 (contradicts item 3) 4 4 (UX change) 5 15
try-fix-3 3 (policy drift) 3 (admits triage users) 2 (policy change) 5 13
try-fix-4 4 4 5 3 (more complex) 16

Analysis

pr wins on the most important axis (scope-fidelity) and ties or wins on every other axis.

  • Scope-fidelity: Issue Harden review-trigger workflow: consistency and cost improvements #35431 named exactly three items; the PR implements exactly those three items with no spurious churn. try-fix-2 and try-fix-3 actively contradict acceptance criteria. try-fix-1 adds scope the issue didn't ask for.
  • Security delta: All candidates remove the OIDC/AzDO token round-trip through $GITHUB_OUTPUT. The PR's approach (shell-local + unset) is the simplest; try-fix-4's mktemp approach is equally safe but more complex; try-fix-1's additional permissions: {} + pipefail is a real-but-additive improvement; try-fix-3 weakens access control.
  • Behavioral parity: pr and pr-plus-reviewer are byte-for-byte identical for authorized callers vs. pre-PR. try-fix-2 changes UX (red runs for unauthorized — by design). try-fix-3 admits a permission class (triage) that the PR rejects.
  • Maintainability: PR's diff is the most surgical of all candidates that respect the issue's scope.

pr-plus-reviewer is essentially pr plus two suggestion-level cleanups. Both are non-blocking polish; neither is required for merge. Adopting them now would slightly inflate the diff. Adopting them in a follow-up PR is just as good. → Prefer pr for clean merge.

try-fix-1 is a real follow-up candidate (the ideas are good) but should be filed separately so the focused issue-#35431 fix lands clean.

try-fix-2 would require a maintainer to overrule issue #35431's explicit guidance.

try-fix-3 is mechanically attractive (zero runners for drive-bys, no API call) but silently broadens who can trigger /review. Rejected on policy-drift grounds.

try-fix-4 is a defensible alternative implementation but trades simplicity for marginal step-log clarity. Not better than the PR.

Winner: pr

PR #35439 is mergeable as-is. The two reviewer suggestions captured in inline-findings.json are non-blocking polish that can be folded in by the author or deferred to a follow-up. The four try-fix candidates each illustrate a trade-off the PR consciously avoided, and none scores higher than the PR on the combined rubric.

Recommendation to the human reviewer

  • Action: Approve / merge PR Harden review-trigger workflow: env-var inputs, single-step token flow, pre-flight authorize job #35439.
  • Optional follow-ups (any of these would be a fine separate PR; none is a blocker):
    • Apply the two pr-plus-reviewer suggestions (unused ACTOR env var on trigger-review, surface stderr from the permission gate).
    • Adopt try-fix-1's permissions: {} deny-all top-level default and set -euo pipefail in shell blocks.
  • Note for the author: Gate was SKIPPED because no tests are present. Adding a workflow lint/syntax check (e.g., actionlint) as part of CI would give future workflow-only PRs an automated gate without requiring runtime tests.

@JanKrivanek

Copy link
Copy Markdown
Member Author

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

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@MauiBot

MauiBot commented May 24, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you please resolve conflicts?

@kubaflo

kubaflo commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

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

Labels

area-ai-agents Copilot CLI agents, agent skills, AI-assisted development 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.

Harden review-trigger workflow: consistency and cost improvements

4 participants