Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
cb39e1d
refactor(no-ticket): pass resolved credentials through credential-hel…
cloudsmith-iduffy Jul 31, 2026
d04eeb5
feat(ENG-13681): add credential-helper domains command
cloudsmith-iduffy Jul 31, 2026
53f0326
fix(ENG-13681): honour the [domains] override wherever hosts resolve
cloudsmith-iduffy Jul 31, 2026
4d4da21
feat(ENG-13683): record custom domain repository scope
cloudsmith-iduffy Jul 31, 2026
764f4b5
fix(ENG-13683): guard read_cache's format-version check against non-d…
cloudsmith-iduffy Jul 31, 2026
aa3e7bd
feat(ENG-13683): cache custom domains for seven days, never cache fai…
cloudsmith-iduffy Jul 31, 2026
58edef5
fix(ENG-13683): cache the 402 custom-domains answer, keep 404 uncached
cloudsmith-iduffy Jul 31, 2026
6d633cb
feat(ENG-13681): list cached organisations' custom domains
cloudsmith-iduffy Jul 31, 2026
313dac7
fix(no-ticket): honour an explicit --config-file for default domains
cloudsmith-iduffy Jul 31, 2026
12d6510
docs(ENG-13681): document repository scoping and the cached domain li…
cloudsmith-iduffy Jul 31, 2026
8ed90e7
feat(ENG-13681): add credential-helper generic command (#338)
cloudsmith-iduffy Aug 3, 2026
856fac0
feat(ENG-13683): add Maven credential helper via shell plugin
cloudsmith-iduffy Jul 31, 2026
3b9680e
fix(ENG-13683): address code review findings
cloudsmith-iduffy Jul 31, 2026
eac3ef4
feat(ENG-13683): build URLs for repository-scoped custom domains
cloudsmith-iduffy Jul 31, 2026
3ea56cb
feat(ENG-13683): bind Maven to repository-scoped custom domains
cloudsmith-iduffy Jul 31, 2026
102d444
fix(ENG-13683): drop repository-scoped hosts on a repo override
cloudsmith-iduffy Jul 31, 2026
209dcab
docs(ENG-13683): document repository-scoped domains in the Maven binding
cloudsmith-iduffy Jul 31, 2026
7e357ba
fix(no-ticket): keep the org segment for a trusted [domains] service …
cloudsmith-iduffy Jul 31, 2026
bbeb603
fix(no-ticket): apply minor findings from the whole-branch review
cloudsmith-iduffy Jul 31, 2026
a12ad54
fix(ENG-13683): bind injected Maven credential to a random per-invoca…
cloudsmith-iduffy Jul 31, 2026
32979fb
fix(ENG-13683): widen deploy detection to fully-qualified plugin coor…
cloudsmith-iduffy Jul 31, 2026
ec1bba9
docs(ENG-13683): record the residual deploy-time credential exposure
cloudsmith-iduffy Jul 31, 2026
152a192
fix(no-ticket): clean up stacked credential-helper rebase
BartoszBlizniak Aug 4, 2026
bc9c7ce
Merge branch 'custom-domains' into maven-shell-plugin
BartoszBlizniak Aug 4, 2026
1bca3d6
fix(ENG-13681): drop the unused 402 branch from custom-domain lookups
cloudsmith-iduffy Aug 4, 2026
2d42e7c
feat(ENG-13681): declare upload hosts in [domains], and stop falling …
cloudsmith-iduffy Aug 4, 2026
740c6ac
Merge branch 'custom-domains' into maven-shell-plugin
cloudsmith-iduffy Aug 4, 2026
ffa5872
feat(ENG-13681): choose overlapping custom domains by server precedence
cloudsmith-iduffy Aug 4, 2026
c2543f8
Merge branch 'custom-domains' into maven-shell-plugin
cloudsmith-iduffy Aug 4, 2026
b18d539
fix(ENG-13681): break precedence ties on host so selection is determi…
cloudsmith-iduffy Aug 4, 2026
6f3d515
Merge branch 'eng-13681-domain-precedence' into work/maven-precedence
cloudsmith-iduffy Aug 4, 2026
bb60188
feat(ENG-13683): bind Maven to the domain Cloudsmith treats as active
cloudsmith-iduffy Aug 4, 2026
9c58d52
fix(no-ticket): make API errors say what actually went wrong
cloudsmith-iduffy Aug 4, 2026
8ec0299
Merge branch 'eng-13681-domain-precedence' into work/maven-precedence
cloudsmith-iduffy Aug 4, 2026
a2299d6
fix(ENG-13681): stop an explicit --config-file masking a trusted [dom…
cloudsmith-iduffy Aug 4, 2026
faf78de
fix(ENG-13681): survive a cache directory that cannot be created
cloudsmith-iduffy Aug 4, 2026
7525812
fix(ENG-13681): read a null primary flag as primary, not secondary
cloudsmith-iduffy Aug 4, 2026
03bf36e
fix(ENG-13681): warn when --refresh has no organisation to fetch from
cloudsmith-iduffy Aug 4, 2026
515269d
docs(ENG-13681): say how to build a URL against a custom domain
cloudsmith-iduffy Aug 4, 2026
476c9d0
merge: bring in master's credential plumbing refactor (#336)
cloudsmith-iduffy Aug 4, 2026
333bf9c
refactor(ENG-13681): name the organisation option --org, not --oidc-org
cloudsmith-iduffy Aug 4, 2026
45db431
refactor(ENG-13681): move domains under `cloudsmith domains list`
cloudsmith-iduffy Aug 4, 2026
fc31192
fix(ENG-13681): carry the resolved organisation and API settings through
cloudsmith-iduffy Aug 4, 2026
71399bb
refactor(ENG-13681): read repository scope from the repository alone
cloudsmith-iduffy Aug 4, 2026
a04a9cd
docs(ENG-13681): keep server internals out, and cut the prose back
cloudsmith-iduffy Aug 4, 2026
59c9b1a
test(ENG-13681): fold duplicated cases together
cloudsmith-iduffy Aug 4, 2026
9a5b080
refactor(ENG-13681): move the unused domain-selection API to its caller
cloudsmith-iduffy Aug 4, 2026
6cc08cd
test(ENG-13681): cover precedence order through get_format_domains
cloudsmith-iduffy Aug 4, 2026
3608677
merge: bring in the slimmed custom-domains
cloudsmith-iduffy Aug 4, 2026
0cb3f91
refactor(ENG-13683): take ownership of the domain-selection API
cloudsmith-iduffy Aug 4, 2026
c3bc1b3
test(no-ticket): stop one test handing its SDK config to the rest of …
cloudsmith-iduffy Aug 4, 2026
0bd725c
refactor(ENG-13681): leave untouched code's comments alone
cloudsmith-iduffy Aug 4, 2026
c4aaf4d
fix(no-ticket): read every page of an organisation's custom domains
cloudsmith-iduffy Aug 4, 2026
820e77b
docs(ENG-13681): cut the changelog entries back to what changed
cloudsmith-iduffy Aug 4, 2026
aab2abe
fix(ENG-13681): ignore an organisation named only by an untrusted config
cloudsmith-iduffy Aug 4, 2026
3e7a634
feat(ENG-13681): report the date that breaks a precedence tie
cloudsmith-iduffy Aug 4, 2026
aa3b87d
merge: bring in the reviewed custom-domains base
cloudsmith-iduffy Aug 4, 2026
f72b795
fix(ENG-13683): read Maven goals, not whatever token looks like one
cloudsmith-iduffy Aug 4, 2026
f44d5b4
fix(ENG-13683): route --domain by the record discovery returned
cloudsmith-iduffy Aug 4, 2026
5725e26
docs(ENG-13683): cut the Maven entries back to what a user does
cloudsmith-iduffy Aug 4, 2026
d0a1e01
feat(ENG-13681): filter `domains list` by format and repository
cloudsmith-iduffy Aug 4, 2026
ae3ed6d
merge: bring in the domains-list filters
cloudsmith-iduffy Aug 4, 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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- `cloudsmith domains list` lists the hosts Cloudsmith can authenticate as a versioned JSON document: `{"version": 1, "domains": [{"host": ..., "format": ..., "enabled": ..., "validated": ..., "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 and on-premise deployments. `--format` and `--repo` narrow the list to the hosts usable for a package format or repository, most-preferred first.
- 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.
- `cloudsmith credential-helper install maven --org <org> --repo <repo>` sets up transparent Maven authentication. Maven has no credential-helper protocol, so the CLI installs an `mvn` shim — put the shims directory on `PATH` with `cloudsmith credential-helper shell-init` — that wraps each run in `cloudsmith exec` and injects an ephemeral, mode-0600 `settings.xml` via `mvn -s`, deleted when the run ends, so no token is written to durable configuration. Wrapped runs do not consult `~/.m2/settings.xml`; passing your own `-s/--settings` runs Maven unwrapped, with a warning. Custom download and upload domains are discovered automatically. Publishing is opt-in: `install` prints the `distributionManagement` snippet to add to `pom.xml`, and because Maven matches a `<server>` by id alone, the id that snippet names is supplied only on a deploy — an ordinary build gets a random one, so a `pom.xml` cannot obtain the token by declaring a repository under a guessed id.
- `cloudsmith exec -- <command>` runs a package-manager command with Cloudsmith credentials provisioned for that single run and cleaned up afterwards — the same machinery the `mvn` shim uses, callable directly (for example in CI, without touching `PATH`). `--org`/`--repo` override the stored binding for one invocation, while `CLOUDSMITH_ORG`/`CLOUDSMITH_REPO` apply only when nothing is stored.
- `cloudsmith credential-helper shell-init` prints the shell initialisation (bash, zsh and fish) that puts the Cloudsmith shims directory first on `PATH`, for `eval "$(cloudsmith credential-helper shell-init)"` in a shell rc file.


### Fixed

- An organisation named only by a `config.ini` in the current directory is no longer used to decide which hosts may receive a credential. That file travels with whatever repository is checked out, so it is now filtered like `api_host`: `--org` and `CLOUDSMITH_ORG` are honoured, a directory-relative `config.ini` is ignored with a warning. A user-level `config.ini` is unaffected.
- Custom-domain lookups now read every page of results. Only the first page was ever requested, so an organisation with more custom domains than fit on one page lost the rest — and since that list decides which hosts may receive a credential, the Docker helper refused credentials for hosts the organisation owns.
- API errors now say what went wrong. `ApiException` was constructed without passing anything to `Exception`, so rendering one as a string produced nothing at all — any message built by interpolation stopped dead at the colon, for example `Failed to fetch custom domains for 'acme': ` with no mention of the 401 behind it. It now renders as its status plus the API's detail (or the status description, when the API sends no detail), which fixes that message along with the `credential-helper install`/`uninstall` failures and the Docker auto-discovery warning.

## [1.21.0] - 2026-08-03

### Added
Expand Down
2 changes: 2 additions & 0 deletions cloudsmith_cli/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
delete,
dependencies,
docs,
domains,
download,
entitlements,
exec_,
help_,
list_,
login,
Expand Down
27 changes: 22 additions & 5 deletions cloudsmith_cli/cli/commands/credential_helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,45 @@
from .docker import docker as docker_cmd
from .generic import generic as generic_cmd
from .manage import install_cmd, list_cmd, uninstall_cmd
from .shell import shell_init


@click.group()
def credential_helper():
"""
Credential helpers for package managers.

These commands provide credentials for package managers like Docker.
Use ``install`` to set up the on-PATH launcher and configure the package
manager automatically, or run the runtime command directly for debugging.
Use ``install`` to set up a helper and configure the package manager
automatically. Docker uses a native credential-helper launcher; Maven has
no such protocol, so it uses an ``mvn`` shim plus ``cloudsmith exec`` —
activate the shims directory with ``credential-helper shell-init``.

``generic`` and ``domains`` emit machine-readable JSON for tools that
shell out to the CLI instead of importing it.

Examples:
# Install Docker credential helper

\b
# Install the Docker credential helper
$ cloudsmith credential-helper install docker

# Test Docker credential helper directly
\b
# Install the Maven helper for one repository
$ cloudsmith credential-helper install maven --org my-org --repo my-repo

\b
# Test the Docker credential helper directly
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker

\b
# Emit a credential as JSON
$ cloudsmith credential-helper generic
"""


credential_helper.add_command(docker_cmd, name="docker")
credential_helper.add_command(generic_cmd, name="generic")
credential_helper.add_command(shell_init, name="shell-init")
credential_helper.add_command(install_cmd, name="install")
credential_helper.add_command(uninstall_cmd, name="uninstall")
credential_helper.add_command(list_cmd, name="list")
Expand Down
16 changes: 13 additions & 3 deletions cloudsmith_cli/cli/commands/credential_helper/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
import click

from ....credential_helpers.docker import execute
from ...decorators import common_api_auth_options, resolve_credentials
from ...decorators import (
common_api_auth_options,
org_from_trusted_source,
resolve_credentials,
)


@click.command()
@click.argument("operation", required=False, default="get")
@common_api_auth_options
@resolve_credentials
def docker(opts, operation):
@click.pass_context
def docker(ctx, opts, operation):
"""
Docker credential helper for Cloudsmith registries.

Expand All @@ -29,7 +34,11 @@ def docker(opts, operation):

Provides credentials for all Cloudsmith Docker registries: ``*.cloudsmith.io``,
``*.cloudsmith.com``, and any custom domains configured for the organisation
(requires CLOUDSMITH_ORG and a valid API key/token).
(requires an organisation - ``--org``, CLOUDSMITH_ORG, or ``org`` in a
user-level ``config.ini`` - and a valid API key/token). An organisation
named only by a ``config.ini`` in the current directory is ignored: it
chooses which hosts receive the token, so it is filtered on the same rule
as ``api_host``.

Input (stdin):
Server URL as plain text (e.g. "docker.cloudsmith.io")
Expand Down Expand Up @@ -57,6 +66,7 @@ def docker(opts, operation):
sys.stdin,
credential=opts.credential,
api_host=opts.api_host,
org=org_from_trusted_source(ctx, opts),
)

if stdout is not None:
Expand Down
3 changes: 2 additions & 1 deletion cloudsmith_cli/cli/commands/credential_helper/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def generic(opts):
Resolves a credential through the full provider chain and writes a
versioned JSON document to stdout. Takes no arguments: a Cloudsmith token
is organisation-wide, so the host it will be used against does not change
which credential resolves.
which credential resolves. Use ``cloudsmith domains list`` to
discover which hosts Cloudsmith can authenticate.

\b
Output (stdout):
Expand Down
72 changes: 59 additions & 13 deletions cloudsmith_cli/cli/commands/credential_helper/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

from __future__ import annotations

import os
import sys

import click

from ....credential_helpers.docker.installer import DockerInstaller
from ....credential_helpers.maven.installer import MavenInstaller
from ....credential_helpers.shellplugin.config import DEFAULT_REGISTRY_ID
from ... import utils
from ...decorators import (
common_api_auth_options,
Expand All @@ -28,6 +29,7 @@

_INSTALLERS: dict[str, type] = {
"docker": DockerInstaller,
"maven": MavenInstaller,
}


Expand Down Expand Up @@ -74,7 +76,8 @@ def _get_installer(name: str):
"--domain",
"domains",
multiple=True,
help="Additional registry hostname to configure (repeatable).",
help="Registry hostname to configure (repeatable). Docker adds each host; "
"maven routes each to its download or upload endpoint.",
)
@click.option(
"--dry-run",
Expand All @@ -86,7 +89,7 @@ def _get_installer(name: str):
"--no-discover",
is_flag=True,
default=False,
help="Disable automatic discovery of custom Docker domains.",
help="Disable automatic discovery of custom domains.",
)
@click.option(
"--refresh",
Expand All @@ -95,9 +98,17 @@ def _get_installer(name: str):
help="Bypass the custom-domain cache and fetch fresh data from the API.",
)
@click.option(
"--org",
"--repo",
default=None,
help="Cloudsmith organisation slug for custom-domain discovery.",
envvar="CLOUDSMITH_REPO",
help="Cloudsmith repository slug.",
)
@click.option(
"--registry-id",
default=DEFAULT_REGISTRY_ID,
show_default=True,
help="Id the credentials are registered under in your project config "
"(e.g. the Maven settings.xml/pom.xml <server> id).",
)
@common_cli_config_options
@common_cli_output_options
Expand All @@ -113,18 +124,25 @@ def install_cmd(
dry_run: bool,
no_discover: bool,
refresh: bool,
org: str | None,
repo: str | None,
registry_id: str,
) -> None:
"""Install a credential helper launcher and configure the package manager.

HELPER is the name of the credential helper to install (e.g. ``docker``).
HELPER is the name of the credential helper to install (``docker`` or
``maven``). The maven helper binds one repository, so it requires
``--org`` and ``--repo``.

Examples:

\b
# Install Docker credential helper
$ cloudsmith credential-helper install docker

\b
# Install the Maven helper for one repository
$ cloudsmith credential-helper install maven --org my-org --repo my-repo

\b
# Install with a custom domain
$ cloudsmith credential-helper install docker --domain my.registry.example.com
Expand All @@ -138,19 +156,44 @@ def install_cmd(
$ cloudsmith credential-helper install docker --no-discover
"""
installer = _get_installer(helper)
org = org or os.environ.get("CLOUDSMITH_ORG", "").strip() or None

org = opts.org
repo = repo.strip() or None if repo else None

per_repo = getattr(installer, "requires_repo", False)
if per_repo and not (org and repo):
click.echo(
f"Error: helper {helper!r} requires --org and --repo.",
err=True,
)
sys.exit(1)

# Shell plugins (per-repo) keep their shims in a fixed dir; --bin-dir only
# applies to launcher-based helpers like Docker.
if per_repo:
if bin_dir:
click.echo(
f"Warning: --bin-dir is ignored for {helper!r}; its shim lives "
"in a fixed shims directory.",
err=True,
)
extra: dict = {"repo": repo, "registry_id": registry_id}
else:
extra = {"bin_dir": bin_dir}
try:
actions = installer.install(
bin_dir=bin_dir,
domains=domains,
dry_run=dry_run,
discover=not no_discover,
refresh=refresh,
org=org,
org=opts.org,
credential=opts.credential,
api_host=opts.api_host,
**extra,
)
except OSError as exc:
except (OSError, ValueError) as exc:
# ValueError: an incomplete trusted [domains] table has no host for a
# kind the helper needs, which is a config error to report, not a crash.
raise click.ClickException(
f"Failed to install {helper!r} credential helper: {exc}"
)
Expand Down Expand Up @@ -210,9 +253,12 @@ def uninstall_cmd(ctx, opts, helper: str, bin_dir: str | None, dry_run: bool) ->
$ cloudsmith credential-helper uninstall docker --dry-run
"""
installer = _get_installer(helper)
extra = {} if getattr(installer, "requires_repo", False) else {"bin_dir": bin_dir}
try:
actions = installer.uninstall(bin_dir=bin_dir, dry_run=dry_run)
except OSError as exc:
actions = installer.uninstall(dry_run=dry_run, **extra)
except (OSError, ValueError) as exc:
# ValueError: an incomplete trusted [domains] table has no host for a
# kind the helper needs, which is a config error to report, not a crash.
raise click.ClickException(
f"Failed to uninstall {helper!r} credential helper: {exc}"
)
Expand Down
36 changes: 36 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2026 Cloudsmith Ltd
"""``cloudsmith credential-helper shell-init`` — print shell init for shims.

Add ``eval "$(cloudsmith credential-helper shell-init)"`` to your shell rc file
to put the Cloudsmith shims directory ahead of the real package-manager
binaries on ``$PATH``.
"""

import click

from ....credential_helpers.shellplugin.shellinit import detect_shell, generate_init


@click.command(name="shell-init")
@click.option(
"--shell",
"shell_name",
type=click.Choice(["bash", "zsh", "fish"]),
default=None,
help="Target shell. Auto-detected from $SHELL when omitted.",
)
def shell_init(shell_name):
"""Print shell init that puts the Cloudsmith shims dir first on PATH.

Examples:

\b
# bash / zsh
$ eval "$(cloudsmith credential-helper shell-init)"

\b
# fish
$ cloudsmith credential-helper shell-init --shell fish | source
"""
shell = shell_name or detect_shell()
click.echo(generate_init(shell), nl=False)
Loading