Skip to content

feat(ENG-13683): add Maven credential helper - #339

Open
cloudsmith-iduffy wants to merge 1 commit into
masterfrom
maven-shell-plugin
Open

feat(ENG-13683): add Maven credential helper#339
cloudsmith-iduffy wants to merge 1 commit into
masterfrom
maven-shell-plugin

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

The last of the stack, rebased onto master now that #336, #337 and #338 have
landed, and squashed to a single commit.

Maven has no credential-helper protocol, so this authenticates it with a shell
plugin instead. credential-helper install maven --org <org> --repo <repo>
writes an mvn shim that wraps every invocation in cloudsmith exec, which
injects an ephemeral mode-0600 settings.xml via mvn -s and deletes it when
the run ends — no token is ever written to durable configuration.

  • credential-helper shell-init prints the shell initialisation (bash, zsh,
    fish) that puts the shims directory first on PATH.
  • cloudsmith exec -- <command> is callable directly, for CI that should not
    touch PATH.
  • Publishing is opt-in: install prints the distributionManagement snippet to
    add to pom.xml.
  • Custom download/upload domains are discovered from the organisation at
    install time. Domains bound to a single repository are left out — they need
    URLs of a different shape, which is its own change — so a repository-scoped
    domain falls back to the default host. Which kind each host is gets recorded
    in package-managers.ini, so wrapped runs need no lookup — the run path
    makes no API call and no cache read.

Credential binding. One <server> id covers download and upload. It
defaults to the literal cloudsmith, matching the distributionManagement
snippet install prints, so a team can share one pom.xml. Maven matches a
server's credentials to a repository by id alone, with no host check, so a
checked-out pom.xml declaring a repository under that id receives the token
on an ordinary mvn compile — the same exposure as the ~/.m2/settings.xml a
Maven user would otherwise keep. Pass --server-id at install time to bind
the credential to an id a third-party pom.xml cannot guess; minting a random
id by default would break a team sharing one pom.xml, so it is opt-in.
Documented in the CHANGELOG's Security section and settings.py's module
docstring.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

Test suite: 682 passed, 40 skipped at this commit.

An earlier, larger draft of this branch was verified end to end in GitHub
Actions with OIDC as the only credential
(run):
a plain mvn -B clean deploy resolved a private dependency through the
organisation's discovered custom domain and published the built jar to
maven.cloudsmith.io/iduffy-demo/default/. That draft carried a cached
settings.xml, a shell fast path and deploy-goal detection; this PR ships
without them to halve the diff, and the shim path itself is unchanged.

Two things a reviewer should know:

  • Wrapped runs do not consult ~/.m2/settings.xml; mirrors, proxies and other
    <server> entries are not seen. Passing your own -s/--settings runs Maven
    unwrapped, with a warning. This is the designed mechanism — Maven has no
    settings-merge — and is called out at install time and in the CHANGELOG.
  • One known limitation ships knowingly. The ephemeral settings.xml is removed
    in a finally, so a SIGKILL/SIGTERM of cloudsmith exec (a cancelled CI step)
    leaves the 0600 token file in /tmp until the agent is recycled; a signal
    handler is the fix and wants its own change.

