Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0cc2384
Switch evaluate-pr-tests to pull_request_target for fork PR support
github-actions[bot] Mar 26, 2026
2dfcf71
Gate workflow_dispatch checkout to PRs from authors with write access
github-actions[bot] Mar 26, 2026
d79ff20
Move write-access gate into Checkout-GhAwPr.ps1 for reuse
github-actions[bot] Mar 26, 2026
8234b33
Skip workflow_dispatch checkout for fork PRs
github-actions[bot] Mar 26, 2026
e1dee57
Restore entire .github/ from base branch instead of individual paths
github-actions[bot] Mar 26, 2026
f4c4791
Use merge instead of restore for workflow_dispatch checkout
github-actions[bot] Mar 26, 2026
c089534
Gate auto-evaluation on author_association for write access
github-actions[bot] Mar 27, 2026
e6004b7
Fix review findings: shallow clone, write-access gating, fork message
github-actions[bot] Mar 27, 2026
00af259
Add dry-run mode and noop guidance to evaluate-tests workflow
github-actions[bot] Mar 28, 2026
17c0f27
Add null guard for PrInfo in Checkout-GhAwPr.ps1
github-actions[bot] Apr 2, 2026
f4f02df
Update gh-aw security docs: accurate credential model, defense layers
github-actions[bot] Apr 2, 2026
27e14d6
Add copilot[bot] to bots allowlist for auto-evaluation
github-actions[bot] Apr 2, 2026
69c0a16
Allow fork PRs from write-access authors in workflow_dispatch
github-actions[bot] Apr 2, 2026
f3dc6a9
Address review feedback: rename suppress_output, fork guard, no-op re…
github-actions[bot] Apr 7, 2026
7bcc980
Fix bot identity: copilot[bot] → copilot-swe-agent[bot]
github-actions[bot] Apr 8, 2026
04bf387
Fix gate step for large PRs (300+ files)
github-actions[bot] Apr 8, 2026
082ed7a
Hide older evaluation comments when posting new ones
github-actions[bot] Apr 9, 2026
e7fdf22
Use slash_command trigger and add built-in feature discovery guide
github-actions[bot] Apr 13, 2026
b40ccb5
Add workflow labels and update fork PR behavior docs for slash_command
github-actions[bot] Apr 13, 2026
d142b43
Fix gate step to run for all triggers, bump timeout to 20min
github-actions[bot] Apr 13, 2026
cfee2bc
Fix gate to succeed cleanly for no-test PRs, fix permission denial ex…
github-actions[bot] Apr 14, 2026
e76545e
Skip checkout when gate finds no test files, fix bot author permissio…
github-actions[bot] Apr 14, 2026
bf19b8c
Gate exit 1 to stop workflow on no-test PRs, remove HAS_TEST_FILES
github-actions[bot] Apr 14, 2026
0faafa9
Limit evaluate-pr-tests to slash_command trigger only
github-actions[bot] Apr 14, 2026
f8ab3c5
Guard against non-PR issues and closed/merged PRs
github-actions[bot] Apr 14, 2026
6154bfd
Address review findings: fork gate, fatal restore, doc alignment
github-actions[bot] Apr 14, 2026
3b49c7a
Fix remaining review findings: gate error handling, REST fallback, ta…
github-actions[bot] Apr 14, 2026
72356a7
Surface REST API fallback errors instead of masking as 'no test files'
github-actions[bot] Apr 14, 2026
338d7c1
Re-enable workflow_dispatch for manual triggering
github-actions[bot] Apr 15, 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
63 changes: 44 additions & 19 deletions .github/scripts/Checkout-GhAwPr.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@

.DESCRIPTION
Checks out a PR branch and restores trusted agent infrastructure (skills,
instructions) from the base branch. Works for both same-repo and fork PRs.
instructions) from the base branch. This gives the agent the PR's code
changes with the latest skills and instructions from main.

This script is only invoked for workflow_dispatch triggers. For pull_request
and issue_comment, the gh-aw platform's checkout_pr_branch.cjs handles PR
checkout automatically (it runs as a platform step after all user steps).
workflow_dispatch skips the platform checkout entirely, so this script is
the only thing that gets the PR code onto disk.
This script is only invoked for workflow_dispatch triggers. For
pull_request_target and issue_comment, the gh-aw platform's
checkout_pr_branch.cjs handles PR checkout automatically.
workflow_dispatch skips the platform checkout entirely, so this script
is the only thing that gets the PR code onto disk.

