diff --git a/.circleci/config.yml b/.circleci/config.yml
index 95f98f8..1e35e82 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,67 +1,65 @@
---
version: 2.1
-
-parameters:
- publish-orb-name:
- type: string
- default: cloudsmith/cloudsmith
+setup: true
orbs:
orb-tools: circleci/orb-tools@12.4.0
+ shellcheck: circleci/shellcheck@3.4.0
# Run jobs on all branches and tags
filters: &filters
tags:
only: /.*/
-# Run jobs only on semver release tags (e.g. v2.0.0)
-release-filters: &release-filters
- branches:
- ignore: /.*/
- tags:
- only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
+jobs:
+ # The installer in src/scripts/install.sh is vendored from
+ # https://github.com/cloudsmith-io/cloudsmith-cli-install-script and must
+ # only change together with src/scripts/install.sh.version.
+ installer-drift-check:
+ docker:
+ - image: cimg/base:current
+ steps:
+ - checkout
+ - run:
+ name: Verify vendored installer matches the recorded checksum
+ command: |
+ expected="$(awk -F= '$1 == "sha256" {print $2}' src/scripts/install.sh.version)"
+ actual="$(sha256sum src/scripts/install.sh | awk '{print $1}')"
+ if [ "$expected" != "$actual" ]; then
+ echo "src/scripts/install.sh does not match src/scripts/install.sh.version" >&2
+ echo "expected sha256: $expected" >&2
+ echo "actual sha256: $actual" >&2
+ echo "Update both files together when vendoring a new installer release." >&2
+ exit 1
+ fi
+ version="$(awk -F= '$1 == "version" {print $2}' src/scripts/install.sh.version)"
+ echo "Vendored installer (version: $version) matches the recorded checksum."
workflows:
- lint-pack-publish:
+ lint-pack:
jobs:
- # Lint all YAML files in src/
- orb-tools/lint:
filters: *filters
- # Pack src/ into a single orb.yml and validate it
- orb-tools/pack:
filters: *filters
- # Review orb source against CircleCI best practices
- orb-tools/review:
- filters: *filters
orb_name: cloudsmith
- # RC009: long inline commands (we use inline bash intentionally)
- # RC010: snake_case naming (our params use kebab-case by convention)
+ # RC009: inline commands are used for trivial steps intentionally
+ # RC010: parameters use kebab-case by convention
exclude: RC009,RC010
- # Publish a dev version on every push (except release tags and PRs)
- - orb-tools/publish:
- name: publish-dev
- orb_name: << pipeline.parameters.publish-orb-name >>
- vcs_type: << pipeline.project.type >>
- pub_type: dev
- context: orb-publishing
- requires:
- - orb-tools/lint
- - orb-tools/pack
- - orb-tools/review
- filters:
- branches:
- ignore: /^pull\/[0-9]+/
- tags:
- ignore: /^v[0-9]+\.[0-9]+\.[0-9]+$/
- # Publish a production version when a semver tag is pushed
- - orb-tools/publish:
- name: publish-production
- orb_name: << pipeline.parameters.publish-orb-name >>
+ filters: *filters
+ - shellcheck/check:
+ filters: *filters
+ - installer-drift-check:
+ filters: *filters
+ - orb-tools/continue:
+ pipeline_number: << pipeline.number >>
vcs_type: << pipeline.project.type >>
- pub_type: production
- context: orb-publishing
+ orb_name: cloudsmith
requires:
- orb-tools/lint
- orb-tools/pack
- orb-tools/review
- filters: *release-filters
+ - shellcheck/check
+ - installer-drift-check
+ filters: *filters
diff --git a/.circleci/test-deploy.yml b/.circleci/test-deploy.yml
new file mode 100644
index 0000000..f28e975
--- /dev/null
+++ b/.circleci/test-deploy.yml
@@ -0,0 +1,166 @@
+---
+version: 2.1
+
+orbs:
+ # The orb built from src/ is injected here by orb-tools/continue.
+ cloudsmith: {}
+ orb-tools: circleci/orb-tools@12.4.0
+
+parameters:
+ test-organization:
+ type: string
+ default: ""
+ description: Cloudsmith organization used by the OIDC integration test.
+ test-service-account:
+ type: string
+ default: ""
+ description: Cloudsmith service account used by the OIDC integration test.
+
+# Run jobs on all branches and tags
+filters: &filters
+ tags:
+ only: /.*/
+
+# Run jobs only on semver release tags (e.g. v3.0.0)
+release-filters: &release-filters
+ branches:
+ ignore: /.*/
+ tags:
+ only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
+
+executors:
+ linux-docker:
+ docker:
+ - image: cimg/base:current
+ alpine-docker:
+ docker:
+ - image: alpine:3.21
+ linux-machine:
+ machine:
+ image: ubuntu-2404:current
+ macos:
+ macos:
+ xcode: "16.2"
+
+jobs:
+ install-test:
+ parameters:
+ executor:
+ type: executor
+ executor: << parameters.executor >>
+ steps:
+ - run:
+ name: Install job dependencies (Alpine only)
+ shell: /bin/sh
+ command: |
+ if command -v apk > /dev/null 2>&1; then
+ apk add --no-cache bash curl ca-certificates
+ fi
+ - cloudsmith/install-cli
+ - run:
+ name: Verify the CLI is on PATH in later steps
+ command: |
+ command -v cloudsmith
+ cloudsmith --version
+
+ install-pinned-version-test:
+ executor: linux-docker
+ steps:
+ - cloudsmith/install-cli:
+ cli-version: "1.19.2"
+ - run:
+ name: Verify the pinned CLI version was installed
+ command: |
+ cloudsmith --version | grep -Fx "CLI Package Version: 1.19.2"
+
+ api-key-test:
+ executor: linux-docker
+ steps:
+ - run:
+ name: Skip unless CLOUDSMITH_API_KEY is available
+ command: |
+ if [ -z "${CLOUDSMITH_API_KEY:-}" ]; then
+ echo "CLOUDSMITH_API_KEY is not set; skipping the API key test."
+ circleci-agent step halt
+ fi
+ - cloudsmith/ensure-api-key
+ - cloudsmith/install-cli
+ - run:
+ name: Verify API key authentication
+ command: cloudsmith whoami
+
+ oidc-test:
+ executor: linux-docker
+ steps:
+ - run:
+ name: Skip unless OIDC testing is enabled
+ command: |
+ if [ "${CLOUDSMITH_OIDC_TEST:-}" != "true" ]; then
+ echo "CLOUDSMITH_OIDC_TEST is not 'true'; skipping the OIDC test."
+ circleci-agent step halt
+ fi
+ if [ -z "<< pipeline.parameters.test-organization >>" ] || [ -z "<< pipeline.parameters.test-service-account >>" ]; then
+ echo "test-organization/test-service-account pipeline parameters are not set; skipping the OIDC test."
+ circleci-agent step halt
+ fi
+ if [ -z "${CIRCLE_OIDC_TOKEN_V2:-}" ] && [ -z "${CIRCLE_OIDC_TOKEN:-}" ]; then
+ echo "No OIDC token; the job must use a context. Skipping the OIDC test."
+ circleci-agent step halt
+ fi
+ - cloudsmith/install-cli
+ - cloudsmith/configure-oidc:
+ organization: << pipeline.parameters.test-organization >>
+ service-account: << pipeline.parameters.test-service-account >>
+ verify-auth: true
+ - run:
+ name: Verify OIDC configuration persists to later steps
+ command: cloudsmith whoami
+
+workflows:
+ test-deploy:
+ jobs:
+ - install-test:
+ matrix:
+ parameters:
+ executor: [linux-docker, alpine-docker, linux-machine, macos]
+ filters: *filters
+ - install-pinned-version-test:
+ filters: *filters
+ - api-key-test:
+ context: cloudsmith-api-key-test
+ filters: *filters
+ - oidc-test:
+ context: orb-publishing
+ filters: *filters
+ - orb-tools/pack:
+ filters: *filters
+ # Publish a dev version on every push (except forked PRs and release tags)
+ - orb-tools/publish:
+ name: publish-dev
+ orb_name: cloudsmith/cloudsmith
+ vcs_type: << pipeline.project.type >>
+ pub_type: dev
+ context: orb-publishing
+ requires:
+ - orb-tools/pack
+ - install-test
+ - install-pinned-version-test
+ - api-key-test
+ - oidc-test
+ filters:
+ branches:
+ ignore: /^pull\/[0-9]+/
+ # Publish a production version when a semver tag is pushed
+ - orb-tools/publish:
+ name: publish-production
+ orb_name: cloudsmith/cloudsmith
+ vcs_type: << pipeline.project.type >>
+ pub_type: production
+ context: orb-publishing
+ requires:
+ - orb-tools/pack
+ - install-test
+ - install-pinned-version-test
+ - api-key-test
+ - oidc-test
+ filters: *release-filters
diff --git a/README.md b/README.md
index d273528..66c9374 100644
--- a/README.md
+++ b/README.md
@@ -1,132 +1,252 @@
# Cloudsmith Orb for CircleCI
-CircleCI orb for publishing packages to (and interacting with) Cloudsmith repositories.
+[](https://dl.circleci.com/status-badge/redirect/gh/cloudsmith-io/orb/tree/master)
+[](https://circleci.com/developer/orbs/orb/cloudsmith/cloudsmith)
-See [onsite documentation](https://circleci.com/orbs/registry/orb/cloudsmith/cloudsmith) for further details.
+Install the standalone [Cloudsmith CLI](https://github.com/cloudsmith-io/cloudsmith-cli), add it to `PATH`, and configure authentication for the rest of a CircleCI job. The orb does not require Python, pip, or `jq` on the executor image.
-## Commands
+[Quick start](#quick-start) · [Configuration](#configuration) · [Migration guide](#migrating-from-v2) · [Contributing](#contributing)
-### `authenticate-with-oidc`
+## At a glance
-Authenticate with Cloudsmith using OpenID Connect (OIDC) to obtain a short-lived API token. The token is exported as the `CLOUDSMITH_API_KEY` environment variable for use by the Cloudsmith CLI or any subsequent steps.
+| Capability | Support |
+| --- | --- |
+| Authentication | OpenID Connect (OIDC) or API key |
+| Executors | Linux and macOS with `bash` and `curl` or `wget` |
+| Architectures | x86-64, plus Linux and macOS ARM64 |
+| Installation | Standalone CLI binary with SHA-256 download verification |
+| Version selection | Latest release or a specific CLI version |
-| Parameter | Type | Default | Description |
-|---|---|---|---|
-| `organization` | string | *required* | Cloudsmith organization name |
-| `service-account` | string | *required* | Cloudsmith service account name |
-| `oidc-audience` | string | `""` | Custom audience for the OIDC token exchange (omitted when empty) |
-| `oidc-auth-retry` | integer | `3` | Number of token exchange attempts (5 s delay between retries) |
+## Quick start
+### Authenticate with OIDC
-### `install-cli`
-
-Installs the Cloudsmith CLI by downloading the zipapp from Cloudsmith. Set `pip-install: true` to install via pip instead. Optional parameters configure the CLI via `~/.cloudsmith/config.ini`.
-
-| Parameter | Type | Default | Description |
-|---|---|---|---|
-| `cli-version` | string | `""` | Pin a specific CLI version (e.g. `"1.2.0"`). Empty installs the latest |
-| `pip-install` | boolean | `false` | Install via pip instead of the default zipapp |
-| `install-path` | string | `$HOME/bin` | Directory where the zipapp binary is installed and added to `PATH` (ignored when using pip) |
-| `api-host` | string | `""` | Override `api_host` in config.ini (default: `api.cloudsmith.io`) |
-| `api-proxy` | string | `""` | HTTP/HTTPS proxy (`api_proxy` in config.ini) |
-| `api-ssl-verify` | boolean | `true` | Enable/disable SSL verification (`api_ssl_verify` in config.ini) |
-| `api-user-agent` | string | `""` | Custom user-agent (`api_user_agent` in config.ini) |
-
-### `ensure-api-key`
+OIDC is the recommended option for CI/CD because it uses short-lived credentials instead of a stored API key. Configure a Cloudsmith service account and an OIDC provider by following the [Cloudsmith OIDC documentation](https://docs.cloudsmith.com/authentication/openid-connect) before using this example.
-Validates that the `CLOUDSMITH_API_KEY` environment variable is set. Fails the build immediately if it is missing.
+> [!IMPORTANT]
+> The job must use at least one [CircleCI context](https://circleci.com/docs/contexts/). Without a context, CircleCI does not issue the OIDC token used to authenticate with Cloudsmith.
-### `publish` *(deprecated)*
+```yaml
+version: 2.1
-Wraps individual `cloudsmith push` calls. This command will be removed in a future major version. The recommended approach is to call `install-cli` and `authenticate-with-oidc` (or `ensure-api-key`), then invoke the Cloudsmith CLI directly in your run steps.
+orbs:
+ cloudsmith: cloudsmith/cloudsmith@3.0.0
-## Executor
+workflows:
+ verify:
+ jobs:
+ - verify:
+ context: my-context
-The `default` executor uses the `cimg/python` convenience image (default tag `3.10`), which has the prerequisites for installing the Cloudsmith CLI.
+jobs:
+ verify:
+ executor: cloudsmith/default
+ steps:
+ - cloudsmith/install-cli
+ - cloudsmith/configure-oidc:
+ organization: YOUR-NAMESPACE
+ service-account: YOUR-SERVICE-ACCOUNT
+ - run: cloudsmith whoami
+```
-## Usage
+### Authenticate with an API key
-### Recommended — OIDC authentication with direct CLI usage
+Provide the API key through the `CLOUDSMITH_API_KEY` environment variable, typically from a [CircleCI context](https://circleci.com/docs/contexts/). For automated pipelines, use a [Cloudsmith service account](https://docs.cloudsmith.com/accounts-and-teams/service-accounts) rather than a personal API key.
```yaml
version: 2.1
orbs:
- cloudsmith: cloudsmith/cloudsmith@2.0.0
+ cloudsmith: cloudsmith/cloudsmith@3.0.0
workflows:
- publish:
+ verify:
jobs:
- - publish
+ - verify:
+ context: my-context
jobs:
- publish:
+ verify:
executor: cloudsmith/default
steps:
- - checkout
- - cloudsmith/authenticate-with-oidc:
- organization: my-org
- service-account: my-service-account
+ - cloudsmith/ensure-api-key
- cloudsmith/install-cli
- - run:
- name: Build and publish Python package
- command: |
- pip install build
- python -m build --wheel
- cloudsmith push python my-org/my-repo dist/*.whl
+ - run: cloudsmith whoami
```
-### API key authentication
+Personal API keys are available from [Cloudsmith API settings](https://cloudsmith.io/user/settings/api/).
+
+## Authentication
+
+Choose one of the following authentication methods:
+
+| Method | Configuration | Credential handling | Best suited to |
+| --- | --- | --- | --- |
+| OIDC | `organization` and `service-account` on `configure-oidc` | The CLI exchanges a CircleCI OIDC token on its first authenticated command | CI/CD pipelines |
+| API key | `CLOUDSMITH_API_KEY`, optionally checked by `ensure-api-key` | CircleCI supplies the key through a context or project environment variable | Pipelines that cannot use OIDC |
+
+With OIDC, `configure-oidc` exports the service account context needed by the CLI. The Cloudsmith access token is requested only when the CLI first needs to authenticate and is not exposed by the orb.
+
+```mermaid
+flowchart LR
+ A[configure-oidc command] -->|Exports OIDC settings| B[Cloudsmith CLI command]
+ B -->|Requests identity token| C[CircleCI OIDC]
+ C -->|Exchanges identity| D[Cloudsmith]
+```
+
+Set `verify-auth: true` on `configure-oidc` to run `cloudsmith whoami` during setup and fail early if authentication is not configured correctly.
+
+## Configuration
+
+### `install-cli`
+
+Installs the standalone Cloudsmith CLI on a Linux or macOS executor and adds its binary directory to `PATH` for later steps through `$BASH_ENV`.
+
+#### Installation parameters
+
+| Parameter | Description | Required | Default |
+| --- | --- | --- | --- |
+| `cli-version` | CLI version to install, such as `1.20.0` | No | `latest` |
+| `install-path` | Root directory for versioned CLI installations | No | `$XDG_DATA_HOME/cloudsmith-cli` or `$HOME/.local/share/cloudsmith-cli` |
+
+#### API configuration parameters
+
+When supplied, these values are written to `~/.cloudsmith/config.ini`.
+
+| Parameter | Description | Required | Default |
+| --- | --- | --- | --- |
+| `api-host` | Cloudsmith API host override | No | `api.cloudsmith.io` |
+| `api-proxy` | HTTP or HTTPS proxy for Cloudsmith API calls | No | — |
+| `api-ssl-verify` | Whether to verify API SSL certificates | No | `true` |
+| `api-user-agent` | User agent override for Cloudsmith API requests | No | — |
+
+### `configure-oidc`
+
+Configures the CLI for CircleCI-native OIDC. The job must use at least one context so CircleCI issues an OIDC token.
+
+| Parameter | Description | Required | Default |
+| --- | --- | --- | --- |
+| `organization` | Cloudsmith organization or namespace | Yes | — |
+| `service-account` | Cloudsmith service account slug | Yes | — |
+| `verify-auth` | Run `cloudsmith whoami` after configuration | No | `false` |
+
+Run `install-cli` before using `verify-auth`.
+
+### `ensure-api-key`
+
+Validates that `CLOUDSMITH_API_KEY` is present and fails the job immediately when it is missing.
+
+### Default executor
+
+The `default` executor uses the `cimg/base` convenience image with the `current` tag. You can also use the orb commands with another Linux or macOS executor that provides `bash` and either `curl` or `wget`.
+
+## Environment variables
+
+The orb persists configuration for later steps through CircleCI’s `$BASH_ENV` file.
+
+| Authentication method | Variable | Handling |
+| --- | --- | --- |
+| OIDC | `CLOUDSMITH_ORG` | Exported by `configure-oidc` |
+| OIDC | `CLOUDSMITH_SERVICE_SLUG` | Exported by `configure-oidc` |
+| OIDC | `CIRCLE_OIDC_TOKEN_V2` or `CIRCLE_OIDC_TOKEN` | Issued automatically when the job uses a CircleCI context |
+| API key | `CLOUDSMITH_API_KEY` | Supply through a context or project environment variable |
+
+## Publish a package
+
+The following workflow installs the CLI with OIDC authentication and publishes a raw package:
```yaml
version: 2.1
orbs:
- cloudsmith: cloudsmith/cloudsmith@2.0.0
+ cloudsmith: cloudsmith/cloudsmith@3.0.0
+
+workflows:
+ publish:
+ jobs:
+ - publish:
+ context: my-context
jobs:
publish:
executor: cloudsmith/default
steps:
- checkout
- - cloudsmith/ensure-api-key
- cloudsmith/install-cli
+ - cloudsmith/configure-oidc:
+ organization: YOUR-NAMESPACE
+ service-account: YOUR-SERVICE-ACCOUNT
- run:
- name: Build and publish
- command: |
- pip install build
- python -m build --wheel
- cloudsmith push python cloudsmith/examples dist/*.whl
+ name: Publish package
+ command: cloudsmith push raw YOUR-NAMESPACE/YOUR-REPOSITORY dist/app.tar.gz
```
-## Development
+See [Supported Formats](https://docs.cloudsmith.com/formats) for the upload command and options for each package format.
-We use the [CircleCI CLI](https://circleci.com/docs/guides/toolkit/local-cli/) to perform common development and release tasks for this orb. Please first ensure you have it installed and configured with appropriate credentials.
+## Migrating from v2
-### Generating the orb
+Version 3 installs the standalone CLI instead of the Python package and uses CLI-native OIDC authentication.
-We store the orb in git as individual YAML files. Before we can use the orb or perform further actions we need to "pack" it up into a single `orb.yml` file. We do so with the `pack` command:
+> [!NOTE]
+> OIDC authentication is now lazy: the CLI exchanges the token on its first authenticated command. Use `verify-auth: true` if the setup step should validate credentials immediately.
-```
-$ circleci config pack src/ > orb.yml
-```
+
+View removed commands, parameters, and migration steps
-### Validating the orb
+### Installation changes
-Once generated, we can use the CLI to validate that the orb is correctly structured and meets basic standards:
+| In v2 | In v3 | Migration |
+| --- | --- | --- |
+| `install-cli` downloads a Python zipapp | Installs a SHA-256-verified standalone binary | Remove Python and pip setup used only by this orb. |
+| `pip-install: true` | Removed | Delete the parameter. |
+| `install-path` defaults to `$HOME/bin` and receives the binary directly | Defines the versioned installation root | Update custom paths if the old single-file layout is required elsewhere. |
+| `cli-version: ""` selects the latest release | `latest` is the explicit default | Remove empty overrides or replace them with `latest`. |
+| `default` executor uses `cimg/python` | Uses `cimg/base` | Add Python explicitly only when other job steps require it. |
-```
-$ circleci orb validate orb.yml
+### Authentication changes
+
+| In v2 | In v3 | Migration |
+| --- | --- | --- |
+| `authenticate-with-oidc` | Replaced by `configure-oidc` | Rename the command. The CLI now performs the token exchange itself. |
+| `authenticate-with-oidc` exports `CLOUDSMITH_API_KEY` | No Cloudsmith API token is exported | Use `cloudsmith` commands for authenticated operations. |
+| `oidc-audience` | Removed | Configure custom audiences on the Cloudsmith service account’s OIDC provider. |
+| `oidc-auth-retry` | Removed | The CLI manages token exchange retries. |
+
+### Publishing changes
+
+The `publish` command has been removed. Run `cloudsmith push` directly after installation and authentication.
+
+| Removed `publish` parameter | CLI equivalent |
+| --- | --- |
+| `package-format`, `cloudsmith-repository`, `package-path` | `cloudsmith push FORMAT OWNER/REPOSITORY FILE` |
+| `allow-republish: true` | `--republish` |
+| `package-distribution` | Distribution path, such as `OWNER/REPOSITORY/ubuntu/focal` |
+| `package-pom-file` | `--pom-file PATH` |
+| Raw package metadata | `--name`, `--version`, `--summary`, and `--description` |
+
+
+
+## Contributing
+
+The orb source is stored as individual YAML and shell files under `src/`. The CircleCI CLI packs these files into `orb.yml` for validation and publishing.
+
+
+View local development and release commands
+
+Pack and validate the orb from the repository root:
+
+```bash
+circleci orb pack src/ > orb.yml
+circleci orb validate orb.yml
+shellcheck src/scripts/*.sh
```
-## Release Management
+`src/scripts/install.sh` is synchronized with the [Cloudsmith CLI installer](https://github.com/cloudsmith-io/cloudsmith-cli-install-script) and bundled into the published orb. Do not edit it directly; update it together with `src/scripts/install.sh.version`.
-Releasing the orb happens automatically from CI using the [`circleci/orb-tools`](https://circleci.com/developer/orbs/orb/circleci/orb-tools) orb. The orb source is linted, reviewed for best practices, packed, and validated as part of the pipeline.
+Branches publish development releases automatically. To create a production release after merging to `master`, create a `v`-prefixed semantic version tag such as `v3.0.0`.
-### Dev/Alpha releases
-To make a development (or alpha) release, simply push your changes to a branch on GitHub. CircleCI will automatically build the orb and push a development release to the version `cloudsmith/cloudsmith@dev:$BRANCH_NAME`.
+
-### Production releases
-Once happy with your changes, merge to master as normal via a PR and then tag a new release (either via CI or the GitHub UI) with an appropriate `v`-prefixed semver version.
+## Support
-For example, if you create a tag named `v2.0.0` it'll result in a public release to `cloudsmith/cloudsmith@2.0.0`.
+For help, [open a GitHub issue](https://github.com/cloudsmith-io/orb/issues) or contact [Cloudsmith Support](https://support.cloudsmith.com/).
diff --git a/src/@orb.yml b/src/@orb.yml
index 56d8ecf..010c7b1 100644
--- a/src/@orb.yml
+++ b/src/@orb.yml
@@ -2,8 +2,8 @@
version: 2.1
description: |
- Install the Cloudsmith CLI and publish packages to Cloudsmith repositories.
- Supports OIDC and API key authentication with configurable CLI options.
+ Install the standalone Cloudsmith CLI and authenticate with OIDC or an
+ API key, then run any Cloudsmith CLI command directly in your steps.
display:
home_url: https://cloudsmith.io/
diff --git a/src/commands/authenticate-with-oidc.yml b/src/commands/authenticate-with-oidc.yml
deleted file mode 100644
index 99c997b..0000000
--- a/src/commands/authenticate-with-oidc.yml
+++ /dev/null
@@ -1,102 +0,0 @@
----
-description: >
- Authenticate with Cloudsmith using OpenID Connect (OIDC) to generate a
- short-lived API token for use in subsequent Cloudsmith requests. This
- sets the CLOUDSMITH_API_KEY environment variable to the retrieved token
- for use with the Cloudsmith CLI. Retries the token exchange up to
- oidc-auth-retry times (default 3) with a 5-second delay between attempts.
- To use OIDC authentication without installing the CLI, simply call this
- command on its own and omit install-cli from your workflow steps.
-parameters:
- organization:
- type: string
- description: >
- The name of the Cloudsmith organization to use for authentication.
- service-account:
- type: string
- description: >
- The name of the Cloudsmith service account to use for authentication.
- oidc-audience:
- type: string
- default: ""
- description: >
- Custom audience value sent in the OIDC token exchange request body.
- Leave empty to omit the field and use Cloudsmith's default audience.
- oidc-auth-retry:
- type: integer
- default: 3
- description: >
- Total number of attempts for the OIDC token exchange. Each retry waits
- 5 seconds before the next attempt. The step fails if all attempts are
- exhausted without a valid token.
-steps:
- - run:
- name: Authenticate to Cloudsmith with OIDC
- command: |
- organization="<>"
- service_account="<>"
- oidc_audience="<>"
- max_retries=<>
- oidc_endpoint="https://api.cloudsmith.io/openid/$organization/"
-
- # Verify required tools are available
- for cmd in curl jq; do
- if ! command -v "$cmd" > /dev/null; then
- echo "$cmd is required but not found. Ensure it is installed."
- exit 1
- fi
- done
-
- if [[ -z "$CIRCLE_OIDC_TOKEN_V2" ]]; then
- echo "CIRCLE_OIDC_TOKEN_V2 is not set."
- echo "Enable OIDC: Project Settings -> Advanced -> Enable OIDC."
- exit 1
- fi
-
- attempt=0
- while [[ $attempt -lt $max_retries ]]; do
- attempt=$((attempt + 1))
- echo "OIDC authentication attempt $attempt of $max_retries"
-
- payload=$(jq -n \
- --arg oidc_token "$CIRCLE_OIDC_TOKEN_V2" \
- --arg service_slug "$service_account" \
- '{
- oidc_token: $oidc_token,
- service_slug: $service_slug
- }')
-
- if [[ -n "$oidc_audience" ]]; then
- payload=$(echo "$payload" | jq \
- --arg audience "$oidc_audience" \
- '. + {audience: $audience}')
- fi
-
- response=$(curl -s -X POST \
- -H "Content-Type: application/json" \
- -d "$payload" \
- --write-out '\n%{http_code}' \
- "$oidc_endpoint") || true
-
- http_code=$(echo "$response" | tail -n1)
- body=$(echo "$response" | sed '$d')
-
- if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
- token=$(echo "$body" | jq -r '.token // empty')
-
- if [[ -n "$token" ]]; then
- echo "export CLOUDSMITH_API_KEY=\"$token\"" >> "$BASH_ENV"
- echo "Successfully authenticated with Cloudsmith via OIDC."
- exit 0
- fi
- fi
-
- echo "OIDC token exchange failed (attempt $attempt, HTTP $http_code)."
-
- if [[ $attempt -lt $max_retries ]]; then
- echo "Retrying in 5 seconds..."
- sleep 5
- fi
- done
- echo "OIDC authentication failed after $max_retries attempt(s)."
- exit 1
diff --git a/src/commands/configure-oidc.yml b/src/commands/configure-oidc.yml
new file mode 100644
index 0000000..dff153b
--- /dev/null
+++ b/src/commands/configure-oidc.yml
@@ -0,0 +1,34 @@
+---
+description: >
+ Configures the Cloudsmith CLI to authenticate with CircleCI's OIDC token.
+ Exports CLOUDSMITH_ORG and CLOUDSMITH_SERVICE_SLUG for subsequent steps via
+ $BASH_ENV; the CLI exchanges the CircleCI OIDC token for Cloudsmith
+ credentials when the first authenticated command runs. No API token is
+ exchanged or exported by this command. The job must use at least one
+ CircleCI context, otherwise no OIDC token is issued.
+parameters:
+ organization:
+ type: string
+ description: Cloudsmith organization (workspace) slug.
+ service-account:
+ type: string
+ description: Cloudsmith service account slug used for the OIDC token exchange.
+ verify-auth:
+ type: boolean
+ default: false
+ description: >
+ Run "cloudsmith whoami" to verify authentication. Requires the CLI to
+ be installed first (see install-cli).
+steps:
+ - run:
+ name: Configure Cloudsmith OIDC
+ environment:
+ PARAM_ORGANIZATION: <>
+ PARAM_SERVICE_ACCOUNT: <>
+ command: <>
+ - when:
+ condition: <>
+ steps:
+ - run:
+ name: Verify Cloudsmith authentication
+ command: cloudsmith whoami
diff --git a/src/commands/install-cli.yml b/src/commands/install-cli.yml
index 13b9ccf..f50a53f 100644
--- a/src/commands/install-cli.yml
+++ b/src/commands/install-cli.yml
@@ -1,28 +1,24 @@
---
description: >
- Installs the Cloudsmith CLI by downloading the zipapp from Cloudsmith.
- Set pip-install to true to install via pip instead. Pass cli-version to
- pin a specific release. Optional API configuration parameters are written
- to the CLI config file (~/.cloudsmith/config.ini) for use in subsequent steps.
+ Installs the standalone Cloudsmith CLI binary on Linux or macOS executors.
+ The installer is bundled with the published orb and verifies the downloaded
+ archive against its SHA-256 checksum. The CLI directory is added to PATH for
+ subsequent steps via $BASH_ENV. Optional API configuration parameters are
+ written to the CLI config file (~/.cloudsmith/config.ini).
parameters:
cli-version:
type: string
- default: ""
- description: >
- Pin a specific Cloudsmith CLI version (e.g. "1.2.0"). Leave empty to
- install the latest available release.
- pip-install:
- type: boolean
- default: false
+ default: "latest"
description: >
- Install via pip instead of the default zipapp. When true pip must be
- available — the step fails if pip is not found.
+ Exact Cloudsmith CLI version to install (e.g. "1.20.0"), or "latest"
+ for the newest release.
install-path:
type: string
- default: "$HOME/bin"
+ default: ""
description: >
- Directory where the Cloudsmith CLI zipapp binary is installed. Ignored
- when installing via pip.
+ Absolute path to the root directory for versioned CLI installations.
+ Leave empty to use the default ($XDG_DATA_HOME/cloudsmith-cli or
+ $HOME/.local/share/cloudsmith-cli).
api-host:
type: string
default: ""
@@ -45,152 +41,22 @@ parameters:
type: string
default: ""
description: >
- Custom user-agent string sent with Cloudsmith API requests (api_user_agent
- in config.ini). Leave empty to use the default.
+ Custom user-agent string sent with Cloudsmith API requests
+ (api_user_agent in config.ini). Leave empty to use the default.
steps:
- run:
- name: Install Cloudsmith CLI
- command: |
- cli_version="<>"
- force_pip="<>"
- install_path="<>"
- api_host="<>"
- api_proxy="<>"
- api_ssl_verify="<>"
- api_user_agent="<>"
-
- write_cli_config() {
- if [[ -z "$api_host" && -z "$api_proxy" && "$api_ssl_verify" != "false" && -z "$api_user_agent" ]]; then
- return
- fi
-
- local config_dir="${HOME}/.cloudsmith"
- local config_file="${config_dir}/config.ini"
-
- mkdir -p "$config_dir"
- {
- echo "[default]"
- echo "api_host=${api_host}"
- echo "api_proxy=${api_proxy}"
- echo "api_ssl_verify=${api_ssl_verify}"
- echo "api_user_agent=${api_user_agent}"
- } > "$config_file"
-
- echo "Cloudsmith CLI config written to $config_file"
- }
-
- add_install_path() {
- case ":$PATH:" in
- *":$install_path:"*) ;;
- *)
- export PATH="$install_path:$PATH"
- echo "export PATH=\"$install_path:\$PATH\"" >> "$BASH_ENV"
- ;;
- esac
- }
-
- install_with_pip() {
- local pip_cmd
- local install_args=("install")
-
- pip_cmd=$(command -v pip 2>/dev/null || command -v pip3 2>/dev/null)
- if [[ -z "$pip_cmd" ]]; then
- echo "pip-install is true but pip was not found. Ensure pip is" \
- "available in this executor."
- exit 1
- fi
-
- if [[ -n "$cli_version" ]]; then
- install_args+=("cloudsmith-cli==$cli_version")
- else
- install_args+=("cloudsmith-cli" "--upgrade")
- fi
-
- if command -v sudo > /dev/null; then
- sudo "$pip_cmd" "${install_args[@]}"
- else
- "$pip_cmd" "${install_args[@]}" --user
- fi
- }
-
- zipapp_url() {
- if [[ -n "$cli_version" ]]; then
- echo "https://dl.cloudsmith.io/public/cloudsmith/cli-zipapp/raw/names/cloudsmith-cli/versions/${cli_version}/cloudsmith.pyz"
- else
- echo "https://dl.cloudsmith.io/public/cloudsmith/cli-zipapp/raw/names/cloudsmith-cli/versions/latest/cloudsmith.pyz"
- fi
- }
-
- download_zipapp() {
- local pyz_url="$1"
- local destination="$2"
-
- if ! curl -sSfL "$pyz_url" -o "$destination"; then
- echo "Failed to download Cloudsmith CLI zipapp from:"
- echo " $pyz_url"
- exit 1
- fi
- }
-
- install_zipapp() {
- local pyz_url
- local target
- local probe_file
- local tmpfile
-
- pyz_url="$(zipapp_url)"
- target="${install_path}/cloudsmith"
- probe_file="${install_path}/.cloudsmith-write-test-$$"
-
- if mkdir -p "$install_path" 2>/dev/null && touch "$probe_file" 2>/dev/null; then
- rm -f "$probe_file"
- download_zipapp "$pyz_url" "$target"
- chmod +x "$target"
- elif command -v sudo > /dev/null; then
- tmpfile="$(mktemp)"
- download_zipapp "$pyz_url" "$tmpfile"
-
- if ! sudo mkdir -p "$install_path"; then
- echo "Failed to create install path '$install_path'."
- rm -f "$tmpfile"
- exit 1
- fi
-
- if ! sudo mv "$tmpfile" "$target"; then
- echo "Failed to move Cloudsmith CLI zipapp into '$install_path'."
- rm -f "$tmpfile"
- exit 1
- fi
-
- sudo chmod +x "$target"
- else
- echo "Install path '$install_path' is not writable and sudo is not available."
- echo "Set 'install-path' to a user-writable directory such as \$HOME/bin."
- exit 1
- fi
-
- add_install_path
- echo "Cloudsmith CLI installed to $target"
- }
-
- if [[ "$install_path" == ~* ]]; then
- install_path="${install_path/#\~/$HOME}"
- fi
-
- write_cli_config
-
- # Install the Cloudsmith CLI
- if command -v cloudsmith > /dev/null && [[ "$force_pip" != "true" ]]; then
- if [[ -n "$cli_version" ]]; then
- echo "Warning: Cloudsmith CLI is already installed." \
- "cli-version='$cli_version' was ignored. Set pip-install: true" \
- "to enforce the version."
- else
- echo "Cloudsmith CLI is already installed."
- fi
-
- elif [[ "$force_pip" == "true" ]]; then
- install_with_pip
- else
- install_zipapp
- fi
+ name: Download and verify Cloudsmith CLI
+ environment:
+ CLOUDSMITH_CLI_VERSION: <>
+ CLOUDSMITH_CLI_INSTALL_ROOT: <>
+ CLOUDSMITH_CLI_OUTPUT_FILE: /tmp/cloudsmith-cli-install/result.txt
+ command: <>
+ - run:
+ name: Add Cloudsmith CLI to PATH
+ environment:
+ CLOUDSMITH_CLI_OUTPUT_FILE: /tmp/cloudsmith-cli-install/result.txt
+ PARAM_API_HOST: <>
+ PARAM_API_PROXY: <>
+ PARAM_API_SSL_VERIFY: <>
+ PARAM_API_USER_AGENT: <>
+ command: <>
diff --git a/src/commands/publish.yml b/src/commands/publish.yml
deleted file mode 100644
index 5d13edc..0000000
--- a/src/commands/publish.yml
+++ /dev/null
@@ -1,100 +0,0 @@
----
-description: >
- DEPRECATED: This command wraps individual cloudsmith push calls and will be
- removed in a future major version. The recommended approach is to call
- install-cli and authenticate-with-oidc (or ensure-api-key), then invoke the
- Cloudsmith CLI directly in your run steps — e.g. "cloudsmith push python
- my-org/my-repo dist/package-*.whl". All existing parameters continue to work
- as before.
-parameters:
- cloudsmith-repository:
- type: string
- description: The Cloudsmith repository to which packages will be published.
- package-format:
- type: string
- description: Format of the package being published.
- package-path:
- type: string
- description: Path to the package to be published (accepts shell globs).
- package-distribution:
- type: string
- default: ""
- description: distro/release for which the package is built (required only for deb, rpm and alpine).
- package-pom-file:
- type: string
- default: ""
- description: Path to the POM file associated with a maven release (required only for maven).
- allow-republish:
- type: boolean
- default: false
- description: Allow publishing a package that overwrites an existing version on Cloudsmith.
- package-description:
- type: string
- default: ""
- description: Optional description for the package being published (applied only to raw).
- package-name:
- type: string
- default: ""
- description: Name of the package being published (applied only to raw).
- package-summary:
- type: string
- default: ""
- description: Summary for the package being published (applied only to raw).
- package-version:
- type: string
- default: ""
- description: Version of the package being published (applied only to raw).
-steps:
- - run:
- name: Publish Package
- command: |
- cloudsmith_repository="<>"
- package_format="<>"
- package_distribution="<>"
- package_pom_file="<>"
- package_description="<>"
- package_name="<>"
- package_summary="<>"
- package_version="<>"
-
- # Use `ls` to resolve any shell globs in parameters.package-path
- package_path="$(ls <>)"
-
- # Handle optional arguments
- additional_args=
- <<# parameters.allow-republish >>
- additional_args+="--republish"
- < parameters.allow-republish >>
-
- # Handle per-format required arguments
- if [[ "$package_format" == "alpine" || "$package_format" == "deb" || "$package_format" == "rpm" ]]; then
- if [[ "$package_distribution" == "" ]]; then
- echo "\"package-distribution\" parameter is required when \"package-format\" is alpine, deb, or rpm"
- exit 1
- fi
- cloudsmith_repository+="/$package_distribution"
- fi
- if [[ "$package_format" == "maven" ]]; then
- if [[ "$package_pom_file" == "" ]]; then
- echo "\"package-pom-file\" parameter is required when \"package-format\" is maven"
- exit 1
- fi
- additional_args+=" --pom-file $package_pom_file"
- fi
- if [[ "$package_format" == "raw" ]]; then
- if [[ ! -z "$package_description" ]]; then
- additional_args+=" --description $package_description"
- fi
- if [[ ! -z "$package_name" ]]; then
- additional_args+=" --name $package_name"
- fi
- if [[ ! -z "$package_summary" ]]; then
- additional_args+=" --summary $package_summary"
- fi
- if [[ ! -z "$package_version" ]]; then
- additional_args+=" --version $package_version"
- fi
- fi
-
- # Publish package using the Cloudsmith CLI
- cloudsmith push $package_format $additional_args $cloudsmith_repository $package_path
diff --git a/src/examples/authenticate-with-oidc.yml b/src/examples/authenticate-with-oidc.yml
deleted file mode 100644
index 5768444..0000000
--- a/src/examples/authenticate-with-oidc.yml
+++ /dev/null
@@ -1,25 +0,0 @@
----
-description: >
- Authenticate with Cloudsmith using OpenID Connect (OIDC) to obtain a
- short-lived API token. The token is exported as CLOUDSMITH_API_KEY and can
- be used in subsequent steps without installing the Cloudsmith CLI.
-usage:
- version: 2.1
-
- orbs:
- cloudsmith: cloudsmith/cloudsmith@2.0.0
-
- workflows:
- cloudsmith_oidc_auth:
- jobs:
- - authenticate
-
- jobs:
- authenticate:
- executor:
- cloudsmith/default
- steps:
- - checkout
- - cloudsmith/authenticate-with-oidc:
- organization: my-org
- service-account: my-service-account
diff --git a/src/examples/cli-with-oidc.yml b/src/examples/cli-with-oidc.yml
deleted file mode 100644
index 27e4a74..0000000
--- a/src/examples/cli-with-oidc.yml
+++ /dev/null
@@ -1,32 +0,0 @@
----
-description: >
- Recommended workflow — authenticate with Cloudsmith via OIDC and install the
- CLI, then invoke the CLI directly in your run steps. This pattern gives you
- full access to all Cloudsmith CLI commands.
-usage:
- version: 2.1
-
- orbs:
- cloudsmith: cloudsmith/cloudsmith@2.0.0
-
- workflows:
- cloudsmith_oidc_publish:
- jobs:
- - publish
-
- jobs:
- publish:
- executor:
- cloudsmith/default
- steps:
- - checkout
- - cloudsmith/authenticate-with-oidc:
- organization: my-org
- service-account: my-service-account
- - cloudsmith/install-cli
- - run:
- name: Build and publish Python package
- command: |
- pip install build
- python -m build --wheel
- cloudsmith push python my-org/my-repo dist/*.whl
diff --git a/src/examples/command-publish.yml b/src/examples/command-publish.yml
deleted file mode 100644
index cdfd752..0000000
--- a/src/examples/command-publish.yml
+++ /dev/null
@@ -1,28 +0,0 @@
----
-description: Customize your Cloudsmith workflow using the commands from this orb
-usage:
- version: 2.1
-
- orbs:
- cloudsmith: cloudsmith/cloudsmith@2.0.0
-
- workflows:
- cloudsmith_publish:
- jobs:
- - publish
-
- jobs:
- publish:
- executor:
- cloudsmith/default
- steps:
- - checkout
- - cloudsmith/ensure-api-key
- - cloudsmith/install-cli
- - run:
- name: Build Python package
- command: python setup.py bdist_wheel
- - cloudsmith/publish:
- cloudsmith-repository: cloudsmith/examples
- package-path: dist/package-*.whl
- package-format: python
diff --git a/src/examples/install-with-api-key.yml b/src/examples/install-with-api-key.yml
new file mode 100644
index 0000000..dbd12f2
--- /dev/null
+++ b/src/examples/install-with-api-key.yml
@@ -0,0 +1,26 @@
+---
+description: >
+ Install the Cloudsmith CLI and authenticate with an API key provided via
+ the CLOUDSMITH_API_KEY environment variable (for example from a context).
+usage:
+ version: 2.1
+
+ orbs:
+ cloudsmith: cloudsmith/cloudsmith@3.0.0
+
+ workflows:
+ cloudsmith_publish:
+ jobs:
+ - publish:
+ context: my-context
+
+ jobs:
+ publish:
+ executor: cloudsmith/default
+ steps:
+ - checkout
+ - cloudsmith/ensure-api-key
+ - cloudsmith/install-cli
+ - run:
+ name: Publish package
+ command: cloudsmith push raw my-org/my-repo dist/app.tar.gz
diff --git a/src/examples/install-with-oidc.yml b/src/examples/install-with-oidc.yml
new file mode 100644
index 0000000..8ebd31a
--- /dev/null
+++ b/src/examples/install-with-oidc.yml
@@ -0,0 +1,30 @@
+---
+description: >
+ Install the Cloudsmith CLI and authenticate with CircleCI OIDC, then run
+ CLI commands directly. The job must use a context so that CircleCI issues
+ an OIDC token.
+usage:
+ version: 2.1
+
+ orbs:
+ cloudsmith: cloudsmith/cloudsmith@3.0.0
+
+ workflows:
+ cloudsmith_oidc_publish:
+ jobs:
+ - publish:
+ context: my-context
+
+ jobs:
+ publish:
+ executor: cloudsmith/default
+ steps:
+ - checkout
+ - cloudsmith/install-cli
+ - cloudsmith/configure-oidc:
+ organization: my-org
+ service-account: my-service-account
+ verify-auth: true
+ - run:
+ name: Publish package
+ command: cloudsmith push raw my-org/my-repo dist/app.tar.gz
diff --git a/src/executors/default.yml b/src/executors/default.yml
index 89e10b8..46be432 100644
--- a/src/executors/default.yml
+++ b/src/executors/default.yml
@@ -1,12 +1,13 @@
---
description: |
- Uses the cimg/python convenience image, which has the prerequisites for installing Cloudsmith's CLI.
+ A minimal cimg/base image. The standalone Cloudsmith CLI has no Python or
+ other runtime requirements.
parameters:
tag:
type: string
- default: "3.10"
+ default: "current"
description: >-
- Pick a specific cimg/python image version tag:
- https://circleci.com/developer/images/image/cimg/python
+ Pick a specific cimg/base image tag:
+ https://circleci.com/developer/images/image/cimg/base
docker:
- - image: cimg/python:<>
+ - image: cimg/base:<>
diff --git a/src/scripts/configure-oidc.sh b/src/scripts/configure-oidc.sh
new file mode 100644
index 0000000..a1dc701
--- /dev/null
+++ b/src/scripts/configure-oidc.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+# Exports CLOUDSMITH_ORG and CLOUDSMITH_SERVICE_SLUG for subsequent steps.
+# The Cloudsmith CLI exchanges the CircleCI OIDC token itself when the first
+# authenticated command runs.
+
+if [ -z "${BASH_VERSION:-}" ]; then
+ echo "configure-oidc requires bash; install it first (e.g. apk add bash)" >&2
+ exit 1
+fi
+set -euo pipefail
+: "${BASH_ENV:?BASH_ENV is required to persist environment variables between steps}"
+
+[[ -n "$PARAM_ORGANIZATION" ]] || { echo "organization must not be empty" >&2; exit 1; }
+[[ -n "$PARAM_SERVICE_ACCOUNT" ]] || { echo "service-account must not be empty" >&2; exit 1; }
+
+if [[ -z "${CIRCLE_OIDC_TOKEN_V2:-}" && -z "${CIRCLE_OIDC_TOKEN:-}" ]]; then
+ echo "No CircleCI OIDC token is available (CIRCLE_OIDC_TOKEN_V2 / CIRCLE_OIDC_TOKEN)." >&2
+ echo "CircleCI only issues OIDC tokens to jobs that use at least one context." >&2
+ echo "Add a context to this job in your workflow configuration and re-run." >&2
+ exit 1
+fi
+
+export CLOUDSMITH_ORG="$PARAM_ORGANIZATION"
+export CLOUDSMITH_SERVICE_SLUG="$PARAM_SERVICE_ACCOUNT"
+printf 'export CLOUDSMITH_ORG=%q\n' "$CLOUDSMITH_ORG" >> "$BASH_ENV"
+printf 'export CLOUDSMITH_SERVICE_SLUG=%q\n' "$CLOUDSMITH_SERVICE_SLUG" >> "$BASH_ENV"
+
+echo "Cloudsmith OIDC configured for organization $CLOUDSMITH_ORG"
diff --git a/src/scripts/install.sh b/src/scripts/install.sh
new file mode 100755
index 0000000..3288337
--- /dev/null
+++ b/src/scripts/install.sh
@@ -0,0 +1,379 @@
+#!/usr/bin/env sh
+# Cloudsmith CLI standalone binary installer.
+#
+# Detects the target, verifies the release archive, and installs it into a
+# versioned directory. Does not touch PATH or authenticate.
+set -eu
+
+PROGRAM="install.sh"
+
+# Where releases are downloaded from. Flags and CLOUDSMITH_CLI_* environment
+# variables override the defaults; --manifest-url bypasses URL construction.
+DOWNLOAD_BASE_URL="https://dl.cloudsmith.io/public"
+# TODO: revert to the production repository before release.
+DEFAULT_REPOSITORY="bart-demo-org-terraform/cli-binary-release-test"
+MANIFEST_NAME_PREFIX="cloudsmith-cli-manifest"
+
+REPOSITORY="${CLOUDSMITH_CLI_REPOSITORY:-$DEFAULT_REPOSITORY}"
+REQUESTED_VERSION="${CLOUDSMITH_CLI_VERSION:-latest}"
+INSTALL_ROOT="${CLOUDSMITH_CLI_INSTALL_ROOT:-}"
+TARGET_OVERRIDE="${CLOUDSMITH_CLI_TARGET:-}"
+OUTPUT_FILE="${CLOUDSMITH_CLI_OUTPUT_FILE:-}"
+MANIFEST_URL="${CLOUDSMITH_CLI_MANIFEST_URL:-}"
+ALLOW_INSECURE_URLS="${CLOUDSMITH_CLI_ALLOW_INSECURE_URLS:-false}"
+FORCE="0"
+
+WORK_DIR=""
+LOCK_DIR=""
+
+log() { printf '%s: %s\n' "$PROGRAM" "$*" >&2; }
+die() { log "$*"; exit 1; }
+
+usage() {
+ cat <<'USAGE'
+Usage: install.sh [options]
+
+ --version VERSION CLI version, or "latest"
+ --install-root DIR Versioned installation root
+ --target TARGET Override target detection
+ --output-file FILE Write key=value installation results
+ --repository OWNER/REPO Cloudsmith public raw repository (advanced)
+ --manifest-url URL Override generated manifest URL (advanced/testing)
+ --force Reinstall an existing matching version
+ -h, --help Show help
+USAGE
+}
+
+require_value() {
+ [ -n "${2-}" ] || die "$1 requires a value"
+}
+
+parse_args() {
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --version) require_value "$1" "${2-}"; REQUESTED_VERSION="$2"; shift 2 ;;
+ --install-root) require_value "$1" "${2-}"; INSTALL_ROOT="$2"; shift 2 ;;
+ --target) require_value "$1" "${2-}"; TARGET_OVERRIDE="$2"; shift 2 ;;
+ --output-file) require_value "$1" "${2-}"; OUTPUT_FILE="$2"; shift 2 ;;
+ --repository) require_value "$1" "${2-}"; REPOSITORY="$2"; shift 2 ;;
+ --manifest-url) require_value "$1" "${2-}"; MANIFEST_URL="$2"; shift 2 ;;
+ --force) FORCE="1"; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) die "unknown option: $1" ;;
+ esac
+ done
+
+ case "$REQUESTED_VERSION" in ''|.|..|*[!A-Za-z0-9._+-]*) die "invalid version: $REQUESTED_VERSION" ;; esac
+ case "$REPOSITORY" in */*) ;; *) die "repository must be OWNER/REPOSITORY" ;; esac
+ case "$REPOSITORY" in *[!A-Za-z0-9._/-]*) die "invalid repository: $REPOSITORY" ;; esac
+}
+
+default_install_root() {
+ if [ -n "${XDG_DATA_HOME:-}" ]; then
+ printf '%s\n' "$XDG_DATA_HOME/cloudsmith-cli"
+ elif [ -n "${HOME:-}" ]; then
+ printf '%s\n' "$HOME/.local/share/cloudsmith-cli"
+ else
+ die "HOME or XDG_DATA_HOME must be set unless --install-root is provided"
+ fi
+}
+
+set_default_install_root() {
+ [ -n "$INSTALL_ROOT" ] || INSTALL_ROOT="$(default_install_root)"
+}
+
+cleanup() {
+ [ -z "$LOCK_DIR" ] || rmdir "$LOCK_DIR" 2>/dev/null || true
+ [ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR"
+}
+
+require_https() {
+ # Args: