Skip to content
Open
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The CLI is a Click app that wraps the auto-generated `cloudsmith-api` Python SDK

### Push + metadata coupling

`cli/commands/push.py` is the most complex command. Every `push <format>` subcommand accepts `--metadata-*` flags resolved via `metadata_common`. Push validates metadata both locally and against the API **before** any file upload so malformed SBOM/BuildInfo payloads cannot leave orphan packages behind. Failure behavior is configurable with precedence: `--on-metadata-failure` flag > `$CLOUDSMITH_METADATA_FAILURE_MODE` env > `metadata_failure_mode` config key > `error` default. The kwarg names listed in `METADATA_KWARG_NAMES` and `METADATA_FAILURE_MODE_KWARG` must be popped off the kwargs before they reach the API client, which will reject unknown keys.
`cli/commands/push.py` is the most complex command. The per-format subcommands and their options are generated from the API's package-format model, so format-specific client-side behaviour is layered on top of that loop rather than branching inside it — see the `deb`-only `--dsc-file` registration at the end of `create_push_handlers()`, which pairs with `cli/dsc_parser.py` to derive `sources_file`/`changes_file` from a Debian `.dsc`. Every `push <format>` subcommand accepts `--metadata-*` flags resolved via `metadata_common`. Push validates metadata both locally and against the API **before** any file upload so malformed SBOM/BuildInfo payloads cannot leave orphan packages behind. Failure behavior is configurable with precedence: `--on-metadata-failure` flag > `$CLOUDSMITH_METADATA_FAILURE_MODE` env > `metadata_failure_mode` config key > `error` default. The kwarg names listed in `METADATA_KWARG_NAMES` and `METADATA_FAILURE_MODE_KWARG` must be popped off the kwargs before they reach the API client, which will reject unknown keys.

### Output formatting convention

Expand Down
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## [1.22.0] - 2026-08-11

### Added

