Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Fixed

- The docker, pnpm, cargo and terraform launchers of the standalone binary now quote the path of the binary correctly. A path with `$` or a backtick no longer breaks the launcher on Linux and macOS.
- `cloudsmith credential-helper list` now finds a launcher that `install` wrote to the default directory, `~/.cloudsmith/bin`. Before, `list` looked in the directory of the `cloudsmith` executable and reported the helper as not installed.

## [1.29.0] - 2026-10-15
Expand Down
4 changes: 2 additions & 2 deletions cloudsmith_cli/cli/tests/commands/test_credential_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ....core.credentials.models import CredentialResult
from ....credential_helpers.backends import BackendKind
from ....credential_helpers.common import (
REFUSAL_MESSAGE,
is_cloudsmith_domain,
is_standard_cloudsmith_domain,
)
Expand All @@ -28,7 +29,6 @@
write_cache,
)
from ....credential_helpers.docker.runtime import (
_REFUSAL_MESSAGE,
execute,
)
from ....credential_helpers.docker.runtime import (
Expand Down Expand Up @@ -133,7 +133,7 @@ def read(self):

assert code == 1
assert stdout is None
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

from ....cli.commands.credential_helper.generic import generic
from ....core.credentials.models import CredentialResult
from ....credential_helpers.generic import _REFUSAL_MESSAGE, PROTOCOL_VERSION, execute
from ....credential_helpers.common import REFUSAL_MESSAGE
from ....credential_helpers.generic import PROTOCOL_VERSION, execute

TOKEN = "k_secret_token_value"

Expand Down Expand Up @@ -54,7 +55,7 @@ def test_execute_empty_api_key_refuses():

assert code == 1
assert stdout is None
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


def test_execute_degrades_on_unexpected_exception():
Expand All @@ -71,7 +72,7 @@ def api_key(self):

assert code == 1
assert stdout is None
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


def test_token_only_ever_appears_on_stdout():
Expand Down Expand Up @@ -109,7 +110,7 @@ def test_cli_refusal_exits_1_with_empty_stdout(runner):
"""A refusal must not emit a partial document on stdout."""
with patch(
"cloudsmith_cli.cli.commands.credential_helper.generic.execute",
return_value=(1, None, _REFUSAL_MESSAGE),
return_value=(1, None, REFUSAL_MESSAGE),
):
result = runner.invoke(generic, args=HERMETIC_ARGS, catch_exceptions=False)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,7 @@ def test_frozen_install_launcher_targets_executable(tmp_path, monkeypatch):
monkeypatch.setenv("DOCKER_CONFIG", str(docker_dir))
bin_dir = tmp_path / "bin"
monkeypatch.setenv("PATH", str(bin_dir))
exe = tmp_path / "cloudsmith"
exe = tmp_path / "price$USD dir" / "cloudsmith"

with (
patch.object(sys, "frozen", True, create=True),
Expand All @@ -1411,5 +1411,4 @@ def test_frozen_install_launcher_targets_executable(tmp_path, monkeypatch):
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
assert f"exec '{exe}' credential-helper docker \"$@\"" in body
8 changes: 4 additions & 4 deletions cloudsmith_cli/cli/tests/test_credential_helper_cargo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from ...core.credentials.models import CredentialResult
from ...credential_helpers.backends import BackendKind
from ...credential_helpers.cargo.installer import CargoInstaller
from ...credential_helpers.common import REFUSAL_MESSAGE
from ...credential_helpers.cargo.runtime import (
_REFUSAL_MESSAGE,
SUPPORTED_VERSIONS,
execute,
get_credentials,
Expand Down Expand Up @@ -190,7 +190,7 @@ def test_get_reports_not_found_when_no_credential_resolves():

assert messages[1] == {"Err": {"kind": "not-found"}}
assert code == 1
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


def test_get_reports_not_found_for_a_credential_without_an_api_key():
Expand Down Expand Up @@ -299,7 +299,7 @@ def write(self, _s):
code, stderr = execute(io.StringIO(""), BrokenPipe(), credential=credential)

assert code == 1
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


def test_domain_lookup_failure_degrades_to_a_clean_exit(credential):
Expand All @@ -310,7 +310,7 @@ def test_domain_lookup_failure_degrades_to_a_clean_exit(credential):
):
code, stderr, messages = _session(_request(), credential=credential)

assert (code, stderr) == (1, _REFUSAL_MESSAGE)
assert (code, stderr) == (1, REFUSAL_MESSAGE)
assert messages == [hello()]


Expand Down
8 changes: 4 additions & 4 deletions cloudsmith_cli/cli/tests/test_credential_helper_pnpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

from ...core.credentials.models import CredentialResult
from ...credential_helpers.backends import BackendKind
from ...credential_helpers.common import REFUSAL_MESSAGE
from ...credential_helpers.pnpm.runtime import (
_REFUSAL_MESSAGE,
execute,
get_pnpm_credentials,
)
Expand Down Expand Up @@ -159,14 +159,14 @@ def test_execute_refuses_without_credentials():
"""No credential is a hard refusal (exit 1) with a refusal message on stderr."""
exit_code, stdout, stderr = execute(CLOUDSMITH_HOST, credential=None)

assert (exit_code, stdout, stderr) == (1, None, _REFUSAL_MESSAGE)
assert (exit_code, stdout, stderr) == (1, None, REFUSAL_MESSAGE)


def test_execute_refuses_for_a_foreign_registry(credential):
"""A registry we don't serve is not our concern — exit 1, no leaked token."""
exit_code, stdout, stderr = execute(FOREIGN_HOST, credential=credential)

assert (exit_code, stdout, stderr) == (1, None, _REFUSAL_MESSAGE)
assert (exit_code, stdout, stderr) == (1, None, REFUSAL_MESSAGE)


def test_execute_degrades_cleanly_on_domain_lookup_failure(credential):
Expand All @@ -180,7 +180,7 @@ def test_execute_degrades_cleanly_on_domain_lookup_failure(credential):
)

assert (exit_code, stdout) == (1, None)
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
from ...core.api.exceptions import ApiException
from ...core.credentials.models import CredentialResult
from ...credential_helpers.backends import BackendKind
from ...credential_helpers.common import REFUSAL_MESSAGE
from ...credential_helpers.terraform.runtime import (
_MISSING_ORG_MESSAGE,
_MISSING_REPO_MESSAGE,
_REFUSAL_MESSAGE,
execute,
get_token,
)
Expand Down Expand Up @@ -203,7 +203,7 @@ def test_execute_get_refuses_when_the_domain_scope_lookup_fails(
repo="myrepo",
)

assert (exit_code, stdout, stderr) == (1, None, _REFUSAL_MESSAGE)
assert (exit_code, stdout, stderr) == (1, None, REFUSAL_MESSAGE)


def test_execute_get_custom_domain_does_not_require_an_org_in_the_token(credential):
Expand Down Expand Up @@ -245,7 +245,7 @@ def test_execute_get_refuses_cloudsmith_host_without_credentials():
"get", CLOUDSMITH_HOST, credential=None, org="acme", repo="myrepo"
)

