Skip to content

Honor preinstalled CLI path in .NET MSBuild targets (#921)#1318

Merged
stephentoub merged 2 commits into
mainfrom
stephentoub/scaling-succotash
May 17, 2026
Merged

Honor preinstalled CLI path in .NET MSBuild targets (#921)#1318
stephentoub merged 2 commits into
mainfrom
stephentoub/scaling-succotash

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

Closes #921.

The SDK's MSBuild targets download the Copilot CLI from the npm registry via the DownloadFile task, which doesn't support credentials, so consumers behind authenticated npm mirrors fail at build time. The existing CopilotSkipCliDownload=true escape hatch wasn't a workaround either: it also disabled the copy/register targets, so even if you pre-downloaded the CLI yourself there was no supported way to hand it to the build. On top of that, _CopilotCliBinaryPath was reassigned unconditionally inside every target, so setting it as a global property had no effect.

This change makes the predownloaded-CLI scenario a first-class option without changing default download behavior.

Approach

Add a public CopilotCliBinaryPath property. When set, the targets:

  • skip _DownloadCopilotCli (no network access);
  • still run _CopyCopilotCliToOutput and _RegisterCopilotCliForCopy so the supplied binary is placed at the canonical runtimes/<rid>/native/copilot[.exe] path that Client.GetBundledCliPath searches at runtime;
  • fail the build with an actionable <Error> if the path is missing, naming the offending path.

The internal _CopilotCliBinaryPath / _CopilotCacheDir assignments inside each target are now conditioned on emptiness so a caller-supplied value isn't clobbered.

The <Copy> task switches from DestinationFolder to DestinationFiles="$(_CopilotOutputDir)\$(_CopilotBinary)" so an off-spec source filename (e.g. one a user extracted from their own mirror) is normalized to copilot[.exe] on copy. This is safe for the default download path because the npm tarball already extracts to that canonical name.

Default behavior with no properties set is unchanged.

Tests

Adds dotnet/test/Unit/MSBuildTargetsTests.cs, the first MSBuild-target test in the repo. It imports the real GitHub.Copilot.SDK.targets into a throwaway csproj under the system temp directory (to avoid inheriting dotnet/Directory.Build.props) and shells out to dotnet build in a subprocess, so MSBuild evaluation, conditions, and the <Copy> task all run for real. Five [Fact] scenarios cover:

  1. CopilotCliBinaryPath alone: download skipped, binary copied to the canonical runtimes path with matching contents.
  2. Off-spec source filename: normalized to copilot[.exe] on copy.
  3. CopilotSkipCliDownload=true alone: build succeeds, no binary produced.
  4. CopilotCliBinaryPath + CopilotSkipCliDownload=true: still copies to output.
  5. Non-existent CopilotCliBinaryPath: build fails with an error message containing "Copilot CLI binary not found" and the offending path.

These are scoped as regression tests for #921 and intentionally don't exercise the network download path; the existing SDK build itself continues to provide implicit smoke coverage of that path.

Validation

  • dotnet test test\GitHub.Copilot.SDK.Test.csproj -- all 84 unit tests pass, including the 5 new ones.
  • dotnet format --verify-no-changes -- clean.
  • Default SDK build still produces runtimes/win-x64/native/copilot.exe.
  • Manually validated all four targets-property combinations on Windows.
  • Code-review/fix loop with claude-opus-4.7-xhigh, then a second pass with gpt-5.5 and claude-opus-4.6 covering MSBuild correctness, best-practice conformance, and security -- all three reviewers came back with no findings.

The `CopilotSkipCliDownload=true` escape hatch also gated the copy and
register targets, so consumers behind authenticated npm mirrors had no way
to point the build at a pre-downloaded copilot CLI. `_CopilotCliBinaryPath`
was also unconditionally reassigned inside each target, so even setting it
via a global property had no effect.

Add a public `CopilotCliBinaryPath` property that:
  * suppresses `_DownloadCopilotCli`
  * still runs `_CopyCopilotCliToOutput` and `_RegisterCopilotCliForCopy`
    so the supplied binary is placed at the canonical
    `runtimes/<rid>/native/copilot[.exe]` path that `Client.GetBundledCliPath`
    searches at runtime
  * fails the build with an actionable `<Error>` if the path is missing

Switch the `<Copy>` task from `DestinationFolder` to `DestinationFiles`
so off-spec source filenames are normalized to `copilot[.exe]` on copy.

Add `dotnet/test/Unit/MSBuildTargetsTests.cs` -- the first MSBuild-target
test in the repo -- with 5 integration tests that import the real targets
file into a throwaway csproj and shell out to `dotnet build` to validate
the four scenarios (preinstalled, preinstalled + skip, skip only, missing
path) end-to-end.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub requested a review from a team as a code owner May 17, 2026 02:25
Copilot AI review requested due to automatic review settings May 17, 2026 02:25
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs Fixed
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs Fixed
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs Fixed
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the .NET SDK’s MSBuild targets to support a first-class “preinstalled Copilot CLI” scenario by honoring a user-supplied CLI binary path, avoiding the unauthenticated DownloadFile path that breaks behind authenticated npm mirrors (issue #921).

Changes:

  • Add CopilotCliBinaryPath to let consumers provide a pre-downloaded CLI and automatically skip _DownloadCopilotCli while still copying/registering the binary into runtimes/<rid>/native/.
  • Prevent caller-supplied internal properties (e.g., _CopilotCliBinaryPath / _CopilotCacheDir) from being unconditionally overwritten inside targets.
  • Add new integration-style unit tests that import the real .targets file into a temp project and execute dotnet build to validate key property combinations and error messaging.
Show a summary per file
File Description
dotnet/src/build/GitHub.Copilot.SDK.targets Introduces CopilotCliBinaryPath, adjusts target conditions/property assignment, and normalizes copied output filename to copilot[.exe].
dotnet/test/Unit/MSBuildTargetsTests.cs Adds end-to-end MSBuild-target coverage via subprocess dotnet build against a throwaway imported project.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
- Also drop RuntimeIdentifier from the subprocess env (the comment said
  it but only MSBuildSDKsPath was actually removed), so CI workers
  that set a RID don't pull the build into a different runtimes folder
  than ExpectedOutputBinary() expects.
- Narrow the two best-effort catch clauses (process.Kill on timeout,
  and Directory.Delete in Dispose) to the specific exception types
  those operations actually throw, while keeping the same swallow-and-
  continue semantics.
- Sanitize the caller-supplied fileName in WritePreinstalledBinary
  via Path.GetFileName so a rooted or directory-containing value
  can't silently escape preinstallDir via Path.Combine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
Comment thread dotnet/test/Unit/MSBuildTargetsTests.cs
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR modifies only .NET-specific files:

  • dotnet/src/build/GitHub.Copilot.SDK.targets
  • dotnet/test/Unit/MSBuildTargetsTests.cs

The changes are purely build infrastructure — adding the CopilotCliBinaryPath MSBuild property so users behind authenticated npm mirrors can supply a pre-downloaded CLI binary without network access at build time.

No cross-SDK consistency issues. MSBuild targets are inherently .NET-specific; Node.js, Python, and Go users resolve the CLI binary through runtime environment variables (COPILOT_CLI_PATH) or their own toolchain mechanisms — there's no equivalent concept to bring into parity.

Generated by SDK Consistency Review Agent for issue #1318 · ● 133.8K ·

@stephentoub stephentoub merged commit 25b15be into main May 17, 2026
30 of 31 checks passed
@stephentoub stephentoub deleted the stephentoub/scaling-succotash branch May 17, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C#] MSBuild task to download Copilt CLI does not work with authenticated npm feeds

3 participants