Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
28a004b
refactor(no-ticket): read the keyring settings through Click
cloudsmith-iduffy Oct 21, 2026
6db2380
feat(ENG-15016): let credential-helper launchers keep install-time se…
cloudsmith-iduffy Oct 21, 2026
a3667bb
feat(ENG-15016): keep install-time settings in the Docker credential …
cloudsmith-iduffy Oct 21, 2026
796438a
fix(ENG-15016): keep each Docker helper name to one install
cloudsmith-iduffy Oct 21, 2026
34a43a1
fix(ENG-15016): list each Docker helper and accept --org in uninstall
cloudsmith-iduffy Oct 22, 2026
0352b88
fix(no-ticket): end the pnpm .npmrc with a newline (#528)
cloudsmith-iduffy Oct 28, 2026
66fb588
feat(ENG-15016): let credential-helper launchers keep install-time se…
cloudsmith-iduffy Oct 21, 2026
caef7be
Merge origin/iduffy/launcher-install-time-settings into iduffy/eng-15…
cloudsmith-iduffy Oct 28, 2026
b7f68a5
fix(no-ticket): stop retrying unknown hosts and show backoff retries …
cloudsmith-iduffy Nov 2, 2026
25fd194
feat(ENG-15016): let credential-helper launchers keep install-time se…
cloudsmith-iduffy Oct 21, 2026
bf5c4a6
Merge origin/master into iduffy/launcher-install-time-settings
cloudsmith-iduffy Nov 2, 2026
63aa192
Merge origin/iduffy/launcher-install-time-settings into iduffy/eng-15…
cloudsmith-iduffy Nov 2, 2026
1f34ef5
Save the resolved TLS setting in the launcher
cloudsmith-iduffy Nov 3, 2026
9d84488
Merge iduffy/launcher-install-time-settings into iduffy/eng-15016-doc…
cloudsmith-iduffy Nov 3, 2026
7c98fbe
Name the bin directory in the Docker scope conflict command
cloudsmith-iduffy Nov 3, 2026
2c7169d
Let a root -P flag override CLOUDSMITH_PROFILE
cloudsmith-iduffy Nov 3, 2026
8de21f4
Merge origin/master into iduffy/eng-15016-docker-launcher-settings
cloudsmith-iduffy Nov 3, 2026
cb77e5e
Quote cmd.exe metacharacters in the Docker conflict command
cloudsmith-iduffy Nov 3, 2026
c35a039
Treat an empty Docker profile or Workspace as none
cloudsmith-iduffy Nov 3, 2026
66c3f7a
Merge origin/master into iduffy/eng-15016-docker-launcher-settings
cloudsmith-iduffy Nov 5, 2026
68e9a03
Keep the settings of deprecated spellings in the Docker launcher
cloudsmith-iduffy Nov 5, 2026
263eaa4
Name each Docker helper in list and read launchers safely
cloudsmith-iduffy Nov 5, 2026
29b22d6
Merge origin/master into iduffy/eng-15016-docker-launcher-settings
cloudsmith-iduffy Nov 5, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed

- After a SAML login, `cloudsmith auth` saves the Workspace to `config.ini`. It writes to the `[profile:NAME]` section of the selected profile, or to `[default]` when you do not select a profile. The CLI writes to the file or directory that `--config-file` gives, or else to your user-level `config.ini`. If the `config.ini` does not exist, the CLI creates it.
- `cloudsmith credential-helper install docker` now keeps the `CLOUDSMITH_*` settings of the install, such as the profile, Workspace and API host, in the launcher. Docker then gets the same credentials from Docker Desktop, an IDE or a new shell. The launcher never contains API keys, custom headers or the proxy URL. An install with a profile or a Workspace writes its own helper, for example `docker-credential-cloudsmith-<profile>-<workspace>`. To remove it, run `cloudsmith credential-helper uninstall docker -P <profile> -w <workspace>`. An install without them writes `docker-credential-cloudsmith`, as before.
- Using `#!/usr/bin/env sh` as shebang path for unix credential helpers.
- The minimum version of `cloudsmith-api` is now 2.0.42.

Expand Down Expand Up @@ -91,6 +92,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `cloudsmith mcp start` now stops without an error when the MCP client closes the connection while the server writes a reply. Previously, the command stopped with `BrokenPipeError` and exit status 1.
- The CLI now exits with status 1 when you interrupt it with Ctrl+C after the reader of its stderr has closed the pipe. Before, the CLI sent an error report and exited with status 120.
- `cloudsmith check rates` no longer stops with `TypeError` when the API does not give a value for an interval, limit, remaining count, reset time or throttled status. The command now shows `None` for that value.
- A `-P` flag before the command, for example `cloudsmith -P meta whoami`, now overrides `CLOUDSMITH_PROFILE`. Before, the environment variable took precedence.

## [1.30.0] - 2026-10-22

Expand Down
111 changes: 95 additions & 16 deletions cloudsmith_cli/cli/commands/credential_helper/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@
)