@cloudsmith-iduffy cloudsmith-iduffy changed the title maven shell plugin feat(ENG-13683): add Maven credential helper via shell plugin Aug 1, 2026
@BartoszBlizniak
BartoszBlizniak changed the base branch from credential-generic to custom-domains August 3, 2026 11:45
cloudsmith-iduffy added a commit that referenced this pull request Aug 4, 2026
This branch shipped an API surface nothing in it calls. Every consumer is in
the Maven helper (#339), which is stacked on top of this branch, so the code
and its tests move there, where the first caller lives and where a reviewer
can see what they are for.

Moved: select_custom_domain and CustomDomain.serves_repository; the
single-host resolvers default_host, default_host_for_type, builtin_host and
builtin_host_for_type with their two private helpers; and domain_scope.
With them go the twelve select_custom_domain tests, the six host-resolver
tests and the persisted-scope test.

Staying: _precedence_key and the CustomDomain.scope / is_bound_to pair it
reads, because get_format_domains sorts by it and the Docker installer and
runtime helper both call that. The DomainScope enum stays with them; only
its string parser moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from custom-domains to master August 9, 2026 01:15
@cloudsmith-iduffy
cloudsmith-iduffy force-pushed the maven-shell-plugin branch 2 times, most recently from d58abdb to da913d2 Compare August 9, 2026 02:35
@cloudsmith-iduffy cloudsmith-iduffy changed the title feat(ENG-13683): add Maven credential helper via shell plugin feat(ENG-13683): add Maven credential helper Aug 16, 2026
@cloudsmith-iduffy
cloudsmith-iduffy marked this pull request as ready for review August 16, 2026 20:09
@cloudsmith-iduffy
cloudsmith-iduffy requested a review from a team as a code owner August 16, 2026 20:09
Copilot AI lite review requested due to automatic review settings August 16, 2026 20:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Maven support to the credential-helper stack by installing an mvn shim that routes through a new cloudsmith exec command, which injects an ephemeral settings.xml (mode 0600) for authenticated dependency resolution and optional publishing, and cleans it up after each run. This extends the existing credential-helper infrastructure (launchers + default/custom domain handling) and adds targeted CLI wiring plus tests.

Changes:

  • Added Maven credential-helper implementation (binding persistence, domain selection, settings.xml generation, and runtime wrapper that injects -s <temp settings.xml> for Maven invocations).
  • Added cloudsmith exec -- <command> and cloudsmith credential-helper shell-init to support shim-based workflows and CI usage without PATH mutation.
  • Refactored/extended helper plumbing for frozen builds and default-domain resolution, plus comprehensive tests and CHANGELOG entries.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cloudsmith_cli/templates/maven_settings.xml.tmpl New template for injected Maven settings.xml (server credentials + active profile + repo URLs).
cloudsmith_cli/templates/maven_distribution_management.xml.tmpl New template for opt-in distributionManagement snippet for publishing.
cloudsmith_cli/credential_helpers/maven/settings.py Builds and securely writes ephemeral settings.xml; constructs download/upload URLs.
cloudsmith_cli/credential_helpers/maven/runner.py Implements runtime command wrapper (cloudsmith exec) and Maven detection/settings injection logic.
cloudsmith_cli/credential_helpers/maven/installer.py Implements install/uninstall/status for Maven shim and repository/domain binding.
cloudsmith_cli/credential_helpers/maven/config.py Persists Maven binding in package-managers.ini and defines shims directory.
cloudsmith_cli/credential_helpers/maven/init.py Adds Maven helper package entry point/module doc.
cloudsmith_cli/credential_helpers/launchers.py Adds frozen-build-safe command construction and public launcher filename helper.
cloudsmith_cli/credential_helpers/docker/installer.py Switches Docker launcher target command to shared cloudsmith_command(...) helper.
cloudsmith_cli/credential_helpers/default_domains.py Adds default_host/default_host_for_type helpers that honor trusted [domains] overrides strictly.
cloudsmith_cli/credential_helpers/common.py Adds default-host detection and repository path shaping based on host kind.
cloudsmith_cli/cli/tests/commands/test_default_domains.py Adds coverage for new default-host resolution semantics (including strict override behavior).
cloudsmith_cli/cli/tests/commands/test_credential_helper_maven.py New tests for Maven settings generation, binding persistence, and exec/shim runtime behavior.
cloudsmith_cli/cli/tests/commands/test_credential_helper_maven_installer.py New tests for Maven install/uninstall/list wiring and domain-binding behavior.
cloudsmith_cli/cli/tests/commands/conftest.py Adds fixtures for isolated CLI config dir and a resolved credential for helper tests.
cloudsmith_cli/cli/commands/exec_.py New cloudsmith exec Click command that resolves credentials and runs the wrapper.
cloudsmith_cli/cli/commands/credential_helper/shell.py New credential-helper shell-init command for PATH initialization snippets (bash/zsh/fish).
cloudsmith_cli/cli/commands/credential_helper/manage.py Wires Maven into credential-helper install/uninstall; adds --repo and --server-id.
cloudsmith_cli/cli/commands/credential_helper/init.py Registers shell-init and updates help text/examples to include Maven + generic usage.
cloudsmith_cli/cli/commands/init.py Registers new exec_ command module for CLI import-time command registration.
CHANGELOG.md Documents new Maven helper, exec, and shell-init, plus Maven server-id security note.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +80 to +81
if not (org and credential):
return []
Comment on lines +118 to +122
def _write(parser: configparser.ConfigParser) -> None:
path = config_path()
path.parent.mkdir(parents=True, exist_ok=True)
with click.open_file(str(path), "w") as handle:
parser.write(handle)
Maven has no credential-helper protocol, so `credential-helper install
maven --org <org> --repo <repo>` writes an `mvn` shim that wraps every
invocation in `cloudsmith exec`. Wrapped runs get a mode-0600
settings.xml injected via `mvn -s` and deleted when the run ends, so
dependency resolution authenticates with no pom.xml edits and nothing is
ever written to ~/.m2.

`cloudsmith exec -- <command>` is the same machinery callable directly,
for CI that would rather not touch PATH. `credential-helper shell-init`
prints the PATH line that activates the shims directory.

Publishing stays opt-in: install prints the distributionManagement
snippet to paste into pom.xml, keyed by the same `<server>` id the
generated settings.xml uses (`cloudsmith`, or `--server-id`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@BartoszBlizniak BartoszBlizniak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-approving with some comments:


# Flags that short-circuit Maven wherever they appear, so there is nothing to
# authenticate.
_SKIP_AUTH_ARGS = frozenset({"--help", "-h", "--version", "-v", "help"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare "help" in _SKIP_AUTH_ARGS matches anywhere in the args, so option values trip it: mvn -pl help clean install (a module named help) or mvn -P help ... silently runs without credential injection and fails on the first private dependency, with nothing pointing at the shim.

Since bare mvn help isn't a valid Maven 3 invocation anyway (help is only usable as a help:<goal> prefix, which this doesn't match), the entry buys nothing - I'd drop it and keep just the -h/--help/-v/--version flags.

file=sys.stderr,
)

temp_dir = tempfile.mkdtemp(prefix="cloudsmith-maven-")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the acknowledged settings.xml-outlives-a-killed-run limitation (_run_with_settings): I reproduced it - kill -9 of cloudsmith exec mid-run leaves the 0600 token file in /tmp. Worth noting SIGTERM (the ordinary CI-cancel signal) leaks the same way, since Python's default SIGTERM disposition skips finally too, so this is the common cancellation path rather than just the unblockable-SIGKILL corner.

Rather than the planned signal handler (which can't cover SIGKILL), consider not putting the token in the file at all: Maven interpolates ${env.NAME} in settings.xml, so the generated file could carry <password>${env.CLOUDSMITH_MAVEN_TOKEN}</password> and the runner pass the token in the child's environment. A leftover file is then harmless.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable finding ^ left it as it's interesting for children projects, maven can be nasty...

summary = "Maven credential helper for Cloudsmith repositories"
requires_repo = True

def install(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

install never checks that --repo actually exists, so a typo'd slug installs cleanly and only surfaces later as an opaque 401/404 from a wrapped mvn run. Since discovery already makes an authenticated API call at install time, a repo-existence check (or at least a warning when it can't be verified) would catch this where the user can still see the cause.

# the machine and refuses to run one it has no binding for, so a shim
# written ahead of a failed set_binding would leave Maven unusable
# rather than merely uninstalled.
config.set_binding(binding)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.set_binding(binding) silently replaces any existing binding, and the binding is machine-wide - so installing for project B quietly re-points every mvn on the machine away from project A's repo. Probably fine as a v1 constraint, but the install output should say it replaced old-org/old-repo when it did, so the developer working across two projects understands what just changed.

}


def detect_shell():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detect_shell falls back to bash, so on Windows (where $SHELL is unset) shell-init prints POSIX export PATH=... that neither cmd nor PowerShell can eval - even though the launcher machinery happily writes an mvn.cmd shim there. If Windows isn't meant to be supported yet, it'd be better for shell-init to say so explicitly than to print a statement that can't work; otherwise a powershell entry in _STATEMENTS would close the gap.

\b
$ cloudsmith exec -- mvn clean install
"""
sys.exit(runner.run(list(command), credential=opts.credential))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec's help promises "a package-manager command authenticated against Cloudsmith", but an unsupported tool passes through with no credentials and no message - cloudsmith exec -- npm ci runs plain npm and the user has no signal that npm isn't wired up. A one-line stderr note ("npm is not a supported package manager; running unwrapped") would prevent people relying on authentication they aren't getting.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable find - might be worth doublechecking docs vs execution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants