Skip to content

Commit 894bf81

Browse files
feat: add Bitbucket Pipelines OIDC detector (#303)
Add 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. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a12a03 commit 894bf81

5 files changed

Lines changed: 107 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010

1111
### Added
1212

13+
- 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.
1314
- 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.
1415
- 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.
1516
- 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.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,21 @@ pip install cloudsmith-cli[all]
165165

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

168+
#### Bitbucket Pipelines OIDC Support
169+
170+
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>`):
171+
172+
```yaml
173+
pipelines:
174+
default:
175+
- step:
176+
oidc: true
177+
script:
178+
- cloudsmith push ...
179+
```
180+
181+
See the [Bitbucket Pipelines OIDC documentation](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/).
182+
168183
#### CircleCI OIDC Support
169184
170185
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).

cloudsmith_cli/core/credentials/oidc/detectors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .aws import AWSDetector
99
from .azure_devops import AzureDevOpsDetector
1010
from .base import EnvironmentDetector
11+
from .bitbucket_pipelines import BitbucketPipelinesDetector
1112
from .circleci import CircleCIDetector
1213
from .generic import GenericDetector
1314
from .github_actions import GitHubActionsDetector
@@ -21,6 +22,7 @@
2122
CircleCIDetector,
2223
AzureDevOpsDetector,
2324
GitHubActionsDetector,
25+
BitbucketPipelinesDetector,
2426
AWSDetector,
2527
GenericDetector,
2628
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2026 Cloudsmith Ltd
2+
"""Bitbucket Pipelines OIDC detector.
3+
4+
Reads an OIDC token from the ``BITBUCKET_STEP_OIDC_TOKEN`` environment variable,
5+
which Bitbucket populates when ``oidc: true`` is set on a pipeline step.
6+
7+
References:
8+
https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/
9+
https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import os
15+
16+
from .base import EnvironmentDetector
17+
18+
19+
class BitbucketPipelinesDetector(EnvironmentDetector):
20+
"""Detects Bitbucket Pipelines and reads its OIDC token from environment."""
21+
22+
name = "Bitbucket Pipelines"
23+
24+
def detect(self) -> bool:
25+
return bool(os.environ.get("BITBUCKET_STEP_OIDC_TOKEN"))
26+
27+
def get_token(self) -> str:
28+
token = os.environ.get("BITBUCKET_STEP_OIDC_TOKEN")
29+
if not token:
30+
raise ValueError(
31+
"BITBUCKET_STEP_OIDC_TOKEN is not set. Enable OIDC on the "
32+
"pipeline step with 'oidc: true'."
33+
)
34+
return token
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Tests for the Bitbucket Pipelines OIDC detector."""
2+
3+
from unittest import mock
4+
5+
import pytest
6+
7+
from cloudsmith_cli.core.credentials.models import CredentialContext
8+
from cloudsmith_cli.core.credentials.oidc.detectors import detect_environment
9+
from cloudsmith_cli.core.credentials.oidc.detectors.bitbucket_pipelines import (
10+
BitbucketPipelinesDetector,
11+
)
12+
13+
14+
@pytest.fixture
15+
def bitbucket_env():
16+
env = {
17+
"BITBUCKET_STEP_OIDC_TOKEN": "the-jwt",
18+
}
19+
with mock.patch.dict("os.environ", env, clear=True):
20+
yield env
21+
22+
23+
class TestDetect:
24+
def test_detects_when_token_present(self, bitbucket_env):
25+
detector = BitbucketPipelinesDetector(context=CredentialContext())
26+
assert detector.detect() is True
27+
28+
def test_not_detected_when_unset(self):
29+
with mock.patch.dict("os.environ", {}, clear=True):
30+
detector = BitbucketPipelinesDetector(context=CredentialContext())
31+
assert detector.detect() is False
32+
33+
def test_not_detected_when_token_empty(self, bitbucket_env):
34+
bitbucket_env["BITBUCKET_STEP_OIDC_TOKEN"] = ""
35+
with mock.patch.dict("os.environ", bitbucket_env, clear=True):
36+
detector = BitbucketPipelinesDetector(context=CredentialContext())
37+
assert detector.detect() is False
38+
39+
40+
class TestGetToken:
41+
def test_returns_token(self, bitbucket_env):
42+
detector = BitbucketPipelinesDetector(context=CredentialContext())
43+
assert detector.get_token() == "the-jwt"
44+
45+
def test_raises_when_token_missing(self):
46+
with mock.patch.dict("os.environ", {}, clear=True):
47+
detector = BitbucketPipelinesDetector(context=CredentialContext())
48+
with pytest.raises(ValueError):
49+
detector.get_token()
50+
51+
52+
class TestIntegration:
53+
def test_detect_environment_selects_bitbucket_pipelines(self, bitbucket_env):
54+
detector = detect_environment(CredentialContext())
55+
assert isinstance(detector, BitbucketPipelinesDetector)

0 commit comments

Comments
 (0)