SECURITY NOTE: This script checks out PR code onto disk. This is safe
because NO subsequent user steps execute workspace code — the gh-aw
platform copies the workspace into a sandboxed container with scrubbed
credentials before starting the agent. The classic "pwn-request" attack
requires checkout + execution; we only do checkout.
SECURITY: Before checkout, the script verifies the PR is not from a
fork and that the author has write access (write, maintain, or admin).
Fork PRs are evaluated via pull_request_target instead (where the
platform handles checkout safely inside a sandboxed container).

DO NOT add steps after this that run scripts from the workspace
(e.g., ./build.sh, pwsh ./script.ps1). That would create an actual
fork code execution vulnerability. See:
(e.g., ./build.sh, pwsh ./script.ps1). That would create a code
execution vulnerability. See:
https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

.NOTES
Expand All @@ -42,16 +42,42 @@ if (-not $env:PR_NUMBER -or $env:PR_NUMBER -eq '0') {

$PrNumber = $env:PR_NUMBER

# ── Verify PR is same-repo and author has write access ───────────────────────

$PrInfo = gh pr view $PrNumber --repo $env:GITHUB_REPOSITORY --json author,isCrossRepository --jq '{author: .author.login, isFork: .isCrossRepository}' | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Failed to fetch PR #$PrNumber metadata"
exit 1
}

if ($PrInfo.isFork) {
Write-Host "⏭️ PR #$PrNumber is from a fork. workflow_dispatch does not check out fork PRs."
Write-Host " Fork PRs are evaluated automatically via pull_request_target."
exit 1
}

$Permission = gh api "repos/$($env:GITHUB_REPOSITORY)/collaborators/$($PrInfo.author)/permission" --jq '.permission'
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Failed to check permissions for '$($PrInfo.author)'"
exit 1
}

$AllowedRoles = @('admin', 'write', 'maintain')
if ($Permission -notin $AllowedRoles) {
Write-Host "⏭️ PR author '$($PrInfo.author)' has '$Permission' access. workflow_dispatch only processes PRs from authors with write access."
exit 1
}
Write-Host "✅ PR #$PrNumber by '$($PrInfo.author)' ($Permission access, same-repo)"

# ── Save base branch SHA ─────────────────────────────────────────────────────
# Must be captured BEFORE checkout replaces HEAD.
# Exported for potential use by downstream platform steps (e.g., checkout_pr_branch.cjs)

$BaseSha = git rev-parse HEAD
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Failed to get current HEAD SHA"
exit 1
}
Add-Content -Path $env:GITHUB_ENV -Value "BASE_SHA=$BaseSha"
Write-Host "Base branch SHA: $BaseSha"

# ── Checkout PR branch ──────────────────────────────────────────────────────

Expand All @@ -65,10 +91,9 @@ Write-Host "✅ Checked out PR #$PrNumber"
git log --oneline -1

# ── Restore agent infrastructure from base branch ────────────────────────────
# This script only runs for workflow_dispatch (other triggers use the platform's
# checkout_pr_branch.cjs instead). For workflow_dispatch the platform checkout is
# skipped, so this restore IS the final workspace state.
# rm -rf first to prevent fork-added files from surviving the restore.
# Replace skills and instructions with base branch versions to ensure the agent
# always uses trusted infrastructure from main. Uses git checkout to read files
# directly from the commit tree — works in shallow clones (no history traversal).

if (Test-Path '.github/skills/') { Remove-Item -Recurse -Force '.github/skills/' }
if (Test-Path '.github/instructions/') { Remove-Item -Recurse -Force '.github/instructions/' }
Expand Down
22 changes: 14 additions & 8 deletions .github/workflows/copilot-evaluate-tests.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 31 additions & 8 deletions .github/workflows/copilot-evaluate-tests.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
description: Evaluates test quality, coverage, and appropriateness on PRs that add or modify tests
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
forks: ["*"]
pull_request_target:
types: [opened, synchronize, reopened]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do think this will still lead to the 'Approve and run workflows' button showing up for PRs from untrusted forks. We need to solidify the guidance we give for when to hit that button. I really wish that button navigated into a list of workflows needing approval for the PR with boxes to select which to approve.

paths:
- 'src/**/tests/**'
- 'src/**/test/**'
Expand All @@ -15,9 +14,14 @@ on:
description: 'PR number to evaluate'
required: true
type: number
suppress_comment:
description: 'Dry-run — evaluate but do not post a comment on the PR'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Future-proofing: I suggest renaming to suppress_output in case the output changes (to a PR review for example).

Suggested change
suppress_comment:
description: 'Dry-run evaluate but do not post a comment on the PR'
suppress_output:
description: 'Dry-run - evaluate but do not post output on the PR'

