Skip to content

Commit 4253946

Browse files
Bundling (#382)
* Fetch Copilot CLI at build time * Update tests to use bundled CLI * Always get from NPM * CR feedback * Node: use bundled CLI * Bundling for Python * Formatting * Update test_client.py * Python test fix * Consistent node version for CI * Make publish.yml safer - don't actually deploy to package managers if running on a branch * Fix license file
1 parent 06ff2ae commit 4253946

23 files changed

Lines changed: 670 additions & 228 deletions

.github/workflows/dotnet-sdk-tests.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ on:
77
- 'test/**'
88
- 'nodejs/package.json'
99
- '.github/workflows/dotnet-sdk-tests.yml'
10-
- '.github/actions/setup-copilot/**'
1110
- '!**/*.md'
1211
- '!**/LICENSE*'
1312
- '!**/.gitignore'
@@ -39,17 +38,16 @@ jobs:
3938
working-directory: ./dotnet
4039
steps:
4140
- uses: actions/checkout@v6.0.2
42-
- uses: ./.github/actions/setup-copilot
43-
id: setup-copilot
4441
- uses: actions/setup-dotnet@v5
4542
with:
4643
dotnet-version: "8.0.x"
4744
- uses: actions/setup-node@v6
4845
with:
46+
node-version: "24"
4947
cache: "npm"
5048
cache-dependency-path: "./nodejs/package-lock.json"
5149

52-
- name: Install Node.js dependencies (for CLI)
50+
- name: Install Node.js dependencies (for CLI version extraction)
5351
working-directory: ./nodejs
5452
run: npm ci --ignore-scripts
5553

@@ -80,5 +78,4 @@ jobs:
8078
- name: Run .NET SDK tests
8179
env:
8280
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
83-
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
8481
run: dotnet test --no-build -v n

.github/workflows/nodejs-sdk-tests.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ on:
99
- 'nodejs/**'
1010
- 'test/**'
1111
- '.github/workflows/nodejs-sdk-tests.yml'
12-
- '.github/actions/setup-copilot/**'
1312
- '!**/*.md'
1413
- '!**/LICENSE*'
1514
- '!**/.gitignore'
@@ -45,9 +44,7 @@ jobs:
4544
with:
4645
cache: "npm"
4746
cache-dependency-path: "./nodejs/package-lock.json"
48-
node-version: 22
49-
- uses: ./.github/actions/setup-copilot
50-
id: setup-copilot
47+
node-version: 24
5148
- name: Install dependencies
5249
run: npm ci --ignore-scripts
5350

@@ -72,5 +69,4 @@ jobs:
7269
- name: Run Node.js SDK tests
7370
env:
7471
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
75-
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
7672
run: npm test

.github/workflows/publish.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ jobs:
106106
name: nodejs-package
107107
path: nodejs/*.tgz
108108
- name: Publish to npm
109+
if: github.ref == 'refs/heads/main'
109110
run: npm publish --tag ${{ github.event.inputs.dist-tag }} --access public --registry https://registry.npmjs.org
110111

111112
publish-dotnet:
@@ -130,6 +131,7 @@ jobs:
130131
name: dotnet-package
131132
path: dotnet/artifacts/*.nupkg
132133
- name: NuGet login (OIDC)
134+
if: github.ref == 'refs/heads/main'
133135
uses: NuGet/login@v1
134136
id: nuget-login
135137
with:
@@ -139,6 +141,7 @@ jobs:
139141
# are associated with individual maintainers' accounts too.
140142
user: stevesanderson
141143
- name: Publish to NuGet
144+
if: github.ref == 'refs/heads/main'
142145
run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
143146

144147
publish-python:
@@ -153,18 +156,25 @@ jobs:
153156
- uses: actions/setup-python@v6
154157
with:
155158
python-version: "3.12"
159+
- uses: actions/setup-node@v6
160+
with:
161+
node-version: "22.x"
156162
- name: Set up uv
157163
uses: astral-sh/setup-uv@v7
164+
- name: Install Node.js dependencies (for CLI version)
165+
working-directory: ./nodejs
166+
run: npm ci --ignore-scripts
158167
- name: Set version
159168
run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml
160-
- name: Build package
161-
run: uv build
169+
- name: Build platform wheels
170+
run: node scripts/build-wheels.mjs --output-dir dist
162171
- name: Upload artifact
163172
uses: actions/upload-artifact@v6
164173
with:
165174
name: python-package
166175
path: python/dist/*
167176
- name: Publish to PyPI
177+
if: github.ref == 'refs/heads/main'
168178
uses: pypa/gh-action-pypi-publish@release/v1
169179
with:
170180
packages-dir: python/dist/

.github/workflows/python-sdk-tests.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ on:
1010
- 'test/**'
1111
- 'nodejs/package.json'
1212
- '.github/workflows/python-sdk-tests.yml'
13-
- '.github/actions/setup-copilot/**'
1413
- '!**/*.md'
1514
- '!**/LICENSE*'
1615
- '!**/.gitignore'
@@ -42,11 +41,14 @@ jobs:
4241
working-directory: ./python
4342
steps:
4443
- uses: actions/checkout@v6.0.2
45-
- uses: ./.github/actions/setup-copilot
46-
id: setup-copilot
4744
- uses: actions/setup-python@v6
4845
with:
4946
python-version: "3.12"
47+
- uses: actions/setup-node@v6
48+
with:
49+
node-version: "24"
50+
cache: "npm"
51+
cache-dependency-path: "./nodejs/package-lock.json"
5052

5153
- name: Set up uv
5254
uses: astral-sh/setup-uv@v7
@@ -56,6 +58,10 @@ jobs:
5658
- name: Install Python dev dependencies
5759
run: uv sync --locked --all-extras --dev
5860

61+
- name: Install Node.js dependencies (for CLI in tests)
62+
working-directory: ./nodejs
63+
run: npm ci --ignore-scripts
64+
5965
- name: Run ruff format check
6066
run: uv run ruff format --check .
6167

@@ -76,5 +82,4 @@ jobs:
7682
- name: Run Python SDK tests
7783
env:
7884
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
79-
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
8085
run: uv run pytest -v -s

dotnet/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
bin/
33
obj/
44

5+
# Generated build props (contains CLI version)
6+
src/build/GitHub.Copilot.SDK.props
7+
58
# NuGet packages
69
*.nupkg
710
*.snupkg

dotnet/src/Client.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,9 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
873873

874874
private static async Task<(Process Process, int? DetectedLocalhostTcpPort)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
875875
{
876-
var cliPath = options.CliPath ?? "copilot";
876+
// Use explicit path or bundled CLI - no PATH fallback
877+
var cliPath = options.CliPath ?? GetBundledCliPath(out var searchedPath)
878+
?? throw new InvalidOperationException($"Copilot CLI not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit CliPath.");
877879
var args = new List<string>();
878880

879881
if (options.CliArgs != null)
@@ -976,6 +978,14 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
976978
return (cliProcess, detectedLocalhostTcpPort);
977979
}
978980

981+
private static string? GetBundledCliPath(out string searchedPath)
982+
{
983+
var binaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot";
984+
var rid = Path.GetFileName(System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier);
985+
searchedPath = Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", binaryName);
986+
return File.Exists(searchedPath) ? searchedPath : null;
987+
}
988+
979989
private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(string cliPath, IEnumerable<string> args)
980990
{
981991
var isJsFile = cliPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase);
@@ -985,13 +995,6 @@ private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(str
985995
return ("node", new[] { cliPath }.Concat(args));
986996
}
987997

988-
// On Windows with UseShellExecute=false, Process.Start doesn't search PATHEXT,
989-
// so use cmd /c to let the shell resolve the executable
990-
if (OperatingSystem.IsWindows() && !Path.IsPathRooted(cliPath))
991-
{
992-
return ("cmd", new[] { "/c", cliPath }.Concat(args));
993-
}
994-
995998
return (cliPath, args);
996999
}
9971000

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
<Version>0.1.0</Version>
9+
<Description>SDK for programmatic control of GitHub Copilot CLI</Description>
10+
<Authors>GitHub</Authors>
11+
<Company>GitHub</Company>
12+
<Copyright>Copyright (c) Microsoft Corporation. All rights reserved.</Copyright>
13+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14+
<PackageReadmeFile>README.md</PackageReadmeFile>
15+
<RepositoryUrl>https://github.com/github/copilot-sdk</RepositoryUrl>
16+
<PackageTags>github;copilot;sdk;jsonrpc;agent</PackageTags>
17+
<IsAotCompatible>true</IsAotCompatible>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<None Include="../README.md" Pack="true" PackagePath="/" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
26+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
27+
<PackageReference Include="StreamJsonRpc" Version="2.24.84" PrivateAssets="compile" />
28+
<PackageReference Include="System.Text.Json" Version="10.0.2" />
29+
</ItemGroup>
30+
31+
<!-- Generate version props file at build time (gitignored) -->
32+
<Target Name="_GenerateVersionProps" BeforeTargets="BeforeBuild">
33+
<Exec Command="node -e &quot;console.log(require('./nodejs/package-lock.json').packages['node_modules/@github/copilot'].version)&quot;" WorkingDirectory="$(MSBuildThisFileDirectory)../.." ConsoleToMSBuild="true" StandardOutputImportance="low">
34+
<Output TaskParameter="ConsoleOutput" PropertyName="CopilotCliVersion" />
35+
</Exec>
36+
<Error Condition="'$(CopilotCliVersion)' == ''" Text="CopilotCliVersion could not be read from nodejs/package-lock.json" />
37+
<PropertyGroup>
38+
<_VersionPropsContent>
39+
<![CDATA[<Project>
340
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
5-
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
7-
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8-
<Version>0.1.0</Version>
9-
<Description>SDK for programmatic control of GitHub Copilot CLI</Description>
10-
<Authors>GitHub</Authors>
11-
<Company>GitHub</Company>
12-
<Copyright>Copyright (c) Microsoft Corporation. All rights reserved.</Copyright>
13-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14-
<PackageReadmeFile>README.md</PackageReadmeFile>
15-
<RepositoryUrl>https://github.com/github/copilot-sdk</RepositoryUrl>
16-
<PackageTags>github;copilot;sdk;jsonrpc;agent</PackageTags>
17-
<IsAotCompatible>true</IsAotCompatible>
41+
<CopilotCliVersion>$(CopilotCliVersion)</CopilotCliVersion>
1842
</PropertyGroup>
43+
</Project>]]>
44+
</_VersionPropsContent>
45+
</PropertyGroup>
46+
<WriteLinesToFile File="$(MSBuildThisFileDirectory)build\GitHub.Copilot.SDK.props" Lines="$(_VersionPropsContent)" Overwrite="true" WriteOnlyWhenDifferent="true" />
47+
</Target>
1948

