The Copilot Coding Agent: Assigning Issues, the GitHub Actions Environment, Setup Steps, the Firewall and Reviewing the Resulting PR
No red check on a pull request from the Copilot coding agent does not mean the tests passed: by default workflows do not run on Copilot's pushes and wait for Approve and run workflows. Writing and assigning the issue, a .NET, Node and SQL Server environment with copilot-setup-steps.yml, rules actionlint misses, Agents secrets, the firewall, reviewing the PR and Agent Merge.
Version note: The code in this post was built and run with .NET 10, xUnit.net v3 4.0.3, Angular 22.2 (TypeScript 6.0) and Vitest 5.1; the tests ran against SQL Server 2025, and copilot-setup-steps.yml was validated with actionlint 1.7.12. The Copilot coding agent was not run for this post (no GitHub account or Copilot subscription was used): we implemented the issue by hand, following the steps the agent would take, and show the real output of every step. The agent's environment, the firewall, Agent Merge and ruleset behavior are taken from docs.github.com as of 15 November 2026.
Guess First
You assigned an issue for Bookstore, the online bookstore used as the shared example across this site's series, to Copilot. Half an hour later a notification arrives: Copilot opened a pull request and added you as a reviewer. There is no red check on the pull request, and the description says "added unit, integration and component tests".
What is your guess? Did the CI tests run on this pull request? What does the absence of a red check prove? Are the tests the agent ran in its own environment the same thing as the repository's CI? The answer is in the middle of this post, in a single sentence of GitHub's documentation.
Where the Coding Agent Runs
The agent in the fifth post ran in your IDE, on your machine. The Copilot coding agent (the cloud agent, in GitHub's newer naming) does the work in GitHub's environment instead: it clones the repository, creates a branch, changes the code, runs build and tests, and presents the change as a pull request. Since 28 October 2026 the new experience called "Copilot in the cloud" is available on paid plans, and the agent can run in one of three environments:
| Aspect | GitHub Actions | Cloud sandbox |
|---|---|---|
| Where it starts | Issue assignment, Agent Merge, when chosen as the environment | The default wherever an environment can be chosen |
| Repository scope | One repository | Several repositories in the same organization |
| Setup | copilot-setup-steps.yml, larger runners, self-hosted runners | No setup steps or custom runners; personal settings through a dotfiles repository |
| Continuing a session | Only on the GitHub website | Across the CLI, VS Code, the Copilot app and the web |
| Chat data retention | 28 days | For the life of the account |
The third option is Remote: connecting to a custom environment you set up. The scenario in this post is the GitHub Actions environment, because tasks started by assigning an issue run there. Its limits are clear: each session can run for at most 59 minutes and that cannot be extended; the agent works on one branch per task and opens one pull request; it can only make changes in the repository where you started the task; and in a repository you cannot write to, the session is read-only. The environment's run time is separate from AI Credits and comes out of the account's monthly compute allowance.
Starting a Task
According to the documentation, the most common way is to assign the issue to Copilot: you pick Copilot from the Assignees menu on the right of the issue, and the dialog that opens offers the target repository, the starting branch, a custom agent if there is one, the model and an "Optional prompt" field. You can choose a custom agent such as migration-reviewer from the sixth post here.
At the moment of assignment Copilot receives the issue title, the description, the comments so far and your extra instructions. It does not see comments added to the issue later; additional information belongs on the pull request. A comment on the pull request that mentions @copilot runs the agent again on the same branch; Fix with Copilot on a review comment in the Files changed tab hands that comment to the agent, and Add to batch hands over several. Copilot only responds to comments from people with write access to the repository, so an outsider cannot steer the agent by commenting on a pull request.
Other entry points are the repository's Agents tab, github.com/copilot, GitHub Mobile, Copilot CLI and the IDEs. This is also where this post meets the ninth one: you can delegate a task from the CLI to the cloud agent.
A Good Issue for the Agent
GitHub's best-results guide lists three parts of a good task: a clear description of the problem, acceptance criteria for a good solution, and a hint about which files need to change. The issue in Bookstore:
Title: Show the ISBN-10 on the book detail page
Librarians in our institutional customers (see Institutional) still search their systems by ISBN-10.
The book detail page shows only the ISBN-13.
## What to change
- GET /api/books/{isbn} returns a new field `isbn10` (string or null).
- ISBN-13s starting with 978 convert to ISBN-10 (check digit mod 11, 10 is written as X).
ISBN-13s starting with 979 have no ISBN-10: return null.
- The Angular book detail page shows "ISBN-10 <value>" next to the ISBN-13 when it is not null.
## Acceptance criteria
- Unit tests for the conversion: Dune 9780441172719 -> 0441172717, a check digit of X, a 979 ISBN, a wrong check digit.
- An integration test for GET /api/books/9780441172719.
- A Vitest spec for both cases on the book detail page.
- dotnet test and npm test pass. Do not change existing tests or migrations; no database change is needed.
The issue came from institutional customers: libraries still search for books by the old ten-digit ISBN. This issue leaves the agent nothing to guess: the field's name and type, the behavior for the 979 prefix, the X rule and the tests are all written down. The last line matters: "do not change existing tests or migrations". In the fifth post we saw how hard it is to notice an agent that turns a red test green by changing the test; this sentence gives you a yardstick for the review. The guide recommends doing some work yourself instead of handing it to the agent: production-critical issues, anything touching security, personal data or authentication, incident response, ambiguous requests and broad refactoring.
Preparing the Environment: copilot-setup-steps.yml
If the agent tries to install the SDK, the npm packages and SQL Server in a .NET and Angular repository by trial and error, it is slow and eats part of the 59 minutes. In the GitHub Actions environment the file that does this up front is .github/workflows/copilot-setup-steps.yml:
name: Copilot Setup Steps
# Runs on its own when this file changes, so a broken setup shows up as a failed check on the pull request.
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job must be called copilot-setup-steps, or Copilot ignores it.
copilot-setup-steps:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Check out the repository
uses: actions/checkout@v7
- name: Set up .NET 10
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Set up Node.js 24
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: web/bookstore-web/package-lock.json
- name: Restore .NET packages and tools
run: |
dotnet restore
dotnet tool restore
- name: Install Angular dependencies
working-directory: web/bookstore-web
run: npm ci
- name: Start SQL Server and store the connection string in user-secrets
run: |
# A throwaway password made at run time: nothing to keep in secrets, masked in the log.
SQL_PASSWORD="Bk1-$(openssl rand -hex 16)"
echo "::add-mask::$SQL_PASSWORD"
docker run -d --name bookstore-sql -p 1433:1433 \
-e ACCEPT_EULA=Y -e "MSSQL_SA_PASSWORD=$SQL_PASSWORD" \
mcr.microsoft.com/mssql/server:2025-latest
for attempt in $(seq 1 30); do
if docker exec bookstore-sql /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P "$SQL_PASSWORD" -Q "SELECT 1" > /dev/null 2>&1; then
echo "SQL Server is ready after $attempt attempt(s)"
break
fi
if [ "$attempt" -eq 30 ]; then
echo "SQL Server did not start" >&2
exit 1
fi
sleep 2
done
dotnet user-secrets set "ConnectionStrings:BookstoreDb" \
"Server=localhost,1433;Database=BookstoreDb;User Id=sa;Password=$SQL_PASSWORD;TrustServerCertificate=True" \
--project src/Bookstore.Api
- name: Build once so the agent starts from a warm state
run: dotnet build --no-restore
The file looks like a normal workflow but has rules. The job must be called copilot-setup-steps. Only steps, permissions, runs-on, services, snapshot and timeout-minutes (at most 59) are honored on this job; any other key is silently ignored. If the file is not on the default branch, it does not run at all. If a step fails, Copilot skips the remaining steps and starts working with the environment as it is. The fetch-depth value of actions/checkout is also overridden so the agent can roll back commits.
We keep no secret for SQL Server. The container starts from scratch in every session, the password is generated inside the step, hidden in the log with ::add-mask::, and the connection string goes into user-secrets, as in the README. Because the test factory reads the connection string from user-secrets, the agent's dotnet test works without any extra setting; we tried this mechanism on our own machine with no environment variable, only user-secrets, and all 22 tests passed. We left the runner as ubuntu-latest: a Windows runner is possible, but according to the documentation the agent's built-in firewall does not work on Windows.
The first step in validating the file is actionlint. It checks the workflow syntax, the expressions and the inputs of known actions:
$ actionlint .github/workflows/copilot-setup-steps.yml
$ echo $?
0
$ dotnet run eng/check-instructions.cs
...
.github/workflows/copilot-setup-steps.yml: job copilot-setup-steps (runs-on, timeout-minutes, permissions, steps)
Always loaded (AGENTS.md + copilot-instructions.md): 2,334 characters, budget 8,000
OK
actionlint does not know Copilot's rules. To see that, we broke the file on purpose: we set timeout-minutes to 90 and added a job-level env block. Our first draft really did have a job-level env; Copilot would have ignored it. actionlint still passed, while the rule we added to the check script from the fourth post caught both:
$ actionlint .github/workflows/copilot-setup-steps.yml
$ echo $?
0
$ dotnet run eng/check-instructions.cs
...
.github/workflows/copilot-setup-steps.yml: job copilot-setup-steps (runs-on, timeout-minutes, permissions, env, steps)
Always loaded (AGENTS.md + copilot-instructions.md): 2,334 characters, budget 8,000
error: .github/workflows/copilot-setup-steps.yml: timeout-minutes is 90, the maximum is 59
error: .github/workflows/copilot-setup-steps.yml: 'env' is ignored by Copilot (allowed: steps, permissions, runs-on, services, snapshot, timeout-minutes)
2 error(s)
When the file changes, the workflow runs on its own on push and pull request; you see a broken setup in the pull request's check before any agent session. Once it is on the default branch, you can also run it by hand from the Actions tab.
Secrets and the Firewall
If the agent needs a private NuGet feed or npm registry, you define secrets under Settings, Secrets and variables, Agents. These are separate from Actions secrets; the agent does not see Actions, Codespaces or Dependabot secrets. Agents secrets reach the agent's environment as environment variables and are masked in session logs; those whose names start with COPILOT_MCP_ only go to MCP servers. In a cloud sandbox only secrets arrive, not variables.
By default a firewall limits the agent's internet access. GitHub's and Copilot's own hosts are always open; the recommended allowlist, enabled by default, covers operating system package repositories, common container registries, language package registries including NuGet and npm, and certificate authorities. A blocked request shows up as a warning in the pull request description or a comment, with the address and the command that made the request. At organization and repository level you can add your own domain or URL rules.
Mind the documentation's own limitations: the firewall only applies to processes the agent starts through its Bash tool; MCP servers added in repository settings are outside it. A sophisticated attack may get past the firewall; GitHub says not to treat it as a comprehensive security solution. Turning the firewall off means the agent can connect to any host, which raises the risk of code leaking out.
Reviewing the Pull Request
Since we could not run the agent session ourselves, we implemented the issue by hand along the agent's path: a branch with the copilot/ prefix, tests first, then code. As always, we started the review with the diff summary:
$ git diff --stat main...copilot/isbn10-on-book-detail
src/Bookstore.Api/Catalog/CatalogModule.cs | 5 ++-
src/Bookstore.Api/Catalog/Isbn.cs | 34 ++++++++++++++++
tests/Bookstore.Api.Tests/CatalogTests.cs | 9 +++++
tests/Bookstore.Api.Tests/IsbnTests.cs | 25 ++++++++++++
.../src/app/catalog/book-detail-page.html | 7 +++-
.../src/app/catalog/book-detail-page.spec.ts | 46 ++++++++++++++++++++++
web/bookstore-web/src/app/core/api-models.ts | 2 +
7 files changed, 125 insertions(+), 3 deletions(-)
Seven files, all where the issue said; no migration, no deleted line in an existing test. The actual conversion code is small:
using System.Globalization;
namespace Bookstore.Catalog;
// ISBN-13 to ISBN-10 for display. Only ISBN-13s with the 978 prefix have an ISBN-10; 979 ones do not.
public static class Isbn
{
public static string? ToIsbn10(string isbn13)
{
if (isbn13.Length != 13 || !isbn13.All(char.IsAsciiDigit) || !isbn13.StartsWith("978") || !HasValidCheckDigit(isbn13))
{
return null;
}
string core = isbn13[3..12];
int sum = 0;
for (int i = 0; i < core.Length; i++)
{
sum += (10 - i) * (core[i] - '0');
}
int check = (11 - sum % 11) % 11;
return core + (check == 10 ? "X" : check.ToString(CultureInfo.InvariantCulture));
}
private static bool HasValidCheckDigit(string isbn13)
{
int sum = 0;
for (int i = 0; i < 12; i++)
{
sum += (isbn13[i] - '0') * (i % 2 == 0 ? 1 : 3);
}
return (10 - sum % 10) % 10 == isbn13[12] - '0';
}
}
$ dotnet test
Test run summary: Passed!
total: 22
failed: 0
succeeded: 22
skipped: 0
$ cd web/bookstore-web && npm test
Test Files 3 passed (3)
Tests 5 passed (5)
Green tests are not enough on their own; you also need to see that they were red before the change. We reverted the template change and ran Vitest again:
$ git stash push src/app/catalog/book-detail-page.html
$ npx ng test --no-watch
× shows the ISBN-10 next to the ISBN-13 22ms
× hides the ISBN-10 when the book has none 4ms
Tests 2 failed | 3 passed (5)
Both tests are red, but the second one for a different reason: the old template has no data-testid="isbn", so the test measures whether the element exists, not whether the ISBN-10 is hidden. That is the kind of finding you write as a review comment. Two more notes: adding the isbn10 field to the API is a contract change, and the BookDetails record of the MCP server from the sixth post does not know the field; an agent that does not know there is another client in the repository will not notice.
Now back to Guess First. GitHub's documentation is explicit: by default, GitHub Actions workflows do not run automatically when Copilot pushes to a pull request. Until you press Approve and run workflows in the merge box, CI has not run at all; no red check may mean the tests never ran, not that they passed. The agent running tests in its own environment is a separate thing, and you only see that output in the session log. Letting workflows run without approval can be turned on in the repository settings, but that also means a workflow change written by the agent runs without approval.
Agent Merge and Rulesets
Agent Merge is turned on with Watch and fix in the pull request's merge box. It watches the blockers you choose: failed GitHub Actions workflows, merge conflicts or both. When one appears, Copilot starts fixing it in a GitHub Actions-based environment and writes to the pull request timeline why it started; it uses the same copilot-setup-steps.yml, instructions and skills. Agent Merge does not turn on auto-merge and does not replace review. The AI Credits it uses are charged to the person who enabled Agent Merge for that pull request, not necessarily its author; it does not work on pull requests from forks or on stacked pull requests.
Rulesets and branch protection rules can prevent Copilot from creating or updating pull requests; repository administrators can define a bypass for Copilot. Think before you open a bypass as a convenience: the rule we want on Bookstore's main is that the agent's pull request does not merge without human approval and passing CI either. We covered the basics of rulesets in the Git & GitHub series; requiring Copilot code review with a ruleset is the subject of the next post.
In Production
Do not put production secrets in the agent's environment. Agents secrets appear as environment variables in every command the agent runs; an issue or dependency with a prompt injection can read the value. Generate the values tests need at run time, like the SQL password in Bookstore.
Keep the permissions block of the setup steps narrow. The example in the documentation also gives only contents: read; Copilot gets its own token for its operations.
Account for the 59 minutes. In Bookstore the setup steps take as long as the package installs and SQL Server's start-up; a longer setup and a slow test suite leave the agent very little time to write code. Larger runners are available if needed.
The cost has two parts: the agent's model use comes out of AI Credits, the environment's run time out of the account's compute allowance. Agent Merge watching a pull request for hours and running again on every broken workflow increases both.
Common Pitfalls
Reviewing the agent's pull request less carefully than a human's. Read the diff with the same checklist; the one from the third post of the Code Review series applies here too.
Treating no red check as green. CI may not run until you press Approve and run workflows.
Adding information to the issue after assignment. Copilot sees the text at the moment of assignment; later information should go to the pull request with @copilot.
Using a job-level env or a timeout-minutes above 59 in the setup steps. Copilot silently ignores both, and actionlint does not catch them.
Opening secrets to the agent environment more widely than needed. Opening an organization-level Agents secret with "All repositories" gives it to every repository's agent.
A Small Experiment
★☆☆ Write a copilot-setup-steps.yml for your own repository, run it through actionlint and change the job name on purpose. Does actionlint say anything?
★★☆ Rewrite the second Vitest test in Bookstore so that it stays meaningful against the old template: first assert that the ISBN line exists, then that the ISBN-10 is hidden. Revert the template change and see which test is red for which reason.
★★★ Write an issue that adds the ISBN-10 to the get_book tool of the MCP server from the sixth post, including acceptance criteria and a list of files that must not change. Compare it with the issue in this post: what is left for the agent to guess?
We reviewed the agent's pull request ourselves. In the next post we hand the review itself to Copilot: automatic code review on pull requests, requiring it with a ruleset, review instructions, and what Copilot catches and what it misses.