Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 420 additions & 0 deletions .github/workflows/binaries.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ on:

permissions: {}

concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
zizmor:
name: Scan GitHub Actions workflows
Expand Down
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ repos:
"-sn", # Don't display the score
"--rcfile=.pylintrc", # Link to your config file
]
- id: binary-constraints
name: regenerate binary constraints
# Keep packaging/constraints.txt in sync with uv.lock for the PyInstaller
# build. Mirrors the CI "Verify lockfile and binary constraints" gate so
# drift is caught locally. Auto-regenerates (like black); re-stage on fail.
language: system
entry: uv export --locked --no-dev --group binary --extra all --no-emit-project --no-hashes --no-header -o packaging/constraints.txt
pass_filenames: false
files: ^(uv\.lock|pyproject\.toml|packaging/constraints\.txt)$

- repo: https://github.com/crate-ci/typos
rev: v1.42.1
Expand Down
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Mno-hime = "Mno-hime"
[default.extend-words]
# Proper names - do not correct
hime = "hime"
# PyInstaller spec API: Analysis(datas=...) — not a typo of "data"
datas = "datas"
McClory = "McClory"
mcclory = "mcclory"
Clory = "Clory"
Expand Down
5 changes: 4 additions & 1 deletion cloudsmith_cli/cli/commands/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def configure(ctx, opts, client, is_global): # pylint: disable=unused-argument

def _get_server_config(profile=None):
"""Determine the first available command configuration to run the MCP server."""
# Check if running in a virtual environment
is_frozen = getattr(sys, "frozen", False)
in_venv = hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
)
Expand All @@ -301,6 +301,9 @@ def _get_server_config(profile=None):
if profile:
base_args.extend(["-P", profile])

if is_frozen:
return {"command": sys.executable, "args": base_args + ["mcp", "start"]}

# In a venv, always use python -m to ensure we use the venv's packages
if in_venv:
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import stat
import sys
from pathlib import Path
from unittest.mock import patch

Expand Down Expand Up @@ -716,3 +717,45 @@ def test_uninstall_tolerates_malformed_cred_helpers(tmp_path, monkeypatch):
installer = DockerInstaller()
# Must not raise
installer.uninstall(bin_dir=str(bin_dir))


# ---------------------------------------------------------------------------
# 18. frozen-binary launcher target (PyInstaller standalone)
# ---------------------------------------------------------------------------


def test_default_install_launcher_uses_bare_command(tmp_path, monkeypatch):
"""A pip/source install writes a launcher that forwards to the bare
``cloudsmith`` command resolved via PATH."""
docker_dir = tmp_path / ".docker"
monkeypatch.setenv("DOCKER_CONFIG", str(docker_dir))
bin_dir = tmp_path / "bin"
monkeypatch.setenv("PATH", str(bin_dir))

DockerInstaller().install(bin_dir=str(bin_dir), discover=False)

body = (bin_dir / "docker-credential-cloudsmith").read_text(encoding="utf-8")
assert "exec cloudsmith credential-helper docker" in body


def test_frozen_install_launcher_targets_executable(tmp_path, monkeypatch):
"""A frozen install writes a launcher that execs the absolute executable.

A standalone binary is not guaranteed to be on PATH as ``cloudsmith``, so a
bare-command launcher would leave Docker unable to find the helper.
"""
docker_dir = tmp_path / ".docker"
monkeypatch.setenv("DOCKER_CONFIG", str(docker_dir))
bin_dir = tmp_path / "bin"
monkeypatch.setenv("PATH", str(bin_dir))
exe = tmp_path / "cloudsmith"

with (
patch.object(sys, "frozen", True, create=True),
patch.object(sys, "executable", str(exe)),
):
DockerInstaller().install(bin_dir=str(bin_dir), discover=False)

body = (bin_dir / "docker-credential-cloudsmith").read_text(encoding="utf-8")
assert str(exe) in body
assert "exec cloudsmith credential-helper" not in body
14 changes: 14 additions & 0 deletions cloudsmith_cli/cli/tests/commands/test_mcp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import stat
import sys
from pathlib import Path
from unittest.mock import patch

Expand All @@ -10,6 +11,7 @@
from ....cli.commands.mcp import (
_atomic_write_json,
_configure_claude_code,
_get_server_config,
_safe_update_json,
configure_client,
detect_available_clients,
Expand Down Expand Up @@ -373,6 +375,18 @@ def test_server_respects_tool_filtering(self):
SERVER_CONFIG = {"command": "cloudsmith", "args": ["mcp", "start"]}


class TestMCPServerConfig:
def test_frozen_executable_runs_mcp_directly(self):
with (
patch.object(sys, "frozen", True, create=True),
patch.object(sys, "executable", "/opt/cloudsmith/cloudsmith"),
):
assert _get_server_config("staging") == {
"command": "/opt/cloudsmith/cloudsmith",
"args": ["-P", "staging", "mcp", "start"],
}


class TestMCPConfigureClaudeCode:
def test_user_scope_merges_into_existing_claude_json(self, tmp_path):
claude_json = tmp_path / ".claude.json"
Expand Down
20 changes: 19 additions & 1 deletion cloudsmith_cli/credential_helpers/docker/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json
import logging
import os
import sys
from pathlib import Path

from ...core.cache_utils import merge_json_file
Expand Down Expand Up @@ -56,6 +57,21 @@ class DockerInstaller:
name = "docker"
summary = "Docker credential helper for Cloudsmith registries"

@classmethod
def _resolve_target_cmd(cls) -> str:
"""Return the command the launcher forwards to.

A pip/source install resolves the bare ``cloudsmith`` command via
``PATH``. A frozen standalone binary (PyInstaller) is not guaranteed
to be on ``PATH`` under that name, so point the launcher at the
absolute executable instead — mirroring the frozen handling in
:func:`cloudsmith_cli.cli.commands.mcp._get_server_config`. The path
is quoted so a directory containing spaces still execs correctly.
"""
if getattr(sys, "frozen", False):
return f'"{sys.executable}" credential-helper docker'
return cls.TARGET_CMD

def install(
self,
*,
Expand Down Expand Up @@ -190,7 +206,9 @@ def mutate(config: dict) -> None:
return actions

# Real install
launcher_path = write_launcher(target_dir, self.LAUNCHER_NAME, self.TARGET_CMD)
launcher_path = write_launcher(
target_dir, self.LAUNCHER_NAME, self._resolve_target_cmd()
)
actions.append(f"wrote launcher {launcher_path}")

changed = merge_json_file(config_path, mutate)
Expand Down
Loading