<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Azure DevOps on Microsoft DevOps readiness starts here...</title><link>https://www.pdtit.be/tags/azure-devops/</link><description>Recent content in Azure DevOps on Microsoft DevOps readiness starts here...</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sat, 08 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.pdtit.be/tags/azure-devops/index.xml" rel="self" type="application/rss+xml"/><item><title>Running GitHub Copilot CLI in Azure DevOps Pipelines</title><link>https://www.pdtit.be/post/running-github-copilot-cli-in-azure-devops-pipelines/</link><pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.pdtit.be/post/running-github-copilot-cli-in-azure-devops-pipelines/</guid><description>&lt;p&gt;If you&amp;rsquo;ve been following me for a while, you may have seen my earlier post about &lt;a class="link" href="https://www.pdtit.be/post/github-agentic-workflows-hits-public-preview-and-the-end-of-the-pat/" &gt;GitHub Agentic Workflows&lt;/a&gt;. That got me wondering: can I do something similar from an Azure DevOps pipeline?&lt;/p&gt;
&lt;p&gt;The short answer is &lt;strong&gt;yes&lt;/strong&gt;, although the plumbing is different. GitHub Agentic Workflows compiles a Markdown definition into a guarded GitHub Actions workflow. Azure DevOps does not have that same compiler. Instead, an Azure Pipeline can install GitHub Copilot CLI, authenticate it non-interactively, and run a prompt using the CLI&amp;rsquo;s programmatic mode.&lt;/p&gt;
&lt;p&gt;I will start with a read-only commit review, then take it one step further with a more developer-oriented scenario: diagnosing a failed .NET test run. In both cases Copilot publishes a Markdown artifact. It will not edit code, push a branch, or quietly decide whether production is having a good day (that last one still belongs to us).&lt;/p&gt;
&lt;h2 id="what-we-are-building"&gt;What We Are Building
&lt;/h2&gt;&lt;p&gt;I split the walkthrough into two standalone pipelines. They use the same Copilot credential and security boundaries, but you can run either one without first completing the other:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Commit review:&lt;/strong&gt; the smaller starting point for readers who are new to Azure Pipelines or Copilot CLI. A successful run finishes green and publishes &lt;code&gt;review.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed-build investigation:&lt;/strong&gt; the developer-focused scenario. It runs the .NET tests, asks Copilot to explain a failure, publishes the evidence and report, then deliberately finishes red because the test still failed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first flow is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Azure Pipelines starts a fresh Ubuntu agent.&lt;/li&gt;
&lt;li&gt;The pipeline checks out enough Git history to compare the current commit.&lt;/li&gt;
&lt;li&gt;It installs Node.js 22 and GitHub Copilot CLI.&lt;/li&gt;
&lt;li&gt;Copilot reviews the current change using a prompt and a restricted set of tools.&lt;/li&gt;
&lt;li&gt;The response is stored as &lt;code&gt;review.md&lt;/code&gt; and published as a pipeline artifact.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Microsoft-hosted agents are a good fit for a first test. Every job receives a fresh VM, and that VM is discarded after the job. If you use a self-hosted agent instead, the YAML is nearly identical, but you are responsible for cleaning its workspace, credentials, and Copilot state between runs.&lt;/p&gt;
&lt;p&gt;&lt;img src="../images/2026-08-07_15-31-36.png"
 
 
 loading="lazy"
 
 alt="Azure Pipeline job running the Copilot review"
 
 
 
 &gt;&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites
&lt;/h2&gt;&lt;p&gt;Before touching the pipeline, you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An active &lt;a class="link" href="https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli" target="_blank" rel="noopener"
 &gt;GitHub Copilot subscription&lt;/a&gt;. If Copilot is assigned by an organization or enterprise, its Copilot CLI policy must also allow access.&lt;/li&gt;
&lt;li&gt;An Azure DevOps project with a GitHub connection (we follow the Microsoft guidelines to use repos from GitHub now - although you could clone my sample repo from the link more below into the ADO repos as well) and permission to create or edit a YAML pipeline.&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://cli.github.com/" target="_blank" rel="noopener"
 &gt;GitHub CLI&lt;/a&gt; on a trusted workstation so you can create the OAuth token used in this walkthrough.&lt;/li&gt;
