Copilot Code Review: Automatic PR Reviews, Requiring Them with Rulesets, Steering with Instructions and Limits
Copilot code review reads instructions from the pull request's head branch and never looks at some files, such as tsconfig.json and lock files, so a pull request can loosen its own review rules. We showed this with a script in Bookstore. Automatic reviews and a ruleset JSON, effort levels and cost, review instructions and a code-review skill, Copilot approvals and the split with human review.
Version note: The code and scripts in this post were built and run with .NET 10 and Entity Framework Core 10.0.15; the tests ran against SQL Server 2025, and the ruleset file was validated against the schema in GitHub's REST API OpenAPI description. Copilot code review was not run for this post (no GitHub account or Copilot subscription was used); how reviews are triggered, what they read, the effort levels and the prices are taken from docs.github.com as of 15 November 2026. No review comment written by Copilot is shown anywhere.
Guess First
In Bookstore, the online bookstore used as the shared example across this site's series, a pull request adds a shipping fee to orders, and Copilot code review runs automatically in the repository. The pull request has four problems: a new decimal property without HasPrecision, a test that loosens a Created check to IsSuccessStatusCode, a change that deletes the "ask about changed assertions" line from the review instructions, and noImplicitReturns removed from Angular's tsconfig.json.
What is your guess? Does Copilot catch all four? Which one does it not look at at all? Does the line removed from the instructions affect the review, or does Copilot use the instructions on main? Two of the answers are in the documentation, and we saw one of them with the script in this post.
Requesting a Review and Automatic Reviews
The simplest way is the Request button next to Copilot under Reviewers on the right of the pull request; the documentation says a review usually takes less than 30 seconds. By default Copilot leaves a "Comment" review, not "Approve" or "Request changes", so with default settings it does not count toward required approvals. Its comments can be replied to and resolved like a human's, but Copilot does not see your replies. Suggested changes can be applied in a few clicks or handed to the cloud agent from the seventh post with Fix with Copilot.
You can turn on automatic reviews in three places: in your personal settings for your own pull requests (Pro, Pro+, Max, Business and Enterprise), with a ruleset for a repository, and with an organization ruleset for an organization. These settings do not override each other; if one is on, the review happens, and if both are on there is still a single review. The triggers: when a pull request is opened as "Open" or a draft is switched to "Open" for the first time; on every new push if Review new pushes is selected; and while still a draft if Review draft pull requests is selected. With Review new pushes off, Copilot reviews a pull request only once; for later changes you request the review again by hand. One warning from the documentation: on a re-review, Copilot may repeat comments you resolved or downvoted.
Requiring It with a Ruleset
The rule we want for Bookstore's main has two parts: one human approval on every pull request and a Copilot review on every push. You can create the ruleset in the UI as well; we put it into the repository as a JSON file so it can be reproduced:
{
"name": "main: human review and Copilot review",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": true,
"required_review_thread_resolution": true,
"require_extra_approval_for_unattributed_changes": true
}
},
{
"type": "copilot_code_review",
"parameters": {
"review_on_push": true,
"review_draft_pull_requests": false
}
}
]
}
The copilot_code_review rule has two parameters: review_on_push and review_draft_pull_requests. require_extra_approval_for_unattributed_changes in the pull_request rule is interesting for this series: when Copilot opens a pull request without a human collaborator, it requires one more approval. require_last_push_approval stops the person who made the last push from approving their own pull request.
Before sending the file to GitHub, we validated it locally against the ruleset schema in the OpenAPI description that GitHub's REST API publishes: no errors. In the same test, removing a required field (require_last_push_approval) made the schema reject the file, but a misspelled parameter name (review_on_pushes) was accepted; the schema allows unknown fields. So a misspelled parameter can be silently ignored; check the ruleset in the UI after creating it. To send the file with GitHub CLI (we did not run this for the post):
gh api --method POST repos/OWNER/bookstore/rulesets --input eng/rulesets/main-review.json
There is one more setting: Copilot approvals. If Allow Copilot to approve pull requests and Allow Copilot approvals to count toward merge requirements are turned on in the repository settings, Copilot's approval can satisfy the required approval count like a teammate's; it can be limited to pull requests that only touch certain files with up to 15 globs, and the approval is dismissed when a new commit arrives. We keep this setting off in Bookstore; we will see why below.
Effort Level and Cost
You cannot pick the model for code review; GitHub says this is deliberate and that changing the model would hurt review quality. What you can pick is the effort level:
| Level | What it does | Typical range per review | Average |
|---|---|---|---|
| Lite | Fast review focused on common issues | $0.05 – $1 | $0.25 |
| Balanced (default) | A higher-reasoning model for complex logic and security | $0.25 – $5 | $1.50 |
| Max (public preview) | Several high-reasoning models | $1.50 – $25 | $10 |
The amounts are estimates, in AI Credits, and exclude GitHub Actions minutes: the agentic part of the review that scans the repository runs on an Actions runner. The level is determined in this order: the one chosen when the review is requested, the one used earlier on this pull request, the requester's setting, the repository setting, the organization setting and finally Balanced. Repository administrators can also set a maximum.
Who pays is defined too: for an automatic review the credits come from the pull request's author, for a review someone else requested by hand from that person. For pull requests opened by the cloud agent they go first to the human co-author, and to the organization if the co-author cannot be billed. A Balanced review on every push means an average of 60 dollars for a pull request with 40 pushes; work that out before you turn on Review new pushes in every repository.
Steering with Instructions
In the fourth post we wrote an instruction file that only goes to review, with excludeAgent: "cloud-agent". In this post we extended it and added how to write comments:
---
applyTo: "**"
excludeAgent: "cloud-agent"
---
When reviewing a pull request in this repository, check these first:
- A query inside a loop, or `Include` chains that load more than the endpoint returns.
- A new `decimal` property without `HasPrecision`, or a new string without `HasMaxLength`.
- A migration that drops or renames a column without a data step.
- A changed or deleted assertion in `tests/`; ask why the expected value changed.
- SQL built with string concatenation or interpolation in `FromSqlRaw` or `ExecuteSqlRaw`.
- An endpoint that returns an entity instead of a record, or a module that reads another module's tables.
- In Angular, a subscription without cleanup or state kept outside signals.
How to comment:
- Comment only when you can name the concrete failure (wrong result, data loss, exception, security issue).
- Do not comment on formatting; `dotnet format` and Prettier handle it.
- Put one finding in one comment, with a suggested change when the fix is local.
The last section is about review fatigue: leaving formatting comments to the formatters and not writing a comment that cannot name a concrete failure keeps the team reading Copilot's comments. Besides this, code review reads copilot-instructions.md, the root AGENTS.md, any CLAUDE.md, GEMINI.md and REVIEW.md, and skills. To make sure a review-specific skill is used, the documentation recommends naming its folder code-review:
---
name: code-review
description: Review workflow for Bookstore pull requests. Use when reviewing a pull request or a branch diff in this repository, especially changes under src/Bookstore.Api/Data/Migrations or tests/.
---
Review the pull request in this order:
1. If the diff touches `src/Bookstore.Api/Data/Migrations`, run `dotnet run .github/skills/ef-migration-review/check-migrations.cs` and comment on every `RISK` line that the pull request description does not explain.
2. List every line removed from `tests/` (`git diff --unified=0 <base>...HEAD -- tests/`). A removed or changed `Assert` line needs a reason in the pull request; if there is none, comment and ask for it.
3. If the diff changes `.github/instructions`, `.github/skills`, `.github/copilot-instructions.md` or `AGENTS.md`, say so in the overview: this review used the changed versions.
4. Apply `.github/instructions/code-review.instructions.md` to the remaining changes.
The first step of the skill calls the migration script from the sixth post. MCP servers added in repository settings are available to review as well; only tools with readOnlyHint set to true are used. Which skill or MCP server was used shows in the attributions under each comment and in the review session's log.
What the Review Reads: the Head Branch and Excluded Files
Two rules answer Guess First. First: Copilot reads instructions, skills and agent instructions from the pull request's head branch, not from main. The documentation presents this as a convenience: you can try an instruction change in the same pull request without merging it. But the same rule means a pull request can loosen its own review rules. Second: some files never enter the review; the documented list includes package-lock.json, tsconfig.json, .gitignore, *.lock, *.svg, *.log, dist and node_modules. The concept page also names package.json as an example while the reference list does not; with an inconsistency like this, run your own check.
To see the effect of these two rules on a pull request, we added a small script to the repository:
// Excerpt; Git(), Show(), Field() and GlobToRegex() are small helpers at the end of the file.
string baseRef = args.ElementAtOrDefault(0) ?? "main";
string headRef = args.ElementAtOrDefault(1) ?? "HEAD";
// Part of the list in "Files excluded from GitHub Copilot code review" (docs.github.com, November 2026).
string[] excludedNames = [".gitignore", "package-lock.json", "yarn.lock", "tsconfig.json", "jest.config.js", "requirements.txt", "paket.lock"];
string[] excludedGlobs = ["**/*.svg", "**/*.log", "**/*.lock", "**/dist/**", "**/node_modules/**"];
string[] guidanceFiles = [".github/copilot-instructions.md", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "REVIEW.md"];
string[] changed = await Git($"diff --name-only {baseRef}...{headRef}");
string[] headFiles = await Git($"ls-tree -r --name-only {headRef}");
string[] skipped = changed.Where(f => excludedNames.Contains(Path.GetFileName(f)) || excludedGlobs.Any(g => GlobToRegex(g).IsMatch(f))).ToArray();
string[] reviewed = changed.Except(skipped).ToArray();
Console.WriteLine($"{baseRef}...{headRef}: {changed.Length} changed file(s)");
Console.WriteLine($"Reviewed ({reviewed.Length}): {string.Join(", ", reviewed)}");
Console.WriteLine($"Skipped by Copilot code review ({skipped.Length}): {string.Join(", ", skipped)}");
Console.WriteLine($"Instructions read from {headRef}:");
int characters = 0;
foreach (string file in guidanceFiles.Where(headFiles.Contains))
{
characters += (await Show(headRef, file)).Length;
Console.WriteLine($" always {file}");
}
foreach (string file in headFiles.Where(f => f.StartsWith(".github/instructions/") && f.EndsWith(".instructions.md")))
{
string content = await Show(headRef, file);
string applyTo = Field(content, "applyTo");
if (Field(content, "excludeAgent") == "code-review")
{
Console.WriteLine($" skip {file} (excludeAgent: code-review)");
continue;
}
int matches = reviewed.Count(f => applyTo.Split(',', StringSplitOptions.TrimEntries).Any(g => GlobToRegex(g).IsMatch(f)));
if (matches > 0)
{
characters += content.Length;
}
Console.WriteLine(matches > 0 ? $" path {file} (applyTo {applyTo} -> {matches} reviewed file(s))" : $" skip {file} (applyTo {applyTo} matches no reviewed file)");
}
Console.WriteLine($" {characters:N0} characters of instructions");
string[] skills = headFiles.Where(f => f.StartsWith(".github/skills/") && f.EndsWith("/SKILL.md")).Select(f => f.Split('/')[2]).ToArray();
Console.WriteLine($"Skills: {string.Join(", ", skills.Select(s => s == "code-review" ? s + " (review-focused name)" : s))}");
foreach (string file in changed.Where(f => f.StartsWith(".github/instructions/") || f.StartsWith(".github/skills/") || guidanceFiles.Contains(f)))
{
Console.WriteLine($"warning: this pull request changes {file}; Copilot reviews it with the changed version");
}
On the shipping fee branch:
$ dotnet run eng/review-context.cs main feature/order-shipping-fee
main...feature/order-shipping-fee: 7 changed file(s)
Reviewed (6): .github/instructions/code-review.instructions.md, src/Bookstore.Api/Data/Migrations/20261115004245_AddOrderShippingFee.Designer.cs, src/Bookstore.Api/Data/Migrations/20261115004245_AddOrderShippingFee.cs, src/Bookstore.Api/Data/Migrations/BookstoreContextModelSnapshot.cs, src/Bookstore.Api/Ordering/Order.cs, tests/Bookstore.Api.Tests/OrderingTests.cs
Skipped by Copilot code review (1): web/bookstore-web/tsconfig.json
Instructions read from feature/order-shipping-fee:
always .github/copilot-instructions.md
always AGENTS.md
skip .github/instructions/angular.instructions.md (applyTo web/bookstore-web/src/**/*.ts,web/bookstore-web/src/**/*.html matches no reviewed file)
path .github/instructions/code-review.instructions.md (applyTo ** -> 6 reviewed file(s))
path .github/instructions/dotnet.instructions.md (applyTo src/**/*.cs,tests/**/*.cs -> 5 reviewed file(s))
3,980 characters of instructions
Skills: add-migration, code-review (review-focused name), ef-migration-review, new-endpoint
warning: this pull request changes .github/instructions/code-review.instructions.md; Copilot reviews it with the changed version
The script shows two problems before Copilot has written anything. tsconfig.json is outside the review: Copilot code review will not see that noImplicitReturns was removed. And this pull request changes its own review instructions: Copilot will review it with the "ask about changed assertions" line deleted. That does not mean Copilot is certain to miss the loosened test; the other instructions and its own analysis may still catch it. But you can no longer rely on the rule we wrote for exactly this.
What Copilot Catches and What It Misses
We cannot say what Copilot would write on this pull request without running it; instead we examined the same pull request with our own tools. EF Core already reports the decimal problem while generating the migration:
$ dotnet ef migrations add AddOrderShippingFee --project src/Bookstore.Api --output-dir Data/Migrations
warn: Microsoft.EntityFrameworkCore.Model.Validation[30000]
No store type was specified for the decimal property 'ShippingFee' on entity type 'Order'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.
Done. To undo this action, use 'ef migrations remove'
The migration is created with decimal(18,2), and the warning says values that do not fit will be silently truncated. That is exactly the second item of the review instructions; a reviewer with that instruction is expected to catch this kind of bug, which can be found by reading the code. The second step of the skill shows the loosened test:
$ git diff --unified=0 main...HEAD -- tests/
@@ -26 +26 @@ public sealed class OrderingTests(BookstoreApiFactory factory)
- Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ Assert.True(response.IsSuccessStatusCode);
$ dotnet run .github/skills/ef-migration-review/check-migrations.cs AddOrderShippingFee
OK 20261115004245_AddOrderShippingFee: 1 operations
$ dotnet test
Test run summary: Passed!
total: 22
failed: 0
succeeded: 22
skipped: 0
The migration script sees no problem: an AddColumn carries no data-loss risk and the script does not look at precision. The tests pass 22/22 as well; the loosened test passes because it was loosened. That is why the division of labor in Bookstore is: deterministic checks (the EF Core warning, the migration script, the tests/ diff) in CI and in the skill; the Copilot review as a second pair of eyes for bugs found by reading code; and the human reviewer for business rules, design and the question "why was this changed". The checklist in the third post of the Code Review series and the fourth post of the AI-Assisted Development series cover this division of labor in detail.
This is why we keep Copilot approvals off. Had Copilot's approval satisfied the merge requirement on this pull request, a change in a file outside the review and in a test whose rule had been deleted could have been merged without a human seeing it.
In Production
Tie the instruction files to an owner with CODEOWNERS and require code owner approval in the ruleset. Because of the head branch rule, changes to .github/instructions, .github/skills, AGENTS.md and REVIEW.md matter as much as code changes.
Add separate checks for files outside the review. A human or a CI step should see tsconfig.json or package-lock.json changes; dependency review and secret scanning are separate tools for dependency changes.
Watch the cost. Review new pushes with the Balanced default means hundreds of reviews a month in an active repository. Use Lite in routine repositories and Balanced or Max for security-critical code; set a maximum per repository.
Code review is a separate option in organization policies, and on Business and Enterprise it can be opened to members without a license. We cover that in the tenth post.
Common Pitfalls
Counting Copilot's approval as a human approval. Even with Copilot approvals on, the human review is the only step that checks the business rule and the reason for the change.
Forgetting that a pull request can change its own instructions. The review uses the instructions on the head branch.
Assuming files outside the review are reviewed. tsconfig.json and lock files are on the list.
Too many pointless comments. Formatting and personal-preference comments push the team to stop reading Copilot's comments; write down in the instructions what is worth a comment.
Turning on Max reviews on every push in every repository. Repeating a review that averages 10 dollars dozens of times a day burns through the budget fast.
A Small Experiment
★☆☆ Run eng/review-context.cs in your own repository with the base and head commits of the last merged pull request. Is any file outside the review?
★★☆ Add a warning to check-migrations.cs for columns added with AddColumn<decimal> as decimal(18,2) and try it on the shipping fee branch. Does it say the same thing as EF Core's warning?
★★★ Add a required_status_checks rule to Bookstore's ruleset JSON that requires a CI workflow on main, validate it against the schema and note which parameters are required. Then try again with a misspelled parameter name.
So far we have used Copilot in the IDE and on GitHub. In the next post we move to the terminal: Copilot CLI's interactive session, plan mode, sandboxing, automation with copilot -p and connecting to other tools with ACP.