Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7d87222
[CI] Refactor ci-copilot pipeline: scope env vars per task
Copilot May 22, 2026
29b1518
Fix review findings: persist regression data across phases
Copilot May 22, 2026
647786a
Fix detectedCategories routing: coalesce RunReview with RunGate
Copilot May 22, 2026
3fd884a
Fix null crash in Post/CopilotReview phases + add sentinel check
Copilot May 22, 2026
485e865
Add GH_TOKEN to Gate phase for PR metadata fetches
Copilot May 22, 2026
a22749d
[CI] Plug GH service-connection token leaks in copilot pipeline
T-Gro May 26, 2026
ca5ce6d
[CI] Strip GH_TOKEN from PR-code subprocesses; trust eng/scripts copy
T-Gro May 26, 2026
0d1acf3
[CI] Add security instructions for ci-copilot pipeline surface
T-Gro May 26, 2026
07565bb
Clean up stale MauiBot PR comments
Copilot May 26, 2026
63c4689
Merge remote-tracking branch 'origin/feature/refactor-copilot-yml' in…
Copilot May 26, 2026
d3bcebd
[CI] Compact ci-copilot security instructions
T-Gro May 26, 2026
7ade47d
[CI] Use YAML list + brace alternation for applyTo
T-Gro May 26, 2026
0e3ce9d
[CI] Use officially-documented applyTo syntax + drop scare line
T-Gro May 26, 2026
888febc
[CI] Fix Gate: move token-strip wrap inside verify-tests-fail.ps1
T-Gro May 26, 2026
d8ff0a1
Fix AI summary session replacement
Copilot May 27, 2026
3650971
Remove duplicate full-category UI test run from ReviewPR stage
Copilot May 28, 2026
d4deda6
Fix deep UI setup failure reporting
Copilot May 28, 2026
bbd6055
Hide no-op AI summary sections
Copilot May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/instructions/ci-copilot-pipeline-security.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
description: "Security rules for the Copilot PR-review pipeline. Read before editing."
applyTo: "eng/pipelines/ci-copilot.yml,eng/scripts/detect-ui-test-categories.ps1,.github/scripts/**,.github/pr-review/**,.github/skills/pr-review/**,.github/skills/verify-tests-fail-without-fix/**,.github/skills/try-fix/**,.github/skills/run-device-tests/**,.github/workflows/review-trigger.yml,.github/workflows/pr-review-queue.yml,.github/workflows/copilot-evaluate-tests.*"
---

# CI Copilot pipeline — security rules

This pipeline runs **untrusted PR code** on AzDO agents with these tokens in scope:

- `GH_COMMENT_TOKEN` / `GH_TOKEN` — `maui-bot` PAT (post comments, labels, reviews on any PR)
- `COPILOT_GITHUB_TOKEN` — Copilot CLI install token
- AzDO GitHub service-connection PAT — repo contents, PRs, checks, workflows

Once the PR is merged into the worktree, the author controls every `.csproj`, `Directory.Build.targets`, source generator, analyzer, test, `.ps1`, and `.yml` the pipeline subsequently runs.

## Rules

1. **Per-task `env:` scoping.** Only put tokens a task needs. The Copilot-agent task gets `COPILOT_GITHUB_TOKEN` only — never `GH_TOKEN`. Pass `--secret-env-vars=GH_TOKEN,GITHUB_TOKEN,COPILOT_GITHUB_TOKEN` to the Copilot CLI.

2. **`persistCredentials: false` on every `checkout: self`** unless the task pushes. Default checkout writes the service-connection PAT into `.git/config` as `extraheader`, readable by any subprocess.

3. **Trusted-copy scripts before merging the PR.** Setup task (still on `main`) copies `.github/scripts`, `.github/skills`, `eng/scripts` to `$(Build.ArtifactStagingDirectory)/trusted-github/`, then `chmod -R a-w`. Later tasks invoke scripts from `$TRUSTED/...`, never from the merged worktree. In PowerShell use `$ScriptsDir` / `$SkillsDir` / `$EngScriptsDir` (canonical impl in `Review-PR.ps1`). New post-merge scripts must be added to the Setup copy block.

4. **Strip tokens before invoking PR-controlled code.** Wrap every `dotnet build|test|run|pack`, `msbuild`, `dotnet cake`, `BuildAndRun*.ps1`, `Run-DeviceTests.ps1`, `Invoke-UITestWithRetry.ps1` in `Invoke-WithoutGhTokens { ... }` (defined in `Review-PR.ps1` and `verify-tests-fail.ps1` — saves/clears/restores `GH_TOKEN`, `GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN`). **Wrap as close to the subprocess as possible, not at the outer trusted-script boundary** — a trusted script may itself need `gh` for metadata (e.g., `verify-tests-fail.ps1` calls `Detect-TestsInDiff.ps1` which uses `gh api`), so wrapping the whole script breaks its detection path. Wrap only the line that launches the PR-controlled process. Exception: scripts that ONLY call `gh` for PR metadata (`Detect-TestsInDiff.ps1`, `Find-RegressionRisks.ps1`, `detect-ui-test-categories.ps1`) don't need wrapping at all — they keep the token.

