diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java index 4d9698f8f..6900a4003 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java @@ -1,6 +1,10 @@ package com.databricks.sdk.core; +import com.databricks.sdk.core.utils.Environment; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -121,6 +125,10 @@ public static String asString() { segments.add(String.format("databricks-sdk-java/%s", version)); segments.add(String.format("jvm/%s", jvmVersion())); segments.add(String.format("os/%s", osName())); + String cicdProvider = cicdProvider(); + if (!cicdProvider.isEmpty()) { + segments.add(String.format("cicd/%s", cicdProvider)); + } // Concurrent iteration over ArrayList must be guarded with synchronized. synchronized (otherInfo) { segments.addAll( @@ -130,4 +138,107 @@ public static String asString() { } return segments.stream().collect(Collectors.joining(" ")); } + + // List of CI/CD providers and their environment variables for detection + private static List listCiCdProviders() { + return Arrays.asList( + new CicdProvider("github", Collections.singletonList(new EnvVar("GITHUB_ACTIONS", "true"))), + new CicdProvider("gitlab", Collections.singletonList(new EnvVar("GITLAB_CI", "true"))), + new CicdProvider("jenkins", Collections.singletonList(new EnvVar("JENKINS_URL", ""))), + new CicdProvider("azure-devops", Collections.singletonList(new EnvVar("TF_BUILD", "True"))), + new CicdProvider("circle", Collections.singletonList(new EnvVar("CIRCLECI", "true"))), + new CicdProvider("travis", Collections.singletonList(new EnvVar("TRAVIS", "true"))), + new CicdProvider( + "bitbucket", Collections.singletonList(new EnvVar("BITBUCKET_BUILD_NUMBER", ""))), + new CicdProvider( + "google-cloud-build", + Arrays.asList( + new EnvVar("PROJECT_ID", ""), + new EnvVar("BUILD_ID", ""), + new EnvVar("PROJECT_NUMBER", ""), + new EnvVar("LOCATION", ""))), + new CicdProvider( + "aws-code-build", Collections.singletonList(new EnvVar("CODEBUILD_BUILD_ARN", ""))), + new CicdProvider("tf-cloud", Collections.singletonList(new EnvVar("TFC_RUN_ID", "")))); + } + + // Volatile field to ensure thread-safe lazy initialization + // The 'volatile' keyword ensures that changes to these variables + // are immediately visible to all threads. It prevents instruction + // reordering by the compiler. + protected static volatile String cicdProvider = null; + + protected static Environment env = null; + + // Represents an environment variable with its name and expected value + private static class EnvVar { + private final String name; + private final String expectedValue; + + public EnvVar(String name, String expectedValue) { + this.name = name; + this.expectedValue = expectedValue; + } + } + + // Represents a CI/CD provider with its name and associated environment variables + private static class CicdProvider { + private final String name; + private final List envVars; + + public CicdProvider(String name, List envVars) { + this.name = name; + this.envVars = envVars; + } + + public boolean detect(Environment env) { + for (EnvVar envVar : envVars) { + String value = env.get(envVar.name); + if (value == null) { + return false; + } + if (!envVar.expectedValue.isEmpty() && !value.equals(envVar.expectedValue)) { + return false; + } + } + return true; + } + } + + // Looks up the active CI/CD provider based on environment variables + private static String lookupCiCdProvider(Environment env) { + for (CicdProvider provider : listCiCdProviders()) { + if (provider.detect(env)) { + return provider.name; + } + } + return ""; + } + + // Thread-safe lazy initialization of CI/CD provider detection + private static String cicdProvider() { + // First check (not synchronized) to avoid unnecessary synchronization + if (cicdProvider == null) { + // Synchronize only if cicdProvider is null + synchronized (UserAgent.class) { + // Second check (synchronized) to ensure only one thread initializes + // This is necessary because multiple threads might have passed the first check + if (cicdProvider == null) { + cicdProvider = lookupCiCdProvider(env()); + } + } + } + return cicdProvider; + } + + private static Environment env() { + if (env == null) { + env = + new Environment( + System.getenv(), + System.getenv("PATH").split(File.pathSeparator), + System.getProperty("os.name")); + } + return env; + } } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java index b44c07e7c..9d0b7f6c2 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java @@ -1,5 +1,8 @@ package com.databricks.sdk.core; +import com.databricks.sdk.core.utils.Environment; +import java.util.ArrayList; +import java.util.HashMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -56,4 +59,46 @@ public void testUserAgentWithSemverValue() { String userAgent = UserAgent.asString(); Assertions.assertTrue(userAgent.contains("key1/1.0.0-dev+metadata")); } + + @Test + public void testUserAgentCicdNoProvider() { + UserAgent.cicdProvider = null; + UserAgent.env = + new Environment(new HashMap<>(), new ArrayList<>(), System.getProperty("os.name")); + Assertions.assertFalse(UserAgent.asString().contains("cicd")); + UserAgent.env = null; + } + + @Test + public void testUserAgentCicdOneProvider() { + UserAgent.cicdProvider = null; + UserAgent.env = + new Environment( + new HashMap() { + { + put("GITHUB_ACTIONS", "true"); + } + }, + new ArrayList<>(), + System.getProperty("os.name")); + Assertions.assertTrue(UserAgent.asString().contains("cicd/github")); + UserAgent.env = null; + } + + @Test + public void testUserAgentCicdTwoProvider() { + UserAgent.cicdProvider = null; + UserAgent.env = + new Environment( + new HashMap() { + { + put("GITLAB_CI", "true"); + put("JENKINS_URL", ""); + } + }, + new ArrayList<>(), + System.getProperty("os.name")); + Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab")); + UserAgent.env = null; + } }