&lt;li&gt;A pipeline agent that can reach GitHub and npm over HTTPS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is one detail worth calling out: &lt;code&gt;System.AccessToken&lt;/code&gt; is an Azure DevOps job token. It can authenticate to Azure DevOps resources, but it cannot authenticate GitHub Copilot CLI. These are two different identity systems.&lt;/p&gt;
&lt;h2 id="step-1-get-the-github-token"&gt;Step 1: Get the GitHub Token
&lt;/h2&gt;&lt;p&gt;For a headless environment, GitHub recommends authenticating Copilot CLI through an environment variable. Copilot CLI supports an OAuth token beginning with &lt;code&gt;gho_&lt;/code&gt;, a user-owned fine-grained PAT beginning with &lt;code&gt;github_pat_&lt;/code&gt; and the &lt;strong&gt;Copilot Requests&lt;/strong&gt; account permission, or a GitHub App user-to-server token beginning with &lt;code&gt;ghu_&lt;/code&gt;. A classic &lt;code&gt;ghp_&lt;/code&gt; token is not supported.&lt;/p&gt;
&lt;p&gt;I used the OAuth route for the working pipeline. On a trusted workstation, sign in interactively with the GitHub account that owns the Copilot seat:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gh&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-hostname&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;com&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-web&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gh&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-hostname&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Then copy the stored OAuth token directly to the clipboard without printing it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gh&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-hostname&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;com&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Set-Clipboard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Paste that value into the secret Azure DevOps variable in the next step. Treat it as a user credential, because that is exactly what it is. You can review or revoke the authorization later under &lt;strong&gt;GitHub Settings &amp;gt; Applications &amp;gt; Authorized OAuth Apps&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: This is different from the newer GitHub Agentic Workflows experience&lt;/strong&gt;. Those workflows can use their built-in &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; with &lt;code&gt;copilot-requests: write&lt;/code&gt;. An Azure Pipeline does not receive that GitHub Actions token, so today it still needs its own Copilot credential.&lt;/p&gt;
&lt;h2 id="step-2-store-the-token-in-azure-devops"&gt;Step 2: Store the Token in Azure DevOps
&lt;/h2&gt;&lt;p&gt;In Azure DevOps, open the pipeline, select &lt;strong&gt;Edit&lt;/strong&gt;, then &lt;strong&gt;Variables&lt;/strong&gt;. Add a variable named &lt;code&gt;copilotGithubToken&lt;/code&gt;, paste the OAuth token, and select &lt;strong&gt;Keep this value secret&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Do not place the token in &lt;code&gt;azure-pipelines.yml&lt;/code&gt;. Also do not pass it as a command-line argument or print it for troubleshooting. Azure Pipelines masks known secret values, but masking is not a reason to get adventurous with credentials.&lt;/p&gt;
&lt;p&gt;The YAML below maps the secret directly to &lt;code&gt;COPILOT_GITHUB_TOKEN&lt;/code&gt; only for the task that needs it. Copilot CLI checks that variable before &lt;code&gt;GH_TOKEN&lt;/code&gt; and &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, which also avoids accidentally picking up another GitHub credential from the agent.&lt;/p&gt;
&lt;p&gt;&lt;img src="../images/2026-08-07_15-40-25.png"
 
 
 loading="lazy"
 
 alt="Secret variable configured in Azure DevOps"
 
 
 
 &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: For this quick scenario, you have to define this variable for each pipeline. A more production-ready option is using the variable library to make variables avaialble across multiple pipelines&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="companion-github-repository"&gt;Companion GitHub Repository
&lt;/h2&gt;&lt;p&gt;Everything used in the two scenarios lives in the public &lt;a class="link" href="https://github.com/petender/azure-devops-copilot-cli-failed-build" target="_blank" rel="noopener"
 &gt;Azure DevOps Copilot CLI failed-build sample&lt;/a&gt;. You can inspect or fork it before creating anything in Azure DevOps.&lt;/p&gt;
