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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added Bitbucket Pipelines to OIDC credential auto-discovery. When a pipeline step sets `oidc: true`, the CLI reads the OIDC token from the `BITBUCKET_STEP_OIDC_TOKEN` environment variable and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
- Added CircleCI to OIDC credential auto-discovery. When running in CircleCI, the CLI reads the OIDC token from the `CIRCLE_OIDC_TOKEN_V2` (preferred) or `CIRCLE_OIDC_TOKEN` environment variable and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
- Added Azure DevOps to OIDC credential auto-discovery. When running in an Azure DevOps pipeline, the CLI fetches an OIDC token from the `SYSTEM_OIDCREQUESTURI` endpoint using the pipeline's `SYSTEM_ACCESSTOKEN` and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
- Added GitHub Actions to OIDC credential auto-discovery. When running in GitHub Actions (with `id-token: write` permission), the CLI fetches an OIDC token from the Actions runtime endpoint and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,21 @@ pip install cloudsmith-cli[all]

**Note:** If you don't install the AWS extra, the AWS OIDC detector will gracefully skip itself with no errors.

#### Bitbucket Pipelines OIDC Support

In Bitbucket Pipelines, OIDC credential discovery works out of the box with no extra dependencies. Set `oidc: true` on the pipeline step and the CLI reads the token from the `BITBUCKET_STEP_OIDC_TOKEN` variable that Bitbucket populates. The Cloudsmith OIDC provider must expect the workspace audience that Bitbucket mints (`ari:cloud:bitbucket::workspace/<workspace-uuid>`):

```yaml
pipelines:
default:
- step:
oidc: true
script:
- cloudsmith push ...
```

See the [Bitbucket Pipelines OIDC documentation](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/).

#### CircleCI OIDC Support

In CircleCI, OIDC credential discovery works out of the box with no extra dependencies — the CLI reads the token from the `CIRCLE_OIDC_TOKEN_V2` (preferred) or `CIRCLE_OIDC_TOKEN` environment variable that CircleCI injects into every job. The Cloudsmith OIDC provider must expect the audience CircleCI mints, which is your CircleCI organization UUID. See the [Cloudsmith CircleCI integration guide](https://docs.cloudsmith.com/integrations/integrating-with-circleci).
Expand Down
2 changes: 2 additions & 0 deletions cloudsmith_cli/core/credentials/oidc/detectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .aws import AWSDetector
from .azure_devops import AzureDevOpsDetector
from .base import EnvironmentDetector
from .bitbucket_pipelines import BitbucketPipelinesDetector
from .circleci import CircleCIDetector
from .generic import GenericDetector
from .github_actions import GitHubActionsDetector
Expand All @@ -21,6 +22,7 @@
CircleCIDetector,
AzureDevOpsDetector,
GitHubActionsDetector,
BitbucketPipelinesDetector,
AWSDetector,
GenericDetector,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2026 Cloudsmith Ltd
"""Bitbucket Pipelines OIDC detector.

Reads an OIDC token from the ``BITBUCKET_STEP_OIDC_TOKEN`` environment variable,
which Bitbucket populates when ``oidc: true`` is set on a pipeline step.

References:
https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/
https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/
"""

from __future__ import annotations

import os

from .base import EnvironmentDetector


class BitbucketPipelinesDetector(EnvironmentDetector):
"""Detects Bitbucket Pipelines and reads its OIDC token from environment."""

name = "Bitbucket Pipelines"

def detect(self) -> bool:
return bool(os.environ.get("BITBUCKET_STEP_OIDC_TOKEN"))

def get_token(self) -> str:
token = os.environ.get("BITBUCKET_STEP_OIDC_TOKEN")
if not token:
raise ValueError(
"BITBUCKET_STEP_OIDC_TOKEN is not set. Enable OIDC on the "
"pipeline step with 'oidc: true'."
)
return token
55 changes: 55 additions & 0 deletions cloudsmith_cli/core/tests/test_bitbucket_pipelines_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Tests for the Bitbucket Pipelines OIDC detector."""

from unittest import mock

import pytest

from cloudsmith_cli.core.credentials.models import CredentialContext
from cloudsmith_cli.core.credentials.oidc.detectors import detect_environment
from cloudsmith_cli.core.credentials.oidc.detectors.bitbucket_pipelines import (
BitbucketPipelinesDetector,
)


@pytest.fixture
def bitbucket_env():
env = {
"BITBUCKET_STEP_OIDC_TOKEN": "the-jwt",
}
with mock.patch.dict("os.environ", env, clear=True):
yield env


class TestDetect:
def test_detects_when_token_present(self, bitbucket_env):
detector = BitbucketPipelinesDetector(context=CredentialContext())
assert detector.detect() is True

def test_not_detected_when_unset(self):
with mock.patch.dict("os.environ", {}, clear=True):
detector = BitbucketPipelinesDetector(context=CredentialContext())
assert detector.detect() is False

def test_not_detected_when_token_empty(self, bitbucket_env):
bitbucket_env["BITBUCKET_STEP_OIDC_TOKEN"] = ""
with mock.patch.dict("os.environ", bitbucket_env, clear=True):
detector = BitbucketPipelinesDetector(context=CredentialContext())
assert detector.detect() is False


class TestGetToken:
def test_returns_token(self, bitbucket_env):
detector = BitbucketPipelinesDetector(context=CredentialContext())
assert detector.get_token() == "the-jwt"

def test_raises_when_token_missing(self):
with mock.patch.dict("os.environ", {}, clear=True):
detector = BitbucketPipelinesDetector(context=CredentialContext())
with pytest.raises(ValueError):
detector.get_token()


class TestIntegration:
def test_detect_environment_selects_bitbucket_pipelines(self, bitbucket_env):
detector = detect_environment(CredentialContext())
assert isinstance(detector, BitbucketPipelinesDetector)
Loading