Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `cloudsmith credential-helper install` and `uninstall` no longer rewrite a config file when they make no change to its data. Previously, the CLI always rewrote a file in a different formatting, such as the tab-indented `config.json` that Docker writes. The command also made a `.bak` copy and reported changes that it did not make.
- When the API host name or the proxy host name does not exist, for example because of a typo in `--api-host`, commands now fail immediately. Previously, they retried for approximately 62 seconds without output. The CLI still retries a DNS failure that the operating system reports as temporary.
- When the CLI waits before it retries a failed connection or a 5xx response, it now writes `Request failed: retrying in N second(s) ...` to stderr. Previously, it waited without output.
- 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.

## [1.30.0] - 2026-10-22
Expand Down
8 changes: 8 additions & 0 deletions cloudsmith_cli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections import OrderedDict

import click.exceptions
from click.utils import _PacifyFlushWrapper
from click_didyoumean import DYMGroup

from ..core.certificate_errors import describe_certificate_error
Expand Down Expand Up @@ -202,6 +203,13 @@ def main(self, *args, **kwargs):
e.show()
_echo_certificate_hint(e)
sys.exit(e.exit_code)
except BrokenPipeError:
# Click does not catch a failed write in its Ctrl+C handler.
if not original_standalone_mode:
raise
sys.stdout = _PacifyFlushWrapper(sys.stdout)
sys.stderr = _PacifyFlushWrapper(sys.stderr)
sys.exit(1)
Comment thread
cloudsmith-iduffy marked this conversation as resolved.
Resolved
except Exception as e: # pylint: disable=broad-exception-caught
# Anything reaching here is unexpected: click's own errors and
# handled API errors (which exit via ctx.exit) are dealt with
Expand Down
37 changes: 37 additions & 0 deletions cloudsmith_cli/cli/tests/test_unexpected_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"""

import json
import os
import runpy
import subprocess
import sys
from unittest.mock import patch

Expand Down Expand Up @@ -309,6 +311,41 @@ def test_click_errors_are_not_reported(runner, tmp_path, sentry_events, exc):
assert sentry_events == []


def test_interrupt_with_a_closed_stderr_is_not_reported(
runner, tmp_path, sentry_events
):
"""Click writes a newline on Ctrl+C, which fails when the pipe is gone."""
with (
whoami_raises(KeyboardInterrupt()),
patch("click.core.echo", side_effect=BrokenPipeError),
):
result = runner.invoke(main, whoami_args(tmp_path))

assert result.exit_code == 1
assert result.stderr == ""
assert sentry_events == []


def test_interrupt_with_a_closed_stderr_exits_1(tmp_path):
"""Python flushes stderr again at exit, which must not change the status."""
code = f"""
import os
from cloudsmith_cli.cli.commands.main import main
from cloudsmith_cli.cli.tests.test_unexpected_errors import whoami_args, whoami_raises

read_end, write_end = os.pipe()
os.close(read_end)
os.dup2(write_end, 2)
with whoami_raises(KeyboardInterrupt()):
main(whoami_args({str(tmp_path)!r}))
"""
# Unbuffered stderr keeps no bytes for the exit flush, which hides the failure.
env = {**os.environ, "PYTHONUNBUFFERED": ""}
result = subprocess.run([sys.executable, "-c", code], env=env, check=False)

assert result.returncode == 1


STRICT_HINT = "Hint: The TLS certificate failed a strict X.509 check"


Expand Down
Loading