&lt;p&gt;The repository contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;azure-pipelines-commit-review.yml&lt;/code&gt;: the standalone introductory pipeline. It installs Copilot CLI, reviews the checked-out commit with read-only tools, and publishes &lt;code&gt;review.md&lt;/code&gt;. This is the best place to start if either Azure Pipelines or Copilot CLI is new to you.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;azure-pipelines-failed-build.yml&lt;/code&gt;: the standalone advanced pipeline. It runs the .NET tests, preserves the TRX and console output, asks Copilot to investigate the failure, publishes &lt;code&gt;root-cause-analysis.md&lt;/code&gt;, and restores the original failed test result.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/Checkout.Domain&lt;/code&gt;: the small .NET 8 library used by the advanced scenario.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/Checkout.Domain.Tests&lt;/code&gt;: the xUnit tests, including the intentionally failing 10% discount case.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The application defect is intentionally left in place so the second pipeline has something real to investigate. The two YAML files are complete pipeline definitions, not fragments that need to be combined.&lt;/p&gt;
&lt;h2 id="step-3-connect-azure-pipelines-directly-to-github"&gt;Step 3: Connect Azure Pipelines Directly to GitHub
&lt;/h2&gt;&lt;p&gt;For this walkthrough I am not importing the repository into Azure Repos. The source code and YAML stay in GitHub, while Azure DevOps provides the pipeline runner, test reporting, logs, and artifacts. That gives us a practical hybrid setup: GitHub remains the source-control and collaboration platform, and Azure Pipelines handles CI.&lt;/p&gt;
&lt;p&gt;Azure DevOps uses a GitHub connection to discover and check out the repository directly. I am using the GitHub connection already configured in my Azure DevOps project; I am not installing or configuring the Azure Pipelines GitHub App as part of this walkthrough.&lt;/p&gt;
&lt;p&gt;To connect the sample repository:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In Azure DevOps, open your project and go to &lt;strong&gt;Pipelines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;New pipeline&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Where is your code?&lt;/strong&gt;, select &lt;strong&gt;GitHub&lt;/strong&gt;. Do not select Azure Repos or import the repository first.&lt;/li&gt;
&lt;li&gt;Select or authorize the GitHub connection that can access your copy of the repository.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;&amp;lt;your_github&amp;gt;/azure-devops-copilot-cli-failed-build&lt;/code&gt; from the repository list.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Existing Azure Pipelines YAML file&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;main&lt;/code&gt; branch and &lt;code&gt;/azure-pipelines-commit-review.yml&lt;/code&gt; for the first scenario.&lt;/li&gt;
&lt;li&gt;Save the pipeline, add the secret &lt;code&gt;copilotGithubToken&lt;/code&gt; variable, and run it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There are two separate GitHub authentication paths in this design, and they solve different problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Azure DevOps &lt;strong&gt;GitHub connection&lt;/strong&gt; lets the pipeline discover and check out the repository and respond to configured repository triggers.&lt;/li&gt;
&lt;li&gt;The secret &lt;code&gt;copilotGithubToken&lt;/code&gt; lets the &lt;code&gt;copilot&lt;/code&gt; process call the GitHub Copilot service during one pipeline task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repository connection does not replace the Copilot token, and the Copilot token is not used to check out the repository. Keeping those responsibilities separate makes the hybrid setup easier to reason about and rotate later.&lt;/p&gt;
&lt;h2 id="step-4-add-the-commit-review-pipeline"&gt;Step 4: Add the Commit-Review Pipeline
&lt;/h2&gt;&lt;p&gt;The companion repository contains this complete scenario as &lt;code&gt;azure-pipelines-commit-review.yml&lt;/code&gt;, so you do not need to combine it with the failed-build tasks later in the post.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;azure-pipelines-commit-review.yml&lt;/code&gt; in the root of the application repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;vmImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-24.04&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;self&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fetchDepth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;persistCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;UseNode@1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Use Node.js 22&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22.&lt;/span&gt;&lt;span class="l"&gt;x&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; npm install --global @github/copilot
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; copilot --version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install GitHub Copilot CLI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; outputDirectory=&amp;#34;$(Build.ArtifactStagingDirectory)/copilot-review&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p &amp;#34;$outputDirectory&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; prompt=$(cat &amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Review the commit currently checked out by this pipeline.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Use git status, git show, and git diff to understand the change, then read
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; only the relevant files. Focus on concrete bugs, security issues, missing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; validation, and tests that should have changed with the implementation.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Return Markdown with these sections:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; # Copilot commit review
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Findings
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Missing tests
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Summary
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Order findings by severity. Include file paths and line numbers when
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; possible. If there are no findings, say so clearly. Do not modify files,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; run builds, access the network, or create commits.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; copilot -p &amp;#34;$prompt&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --silent \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --no-color \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --no-ask-user \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --disable-builtin-mcps \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --available-tools=&amp;#39;view,glob,grep,bash&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --allow-tool=&amp;#39;read,shell(git status),shell(git status:*),shell(git show),shell(git show:*),shell(git diff),shell(git diff:*)&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;gt; &amp;#34;$outputDirectory/review.md&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; test -s &amp;#34;$outputDirectory/review.md&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Review current commit with Copilot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;COPILOT_GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(copilotGithubToken)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;COPILOT_HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Agent.TempDirectory)/copilot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;PublishPipelineArtifact@1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publish Copilot review&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;targetPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Build.ArtifactStagingDirectory)/copilot-review&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;copilot-review&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;publishLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pipeline&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A few lines in that YAML do most of the important work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetchDepth: 2&lt;/code&gt; gives &lt;code&gt;git show&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; enough history for a single-commit comparison. Use &lt;code&gt;fetchDepth: 0&lt;/code&gt; if your prompt must inspect a longer branch history.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;persistCredentials: false&lt;/code&gt; prevents the Azure Repos checkout credential from remaining in Git configuration after checkout.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UseNode@1&lt;/code&gt; selects Node.js 22, which is the minimum version required by the npm installation method.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p &amp;quot;$prompt&amp;quot;&lt;/code&gt; sends one prompt programmatically, waits for the response, and exits. There is no interactive Copilot session left running on the agent.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--silent&lt;/code&gt; leaves only the final agent response on standard output. Redirecting that stream to &lt;code&gt;review.md&lt;/code&gt; therefore gives the artifact a predictable format instead of mixing the answer with CLI status messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-ask-user&lt;/code&gt; prevents an unattended run from pausing when Copilot wants clarification. The prompt must contain enough context and a clear fallback, such as saying explicitly when no findings are found.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--available-tools&lt;/code&gt; controls which tools Copilot can consider, while &lt;code&gt;--allow-tool&lt;/code&gt; controls which of those tools may run without an approval prompt. Here the CLI can inspect files and invoke Bash, but automatic approval is limited to reads and the specified read-only Git commands.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-builtin-mcps&lt;/code&gt; keeps the bundled GitHub MCP server out of this local Azure Repos review.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COPILOT_HOME&lt;/code&gt; points Copilot state at the agent&amp;rsquo;s temporary directory instead of treating a reused home folder as permanent state.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-the-prompt-works-in-the-pipeline"&gt;How the prompt works in the pipeline
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;prompt=$(cat &amp;lt;&amp;lt;'EOF' ... EOF)&lt;/code&gt; block creates one multiline Bash variable. Using a here-document keeps the instructions readable in YAML (and saves me from escaping every quote, which is always a pleasant little trap). The quoted &lt;code&gt;EOF&lt;/code&gt; delimiter also tells Bash to treat the prompt body literally instead of expanding shell variables or command substitutions inside it.&lt;/p&gt;
&lt;p&gt;The prompt has four jobs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Define the task:&lt;/strong&gt; review the commit checked out by this pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tell Copilot where to gather evidence:&lt;/strong&gt; use the local Git history and read only relevant files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set priorities and boundaries:&lt;/strong&gt; focus on concrete defects and missing tests, while avoiding edits, builds, network access, and commits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Define the output contract:&lt;/strong&gt; return Markdown with stable headings, severity ordering, file references, and an explicit no-findings result.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last part matters in automation. Copilot&amp;rsquo;s response is still natural language, but a consistent Markdown shape makes the artifact much easier for a developer to scan. I am not parsing the headings to decide whether the build passes; they are a presentation contract, not a deterministic quality gate.&lt;/p&gt;
&lt;p&gt;The prompt itself does not contain the repository contents. Copilot starts in the checked-out working directory and uses the permitted tools to gather only the context it needs. For this review it can inspect &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git show&lt;/code&gt;, and &lt;code&gt;git diff&lt;/code&gt;, then open relevant source or test files. This is better than pasting an entire repository into one enormous prompt, and it keeps the investigation tied to the exact commit Azure Pipelines checked out.&lt;/p&gt;
&lt;p&gt;There are two layers of control here, and they are easy to mix up. &lt;code&gt;--available-tools='view,glob,grep,bash'&lt;/code&gt; exposes a small toolset to the agent. &lt;code&gt;--allow-tool=...&lt;/code&gt; then pre-approves only read operations and the named Git command patterns, because nobody is present to click an approval button. Bash being available does &lt;strong&gt;not&lt;/strong&gt; mean every shell command is approved. A request to run a build, delete a file, call the network, or create a commit falls outside this allowlist and cannot silently proceed.&lt;/p&gt;
&lt;p&gt;Finally, the Azure DevOps secret is mapped to &lt;code&gt;COPILOT_GITHUB_TOKEN&lt;/code&gt; only on this task. Copilot authenticates, gathers evidence through the restricted tools, writes its final answer to standard output, and the shell redirects that answer to &lt;code&gt;review.md&lt;/code&gt;. The &lt;code&gt;test -s&lt;/code&gt; check fails the task if no report was produced, after which &lt;code&gt;PublishPipelineArtifact@1&lt;/code&gt; makes the file available from the run summary.&lt;/p&gt;
&lt;p&gt;I deliberately did &lt;strong&gt;not&lt;/strong&gt; use &lt;code&gt;--allow-all-tools&lt;/code&gt;. In a non-interactive pipeline that flag lets Copilot execute any available command without approval. A disposable VM limits persistence, but it does not make unrestricted commands, source code, and credentials harmless.&lt;/p&gt;
&lt;p&gt;For a production pipeline, I would also pin &lt;code&gt;@github/copilot&lt;/code&gt; to a version already tested by the team instead of installing the latest release on every run. Start with the current package while proving the flow, then trade a little convenience for repeatability.&lt;/p&gt;
&lt;h2 id="step-5-run-and-read-the-result"&gt;Step 5: Run and Read the Result
&lt;/h2&gt;&lt;p&gt;The pipeline was created against &lt;code&gt;/azure-pipelines-commit-review.yml&lt;/code&gt; in the previous step. Open it in Azure DevOps, select &lt;strong&gt;Run pipeline&lt;/strong&gt;, keep the &lt;code&gt;main&lt;/code&gt; branch selected, and start the run.&lt;/p&gt;
&lt;p&gt;The expected outcome for this scenario is a &lt;strong&gt;green pipeline&lt;/strong&gt;. That means Copilot authenticated successfully, inspected the checked-out commit through the approved read-only tools, returned a response, and Azure DevOps published it. A green result does not mean Copilot found no issues; it means the review process itself completed successfully.&lt;/p&gt;
&lt;p&gt;Open the completed run and look for the &lt;strong&gt;copilot-review&lt;/strong&gt; artifact.&lt;/p&gt;
&lt;p&gt;&lt;img src="../images/2026-08-08_08-36-07.png"
 
 
 loading="lazy"
 
 alt="Published Copilot review artifact"
 
 
 
 &gt;&lt;/p&gt;