assert (exit_code, stdout, stderr) == (1, None, _REFUSAL_MESSAGE)
assert (exit_code, stdout, stderr) == (1, None, REFUSAL_MESSAGE)


def test_execute_get_refuses_cloudsmith_host_without_an_org(credential):
Expand Down Expand Up @@ -322,7 +322,7 @@ def test_execute_get_degrades_cleanly_on_domain_lookup_failure(credential):
)

assert (exit_code, stdout) == (1, None)
assert stderr == _REFUSAL_MESSAGE
assert stderr == REFUSAL_MESSAGE


# ---------------------------------------------------------------------------
Expand Down
20 changes: 2 additions & 18 deletions cloudsmith_cli/credential_helpers/cargo/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import logging
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING

Expand All @@ -30,6 +29,7 @@
from ..common import extract_hostname, path_warning
from ..domains import get_format_domains
from ..launchers import (
helper_command,
is_on_path,
is_windows,
remove_launcher,
Expand Down Expand Up @@ -73,7 +73,6 @@
"""

LAUNCHER_NAME = "cargo-credential-cloudsmith"
TARGET_CMD = "cloudsmith credential-helper cargo"
#: The value written into Cargo's config. Cargo resolves a bare name
#: through ``PATH`` (and requires the ``cargo-credential-`` prefix).
PROVIDER_VALUE = "cargo-credential-cloudsmith"
Expand All @@ -87,21 +86,6 @@
name = "cargo"
summary = "Cargo credential provider 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 cargo'
return cls.TARGET_CMD

# ------------------------------------------------------------------
# config.toml mutation
# ------------------------------------------------------------------
Expand Down Expand Up @@ -336,7 +320,7 @@

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

Expand Down Expand Up @@ -475,7 +459,7 @@
if config_path.exists():
try:
data = tomlkit.loads(config_path.read_text(encoding="utf-8"))
except (tomlkit.exceptions.ParseError, ValueError, OSError):

Check warning on line 462 in cloudsmith_cli/credential_helpers/cargo/installer.py

View workflow job for this annotation

GitHub Actions / ty

ty (possibly-missing-submodule)

cloudsmith_cli/credential_helpers/cargo/installer.py:462:21: possibly-missing-submodule: Submodule `exceptions` might not have been imported help: Consider explicitly importing `tomlkit.exceptions`
data = {}

registry = data.get("registry")
Expand Down
13 changes: 3 additions & 10 deletions cloudsmith_cli/credential_helpers/cargo/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,13 @@
import logging

from ..backends import BackendKind
from ..common import is_cloudsmith_domain
from ..common import REFUSAL_MESSAGE, is_cloudsmith_domain

logger = logging.getLogger(__name__)

#: Protocol versions this provider implements, announced in the hello message.
SUPPORTED_VERSIONS = (1,)

_REFUSAL_MESSAGE = (
"Error: Unable to retrieve credentials. "
"Provide credentials via the CLOUDSMITH_API_KEY environment variable, "
"credentials.ini, the system keyring, or an OIDC service. "
"Verify current authentication with `cloudsmith whoami --verbose`."
)


def hello() -> dict:
"""Return the handshake message sent before any request is read.
Expand Down Expand Up @@ -257,9 +250,9 @@ def execute(stdin, stdout, credential=None, api_host=None, org=None):
# TypeError from json.dumps — all degrade to a clean exit.
# (Exception does not catch KeyboardInterrupt/SystemExit, which is correct.)
logger.debug("cargo credential-provider session failed: %s", exc, exc_info=True)
return (1, _REFUSAL_MESSAGE)
return (1, REFUSAL_MESSAGE)

if refused:
return (1, _REFUSAL_MESSAGE)
return (1, REFUSAL_MESSAGE)

return (0, None)
7 changes: 7 additions & 0 deletions cloudsmith_cli/credential_helpers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@

logger = logging.getLogger(__name__)

REFUSAL_MESSAGE = (
"Error: Unable to retrieve credentials. "
"Provide credentials via the CLOUDSMITH_API_KEY environment variable, "
"credentials.ini, the system keyring, or an OIDC service. "
"Verify current authentication with `cloudsmith whoami --verbose`."
)


def path_warning(
target_dir: Path, helper_format: str, launcher: str, windows: bool
Expand Down
20 changes: 2 additions & 18 deletions cloudsmith_cli/credential_helpers/docker/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import json
import logging
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING

Expand All @@ -20,6 +19,7 @@
from ..common import path_warning
from ..domains import get_format_domains
from ..launchers import (
helper_command,
is_on_path,
is_windows,
remove_launcher,
Expand Down Expand Up @@ -61,28 +61,12 @@ class DockerInstaller:
"""

LAUNCHER_NAME = "docker-credential-cloudsmith"
TARGET_CMD = "cloudsmith credential-helper docker"
HELPER_VALUE = "cloudsmith"
DEFAULT_HOST = "docker.cloudsmith.io"

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 @@ -225,7 +209,7 @@ def mutate(config: dict) -> None:

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

Expand Down
13 changes: 3 additions & 10 deletions cloudsmith_cli/credential_helpers/docker/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,10 @@
import logging

from ..backends import BackendKind
from ..common import is_cloudsmith_domain
from ..common import REFUSAL_MESSAGE, is_cloudsmith_domain

logger = logging.getLogger(__name__)

_REFUSAL_MESSAGE = (
"Error: Unable to retrieve credentials. "
"Provide credentials via the CLOUDSMITH_API_KEY environment variable, "
"credentials.ini, the system keyring, or an OIDC service. "
"Verify current authentication with `cloudsmith whoami --verbose`."
)


def get_credentials(server_url, credential=None, api_host=None, org=None):
"""
Expand Down Expand Up @@ -69,7 +62,7 @@ def _execute_get(
server_url, credential=credential, api_host=api_host, org=org
)
if creds is None:
return (1, None, _REFUSAL_MESSAGE)
return (1, None, REFUSAL_MESSAGE)

return (0, json.dumps(creds), None)
except Exception as exc: # pylint: disable=broad-except
Expand All @@ -80,7 +73,7 @@ def _execute_get(
# This is the ONLY intentional broad except in this feature.
# (Exception does not catch KeyboardInterrupt/SystemExit, which is correct.)
logger.debug("docker credential-helper get failed: %s", exc, exc_info=True)
return (1, None, _REFUSAL_MESSAGE)
return (1, None, REFUSAL_MESSAGE)


def execute(
Expand Down
Loading
Loading