5. **Cross-phase signal files in `$(Agent.TempDirectory)`** (or `$TRUSTED`), never `$RepoRoot/...`. PR code can overwrite anything in the worktree, including a gate verdict. Readers must not silently fall back to a worktree path if the trusted one is missing.

6. **Strip `##vso[...]` from PR-controlled stdout.** Pipe through `tr -d '\r' | sed -E 's/##vso\[[^]]*\]//g'` — bare `sed` misses CRLF lines and the agent will execute the directive.

7. **`gh-aw` workflows.** Pin compiler version (≥ v0.68.4 strips `pull-requests: write` per `gh-aw#28767`). Regenerate `.lock.yml` with `gh aw compile` in the **same commit** as any `.md` frontmatter edit (stale lock ⇒ all dispatches fail). `workflow_dispatch` triggers must restore trusted `.github/` from main (see `Checkout-GhAwPr.ps1`).

8. **No token republish.** Don't `setvariable` a token (visible to every later task, even with `issecret=true`). Don't write tokens to worktree files. Don't echo token names.

## Review checklist

- [ ] New `checkout: self` has `persistCredentials: false`.
- [ ] New `env:` block lists only the tokens that task needs; Copilot task has no `GH_TOKEN`.
- [ ] New post-merge script invoked via `$ScriptsDir` / `$SkillsDir` / `$EngScriptsDir`, not `$RepoRoot/...`, AND added to Setup copy block.
- [ ] New invocation of PR-controlled code (`dotnet test|build|run`, `BuildAndRun*`, `Run-DeviceTests`, `Invoke-UITestWithRetry`) is wrapped in `Invoke-WithoutGhTokens` AT THE CALL SITE (not at an outer boundary).
- [ ] New cross-phase state file lives under `$(Agent.TempDirectory)` / `$TRUSTED`.
- [ ] New PR-stdout pipe uses `tr -d '\r' | sed -E 's/##vso\[[^]]*\]//g'`.
- [ ] Edited `.github/workflows/*.md` has matching `.lock.yml` regenerated in same commit.

## Grep these during review

```bash
git grep -nE 'dotnet (test|build|run|pack)' eng/pipelines/ci-copilot.yml .github/scripts .github/skills | grep -v Invoke-WithoutGhTokens
git grep -nE 'Join-Path \$RepoRoot ".*\.(ps1|sh)"' .github/scripts .github/skills
git grep -nA1 'checkout: self' eng/pipelines/ci-copilot.yml | grep -v persistCredentials
git grep -nE 'Set-Content.*\$RepoRoot.*(gate-result|sentinel|verdict)' .github/scripts .github/skills
git grep -nE 'sed.*##vso' eng/pipelines/ci-copilot.yml | grep -v 'tr -d'
```
75 changes: 75 additions & 0 deletions .github/scripts/Post-AISummaryComment.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env pwsh
#Requires -Modules Pester
<#
.SYNOPSIS
Pester tests for pure-function helpers in post-ai-summary-comment.ps1.

.EXAMPLE
Invoke-Pester ./Post-AISummaryComment.Tests.ps1
#>

BeforeAll {
$scriptPath = Join-Path $PSScriptRoot 'post-ai-summary-comment.ps1'
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors -and $parseErrors.Count -gt 0) {
throw ($parseErrors | ForEach-Object { $_.Message }) -join [Environment]::NewLine
}

$function = $ast.Find({
$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$args[0].Name -eq 'Test-PhaseContentIsNoOp'
}, $true)

if (-not $function) {
throw "Function 'Test-PhaseContentIsNoOp' not found"
}

Invoke-Expression $function.Extent.Text
}

Describe 'Test-PhaseContentIsNoOp' {
It 'suppresses the no-UI-tests placeholder' {
Test-PhaseContentIsNoOp `
-PhaseKey 'uitests' `
-Content 'No UI test categories needed for this PR (no UI-relevant changes).' |
Should -BeTrue
}

It 'keeps UI test content when categories or full matrix are present' {
Test-PhaseContentIsNoOp `
-PhaseKey 'uitests' `
-Content '**Detected UI test categories:** `Button,Entry`' |
Should -BeFalse

Test-PhaseContentIsNoOp `
-PhaseKey 'uitests' `
-Content 'Full UI test matrix will run (no specific categories detected from PR changes).' |
Should -BeFalse
}

It 'suppresses regression placeholders when there are no implementation files or risks' {
Test-PhaseContentIsNoOp `
-PhaseKey 'regression-check' `
-Content '🟢 No implementation files modified — skipping regression cross-reference.' |
Should -BeTrue

Test-PhaseContentIsNoOp `
-PhaseKey 'regression-check' `
-Content "## 🔍 Regression Cross-Reference`n`n🟢 No regression risks detected. No labeled bug-fix PRs in the last 6 months touched the modified files." |
Should -BeTrue
}

It 'keeps actionable regression content' {
Test-PhaseContentIsNoOp `
-PhaseKey 'regression-check' `
-Content "## 🔍 Regression Cross-Reference`n`n🟡 **Overlaps with prior bug-fix PRs** — same files modified, but no exact line revert detected." |
Should -BeFalse

Test-PhaseContentIsNoOp `
-PhaseKey 'regression-check' `
-Content '⚠️ Regression cross-reference failed: gh api failed' |
Should -BeFalse
}
}
Loading
Loading