&lt;p&gt;Download or browse &lt;code&gt;review.md&lt;/code&gt;, then check its three sections:&lt;/p&gt;
&lt;p&gt;&lt;img src="../images/2026-08-08_08-38-51.png"
 
 
 loading="lazy"
 
 alt="Published Copilot review content"
 
 
 
 &gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Findings&lt;/strong&gt; contains concrete issues ordered by severity, or an explicit statement that none were found.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing tests&lt;/strong&gt; calls out test coverage that should change with the implementation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summary&lt;/strong&gt; gives the short version for a developer reviewing the run.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because the Azure Pipeline connects directly to the GitHub repository, pushes to &lt;code&gt;main&lt;/code&gt; can trigger the pipeline without copying the code into Azure Repos. The exact status experience shown in GitHub depends on the connection type configured in Azure DevOps. Keep the report advisory: requiring the pipeline to complete is different from trying to parse Copilot&amp;rsquo;s natural-language findings as a deterministic pass or fail decision.&lt;/p&gt;
&lt;p&gt;I would keep this first version advisory. A successful CLI exit means the request completed; it does not mean the code has no bugs. Similarly, parsing natural-language findings to fail the build creates a rather shaky quality gate. Publish the report, let a developer review it, and keep deterministic tests, linters, and security scanners as the actual gates.&lt;/p&gt;
&lt;h2 id="advanced-scenario-investigating-a-failed-build"&gt;Advanced Scenario: Investigating a Failed Build
&lt;/h2&gt;&lt;p&gt;The commit review proves the integration works, but failed-build investigation is where this becomes much more useful to a developer. Instead of dropping a thousand lines of test output into the team chat and asking who has seen this before, the pipeline can give Copilot the log, the current diff, and the relevant source files.&lt;/p&gt;
&lt;p&gt;This is a second, independent pipeline, not a set of tasks to paste underneath the commit-review pipeline.&lt;/p&gt;
&lt;p&gt;The complete file is &lt;code&gt;azure-pipelines-failed-build.yml&lt;/code&gt; in the &lt;a class="link" href="https://github.com/petender/azure-devops-copilot-cli-failed-build" target="_blank" rel="noopener"
 &gt;sample repository&lt;/a&gt;. Create another Azure Pipeline, choose &lt;strong&gt;Existing Azure Pipelines YAML file&lt;/strong&gt;, select &lt;code&gt;/azure-pipelines-failed-build.yml&lt;/code&gt;, and add the same &lt;code&gt;copilotGithubToken&lt;/code&gt; secret. (Remember, a secured variable group is also an option if you do not want to configure the secret separately on both pipelines).&lt;/p&gt;