- `cloudsmith push deb` now derives a Debian source package's members from its `.dsc`, so `cloudsmith push deb <owner>/<repo>/<distro>/<release> foo_1.0-1.dsc` is enough where `--sources-file` and `--changes-file` previously had to be worked out by hand (and their suffixes vary: `.orig.tar.gz`, `.orig.tar.bz2`, `.debian.tar.xz`, `.diff.gz`, ...). The `Checksums-Sha256:` or `Files:` field of the `.dsc` is read — plain or OpenPGP-clearsigned — and the upstream/native source archive becomes `--sources-file` while the Debian packaging archive becomes `--changes-file`, for the `1.0`, `2.0`, `3.0 (native)` and `3.0 (quilt)` source formats. `--dsc-file` names a `.dsc` other than `PACKAGE_FILE`, and an explicit `--sources-file` or `--changes-file` still wins for its own field. A detached upstream signature (`*.orig.tar.*.asc`) is skipped with a warning, since the deb package format has no field to carry it; a multi-component source package (`*.orig-<component>.tar.*`) is rejected outright, because leaving a component behind would upload incomplete source.
- `cloudsmith domains list` lists the hosts Cloudsmith can authenticate as a versioned JSON document: `{"version": 1, "domains": [{"host": ..., "format": ..., "type": ..., "domain_type": ..., "org": ..., "repository": ..., "primary": ..., "created_at": ...}]}`. The built-in list can be replaced by a `[domains]` section in a trusted `config.ini` — each entry maps a hostname to the format it serves, or to `download`/`upload` — for dedicated deployments. An organisation's own custom domains are listed ahead of the built-in hosts, and a custom domain that is disabled or not yet validated is left out entirely, since it serves nothing. `--format` and `--repo` narrow the list to the hosts usable for a package format or repository, most-preferred first, and `--domain-type` to those with one purpose: `download`, `upload`, `api` or `native_api`.
- The Cloudsmith organisation is now named by `--org`, with `--organization` and `--oidc-org` accepted as aliases for the same option, and `org`, `organization` or `oidc_org` accepted in `config.ini`. `--oidc-org` named the setting after the first feature that wanted it; it is read by custom-domain discovery as well as OIDC token exchange, so it is now named after what it is. The `CLOUDSMITH_ORG` environment variable is unchanged, and `credential-helper install` no longer has a separate `--org` of its own.

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ For example, if you wanted to upload a Debian package, you can do it in one-step
cloudsmith push deb your-org/your-repo/ubuntu/xenial libxml2-2.9.4-2.x86_64.deb
```

To upload a Debian *source* package, give it the `.dsc`. The source and Debian packaging archives listed in the `.dsc` are found and uploaded with it:

```
cloudsmith push deb your-org/your-repo/ubuntu/xenial hello_2.10-3.dsc
```

Want to know how to do it with another packaging format? Easy, just ask for help:

```
Expand Down
77 changes: 77 additions & 0 deletions cloudsmith_cli/cli/commands/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
validate_create_package as api_validate_create_package,
)
from .. import command, decorators, utils, validators
from ..dsc_parser import resolve_dsc_files
from ..exceptions import handle_api_exceptions
from ..metadata_common import (
MetadataContentError,
Expand Down Expand Up @@ -75,6 +76,10 @@
#: separately from the metadata payload kwargs so it does not leak into the
#: package-create API call.
METADATA_FAILURE_MODE_KWARG = "cli_metadata_failure_mode"
#: Filename suffix of a Debian source control file. A ``deb`` push whose
#: PACKAGE_FILE carries it is a source-package upload, so its members can be
#: derived without ``--dsc-file`` (GitHub issue #56).
DSC_SUFFIX = ".dsc"


def _metadata_failure_is_warn(opts=None):
Expand Down Expand Up @@ -1019,6 +1024,13 @@ def upload_files_and_create_package(
return slug_perm, slug


def _implied_dsc_file(package_file):
"""Return ``package_file`` if it is itself a Debian ``.dsc``, else None."""
if isinstance(package_file, str) and package_file.endswith(DSC_SUFFIX):
return package_file
return None


def create_push_handlers():
"""Create a handler for upload per package format."""
# pylint: disable=fixme
Expand Down Expand Up @@ -1181,6 +1193,38 @@ def push_handler(ctx, *args, **kwargs):
parameters = context.get(ctx.info_name)
kwargs["package_type"] = ctx.info_name

# deb-only: derive the upstream/native source archive and Debian
# packaging archive from a .dsc (see GitHub issue #56). Only the
# `deb` subcommand ever registers --dsc-file (below), so this is
# a no-op kwargs.pop() for every other format.
dsc_file = kwargs.pop("dsc_file", None)
if dsc_file is None and not kwargs.get("sources_file"):
# Pushing a Debian source package means passing the .dsc as
# PACKAGE_FILE, and the API rejects it without a sources
# archive, so parse it by default rather than making the user
# name the same file twice. An explicit --sources-file means
# the caller is driving the members manually; leave them to it.
dsc_file = _implied_dsc_file(kwargs.get("package_file"))
if dsc_file:
resolved_dsc = resolve_dsc_files(dsc_file)
if resolved_dsc.ignored_files:
click.secho(
"Not uploading {files}: the deb package format has no "
"field for detached upstream signatures.".format(
files=", ".join(resolved_dsc.ignored_files)
),
fg="yellow",
err=utils.should_use_stderr(opts),
)
# Precedence: explicit --sources-file/--changes-file always
# win over values derived from --dsc-file. A user who passes
# both wants a manual override, not to have their explicit
# flag silently clobbered.
if not kwargs.get("sources_file"):
kwargs["sources_file"] = resolved_dsc.sources_file
if not kwargs.get("changes_file"):
kwargs["changes_file"] = resolved_dsc.changes_file

owner_repo = kwargs.pop("owner_repo")
if "distribution" in parameters:
kwargs["distribution"] = "/".join(owner_repo[2:])
Expand Down Expand Up @@ -1307,6 +1351,39 @@ def push_handler(ctx, *args, **kwargs):
)
push_handler = decorator(push_handler)

if key == "deb":
# deb-only shim (GitHub issue #56): the generic per-format loop
# above is driven entirely by the API's package-format model, so
# this stays layered on top rather than becoming a new branch in
# that generic system.
push_handler = click.option(
"--dsc-file",
"dsc_file",
type=ExpandPath(
dir_okay=False, exists=True, writable=False, resolve_path=True
),
default=None,
help=(
"Path to a Debian .dsc control file, plain or "
"OpenPGP-clearsigned. Only needed to point at a .dsc "
"other than PACKAGE_FILE: pushing a source package means "
"passing the .dsc as PACKAGE_FILE, and that is parsed "
"automatically unless --sources-file is given. Its "
"'Checksums-Sha256:' (or 'Files:') field supplies "
"--sources-file with the upstream/native source archive "
"and --changes-file with the Debian packaging archive "
"(.debian.tar.* or .diff.gz). Supports the 1.0, 2.0, "
"3.0 (native) and 3.0 (quilt) source formats, and the "
"referenced files must sit next to the .dsc. An explicit "
"--sources-file or --changes-file always takes precedence "
"over the derived value for that field. Detached upstream "
"signatures (*.asc) are skipped with a warning; a "
"multi-component source package (*.orig-*.tar.*) is "
"rejected, because the deb package format has no field "
"for the extra components."
),
)(push_handler)

handlers[key] = push_handler


Expand Down
Loading