Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 53 additions & 29 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,67 @@
---
version: 2.1

parameters:
publish-orb-name:
type: string
default: cloudsmith/cloudsmith

orbs:
cli: circleci/circleci-cli@0.1.5
orb-tools: circleci/orb-tools@8.27.3
orb-tools: circleci/orb-tools@12.4.0

# Run jobs on all branches and tags
filters: &filters
tags:
only: /.*/

jobs:
publish-orb:
description: Publishes an orb to CircleCI
executor:
cli/default
steps:
- attach_workspace:
at: workspace
- run:
name: >
Publish orb to CircleCI
command: |
if [[ -n $CIRCLE_TAG ]]; then
ORB_REF=$CIRCLE_TAG
else
ORB_REF="dev:$CIRCLE_BRANCH"
fi
circleci orb publish workspace/orb.yml cloudsmith/cloudsmith@$ORB_REF
# 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]+$/

workflows:
verify-and-publish:
lint-pack-publish:
jobs:
# Lint all YAML files in src/
- orb-tools/lint:
filters:
tags:
only: /.*/
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)
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:
only: /.*/
- publish-orb:
filters:
tags:
only: /.*/
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 >>
vcs_type: << pipeline.project.type >>
pub_type: production
context: orb-publishing
requires:
- orb-tools/lint
- orb-tools/pack
- orb-tools/review
filters: *release-filters
11 changes: 11 additions & 0 deletions .yamllint
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
extends: default

rules:
line-length:
max: 200
truthy:
check-keys: true
comments:
min-spaces-from-content: 1
document-start: enable
107 changes: 100 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,104 @@ CircleCI orb for publishing packages to (and interacting with) Cloudsmith reposi

See [onsite documentation](https://circleci.com/orbs/registry/orb/cloudsmith/cloudsmith) for further details.

## Commands

### `authenticate-with-oidc`

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.

| 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) |


### `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`

Validates that the `CLOUDSMITH_API_KEY` environment variable is set. Fails the build immediately if it is missing.

### `publish` *(deprecated)*

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.

## Executor

The `default` executor uses the `cimg/python` convenience image (default tag `3.10`), which has the prerequisites for installing the Cloudsmith CLI.

## Usage

### Recommended — OIDC authentication with direct CLI usage

```yaml
version: 2.1

orbs:
cloudsmith: cloudsmith/cloudsmith@2.0.0

workflows:
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
```

### API key authentication

```yaml
version: 2.1

orbs:
cloudsmith: cloudsmith/cloudsmith@2.0.0

jobs:
publish:
executor: cloudsmith/default
steps:
- checkout
- cloudsmith/ensure-api-key
- cloudsmith/install-cli
- run:
name: Build and publish
command: |
pip install build
python -m build --wheel
cloudsmith push python cloudsmith/examples dist/*.whl
```

## Development

We use the [CircleCI CLI](https://circleci.com/docs/2.0/local-cli/) to perform common development and release tasks for this orb. Please first ensure you have it installed and configured with appropriate credentials.
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.

### Generating the orb

Expand All @@ -26,14 +121,12 @@ $ circleci orb validate orb.yml

## Release Management

Releasing the orb happens automatically from CI. The orb is linted (`yamllint`) and validated (`circleci orb validate`) as part of the CI process.
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.

### Dev/Alpha releases

To make an 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`.
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.

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 version number (must be semver compatible).

For example, if you create a tag named `2.0.0` it'll result in a public release to `cloudsmith/cloudsmith@2.0.0`.
For example, if you create a tag named `v2.0.0` it'll result in a public release to `cloudsmith/cloudsmith@2.0.0`.
8 changes: 0 additions & 8 deletions src/@orb.yaml

This file was deleted.

10 changes: 10 additions & 0 deletions src/@orb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
version: 2.1

description: |
Install the Cloudsmith CLI and publish packages to Cloudsmith repositories.
Supports OIDC and API key authentication with configurable CLI options.

display:
home_url: https://cloudsmith.io/
source_url: https://github.com/cloudsmith-io/orb
95 changes: 79 additions & 16 deletions src/commands/authenticate-with-oidc.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,102 @@
---
description: >
Authenticate with Cloudsmith using OpenID Connect (OIDC) to generate a
short-lived OIDC token for use in subsequent Cloudsmith requests. This
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.
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
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
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="<<parameters.organization>>"
service_account="<<parameters.service-account>>"
oidc_audience="<<parameters.oidc-audience>>"
max_retries=<<parameters.oidc-auth-retry>>
oidc_endpoint="https://api.cloudsmith.io/openid/$organization/"

payload=$(jq -n \
# 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
}'
)
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 -X POST \
response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$payload" \
--silent \
"$oidc_endpoint"
)
--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)."

token=$(echo "$response" | jq -r '.token')
echo "export CLOUDSMITH_API_KEY=$token" >> "$BASH_ENV"
source "$BASH_ENV"
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
6 changes: 4 additions & 2 deletions src/commands/ensure-api-key.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
description: >
This command checks and ensures that a Cloudsmith API key has been configured
correctly in the CI environment such that the Cloudsmith CLI will be able to
Expand All @@ -6,5 +7,6 @@ steps:
- run:
name: Check for CLOUDSMITH_API_KEY environment variable
command: |
: "${CLOUDSMITH_API_KEY?CLOUDSMITH_API_KEY must be set in the build environment}"
echo "Checking for the Cloudsmith API key"
: "${CLOUDSMITH_API_KEY?CLOUDSMITH_API_KEY must be set in the build \
environment}"
echo "Cloudsmith API key found."
Loading