&lt;p&gt;Both YAML files trigger on pushes to &lt;code&gt;main&lt;/code&gt;. If you configure both Azure Pipelines, a push starts both scenarios independently. While learning, you can disable one trigger or queue the pipelines manually so the green commit review and intentionally red failed-build run are easier to compare.&lt;/p&gt;
&lt;p&gt;The important bit is that Copilot does &lt;strong&gt;not&lt;/strong&gt; replace the test runner. The test still decides whether the pipeline passes or fails. Copilot only explains what probably happened and points the developer toward the likely fix.&lt;/p&gt;
&lt;p&gt;That distinction is also the benefit. Azure DevOps and xUnit already tell me &lt;strong&gt;which&lt;/strong&gt; test failed: expected &lt;code&gt;90.00&lt;/code&gt;, actual &lt;code&gt;100.00&lt;/code&gt;. Copilot CLI takes the next step. It reads that log, inspects the implementation and tests, checks the current Git diff, and returns one focused report explaining &lt;strong&gt;why&lt;/strong&gt; the values differ. In the validated run it found the integer division, identified the exact source line, explained why the 0% and 100% boundary tests still pass, and suggested decimal division with a high confidence rating.&lt;/p&gt;
&lt;p&gt;Copilot CLI is being used here as a read-only investigation agent, not as another test runner. The &lt;code&gt;-p&lt;/code&gt; option sends one unattended prompt, &lt;code&gt;--add-dir&lt;/code&gt; grants access to the captured diagnostics, and the tool allowlist lets Copilot read files plus run only &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git show&lt;/code&gt;, and &lt;code&gt;git diff&lt;/code&gt;. Standard output becomes &lt;code&gt;root-cause-analysis.md&lt;/code&gt;; standard error is retained separately for authentication or CLI failures. No source file is changed.&lt;/p&gt;
&lt;p&gt;For the companion sample, I created a small .NET 8 checkout library with an intentional bug in its percentage discount calculation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discountRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;discountRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Because both values in the division are integers, a 10 percent discount produces a rate of &lt;code&gt;0&lt;/code&gt;, not &lt;code&gt;0.1&lt;/code&gt;. One xUnit test expects a subtotal of &lt;code&gt;100.00&lt;/code&gt; to become &lt;code&gt;90.00&lt;/code&gt;, and receives &lt;code&gt;100.00&lt;/code&gt; instead. Small bug, realistic failure, and enough context for Copilot to connect the dots. The complete application and working pipeline are available in my &lt;a class="link" href="https://github.com/petender/azure-devops-copilot-cli-failed-build" target="_blank" rel="noopener"
 &gt;Azure DevOps Copilot CLI failed-build sample&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="what-to-expect-from-the-pipeline"&gt;What to expect from the pipeline
