Skip to content

Commit 5576d32

Browse files
[Internal] Add CICD environment to the User Agent (#866)
## What changes are proposed in this pull request? This PR adds CICD environment to the User Agent of each SDK outbound request. The implementation is similar to the one used in the Go SDK. ## How is this tested? Added unit tests.
1 parent 267d369 commit 5576d32

File tree

4 files changed

+109
-1
lines changed

4 files changed

+109
-1
lines changed

databricks/sdk/useragent.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,58 @@ def to_string(alternate_product_info: Optional[Tuple[str, str]] = None,
148148
base.extend(_extra)
149149
base.extend(_get_upstream_user_agent_info())
150150
base.extend(_get_runtime_info())
151+
if cicd_provider() != "":
152+
base.append((CICD_KEY, cicd_provider()))
151153
return " ".join(f"{k}/{v}" for k, v in base)
154+
155+
156+
# List of CI/CD providers and pairs of envvar/value that are used to detect them.
157+
_PROVIDERS = {
158+
"github": [("GITHUB_ACTIONS", "true")],
159+
"gitlab": [("GITLAB_CI", "true")],
160+
"jenkins": [("JENKINS_URL", "")],
161+
"azure-devops": [("TF_BUILD", "True")],
162+
"circle": [("CIRCLECI", "true")],
163+
"travis": [("TRAVIS", "true")],
164+
"bitbucket": [("BITBUCKET_BUILD_NUMBER", "")],
165+
"google-cloud-build": [("PROJECT_ID", ""), ("BUILD_ID", ""), ("PROJECT_NUMBER", ""), ("LOCATION", "")],
166+
"aws-code-build": [("CODEBUILD_BUILD_ARN", "")],
167+
"tf-cloud": [("TFC_RUN_ID", "")],
168+
}
169+
170+
# Private variable to store the CI/CD provider. This value is computed at
171+
# the first invocation of cicd_providers() and is cached for subsequent calls.
172+
_cicd_provider = None
173+
174+
175+
def cicd_provider() -> str:
176+
"""Return the CI/CD provider if detected, or an empty string otherwise."""
177+
178+
# This function is safe because (i) assignation are atomic, and (ii)
179+
# computating the CI/CD provider is idempotent.
180+
global _cicd_provider
181+
if _cicd_provider is not None:
182+
return _cicd_provider
183+
184+
providers = []
185+
for p in _PROVIDERS:
186+
found = True
187+
for envvar, value in _PROVIDERS[p]:
188+
v = os.getenv(envvar)
189+
if v is None or (value != "" and v != value):
190+
found = False
191+
break
192+
193+
if found:
194+
providers.append(p)
195+
196+
if len(providers) == 0:
197+
_cicd_provider = ""
198+
else:
199+
# TODO: reconsider what to do if multiple providers are detected.
200+
# The current mechanism as the benefit of being deterministic and
201+
# robust to ordering changes in _PROVIDERS.
202+
providers.sort()
203+
_cicd_provider = providers[0]
204+
205+
return _cicd_provider

tests/test_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ class MockUname:
4242
def system(self):
4343
return 'TestOS'
4444

45+
# Clear all environment variables and cached CICD provider.
46+
for k in os.environ:
47+
monkeypatch.delenv(k, raising=False)
48+
useragent._cicd_provider = None
49+
4550
monkeypatch.setattr(platform, 'python_version', lambda: '3.0.0')
4651
monkeypatch.setattr(platform, 'uname', MockUname)
4752
monkeypatch.setenv('DATABRICKS_SDK_UPSTREAM', "upstream-product")

tests/test_core.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import pytest
1010

11-
from databricks.sdk import WorkspaceClient, errors
11+
from databricks.sdk import WorkspaceClient, errors, useragent
1212
from databricks.sdk.core import ApiClient, Config, DatabricksError
1313
from databricks.sdk.credentials_provider import (CliTokenSource,
1414
CredentialsProvider,
@@ -178,6 +178,11 @@ class MockUname:
178178
def system(self):
179179
return 'TestOS'
180180

181+
# Clear all environment variables and cached CICD provider.
182+
for k in os.environ:
183+
monkeypatch.delenv(k, raising=False)
184+
useragent._cicd_provider = None
185+
181186
monkeypatch.setattr(platform, 'python_version', lambda: '3.0.0')
182187
monkeypatch.setattr(platform, 'uname', MockUname)
183188
monkeypatch.setenv('DATABRICKS_SDK_UPSTREAM', "upstream-product")

tests/test_user_agent.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import pytest
24

35
from databricks.sdk.version import __version__
@@ -40,3 +42,45 @@ def test_user_agent_with_partner(user_agent):
4042
user_agent.with_partner('differenttest')
4143
assert 'partner/test' in user_agent.to_string()
4244
assert 'partner/differenttest' in user_agent.to_string()
45+
46+
47+
@pytest.fixture(scope="function")
48+
def clear_cicd():
49+
# Save and clear env vars.
50+
original_env = os.environ.copy()
51+
os.environ.clear()
52+
53+
# Clear cached CICD provider.
54+
from databricks.sdk import useragent
55+
useragent._cicd_provider = None
56+
57+
yield
58+
59+
# Restore env vars.
60+
os.environ = original_env
61+
62+
63+
def test_user_agent_cicd_no_provider(clear_cicd):
64+
from databricks.sdk import useragent
65+
user_agent = useragent.to_string()
66+
67+
assert 'cicd' not in user_agent
68+
69+
70+
def test_user_agent_cicd_one_provider(clear_cicd):
71+
os.environ['GITHUB_ACTIONS'] = 'true'
72+
73+
from databricks.sdk import useragent
74+
user_agent = useragent.to_string()
75+
76+
assert 'cicd/github' in user_agent
77+
78+
79+
def test_user_agent_cicd_two_provider(clear_cicd):
80+
os.environ['GITHUB_ACTIONS'] = 'true'
81+
os.environ['GITLAB_CI'] = 'true'
82+
83+
from databricks.sdk import useragent
84+
user_agent = useragent.to_string()
85+
86+
assert 'cicd/github' in user_agent

0 commit comments

Comments
 (0)