20-
<ItemGroup>
21-
<None Include="../README.md" Pack="true" PackagePath="/" />
22-
</ItemGroup>
23-
24-
<ItemGroup>
25-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
26-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
27-
<PackageReference Include="StreamJsonRpc" Version="2.24.84" PrivateAssets="compile" />
28-
<PackageReference Include="System.Text.Json" Version="10.0.2" />
29-
</ItemGroup>
49+
<!-- Include .targets and .props files in package -->
50+
<!-- Also import the .targets for local dev (same logic consumers get) -->
51+
<ItemGroup>
52+
<None Include="build\GitHub.Copilot.SDK.*" Pack="true" PackagePath="build\" CopyToOutputDirectory="Never" />
53+
</ItemGroup>
54+
<Import Project="build\GitHub.Copilot.SDK.targets" />
3055

3156
</Project>

dotnet/src/Types.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public enum ConnectionState
2424

2525
public class CopilotClientOptions
2626
{
27+
/// <summary>
28+
/// Path to the Copilot CLI executable. If not specified, uses the bundled CLI from the SDK.
29+
/// </summary>
2730
public string? CliPath { get; set; }
2831
public string[]? CliArgs { get; set; }
2932
public string? Cwd { get; set; }
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<Project>
2+
<!-- These targets run in consuming projects when they build -->
3+
<!-- CopilotCliVersion is imported from GitHub.Copilot.SDK.props (generated at SDK build time, packaged alongside) -->
4+
<Import Project="$(MSBuildThisFileDirectory)GitHub.Copilot.SDK.props" Condition="'$(CopilotCliVersion)' == '' And Exists('$(MSBuildThisFileDirectory)GitHub.Copilot.SDK.props')" />
5+
6+
<!-- Resolve RID: use explicit RuntimeIdentifier, or infer from current machine -->
7+
<PropertyGroup>
8+
<_CopilotRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</_CopilotRid>
9+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Windows')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">win-x64</_CopilotRid>
10+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Windows')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">win-arm64</_CopilotRid>
11+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Linux')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">linux-x64</_CopilotRid>
12+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Linux')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">linux-arm64</_CopilotRid>
13+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('OSX')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">osx-x64</_CopilotRid>
14+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('OSX')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">osx-arm64</_CopilotRid>
15+
</PropertyGroup>
16+
17+
<!-- Map RID to platform name used in npm packages -->
18+
<PropertyGroup>
19+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-x64'">win32-x64</_CopilotPlatform>
20+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-arm64'">win32-arm64</_CopilotPlatform>
21+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-x64'">linux-x64</_CopilotPlatform>
22+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-arm64'">linux-arm64</_CopilotPlatform>
23+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-x64'">darwin-x64</_CopilotPlatform>
24+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-arm64'">darwin-arm64</_CopilotPlatform>
25+
<_CopilotBinary Condition="$(_CopilotRid.StartsWith('win-'))">copilot.exe</_CopilotBinary>
26+
<_CopilotBinary Condition="'$(_CopilotBinary)' == ''">copilot</_CopilotBinary>
27+
</PropertyGroup>
28+
29+
<!-- Download and extract CLI binary -->
30+
<Target Name="_DownloadCopilotCli" BeforeTargets="BeforeBuild" Condition="'$(_CopilotPlatform)' != ''">
31+
<Error Condition="'$(CopilotCliVersion)' == ''" Text="CopilotCliVersion is not set. The GitHub.Copilot.SDK.props file may be missing from the NuGet package." />
32+
33+
<!-- Compute paths using version (now available) -->
34+
<PropertyGroup>
35+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)</_CopilotCacheDir>
36+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)\$(_CopilotBinary)</_CopilotCliBinaryPath>
37+
<_CopilotArchivePath>$(_CopilotCacheDir)\copilot.tgz</_CopilotArchivePath>
38+
<_CopilotDownloadUrl>https://registry.npmjs.org/@github/copilot-$(_CopilotPlatform)/-/copilot-$(_CopilotPlatform)-$(CopilotCliVersion).tgz</_CopilotDownloadUrl>
39+
</PropertyGroup>
40+
41+
<!-- Delete archive if binary missing (handles partial/corrupted downloads) -->
42+
<Delete Files="$(_CopilotArchivePath)" Condition="!Exists('$(_CopilotCliBinaryPath)') And Exists('$(_CopilotArchivePath)')" />
43+
44+
<!-- Download if not cached -->
45+
<MakeDir Directories="$(_CopilotCacheDir)" Condition="!Exists('$(_CopilotCliBinaryPath)')" />
46+
<Message Importance="high" Text="Downloading Copilot CLI $(CopilotCliVersion) for $(_CopilotPlatform)..." Condition="!Exists('$(_CopilotCliBinaryPath)')" />
47+
<DownloadFile SourceUrl="$(_CopilotDownloadUrl)" DestinationFolder="$(_CopilotCacheDir)" DestinationFileName="copilot.tgz"
48+
Condition="!Exists('$(_CopilotCliBinaryPath)')" />
49+
50+
<!-- Extract using tar (use Windows system tar explicitly to avoid Git bash tar issues) -->
51+
<PropertyGroup>
52+
<_TarCommand Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(SystemRoot)\System32\tar.exe</_TarCommand>
53+
<_TarCommand Condition="'$(_TarCommand)' == ''">tar</_TarCommand>
54+
</PropertyGroup>
55+
<Exec Command="&quot;$(_TarCommand)&quot; -xzf &quot;$(_CopilotArchivePath)&quot; --strip-components=1 -C &quot;$(_CopilotCacheDir)&quot;"
56+
Condition="!Exists('$(_CopilotCliBinaryPath)')" />
57+
58+
<Error Condition="!Exists('$(_CopilotCliBinaryPath)')" Text="Failed to extract Copilot CLI binary to $(_CopilotCliBinaryPath)" />
59+
</Target>
60+
61+
<!-- Copy CLI binary to output runtimes folder and register for transitive copy -->
62+
<Target Name="_CopyCopilotCliToOutput" AfterTargets="Build" DependsOnTargets="_DownloadCopilotCli" Condition="'$(_CopilotPlatform)' != ''">
63+
<PropertyGroup>
64+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)</_CopilotCacheDir>
65+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)\$(_CopilotBinary)</_CopilotCliBinaryPath>
66+
<_CopilotOutputDir>$(OutDir)runtimes\$(_CopilotRid)\native</_CopilotOutputDir>
67+
</PropertyGroup>
68+
<MakeDir Directories="$(_CopilotOutputDir)" />
69+
<Copy SourceFiles="$(_CopilotCliBinaryPath)" DestinationFolder="$(_CopilotOutputDir)" SkipUnchangedFiles="true" />
70+
</Target>
71+
72+
<!-- Register CLI binary as content so it flows through project references -->
73+
<Target Name="_RegisterCopilotCliForCopy" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="_DownloadCopilotCli" Condition="'$(_CopilotPlatform)' != ''">
74+
<PropertyGroup>
75+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)</_CopilotCacheDir>
76+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)\$(_CopilotBinary)</_CopilotCliBinaryPath>
77+
</PropertyGroup>
78+
<ItemGroup>
79+
<ContentWithTargetPath Include="$(_CopilotCliBinaryPath)"
80+
TargetPath="runtimes\$(_CopilotRid)\native\$(_CopilotBinary)"
81+
CopyToOutputDirectory="PreserveNewest" />
82+
</ItemGroup>
83+
</Target>
84+
</Project>

0 commit comments

Comments
 (0)