&lt;/h3&gt;&lt;p&gt;This sample contains an intentional defect, so a completely green run would actually be wrong. The expected result is an &lt;strong&gt;orange test task followed by a red pipeline&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Pipeline task&lt;/th&gt;
 &lt;th&gt;Expected status&lt;/th&gt;
 &lt;th&gt;Why&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Run tests and preserve failure&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Succeeded with issues&lt;/strong&gt; (orange)&lt;/td&gt;
 &lt;td&gt;The percentage test fails, while &lt;code&gt;continueOnError&lt;/code&gt; allows diagnostics to continue.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Publish test results&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt; (green)&lt;/td&gt;
 &lt;td&gt;The generated TRX is published to the Azure DevOps Tests tab.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Publish raw test diagnostics&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt; (green)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;test-output.log&lt;/code&gt; and the TRX are retained as an artifact.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Analyze failed tests with Copilot&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt; (green)&lt;/td&gt;
 &lt;td&gt;Copilot reads the failure context and writes the Markdown diagnosis.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Publish Copilot failure analysis&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Succeeded&lt;/strong&gt; (green)&lt;/td&gt;
 &lt;td&gt;The diagnosis is made available as a pipeline artifact.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Preserve original test result&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Failed&lt;/strong&gt; (red)&lt;/td&gt;
 &lt;td&gt;The saved &lt;code&gt;dotnet test&lt;/code&gt; exit code is deliberately restored.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So yes, the final job is red on purpose. The orange task keeps the investigation moving, and the final red task makes sure Copilot cannot accidentally turn a genuinely failing build green. Once the implementation is fixed and the tests pass, the Copilot analysis tasks are skipped and the pipeline finishes green.&lt;/p&gt;
