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
31 changes: 30 additions & 1 deletion cloudsmith_cli/cli/commands/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# pylint: disable=too-many-lines

import json
import math
import os
import shlex
Expand Down Expand Up @@ -694,7 +695,7 @@ def display_status(current):
# When using stderr for logs, avoid an interactive progress bar and just poll for status.
while True:
res = get_package_status(owner, repo, slug)
ok, failed, _, _, _, _ = res
ok, failed, _, status_str, stage_str, reason = res
if ok or failed:
break

Expand Down Expand Up @@ -811,6 +812,34 @@ def display_status(current):
attempts=attempts,
)
else:
if use_stderr:
# In JSON output mode the human-readable text above went to
# stderr, so stdout still needs a machine-readable envelope.
# Mirror the shape produced by ``handle_api_exceptions`` so a
# sync failure is parseable the same way as an API failure.
error_data = {
"detail": reason or "Package failed to synchronise.",
"help": {
"context": context_msg,
"hint": None,
},
"meta": {
"status": status_str,
"stage": stage_str or "Unknown",
"seconds": seconds,
},
}

metadata_context = getattr(opts, "push_metadata_info", None)
if metadata_context is not None:
error_data["metadata_attachment"] = metadata_context

click.echo(
json.dumps(
error_data, indent=4 if opts.output == "pretty_json" else None
)
)

ctx.exit(1)


Expand Down
52 changes: 52 additions & 0 deletions cloudsmith_cli/cli/tests/test_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
resolve_push_metadata_options,
upload_files_and_create_package,
validate_metadata_payload,
wait_for_package_sync,
)
from ..metadata_common import ResolvedMetadata

Expand Down Expand Up @@ -1466,3 +1467,54 @@ def test_options_metadata_failure_mode_none_is_noop():
opts = Options()
opts.metadata_failure_mode = None
assert opts.metadata_failure_mode is None


def test_wait_for_package_sync_json_mode_prints_status_reason_on_failure(capsys):
"""Failed sync under JSON output must print the server's status_reason.

The JSON poll path used to discard status_reason, then crash with
UnboundLocalError at ``if reason:`` instead of showing the message.
"""
ctx = click.Context(click.Command("push"))
opts = SimpleNamespace(output="json")
status_reason = (
"A package with name 'eng-13978-repro' already exists. "
"This package should be deleted."
)
failed_status = (
False,
True,
100,
"Failed",
"Parsing Package Metadata",
status_reason,
)

with patch(
"cloudsmith_cli.cli.commands.push.get_package_status",
return_value=failed_status,
):
with pytest.raises(click.exceptions.Exit) as exc_info:
wait_for_package_sync(
ctx=ctx,
opts=opts,
owner="bart-demo-org-terraform",
repo="eng-13978-cli-repro",
slug="eng-13978-repro-100-alpha4tgz",
wait_interval=1.0,
skip_errors=False,
attempts=1,
)

assert exc_info.value.exit_code == 1
captured = capsys.readouterr()
assert "Package failed to synchronise" in captured.err
assert "Parsing Package Metadata" in captured.err
assert "Reason given:" in captured.err
assert status_reason in captured.err

# JSON mode must still leave a machine-readable envelope on stdout.
envelope = json.loads(captured.out)
assert envelope["detail"] == status_reason
assert envelope["meta"]["stage"] == "Parsing Package Metadata"
assert envelope["meta"]["status"] == "Failed"