Skip to content

Commit 059c53b

Browse files
Bundling for Python
1 parent de46143 commit 059c53b

File tree

7 files changed

+408
-91
lines changed

7 files changed

+408
-91
lines changed

.github/workflows/publish.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,18 @@ jobs:
153153
- uses: actions/setup-python@v6
154154
with:
155155
python-version: "3.12"
156+
- uses: actions/setup-node@v6
157+
with:
158+
node-version: "22.x"
156159
- name: Set up uv
157160
uses: astral-sh/setup-uv@v7
161+
- name: Install Node.js dependencies (for CLI version)
162+
working-directory: ./nodejs
163+
run: npm ci --ignore-scripts
158164
- name: Set version
159165
run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml
160-
- name: Build package
161-
run: uv build
166+
- name: Build platform wheels
167+
run: node scripts/build-wheels.mjs --output-dir dist
162168
- name: Upload artifact
163169
uses: actions/upload-artifact@v6
164170
with:

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

Lines changed: 8 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,13 @@ 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+
cache: "npm"
50+
cache-dependency-path: "./nodejs/package-lock.json"
5051

5152
- name: Set up uv
5253
uses: astral-sh/setup-uv@v7
@@ -56,6 +57,10 @@ jobs:
5657
- name: Install Python dev dependencies
5758
run: uv sync --locked --all-extras --dev
5859

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

@@ -76,5 +81,4 @@ jobs:
7681
- name: Run Python SDK tests
7782
env:
7883
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
79-
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
8084
run: uv run pytest -v -s

python/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,10 @@ cython_debug/
162162
# Ruff and ty cache
163163
.ruff_cache/
164164
.ty_cache/
165+
166+
# Build script caches
167+
.cli-cache/
168+
.build-temp/
169+
170+
# Bundled CLI binary (only in platform wheels, not in repo)
171+
copilot/bin/

python/copilot/client.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
import inspect
1717
import os
1818
import re
19-
import shutil
2019
import subprocess
20+
import sys
2121
import threading
2222
from dataclasses import asdict, is_dataclass
23+
from pathlib import Path
2324
from typing import Any, Callable, Optional, cast
2425

2526
from .generated.session_events import session_event_from_dict
@@ -48,6 +49,26 @@
4849
)
4950

5051

52+
def _get_bundled_cli_path() -> Optional[str]:
53+
"""Get the path to the bundled CLI binary, if available."""
54+
# The binary is bundled in copilot/bin/ within the package
55+
bin_dir = Path(__file__).parent / "bin"
56+
if not bin_dir.exists():
57+
return None
58+
59+
# Determine binary name based on platform
60+
if sys.platform == "win32":
61+
binary_name = "copilot.exe"
62+
else:
63+
binary_name = "copilot"
64+
65+
binary_path = bin_dir / binary_name
66+
if binary_path.exists():
67+
return str(binary_path)
68+
69+
return None
70+
71+
5172
class CopilotClient:
5273
"""
5374
Main client for interacting with the Copilot CLI.
@@ -130,8 +151,18 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
130151
else:
131152
self._actual_port = None
132153

133-
# Check environment variable for CLI path
134-
default_cli_path = os.environ.get("COPILOT_CLI_PATH", "copilot")
154+
# Determine CLI path: explicit option > bundled binary
155+
if opts.get("cli_path"):
156+
default_cli_path = opts["cli_path"]
157+
else:
158+
bundled_path = _get_bundled_cli_path()
159+
if bundled_path:
160+
default_cli_path = bundled_path
161+
else:
162+
raise RuntimeError(
163+
"Copilot CLI not found. The bundled CLI binary is not available. "
164+
"Ensure you installed a platform-specific wheel, or provide cli_path."
165+
)
135166

136167
# Default use_logged_in_user to False when github_token is provided
137168
github_token = opts.get("github_token")
@@ -140,7 +171,7 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
140171
use_logged_in_user = False if github_token else True
141172

142173
self.options: CopilotClientOptions = {
143-
"cli_path": opts.get("cli_path", default_cli_path),
174+
"cli_path": default_cli_path,
144175
"cwd": opts.get("cwd", os.getcwd()),
145176
"port": opts.get("port", 0),
146177
"use_stdio": False if opts.get("cli_url") else opts.get("use_stdio", True),
@@ -1037,12 +1068,9 @@ async def _start_cli_server(self) -> None:
10371068
"""
10381069
cli_path = self.options["cli_path"]
10391070

1040-
# Resolve the full path on Windows (handles .cmd/.bat files)
1041-
# On Windows, subprocess.Popen doesn't use PATHEXT to resolve extensions,
1042-
# so we need to use shutil.which() to find the actual executable
1043-
resolved_path = shutil.which(cli_path)
1044-
if resolved_path:
1045-
cli_path = resolved_path
1071+
# Verify CLI exists
1072+
if not os.path.exists(cli_path):
1073+
raise RuntimeError(f"Copilot CLI not found at {cli_path}")
10461074

10471075
args = ["--headless", "--log-level", self.options["log_level"]]
10481076

python/e2e/testharness/context.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,20 @@
1616
from .proxy import CapiProxy
1717

1818

19-
def get_cli_path() -> str:
20-
"""Get CLI path from environment or try to find it. Raises if not found."""
21-
# Check environment variable first
22-
cli_path = os.environ.get("COPILOT_CLI_PATH")
23-
if cli_path and os.path.exists(cli_path):
24-
return cli_path
25-
19+
def get_cli_path_for_tests() -> str:
20+
"""Get CLI path for E2E tests. Uses node_modules CLI during development."""
2621
# Look for CLI in sibling nodejs directory's node_modules
27-
base_path = Path(__file__).parents[3] # equivalent to: path.parent.parent.parent.parent
22+
base_path = Path(__file__).parents[3]
2823
full_path = base_path / "nodejs" / "node_modules" / "@github" / "copilot" / "index.js"
2924
if full_path.exists():
3025
return str(full_path.resolve())
3126

3227
raise RuntimeError(
33-
"CLI not found. Set COPILOT_CLI_PATH or run 'npm install' in the nodejs directory."
28+
"CLI not found for tests. Run 'npm install' in the nodejs directory."
3429
)
3530

3631

37-
CLI_PATH = get_cli_path()
32+
CLI_PATH = get_cli_path_for_tests()
3833
SNAPSHOTS_DIR = Path(__file__).parents[3] / "test" / "snapshots"
3934

4035

@@ -51,12 +46,7 @@ def __init__(self):
5146

5247
async def setup(self):
5348
"""Set up the test context with a shared client."""
54-
cli_path = get_cli_path()
55-
if not cli_path or not os.path.exists(cli_path):
56-
raise RuntimeError(
57-
f"CLI not found at {cli_path}. Run 'npm install' in the nodejs directory first."
58-
)
59-
self.cli_path = cli_path
49+
self.cli_path = get_cli_path_for_tests()
6050

6151
self.home_dir = tempfile.mkdtemp(prefix="copilot-test-config-")
6252
self.work_dir = tempfile.mkdtemp(prefix="copilot-test-work-")

0 commit comments

Comments
 (0)