&lt;p&gt;When reviewing the intentionally red run, do not stop at the final &lt;code&gt;Bash exited with code '1'&lt;/code&gt; message. Check the outputs that the pipeline preserved:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the Azure DevOps &lt;strong&gt;Tests&lt;/strong&gt; tab for the failed assertion.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;test-diagnostics&lt;/strong&gt; artifact for &lt;code&gt;test-output.log&lt;/code&gt; and &lt;code&gt;checkout-tests.trx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;copilot-failure-analysis&lt;/strong&gt; artifact for &lt;code&gt;root-cause-analysis.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once the application defect is fixed, &lt;code&gt;dotnet test&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt;, the Copilot failure-analysis tasks are skipped, the final task also returns &lt;code&gt;0&lt;/code&gt;, and this pipeline becomes green. In other words, red means the application test is still failing; it does not mean the Copilot integration failed.&lt;/p&gt;
&lt;h3 id="how-the-failure-flow-works"&gt;How the failure flow works
&lt;/h3&gt;&lt;p&gt;There is a small control-flow challenge here. If the &lt;code&gt;dotnet test&lt;/code&gt; task immediately fails, later tasks using the default &lt;code&gt;succeeded()&lt;/code&gt; condition will not run. But if I permanently swallow the exit code, the pipeline turns green and hides the real failure. Neither option is useful.&lt;/p&gt;
&lt;p&gt;The pipeline therefore does this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;dotnet test&lt;/code&gt; and send its complete output to both the console and &lt;code&gt;test-output.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save the real exit code in an Azure Pipeline variable.&lt;/li&gt;
&lt;li&gt;Let the task finish with &lt;strong&gt;Succeeded with issues&lt;/strong&gt; so the analysis can still run.&lt;/li&gt;
&lt;li&gt;Ask Copilot to investigate only when the saved exit code is nonzero.&lt;/li&gt;
&lt;li&gt;Publish the raw test log and Copilot analysis as artifacts.&lt;/li&gt;
&lt;li&gt;Exit with the original test code in a final task, returning the pipeline to its correct failed state.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="inside-the-standalone-failed-build-pipeline"&gt;Inside the standalone failed-build pipeline
&lt;/h3&gt;&lt;p&gt;The standalone file repeats the checkout, Node.js, Copilot installation, and secret mapping from the first scenario. That duplication is intentional: this pipeline does not depend on the commit-review YAML. It also installs .NET 8 and initializes a variable that will carry the test exit code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;testExitCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;UseDotNet@2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Use .NET 8 SDK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;packageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sdk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;8.x&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;UseNode@1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Use Node.js 22&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;22.x&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Keep the Copilot CLI installation step from the first example here.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="capture-the-failed-test-without-losing-it"&gt;Capture the failed test without losing it
&lt;/h3&gt;&lt;p&gt;This Bash task uses &lt;code&gt;tee&lt;/code&gt; so the normal test output remains visible in Azure DevOps while also becoming an input artifact. &lt;code&gt;${PIPESTATUS[0]}&lt;/code&gt; captures the exit code from &lt;code&gt;dotnet test&lt;/code&gt;, rather than the exit code from &lt;code&gt;tee&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; set -uo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; logDirectory=&amp;#34;$(Build.ArtifactStagingDirectory)/test-diagnostics&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p &amp;#34;$logDirectory&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; dotnet test tests/Checkout.Domain.Tests/Checkout.Domain.Tests.csproj \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --configuration Release \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --verbosity normal \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --logger &amp;#34;trx;LogFileName=checkout-tests.trx&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --results-directory &amp;#34;$logDirectory&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; 2&amp;gt;&amp;amp;1 | tee &amp;#34;$logDirectory/test-output.log&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; testExitCode=${PIPESTATUS[0]}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;##vso[task.setvariable variable=testExitCode]$testExitCode&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;dotnet test exit code: $testExitCode&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;Files generated in $logDirectory:&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; find &amp;#34;$logDirectory&amp;#34; -maxdepth 2 -type f -print
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exit &amp;#34;$testExitCode&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Run tests and preserve failure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;continueOnError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;PublishTestResults@2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publish test results&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;always()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;testResultsFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;VSTest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;searchFolder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Build.ArtifactStagingDirectory)/test-diagnostics&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;testResultsFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;**/*.trx&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;failTaskOnFailedTests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;failTaskOnMissingResultsFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;failTaskOnFailureToPublishResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;PublishPipelineArtifact@1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publish raw test diagnostics&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;and(succeededOrFailed(), ne(variables[&amp;#39;testExitCode&amp;#39;], &amp;#39;0&amp;#39;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;targetPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Build.ArtifactStagingDirectory)/test-diagnostics&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;test-diagnostics&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;publishLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pipeline&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;continueOnError: true&lt;/code&gt; lets the task return the real &lt;code&gt;dotnet test&lt;/code&gt; status without stopping the job. Azure DevOps displays it as &lt;strong&gt;Succeeded with issues&lt;/strong&gt;, which is more honest than forcing the script to exit with &lt;code&gt;0&lt;/code&gt;. &lt;code&gt;failTaskOnFailedTests: false&lt;/code&gt; is intentional because the final task will restore the original result. The test report still appears in Azure DevOps, including the failed assertion. A failed assertion should still create &lt;code&gt;checkout-tests.trx&lt;/code&gt;; if no TRX exists, &lt;code&gt;failTaskOnMissingResultsFile: true&lt;/code&gt; exposes that the command failed before the tests ran. The raw console log is published either way.&lt;/p&gt;
&lt;h3 id="give-copilot-the-failure-context"&gt;Give Copilot the failure context
&lt;/h3&gt;&lt;p&gt;The Copilot task runs only when &lt;code&gt;testExitCode&lt;/code&gt; is not zero. The log is outside the checked-out repository, so &lt;code&gt;--add-dir&lt;/code&gt; explicitly grants access to that one diagnostics directory. The tool list remains read-only, apart from the three Git inspection commands.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; outputDirectory=&amp;#34;$(Build.ArtifactStagingDirectory)/copilot-failure-analysis&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p &amp;#34;$outputDirectory&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; prompt=$(cat &amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Read and investigate the failed .NET test run at
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; $(Build.ArtifactStagingDirectory)/test-diagnostics/test-output.log.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Correlate the failure with the current Git diff and the relevant source
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; and test files. Do not modify files or rerun the tests.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Return Markdown with these sections:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; # Failed build analysis
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Most likely root cause
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Evidence
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Suggested fix
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ## Alternative explanations
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Quote the failing test name and expected versus actual values. Include
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; file paths and line numbers for the likely defect. Keep the suggested fix
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; concise, and state your confidence level.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; copilot -p &amp;#34;$prompt&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --silent \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --no-color \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --no-ask-user \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --add-dir=&amp;#34;$(Build.ArtifactStagingDirectory)/test-diagnostics&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --disable-builtin-mcps \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --available-tools=&amp;#39;view,glob,grep,bash&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --allow-tool=&amp;#39;read,shell(git status),shell(git status:*),shell(git show),shell(git show:*),shell(git diff),shell(git diff:*)&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; 2&amp;gt; &amp;gt;(tee &amp;#34;$outputDirectory/copilot-stderr.log&amp;#34; &amp;gt;&amp;amp;2) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;gt; &amp;#34;$outputDirectory/root-cause-analysis.md&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; test -s &amp;#34;$outputDirectory/root-cause-analysis.md&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Analyze failed tests with Copilot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;and(succeededOrFailed(), ne(variables[&amp;#39;testExitCode&amp;#39;], &amp;#39;0&amp;#39;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;COPILOT_GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(copilotGithubToken)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;COPILOT_HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Agent.TempDirectory)/copilot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In the validated run, the report did more than repeat the assertion. It identified &lt;code&gt;percentage / 100&lt;/code&gt; as integer division, pointed to the exact source line, and explained that the other tests passed by coincidence because &lt;code&gt;0 / 100&lt;/code&gt; and &lt;code&gt;100 / 100&lt;/code&gt; happen to produce the expected boundary values. It then suggested:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discountRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That correlation is where Copilot CLI earns its place in this pipeline. The test runner remains the source of truth, while Copilot turns the failure, source, tests, and recent change into a useful first investigation for the developer.&lt;/p&gt;
&lt;h3 id="publish-the-report-and-restore-the-failure"&gt;Publish the report and restore the failure
&lt;/h3&gt;&lt;p&gt;The last two tasks publish the report, then fail with the original test exit code. &lt;code&gt;condition: always()&lt;/code&gt; on the final task matters: the pipeline must remain failed even if Copilot itself has a problem.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;PublishPipelineArtifact@1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publish Copilot failure analysis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;and(succeededOrFailed(), ne(variables[&amp;#39;testExitCode&amp;#39;], &amp;#39;0&amp;#39;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;targetPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(Build.ArtifactStagingDirectory)/copilot-failure-analysis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;copilot-failure-analysis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;publishLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pipeline&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;Original dotnet test exit code: $TEST_EXIT_CODE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; if [ &amp;#34;$TEST_EXIT_CODE&amp;#34; -ne 0 ]; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;##vso[task.logissue type=error]The .NET tests failed with exit code $TEST_EXIT_CODE. Review the Publish test results tab and the test-diagnostics and copilot-failure-analysis artifacts.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exit &amp;#34;$TEST_EXIT_CODE&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Preserve original test result&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;always()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;TEST_EXIT_CODE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;$(testExitCode)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The final result is exactly what I want: a red pipeline, the normal Azure DevOps test report, the raw log, and a focused Markdown diagnosis beside it. In this sample, &lt;code&gt;Bash exited with code '1'&lt;/code&gt; in &lt;strong&gt;Preserve original test result&lt;/strong&gt; is expected because the percentage test fails intentionally. The final task now explains that it is restoring the &lt;code&gt;dotnet test&lt;/code&gt; result and points to the useful artifacts. The developer still reviews the evidence and owns the fix. Copilot just makes the first five minutes of failure investigation less repetitive (which is usually where I spend ten minutes finding the right log anyway).&lt;/p&gt;
&lt;p&gt;The GitHub Copilot CLI prompt-based analysis of the Unit tests, then is available as a markdown file inside the generated pipeline artifact:&lt;/p&gt;
&lt;p&gt;&lt;img src="../images/2026-08-07_15-33-50.png"
 
 
 loading="lazy"
 
 alt="Azure Pipeline markdown artifact"
 
 
 
 &gt;&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Running GitHub Copilot CLI from Azure DevOps is technically feasible, and the basic setup is smaller than I expected: Node.js 22, the npm package, one secret variable, and a programmatic prompt. Once that foundation is in place, feeding it a failed test log is only a few additional pipeline tasks.&lt;/p&gt;
&lt;p&gt;The hard part is not installing the CLI. It is deciding what the agent may read, which commands it may run, how its credential is managed, and what authority its answer should have. Starting with a read-only review artifact gives developers something useful without handing an unattended model the keys to the repository.&lt;/p&gt;
&lt;p&gt;This is conceptually close to my GitHub Agentic Workflows example, but it is not identical. GitHub&amp;rsquo;s framework supplies a compiler, safe outputs, and a built-in workflow identity. In Azure DevOps, we assemble those boundaries ourselves in YAML. That is perfectly workable, as long as we are honest about where the guardrails come from.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://www.buymeacoffee.com/pdtit" target="_blank" rel="noopener"
 &gt;&lt;img src="../images/buy_me_a_coffee.png"
 
 
 loading="lazy"
 
 alt="BuyMeACoffee"
 
 
 
 &gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cheers!!&lt;/p&gt;
&lt;p&gt;/Peter&lt;/p&gt;</description></item></channel></rss>