from ....core.session import create_requests_session
from ....credential_helpers.docker.installer import DockerInstaller
from ... import utils
from ....credential_helpers.docker.installer import (
DockerInstaller,
HelperScopeConflict,
)
from ... import deprecation, utils
from ...decorators import (
common_api_auth_options,
common_cli_config_options,
Expand Down Expand Up @@ -186,8 +189,15 @@ def _terraform_proxy_next_steps(opts) -> list[str]:


# Other users can read a launcher, so it never holds a secret. A proxy URL can
# hold credentials.
_NEVER_KEPT = ("api_key", "api_headers", "keyring_key", "proxy")
# hold credentials. The colour settings are for the terminal of the install.
_NEVER_KEPT = (
"api_key",
"api_headers",
"keyring_key",
"proxy",
"color",
"force_color_env",
)

_TLS_ENVVARS = ("CLOUDSMITH_SSL_VERIFY", "CLOUDSMITH_WITHOUT_API_SSL_VERIFY")

Expand All @@ -198,6 +208,24 @@ def _main_envvar(option) -> str:
return names if isinstance(names, str) else names[0]


# A deprecated spelling is a separate option that sets the value of the option
# that replaces it.
_DEPRECATED_SPELLINGS = {
"org": deprecation.WORKSPACE_ALIASES,
"ssl_verify": deprecation.SSL_VERIFY_ALIASES,
"allowed_proxy_suffixes": deprecation.PROXY_SUFFIXES_ALIASES,
}


def _sources(context, option) -> set:
"""Return the sources of the value of *option* and its deprecated spellings."""
names = [option.name] + [
deprecation.alias_param_name(spelling)
for spelling in _DEPRECATED_SPELLINGS.get(option.name, ())
]
return {context.get_parameter_source(name) for name in names}


def _launcher_settings_from_cli(ctx, opts) -> dict[str, str]:
"""Return the ``CLOUDSMITH_*`` settings that a launcher keeps.

Expand All @@ -215,11 +243,16 @@ def _launcher_settings_from_cli(ctx, opts) -> dict[str, str]:
settings = {}
for context in contexts:
for option in context.command.params:
if option.name in _NEVER_KEPT or not option.envvar:
# The command removes the deprecated spellings from ctx.params.
if (
option.name in _NEVER_KEPT
or not option.envvar
or option.name not in context.params
):
continue
envvar = _main_envvar(option)
source = context.get_parameter_source(option.name)
if envvar.startswith("CLOUDSMITH_") and source in supplied:
sources = _sources(context, option)
if envvar.startswith("CLOUDSMITH_") and not sources.isdisjoint(supplied):
value = context.params[option.name]
settings[envvar] = (
str(value).lower() if isinstance(value, bool) else value
Expand All @@ -228,9 +261,21 @@ def _launcher_settings_from_cli(ctx, opts) -> dict[str, str]:
# not keep that precedence, so the launcher keeps only the resolved value.
if [settings.pop(name) for name in _TLS_ENVVARS if name in settings]:
settings["CLOUDSMITH_SSL_VERIFY"] = str(opts.ssl_verify).lower()
# Keep the profile that the install used to load the config files.
if "CLOUDSMITH_PROFILE" in settings:
settings["CLOUDSMITH_PROFILE"] = contexts[-1].meta["profile"]
return settings


def _profile_flag(ctx) -> str | None:
"""Return the ``-P`` value of this command or of a parent command."""
while ctx is not None:
if ctx.get_parameter_source("profile") == ParameterSource.COMMANDLINE:
return ctx.params["profile"]
Comment thread
Copilot marked this conversation as resolved.
Resolved
ctx = ctx.parent
return None


_REPO_FLAGS = ("-r", "--repo", "--repository")


Expand Down Expand Up @@ -447,6 +492,7 @@ def install_cmd(
ExtensionsFileError,
ConfigFileConflict,
UnsafeLauncherValue,
HelperScopeConflict,
) as exc:
raise click.ClickException(str(exc))
except OSError as exc:
Expand Down Expand Up @@ -520,11 +566,25 @@ def install_cmd(
help="Maven and sbt only: remove the setup of the project in the current "
"directory.",
)
@click.option(
"-w",
"--workspace",
"workspace",
help="Docker only: remove the helper that was installed for this Workspace.",
)
@deprecation.alias_options(deprecation.WORKSPACE_ALIASES)
@common_cli_config_options
@common_cli_output_options
@click.pass_context
def uninstall_cmd(
ctx, opts, helper: str, bin_dir: str | None, dry_run: bool, local: bool
ctx,
opts,
helper: str,
bin_dir: str | None,
dry_run: bool,
local: bool,
workspace: str | None,
**workspace_aliases,
) -> None:
"""Uninstall a credential helper launcher and remove its config entries.

Expand All @@ -541,13 +601,27 @@ def uninstall_cmd(
$ cloudsmith credential-helper uninstall docker --dry-run
"""
installer = _get_installer(helper)
alias_workspace = deprecation.pop_alias_options(
ctx, workspace_aliases, deprecation.WORKSPACE_ALIASES
)
uninstall_kwargs = {
"bin_dir": bin_dir,
"dry_run": dry_run,
**_helper_options(ctx, helper),
}
# The scope comes only from flags, so that a shell with CLOUDSMITH_PROFILE
# or CLOUDSMITH_WORKSPACE can still remove the unscoped helper.
if helper == "docker":
uninstall_kwargs["profile"] = _profile_flag(ctx)
uninstall_kwargs["workspace"] = workspace or alias_workspace
try:
actions = installer.uninstall(
bin_dir=bin_dir,
dry_run=dry_run,
**_helper_options(ctx, helper),
)
except (ConfigFileParseError, ProjectNotFound, ConfigFileConflict) as exc:
actions = installer.uninstall(**uninstall_kwargs)
except (
ConfigFileParseError,
ProjectNotFound,
ConfigFileConflict,
HelperScopeConflict,
) as exc:
raise click.ClickException(str(exc))
except OSError as exc:
raise click.ClickException(
Expand Down Expand Up @@ -599,14 +673,17 @@ def list_cmd(ctx, opts) -> None:
data = []
for name, cls in sorted(_INSTALLERS.items()):
installer = cls()
st = installer.status()
data.append(
# Docker can have one helper for each profile and Workspace.
statuses = getattr(installer, "statuses", None)
data.extend(
{
"helper": name,
"summary": installer.summary,
"cred_helper": st.get("name"),
"launcher": st.get("launcher"),
"hosts": st.get("hosts", []),
}
for st in (statuses() if statuses else [installer.status()])
)

if utils.maybe_print_as_json(opts, data):
Expand All @@ -619,6 +696,8 @@ def list_cmd(ctx, opts) -> None:
summary = entry["summary"]

click.echo(f"{name} ({summary})", err=use_stderr)
if entry["cred_helper"]:
click.echo(f" helper : {entry['cred_helper']}", err=use_stderr)
if launcher:
click.echo(f" launcher : {launcher}", err=use_stderr)
else:
Expand Down
5 changes: 4 additions & 1 deletion cloudsmith_cli/cli/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ def common_cli_config_options(f):
def wrapper(ctx, *args, **kwargs):
# pylint: disable=missing-docstring
opts = config.get_or_create_options(ctx)
profile = kwargs.pop("profile") or ctx.meta.get("profile")
profile = kwargs.pop("profile")
# A -P flag of a parent command overrides CLOUDSMITH_PROFILE.
if ctx.get_parameter_source("profile") != ParameterSource.COMMANDLINE:
profile = ctx.meta.get("profile") or profile
config_file = kwargs.pop("config_file") or ctx.meta.get("config_file")
creds_file = kwargs.pop("credentials_file") or ctx.meta.get("creds_file")

Expand Down
Loading
Loading