required: false
type: boolean
default: false

if: >-
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I always guard against forks as well, preventing the workflow from running on forks except for the workflow_dispatch event. Otherwise, PRs within a fork will result in failing workflow runs (vs. starting the workflow and skipping all jobs).

Simple case that needs adapting to your scenario: if: (!github.event.repository.fork) || github.event_name == 'workflow_dispatch'.

Expand Down Expand Up @@ -57,7 +61,7 @@ timeout-minutes: 15

steps:
- name: Gate — skip if no test source files in diff
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
Expand All @@ -73,9 +77,10 @@ steps:
echo "✅ Found test files to evaluate:"
echo "$TEST_FILES" | head -20

# Only needed for workflow_dispatch — for pull_request and issue_comment,
# Only needed for workflow_dispatch — for pull_request_target and issue_comment,
# the gh-aw platform's checkout_pr_branch.cjs handles PR checkout automatically.
# workflow_dispatch skips the platform checkout entirely, so we must do it here.
# The script gates on PR author having write access before checkout.
- name: Checkout PR and restore agent infrastructure
if: github.event_name == 'workflow_dispatch'
env:
Expand Down Expand Up @@ -110,11 +115,27 @@ If the file is **missing**, the fork PR branch is likely not rebased on the late

❌ **Cannot evaluate**: this PR's branch does not include the evaluate-pr-tests skill (`.github/skills/evaluate-pr-tests/SKILL.md` is missing).

**Fix**: rebase your fork on the latest `main` branch, or use the **workflow_dispatch** trigger (Actions tab → "Evaluate PR Tests" → "Run workflow" → enter PR number) which handles this automatically.
**Fix**: rebase your fork on the latest `main` branch and push again. The evaluation will trigger automatically once the skill file is available.
```

Then stop — do not proceed with the evaluation.

## Dry-run mode

When triggered via `workflow_dispatch` with `suppress_comment` = `${{ inputs.suppress_comment }}`:
- If **true**, perform the full evaluation but **do not** post a comment on the PR. Write the evaluation to the workflow log only. This is useful for testing the skill without spamming the PR.
- If **false** (default), post the comment as normal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These expressions get replaced on their way to the model so this would end up embedding the true or false value into the opening statement. I think you need something more like this (but I recommend validating my understanding here). Note I also reflected my suggested input rename from above.

Suggested change
When triggered via `workflow_dispatch` with `suppress_comment` = `${{ inputs.suppress_comment }}`:
- If **true**, perform the full evaluation but **do not** post a comment on the PR. Write the evaluation to the workflow log only. This is useful for testing the skill without spamming the PR.
- If **false** (default), post the comment as normal.
When triggered via `workflow_dispatch`, the `suppress_output` input controls behavior.
- If `${{ inputs.suppress_output }}` == **true**, perform the full evaluation but **do not** post a comment on the PR. Write the evaluation to the workflow log only. This is useful for testing the skill without spamming the PR.
- If `${{ inputs.suppress_output }}` == **false** (default), post the comment as normal.


## When no action is needed

If there is nothing to evaluate (PR has no test files, PR is a docs-only change, etc.), you **must** call the `noop` tool with a message explaining why:

```json
{"noop": {"message": "No action needed: [brief explanation, e.g. 'PR contains no test files']"}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You might want to configure to not generate a no-op run report issue (within the frontmatter).

https://github.github.com/gh-aw/patterns/monitoring/#no-op-run-reports

```

Do not post a comment and do not silently exit — always use `noop` so the workflow run shows a clear reason.

## Running the skill

1. Use `gh pr view <number>` to fetch PR metadata (title, body, labels, base branch). If `gh` CLI is unavailable, use the GitHub MCP tools instead.
Expand All @@ -124,7 +145,9 @@ Then stop — do not proceed with the evaluation.

## Posting Results

Call `add_comment` with `item_number` set to the PR number. Wrap the report in a collapsible `<details>` block:
If dry-run mode is active (`suppress_comment` is true), log the evaluation report to stdout and stop — do **not** call `add_comment`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
If dry-run mode is active (`suppress_comment` is true), log the evaluation report to stdout and stop — do **not** call `add_comment`.
If dry-run mode is active (`suppress_output` is true), log the evaluation report to stdout and stop — do **not** call `add_comment`.


Otherwise, call `add_comment` with `item_number` set to the PR number. Wrap the report in a collapsible `<details>` block:

```markdown
## 🧪 PR Test Evaluation
Expand Down
Loading