├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── BrowserStackAutomateJenkinsPlugin.pdf ├── LICENSE ├── README.md ├── code_formatter ├── eclipse-java-google-style.xml └── intellij-java-google-style.xml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── browserstack │ │ └── automate │ │ └── ci │ │ ├── common │ │ ├── AutomateTestCase.java │ │ ├── BrowserStackBuildWrapperOperations.java │ │ ├── BrowserStackEnvVars.java │ │ ├── Tools.java │ │ ├── analytics │ │ │ ├── Analytics.java │ │ │ └── VersionTracker.java │ │ ├── clienthandler │ │ │ └── ClientHandler.java │ │ ├── constants │ │ │ └── Constants.java │ │ ├── enums │ │ │ └── ProjectType.java │ │ ├── logger │ │ │ └── PluginLogger.java │ │ ├── model │ │ │ └── BrowserStackSession.java │ │ ├── proxysettings │ │ │ └── JenkinsProxySettings.java │ │ ├── report │ │ │ └── XmlReporter.java │ │ ├── tracking │ │ │ ├── PluginsTracker.java │ │ │ └── PluginsTrackerEvents.java │ │ └── uploader │ │ │ ├── AppUploader.java │ │ │ └── AppUploaderHelper.java │ │ └── jenkins │ │ ├── AbstractBrowserStackCypressReportForBuild.java │ │ ├── AbstractBrowserStackReportForBuild.java │ │ ├── AppUploaderBuilder.java │ │ ├── AutomateActionData.java │ │ ├── AutomateTestAction.java │ │ ├── AutomateTestDataPublisher.java │ │ ├── BrowserStackBuildAction.java │ │ ├── BrowserStackBuildWrapper.java │ │ ├── BrowserStackBuildWrapperDescriptor.java │ │ ├── BrowserStackCredentials.java │ │ ├── BrowserStackCypressReportFileCallable.java │ │ ├── BrowserStackCypressReportForBuild.java │ │ ├── BrowserStackCypressReportPublisher.java │ │ ├── BrowserStackReportFileCallable.java │ │ ├── BrowserStackReportForBuild.java │ │ ├── BrowserStackReportPublisher.java │ │ ├── BrowserStackResult.java │ │ ├── VariableInjectorAction.java │ │ ├── integrationService │ │ ├── BrowserStackReportStatus.java │ │ ├── BrowserStackTestReportAction.java │ │ ├── BrowserStackTestReportPublisher.java │ │ └── RequestsUtil.java │ │ ├── local │ │ ├── BrowserStackLocalUtils.java │ │ ├── JenkinsBrowserStackLocal.java │ │ └── LocalConfig.java │ │ ├── observability │ │ ├── AccessControlsFilter.java │ │ ├── BuildWithObservabilityConfigAction.java │ │ ├── BuildWithObservabilityConfigActionFreeStyleFactory.java │ │ ├── BuildWithObservabilityConfigActionWorkflowFactory.java │ │ ├── ObservabilityCause.java │ │ ├── ObservabilityConfig.java │ │ └── ObservabilityEnvironmentContributor.java │ │ ├── pipeline │ │ ├── AppUploadStepExecution.java │ │ ├── AppUploaderStep.java │ │ ├── BrowserStackPipelineStep.java │ │ ├── BrowserStackPipelineStepExecution.java │ │ ├── BrowserStackReportStep.java │ │ ├── BrowserStackReportStepExecution.java │ │ └── ExpanderImpl.java │ │ └── qualityDashboard │ │ ├── QualityDashboardAPIUtil.java │ │ ├── QualityDashboardInit.java │ │ ├── QualityDashboardInitItemListener.java │ │ ├── QualityDashboardPipelineTracker.java │ │ ├── QualityDashboardUtil.java │ │ └── UpstreamPipelineResolver.java ├── resources │ ├── META-INF │ │ └── hudson.remoting.ClassFilter │ ├── com │ │ └── browserstack │ │ │ └── automate │ │ │ └── ci │ │ │ └── jenkins │ │ │ ├── AbstractBrowserStackCypressReportForBuild │ │ │ ├── index.jelly │ │ │ └── summary.jelly │ │ │ ├── AbstractBrowserStackReportForBuild │ │ │ ├── index.jelly │ │ │ └── summary.jelly │ │ │ ├── AccessControlsFilter │ │ │ ├── config.jelly │ │ │ ├── help-allowedHeaders.html │ │ │ ├── help-allowedMethods.html │ │ │ ├── help-allowedOrigins.html │ │ │ ├── help-exposedHeaders.html │ │ │ └── help-maxAge.html │ │ │ ├── AppUploaderBuilder │ │ │ ├── config.jelly │ │ │ └── help-buildFilePath.html │ │ │ ├── AutomateTestAction │ │ │ ├── summary.jelly │ │ │ └── summary.properties │ │ │ ├── BrowserStackBuildWrapper │ │ │ ├── config.jelly │ │ │ ├── config.properties │ │ │ ├── global.jelly │ │ │ ├── global.properties │ │ │ ├── help-credentialsId.html │ │ │ ├── help-enableUsageStats.html │ │ │ ├── help-localConfig.html │ │ │ ├── help-localOptions.html │ │ │ └── help-localPath.html │ │ │ ├── BrowserStackCredentials │ │ │ ├── credentials.jelly │ │ │ ├── help-accesskey.html │ │ │ └── help-username.html │ │ │ ├── BrowserStackCypressReportPublisher │ │ │ └── config.jelly │ │ │ ├── BrowserStackReportPublisher │ │ │ └── config.jelly │ │ │ ├── integrationService │ │ │ ├── BrowserStackTestReportAction │ │ │ │ ├── index.jelly │ │ │ │ └── summary.jelly │ │ │ └── BrowserStackTestReportPublisher │ │ │ │ └── config.jelly │ │ │ └── pipeline │ │ │ ├── AppUploaderStep │ │ │ ├── config.jelly │ │ │ └── help-appPath.html │ │ │ ├── BrowserStackPipelineStep │ │ │ ├── config.jelly │ │ │ ├── config.properties │ │ │ ├── help-credentialsId.html │ │ │ ├── help-enableUsageStats.html │ │ │ ├── help-localConfig.html │ │ │ ├── help-localOptions.html │ │ │ └── help-localPath.html │ │ │ └── BrowserStackReportStep │ │ │ ├── config.jelly │ │ │ └── help-product.html │ ├── index.jelly │ └── plugin.properties └── webapp │ └── images │ ├── ajax-loader-main.gif │ └── logo.png └── test ├── java └── com │ └── browserstack │ └── automate │ ├── ci │ └── jenkins │ │ ├── AutomateTestActionTest.java │ │ ├── AutomateTestDataPublisherTest.java │ │ ├── GlobalConfigTest.java │ │ ├── local │ │ └── JenkinsBrowserStackLocalTest.java │ │ └── pipeline │ │ └── BrowserStackPipelineStepTest.java │ └── jenkins │ └── helpers │ ├── CopyResourceFileToWorkspaceTarget.java │ └── TempCredentialIdGenerator.java └── resources ├── REPORT-com.browserstack.automate.application.tests.TestCaseWithFourUniqueSessions.xml └── TEST-com.browserstack.automate.application.tests.TestCaseWithFourUniqueSessions.xml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 8 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: 8 21 | distribution: 'temurin' 22 | cache: 'maven' 23 | - name: Build 24 | run: mvn clean install -DskipTests 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .DS_Store 3 | 4 | target 5 | build 6 | work 7 | *.log 8 | 9 | # IntelliJ 10 | *.iml 11 | .idea 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | 18 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 19 | hs_err_pid* 20 | 21 | .classpath 22 | .project 23 | .settings/ 24 | /bin/ 25 | .vscode/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk6 5 | - oraclejdk7 6 | - oraclejdk8 7 | 8 | 9 | script: "mvn clean package" 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | 14 | -------------------------------------------------------------------------------- /BrowserStackAutomateJenkinsPlugin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/browserstack-integration-plugin/HEAD/BrowserStackAutomateJenkinsPlugin.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 BrowserStack Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | BrowserStack Logo 3 |

4 | 5 | # Overview 6 | [BrowserStack](https://browserstack.com) gives instant access to 2000+ real mobile devices and browsers that enables developers to test their websites and mobile applications without requiring to install or maintain an internal lab of virtual machines, devices, or emulators. 7 | 8 | This BrowserStack Jenkins plugin helps you integrate and run your test suite from a Jenkins CI server on the BrowserStack device cloud. 9 | 10 | # Features 11 | Use the BrowserStack Jenkins plugin to: 12 | - Configure your BrowserStack credentials for your Jenkins jobs. 13 | - Set up and tear down the BrowserStack Local binary for testing internal, development, and staging environments. 14 | - Upload your app build to the BrowserStack servers (for mobile app testing in App Automate). 15 | - Embed BrowserStack test results, including video, logs, and screenshots in your Jenkins job results. 16 | 17 | 18 | # Prerequisites 19 | You need the following to use the plugin: 20 | - An existing Jenkins CI server (version 1.653+) 21 | - A BrowserStack account. You can [sign-up for free trial](https://www.browserstack.com/users/sign_up) if you do not have an existing account. 22 | 23 | # Installation and setup 24 | - Follow [Automate Jenkins documentation](https://www.browserstack.com/docs/automate/selenium/jenkins) to integrate your Selenium and Appium test suite for website testing in **Automate**. 25 | - Follow [App Automate Jenkins documentation](https://www.browserstack.com/docs/app-automate/appium/integrations/jenkins) to integrate your Appium test suite for native and hybrid mobile app testing in **App Automate**. 26 | 27 | # Latest updates 28 | With the 1.1.10 version of the Jenkins plugin, you can now enable BrowserStack test reporting in Jenkins with all test languages and frameworks with proxy support. 29 | 30 | # Feature requests and bug reports 31 | Please file feature requests and bug reports to [BrowserStack Support team](https://www.browserstack.com/contact?ref=help#technical-support). 32 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/AutomateTestCase.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common; 2 | 3 | import com.browserstack.automate.ci.common.logger.PluginLogger; 4 | import org.apache.commons.lang.StringUtils; 5 | 6 | import java.io.Serializable; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class AutomateTestCase implements Serializable { 11 | 12 | private static final Pattern PATTERN_TEST_SESSION = Pattern.compile("^browserstack:session:([^:]+):test:([^\\{]+)\\{([^\\}]+)\\}(.*)"); 13 | private static final String REGEX_TEST_CLASSPATH = "^\\w+(\\.\\w+)+$"; 14 | private static final String REGEX_TEST_ID_HASH = "^\\w+$"; 15 | private static final String PACKAGE_DEFAULT = "(root)"; 16 | private static final String CLASS_DEFAULT = ""; 17 | private static final String TEST_DEFAULT = ""; 18 | 19 | public final String sessionId; 20 | public final String packageName; 21 | public final String className; 22 | public final String testName; 23 | public final String testFullPath; 24 | public final String testHash; 25 | public final long testIndex; 26 | public final String testCaseObjectId; 27 | 28 | public AutomateTestCase(String sessionId, String testHash, long testIndex, String testCaseObjectId) { 29 | this.sessionId = sessionId; 30 | this.testHash = testHash; 31 | this.testIndex = testIndex; 32 | this.testCaseObjectId = testCaseObjectId; 33 | this.packageName = PACKAGE_DEFAULT; 34 | this.className = CLASS_DEFAULT; 35 | this.testName = TEST_DEFAULT; 36 | this.testFullPath = null; 37 | } 38 | 39 | public AutomateTestCase(String sessionId, String packageName, String className, String testName, long testIndex, 40 | String testCaseObjectId) { 41 | this.sessionId = sessionId; 42 | this.packageName = packageName; 43 | this.className = className; 44 | this.testName = stripTestParams(testName); 45 | this.testIndex = testIndex; 46 | this.testCaseObjectId = testCaseObjectId; 47 | this.testFullPath = (this.packageName.equals(PACKAGE_DEFAULT) ? "" : this.packageName + '.') 48 | + this.className + '.' + this.testName; 49 | this.testHash = null; 50 | } 51 | 52 | public static String stripTestParams(String testCaseName) { 53 | if (StringUtils.isEmpty(testCaseName)) { 54 | return null; 55 | } 56 | 57 | int subscriptIndex = testCaseName.indexOf('['); 58 | if (subscriptIndex != -1) { 59 | return testCaseName.substring(0, subscriptIndex); 60 | } 61 | 62 | return testCaseName; 63 | } 64 | 65 | public static AutomateTestCase parse(final String line) { 66 | Matcher matcher = PATTERN_TEST_SESSION.matcher(line); 67 | if (matcher.find()) { 68 | String sessionId = matcher.group(1); 69 | String testCasePath = matcher.group(2); 70 | String testCaseIndexStr = matcher.group(3); 71 | String testCaseObjectId = matcher.group(4); 72 | long testCaseIndex; 73 | 74 | try { 75 | testCaseIndex = Long.parseLong(testCaseIndexStr); 76 | } catch (NumberFormatException e) { 77 | PluginLogger.logDebug(System.out, "ERROR: Failed to parse testCaseIndex as Long: " + testCaseIndexStr); 78 | return null; 79 | } 80 | 81 | return parseTestCasePath(testCasePath, sessionId, testCaseIndex, testCaseObjectId); 82 | } 83 | 84 | return null; 85 | } 86 | 87 | private static AutomateTestCase parseTestCasePath(final String testCasePath, final String sessionId, 88 | final long testCaseIndex, final String testCaseObjectId) { 89 | if (testCasePath.matches(REGEX_TEST_CLASSPATH)) { 90 | PluginLogger.logDebug(System.out, "Parsing as package.class.testName"); 91 | 92 | int pos = testCasePath.lastIndexOf("."); 93 | if (pos != -1) { 94 | // try for package.class.[testName] 95 | String testCaseName = testCasePath.substring(pos + 1, testCasePath.length()); 96 | String path = testCasePath.substring(0, pos); 97 | 98 | pos = path.lastIndexOf("."); 99 | if (pos != -1) { 100 | // try for [package.class].testName 101 | String className = path.substring(pos + 1, testCasePath.length()); 102 | String packageName = path.substring(0, pos); 103 | PluginLogger.logDebug(System.out, "Parsed as package.class.testName"); 104 | return new AutomateTestCase(sessionId, packageName, className, testCaseName, testCaseIndex, testCaseObjectId); 105 | } else { 106 | // try for [class].testName 107 | PluginLogger.logDebug(System.out, "Parsed as (root).class.testName"); 108 | return new AutomateTestCase(sessionId, PACKAGE_DEFAULT, path, testCaseName, testCaseIndex, testCaseObjectId); 109 | } 110 | } 111 | } 112 | 113 | if (testCasePath.matches(REGEX_TEST_ID_HASH)) { 114 | PluginLogger.logDebug(System.out, "Parsed as test hash"); 115 | return new AutomateTestCase(sessionId, testCasePath, testCaseIndex, testCaseObjectId); 116 | } 117 | 118 | return null; 119 | } 120 | 121 | public boolean hasTestHash() { 122 | return (testHash != null && testHash.length() > 0); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common; 2 | 3 | public interface BrowserStackEnvVars { 4 | String BROWSERSTACK_USER = "BROWSERSTACK_USER"; 5 | String BROWSERSTACK_USERNAME = "BROWSERSTACK_USERNAME"; 6 | String BROWSERSTACK_ACCESSKEY = "BROWSERSTACK_ACCESSKEY"; 7 | String BROWSERSTACK_ACCESS_KEY = "BROWSERSTACK_ACCESS_KEY"; 8 | String BROWSERSTACK_LOCAL = "BROWSERSTACK_LOCAL"; 9 | String BROWSERSTACK_LOCAL_IDENTIFIER = "BROWSERSTACK_LOCAL_IDENTIFIER"; 10 | String BROWSERSTACK_BUILD = "BROWSERSTACK_BUILD"; 11 | String BROWSERSTACK_BUILD_NAME = "BROWSERSTACK_BUILD_NAME"; 12 | 13 | String BROWSERSTACK_PROJECT_NAME = "BROWSERSTACK_PROJECT_NAME"; 14 | String BROWSERSTACK_APP_ID = "BROWSERSTACK_APP_ID"; 15 | String BROWSERSTACK_RERUN = "BROWSERSTACK_RERUN"; 16 | String BROWSERSTACK_RERUN_TESTS = "BROWSERSTACK_RERUN_TESTS"; 17 | String GRR_JENKINS_KEY = "BROWSERSTACK_GRR_REGION"; 18 | String AUTOMATE_API_ENV_KEY = "browserstack.automate.api"; 19 | String APP_AUTOMATE_API_ENV_KEY = "browserstack.app-automate.api"; 20 | String QEI_URL = "QEI_URL"; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/Tools.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common; 2 | 3 | import org.apache.commons.lang.RandomStringUtils; 4 | 5 | import hudson.FilePath; 6 | import hudson.model.Run; 7 | 8 | import java.io.File; 9 | import java.io.PrintWriter; 10 | import java.io.StringWriter; 11 | import java.text.DateFormat; 12 | import java.text.SimpleDateFormat; 13 | import java.util.Arrays; 14 | import java.util.Locale; 15 | import java.util.regex.Pattern; 16 | 17 | public class Tools { 18 | 19 | public static final Pattern BUILD_URL_PATTERN = Pattern.compile("(https?:\\/\\/[\\w-.]+\\/builds\\/\\w+)\\/sessions\\/\\w+"); 20 | public static final SimpleDateFormat READABLE_DATE_FORMAT = new SimpleDateFormat("dd MMM yyyy, HH:mm"); 21 | public static final DateFormat SESSION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH); 22 | 23 | /** 24 | * Returns a string with only '*' of length equal to the length of the inputStr 25 | * 26 | * @param strToMask 27 | * @return masked string 28 | */ 29 | public static String maskString(String strToMask) { 30 | char[] maskChars = new char[strToMask.length()]; 31 | Arrays.fill(maskChars, '*'); 32 | return new String(maskChars); 33 | } 34 | 35 | /** 36 | * Returns human readable form 37 | * 38 | * @param duration in seconds 39 | * @return 40 | */ 41 | public static String durationToHumanReadable(long duration) { 42 | String result = ""; 43 | int seconds = (int) duration % 60; 44 | duration /= 60; 45 | int minutes = (int) duration % 60; 46 | duration /= 60; 47 | int hours = (int) duration % 24; 48 | int days = (int) duration / 24; 49 | 50 | if (days == 0) { 51 | if (hours == 0) { 52 | if (minutes == 0) { 53 | result = String.format("%02ds", seconds); 54 | } else { 55 | result = String.format("%02dm %02ds", minutes, seconds); 56 | } 57 | } else { 58 | result = String.format("%02dh %02dm %02ds", hours, minutes, seconds); 59 | } 60 | } else { 61 | result = String.format("%dd %02dh %02dm %02ds", days, hours, minutes, seconds); 62 | } 63 | return result; 64 | } 65 | 66 | public static String getUniqueString(boolean letters, boolean numbers) { 67 | return RandomStringUtils.random(48, letters, numbers); 68 | } 69 | 70 | /** Gets the directory to store report files */ 71 | public static FilePath getBrowserStackReportDir(Run build, String dirName) { 72 | return new FilePath(new File(build.getRootDir(), dirName)); 73 | } 74 | 75 | public static String getStackTraceAsString(Throwable throwable) { 76 | try { 77 | StringWriter stringWriter = new StringWriter(); 78 | throwable.printStackTrace(new PrintWriter(stringWriter)); 79 | return stringWriter.toString(); 80 | } catch(Throwable e) { 81 | return throwable != null ? throwable.toString() : ""; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/analytics/VersionTracker.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.analytics; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.node.ObjectNode; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Shirish Kamath 12 | * @author Anirudha Khanna 13 | */ 14 | public class VersionTracker { 15 | 16 | private static final String ID_FILENAME = "browserstack-id.txt"; 17 | 18 | private static final ObjectMapper mapper = new ObjectMapper(); 19 | 20 | private final File rootDir; 21 | 22 | private ObjectNode metadata; 23 | 24 | public VersionTracker(File rootDir) { 25 | this.rootDir = rootDir; 26 | } 27 | 28 | public boolean init(String pluginVersion) throws IOException { 29 | try { 30 | loadMetadata(); 31 | } catch (IOException e) { 32 | // first install (?) 33 | saveMetadata(pluginVersion); 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | public String getClientId() throws IOException { 41 | return getProperty("id"); 42 | } 43 | 44 | public String getPluginVersion() { 45 | return getProperty("version"); 46 | } 47 | 48 | public String getProperty(String name) { 49 | return (metadata != null && metadata.has(name)) ? metadata.get(name).asText() : null; 50 | } 51 | 52 | public boolean updateVersion(String version) throws IOException { 53 | String pluginVersion = getPluginVersion(); 54 | boolean needsUpdate = (pluginVersion == null || !pluginVersion.equals(version)); 55 | if (needsUpdate) { 56 | saveMetadata(version); 57 | return true; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | public void loadMetadata() throws IOException { 64 | metadata = mapper.readValue(new File(rootDir, ID_FILENAME), ObjectNode.class); 65 | } 66 | 67 | private void saveMetadata(String pluginVersion) throws IOException { 68 | String instanceId = UUID.randomUUID().toString().replace("\\-", ""); 69 | 70 | ObjectNode metadata = mapper.createObjectNode(); 71 | metadata.put("id", instanceId); 72 | metadata.put("version", pluginVersion); 73 | metadata.put("timestamp", System.currentTimeMillis()); 74 | 75 | File f = new File(rootDir, ID_FILENAME); 76 | try { 77 | mapper.writeValue(f, metadata); 78 | } catch (IOException ex) { 79 | throw new IOException("Failed to store plugin metadata", ex); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/clienthandler/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.clienthandler; 2 | 3 | import com.browserstack.appautomate.AppAutomateClient; 4 | import com.browserstack.automate.AutomateClient; 5 | import com.browserstack.automate.ci.common.enums.ProjectType; 6 | import com.browserstack.automate.ci.common.proxysettings.JenkinsProxySettings; 7 | import com.browserstack.client.BrowserStackClient; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.annotation.Nullable; 11 | import java.io.PrintStream; 12 | 13 | public class ClientHandler { 14 | 15 | /** 16 | * Returns BrowserStackClient based on Project, i.e Automate or App Automate. 17 | * Also decides and sets the proxy for the client 18 | * @param project ProjectType 19 | * @param username Username of BrowserStack 20 | * @param accessKey Access Key of BrowserStack 21 | * @param customProxy Custom Proxy String 22 | * @param logger Logger 23 | * @return BrowserStackClient 24 | */ 25 | public static BrowserStackClient getBrowserStackClient(@Nonnull final ProjectType project, @Nonnull final String username, 26 | @Nonnull final String accessKey, @Nullable final String customProxy, 27 | @Nullable final PrintStream logger) { 28 | BrowserStackClient client = decideAndGetClient(project, username, accessKey); 29 | 30 | JenkinsProxySettings proxy; 31 | if (customProxy != null) { 32 | proxy = new JenkinsProxySettings(customProxy, logger); 33 | } else { 34 | proxy = new JenkinsProxySettings(logger); 35 | } 36 | 37 | if (proxy.hasProxy()) { 38 | client.setProxy(proxy.getHost(), proxy.getPort(), proxy.getUsername(), proxy.getPassword()); 39 | } 40 | 41 | return client; 42 | } 43 | 44 | /** 45 | * Initializes BrowserStack client based on project type 46 | * @param project ProjectType 47 | * @param username Username of BrowserStack 48 | * @param accessKey Access Key of BrowserStack 49 | * @return BrowserStackClient 50 | */ 51 | private static BrowserStackClient decideAndGetClient(@Nonnull final ProjectType project, @Nonnull final String username, @Nonnull final String accessKey) { 52 | if (project == ProjectType.APP_AUTOMATE) { 53 | return new AppAutomateClient(username, accessKey); 54 | } 55 | 56 | return new AutomateClient(username, accessKey); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/constants/Constants.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.constants; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import jenkins.model.Jenkins; 7 | 8 | public class Constants { 9 | public static final String BROWSERSTACK_REPORT_DISPLAY_NAME = "BrowserStack Test Report"; 10 | public static final String BROWSERSTACK_CYPRESS_REPORT_DISPLAY_NAME = "BrowserStack Cypress Test Report"; 11 | public static final String BROWSERSTACK_LOGO = String.format("%s/plugin/browserstack-integration/images/logo.png", Jenkins.RESOURCE_PATH); 12 | public static final String BROWSERSTACK_REPORT_URL = "testReportBrowserStack"; 13 | public static final String BROWSERSTACK_CYPRESS_REPORT_URL = "testReportBrowserStackCypress"; 14 | public static final String BROWSERSTACK_REPORT_PIPELINE_FUNCTION = "browserStackReportPublisher"; 15 | public static final String BROWSERSTACK_REPORT_PATH_PATTERN = "**/browserstack-artifacts/*"; 16 | public static final String JENKINS_CI_PLUGIN = "JenkinsCiPlugin"; 17 | 18 | public static final String CAD_BASE_URL = "https://api-observability.browserstack.com/ext"; 19 | public static final String BROWSERSTACK_CONFIG_DETAILS_ENDPOINT = "/v1/builds/buildReport"; 20 | 21 | public static final String INTEGRATIONS_TOOL_KEY = "jenkins"; 22 | 23 | public static final String BROWSERSTACK_TEST_REPORT_URL = "testReportBrowserStack"; 24 | public static final String BROWSERSTACK_CAD_REPORT_DISPLAY_NAME = "BrowserStack Test Report and Insights"; 25 | public static final String BROWSERSTACK_REPORT_FILENAME = "browserstack-report"; 26 | public static final String BROWSERSTACK_REPORT_FOLDER = "browserstack-artifacts"; 27 | 28 | public static final String BROWSERSTACK_REPORT_AUT_PIPELINE_FUNCTION = "browserStackReportAut"; 29 | 30 | // Product 31 | public static final String AUTOMATE = "automate"; 32 | public static final String APP_AUTOMATE = "app-automate"; 33 | 34 | //GRR REGION vs API URL mapping for Automate 35 | public static Map GRR_AUTO_REGION_VS_APIURL = new HashMap(); 36 | public static Map GRR_APPAUTO_REGION_VS_APIURL = new HashMap(); 37 | static { 38 | GRR_AUTO_REGION_VS_APIURL.put("eu","https://api-eu-only.browserstack.com/automate"); 39 | GRR_AUTO_REGION_VS_APIURL.put("us","https://api-us-only.browserstack.com/automate"); 40 | GRR_APPAUTO_REGION_VS_APIURL.put("eu","https://api-eu-only.browserstack.com/app-automate"); 41 | GRR_APPAUTO_REGION_VS_APIURL.put("us","https://api-us-only.browserstack.com/app-automate"); 42 | } 43 | 44 | public static final String JENKINS_BUILD_TAG = "BUILD_TAG"; 45 | 46 | // Session related info 47 | public static final class SessionInfo { 48 | public static final String NAME = "name"; 49 | public static final String BROWSERSTACK_BUILD_NAME = "buildName"; 50 | public static final String BROWSERSTACK_BUILD_URL = "buildUrl"; 51 | public static final String BROWSERSTACK_BUILD_DURATION = "buildDuration"; 52 | public static final String BROWSER = "browser"; 53 | public static final String OS = "os"; 54 | public static final String STATUS = "status"; 55 | public static final String USER_MARKED = "userMarked"; 56 | public static final String DURATION = "duration"; 57 | public static final String CREATED_AT = "createdAt"; 58 | public static final String CREATED_AT_READABLE = "createdAtReadable"; 59 | public static final String URL = "url"; 60 | } 61 | 62 | // Report 63 | public static final class ReportStatus { 64 | public static final String SUCCESS = "Success"; 65 | public static final String FAILED = "Failed"; 66 | } 67 | 68 | // Session Status (not exhaustive) 69 | public static final class SessionStatus { 70 | public static final String RUNNING = "running"; 71 | public static final String ERROR = "error"; 72 | public static final String FAILED = "failed"; 73 | public static final String UNMARKED = "unmarked"; 74 | public static final String PASSED = "passed"; 75 | } 76 | 77 | public static final class QualityDashboardAPI { 78 | public static final String QEI_DEFAULT_URL = "https://quality-engineering-insights.browserstack.com"; 79 | public static String host = QEI_DEFAULT_URL; 80 | // Cache configuration 81 | public static final long CACHE_DURATION_MS = 60 * 60 * 1000L; // 1 hour in milliseconds 82 | 83 | public static String getHost() { 84 | return host; 85 | } 86 | 87 | public static void setHost(String newHost) { 88 | host = newHost; 89 | } 90 | 91 | public static final String getURLBase() { 92 | return getHost() + "/api/v1/jenkins"; 93 | } 94 | 95 | public static final String getLogMessageEndpoint() { 96 | return getURLBase() + "/log-message"; 97 | } 98 | 99 | public static final String getIsInitSetupRequiredEndpoint() { 100 | return getURLBase() + "/init-setup-required"; 101 | } 102 | 103 | public static final String getHistoryForDaysEndpoint() { 104 | return getURLBase() + "/history-for-days"; 105 | } 106 | 107 | public static final String getSavePipelinesEndpoint() { 108 | return getURLBase() + "/save-pipelines"; 109 | } 110 | 111 | public static final String getSavePipelineResultsEndpoint() { 112 | return getURLBase() + "/save-pipeline-results"; 113 | } 114 | 115 | public static final String getItemCrudEndpoint() { 116 | return getURLBase() + "/item"; 117 | } 118 | 119 | public static final String getIsQdEnabledEndpoint() { 120 | return getURLBase() + "/qd-enabled"; 121 | } 122 | 123 | public static final String getIsPipelineEnabledEndpoint() { 124 | return getURLBase() + "/pipeline-enabled"; 125 | } 126 | 127 | public static final String getResultDirectoryEndpoint() { 128 | return getURLBase() + "/get-result-directory"; 129 | } 130 | 131 | public static final String getUploadResultZipEndpoint() { 132 | return getURLBase() + "/upload-result"; 133 | } 134 | 135 | public static final String getStorePipelineResultsEndpoint() { 136 | return getURLBase() + "/save-results"; 137 | } 138 | 139 | public static final String getProjectsPageSizeEndpoint() { 140 | return getURLBase() + "/projects-page-size"; 141 | } 142 | 143 | public static final String getResultsPageSizeEndpoint() { 144 | return getURLBase() + "/results-page-size"; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/enums/ProjectType.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.enums; 2 | 3 | public enum ProjectType { 4 | AUTOMATE, APP_AUTOMATE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/logger/PluginLogger.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.logger; 2 | 3 | import java.io.PrintStream; 4 | 5 | public class PluginLogger { 6 | private static final String PROPERTY_DEBUG = "browserstack.automate.debug"; 7 | private static String TAG = "[BrowserStack]"; 8 | 9 | public static boolean isDebugEnabled() { 10 | return System.getProperty(PROPERTY_DEBUG, "false").equals("true"); 11 | } 12 | 13 | public static void log(PrintStream printStream, String message) { 14 | printStream.println(TAG + " " + message); 15 | } 16 | 17 | public static void logDebug(PrintStream printStream, String message) { 18 | if (isDebugEnabled()) 19 | printStream.println(TAG + ": " + message); 20 | } 21 | 22 | public static void setTag(String tag) { 23 | TAG = tag; 24 | } 25 | 26 | public static void logError(PrintStream printStream, String message) { 27 | log(printStream, "[ERROR] " + message); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/model/BrowserStackSession.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.model; 2 | 3 | import com.browserstack.automate.ci.common.enums.ProjectType; 4 | 5 | /** 6 | * Description : For storing info of a browserstack session. 7 | */ 8 | 9 | public class BrowserStackSession { 10 | 11 | private static final String KEY_SESSION_ID = "sessionId"; 12 | private static final String KEY_PROJECT_TYPE = "projectType"; 13 | 14 | private String sessionId; 15 | private ProjectType projectType; 16 | 17 | public BrowserStackSession(String sessionId, String projectType) { 18 | this.sessionId = sessionId; 19 | this.projectType = getProjectTypeFromString(projectType); 20 | } 21 | 22 | private ProjectType getProjectTypeFromString(String projectType) { 23 | try { 24 | return ProjectType.valueOf(projectType); 25 | } catch (Exception e) { 26 | return ProjectType.AUTOMATE; // default project type for backward compatibility. 27 | } 28 | } 29 | 30 | public String getSessionId() { 31 | return sessionId; 32 | } 33 | 34 | public ProjectType getProjectType() { 35 | return projectType; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/report/XmlReporter.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.report; 2 | 3 | import com.browserstack.automate.ci.common.model.BrowserStackSession; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import org.w3c.dom.Document; 7 | import org.w3c.dom.Element; 8 | import org.w3c.dom.Node; 9 | import org.w3c.dom.NodeList; 10 | 11 | import javax.xml.parsers.DocumentBuilder; 12 | import javax.xml.parsers.DocumentBuilderFactory; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | /** 19 | * @author Shirish Kamath 20 | * @author Anirudha Khanna 21 | */ 22 | public class XmlReporter { 23 | 24 | public static Map parse(File f) throws IOException { 25 | Map testSessionMap = new HashMap(); 26 | Document doc; 27 | 28 | try { 29 | DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); 30 | DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); 31 | doc = dBuilder.parse(f); 32 | } catch (Exception e) { 33 | throw new IOException(e.getMessage()); 34 | } 35 | 36 | Element documentElement = doc.getDocumentElement(); 37 | NodeList testCaseNodes = documentElement.getElementsByTagName("testcase"); 38 | 39 | for (int i = 0; i < testCaseNodes.getLength(); i++) { 40 | Node n = testCaseNodes.item(i); 41 | 42 | if (n.getNodeType() == Node.ELEMENT_NODE) { 43 | Element el = (Element) n; 44 | if (el.hasAttribute("id") && el.hasChildNodes()) { 45 | String testId = el.getAttribute("id"); 46 | NodeList sessionNode = el.getElementsByTagName("session"); 47 | if (sessionNode.getLength() > 0 48 | && sessionNode.item(0).getNodeType() == Node.ELEMENT_NODE) { 49 | NodeList projectTypeNode = el.getElementsByTagName("projectType"); 50 | String projectType = 51 | projectTypeNode.getLength() > 0 ? projectTypeNode.item(0).getTextContent() : ""; 52 | Gson gson = new GsonBuilder().create(); 53 | 54 | BrowserStackSession session = 55 | new BrowserStackSession(sessionNode.item(0).getTextContent(), projectType); 56 | testSessionMap.put(testId, gson.toJson(session)); 57 | } 58 | } 59 | } 60 | } 61 | 62 | return testSessionMap; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/tracking/PluginsTracker.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.tracking; 2 | 3 | 4 | import com.browserstack.automate.ci.common.Tools; 5 | import com.browserstack.automate.ci.common.constants.Constants; 6 | import com.browserstack.automate.ci.common.proxysettings.JenkinsProxySettings; 7 | import okhttp3.Authenticator; 8 | import okhttp3.Call; 9 | import okhttp3.Callback; 10 | import okhttp3.Credentials; 11 | import okhttp3.MediaType; 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.Request; 14 | import okhttp3.RequestBody; 15 | import okhttp3.Response; 16 | import okhttp3.Route; 17 | import org.json.JSONObject; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.IOException; 21 | import java.net.Proxy; 22 | import java.time.Instant; 23 | import java.util.Optional; 24 | 25 | public class PluginsTracker { 26 | private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); 27 | private static final String URL = "https://api.browserstack.com/ci_plugins/track"; 28 | private final String trackingId; 29 | private transient OkHttpClient client; 30 | private String username; 31 | private String accessKey; 32 | private String customProxy; 33 | 34 | public PluginsTracker(final String username, final String accessKey, @Nullable final String customProxy) { 35 | this.username = username; 36 | this.accessKey = accessKey; 37 | this.customProxy = customProxy; 38 | this.trackingId = Tools.getUniqueString(true, true); 39 | initializeClient(); 40 | } 41 | 42 | public PluginsTracker() { 43 | this(null); 44 | } 45 | 46 | public PluginsTracker(@Nullable final String customProxy) { 47 | this.username = null; 48 | this.accessKey = null; 49 | this.customProxy = customProxy; 50 | this.trackingId = Tools.getUniqueString(true, true); 51 | initializeClient(); 52 | } 53 | 54 | private void asyncPostRequestSilent(final String url, final String json) { 55 | RequestBody body = RequestBody.create(JSON, json); 56 | Request request = new Request.Builder() 57 | .url(url) 58 | .post(body) 59 | .build(); 60 | 61 | client.newCall(request).enqueue(new Callback() { 62 | @Override 63 | public void onFailure(Call call, IOException e) { 64 | } 65 | 66 | @Override 67 | public void onResponse(Call call, Response response) throws IOException { 68 | // closing the response body is important, else it will start leaking 69 | if (response != null && response.body() != null) { 70 | response.body().close(); 71 | } 72 | } 73 | }); 74 | } 75 | 76 | private void initializeClient() { 77 | 78 | JenkinsProxySettings jenkinsProxy; 79 | if (customProxy != null) { 80 | jenkinsProxy = new JenkinsProxySettings(customProxy, null); 81 | } else { 82 | jenkinsProxy = new JenkinsProxySettings(null); 83 | } 84 | 85 | final Proxy proxy = jenkinsProxy.getJenkinsProxy(); 86 | if (proxy != Proxy.NO_PROXY) { 87 | if (jenkinsProxy.hasAuth()) { 88 | final String username = jenkinsProxy.getUsername(); 89 | final String password = jenkinsProxy.getPassword(); 90 | Authenticator proxyAuthenticator = new Authenticator() { 91 | @Override 92 | public Request authenticate(Route route, Response response) throws IOException { 93 | final String credential = Credentials.basic(username, password); 94 | return response.request().newBuilder() 95 | .header("Proxy-Authorization", credential) 96 | .build(); 97 | } 98 | }; 99 | client = new OkHttpClient.Builder().proxy(proxy).proxyAuthenticator(proxyAuthenticator).build(); 100 | } else { 101 | client = new OkHttpClient.Builder().proxy(proxy).build(); 102 | } 103 | } else { 104 | client = new OkHttpClient.Builder().build(); 105 | } 106 | } 107 | 108 | public void trackOperation(String operationType, JSONObject data) { 109 | JSONObject requestData = new JSONObject(); 110 | requestData.put("source", Constants.JENKINS_CI_PLUGIN); 111 | requestData.put("product", Constants.AUTOMATE); 112 | requestData.put("team", Constants.AUTOMATE); 113 | requestData.put("data", data); 114 | requestData.put("event_timestamp", Instant.now().getEpochSecond()); 115 | requestData.put("track_operation_type", operationType); 116 | requestData.put("tracking_id", trackingId); 117 | 118 | Optional.ofNullable(username) 119 | .ifPresent(userName -> requestData.put("username", userName)); 120 | Optional.ofNullable(accessKey) 121 | .ifPresent(accessKey -> requestData.put("access_key", accessKey)); 122 | 123 | asyncPostRequestSilent(URL, requestData.toString()); 124 | } 125 | 126 | public void sendError(String errorMessage, boolean pipelineStatus, String phase) { 127 | JSONObject trackingData = new JSONObject(); 128 | trackingData.put("error", errorMessage); 129 | trackingData.put("pipeline", pipelineStatus); 130 | trackingData.put("phase", phase); 131 | trackOperation(PluginsTrackerEvents.CI_PLUGIN_ERROR, trackingData); 132 | } 133 | 134 | public void pluginInitialized(String buildName, boolean localStatus, boolean pipelineStatus) { 135 | JSONObject trackingData = new JSONObject(); 136 | trackingData.put("build_name", buildName); 137 | trackingData.put("local", localStatus); 138 | trackingData.put("pipeline", pipelineStatus); 139 | trackOperation(PluginsTrackerEvents.CI_PLUGIN_INITIALIZED, trackingData); 140 | } 141 | 142 | public void reportGenerationInitialized(String buildName, String product, boolean pipelineStatus) { 143 | JSONObject trackingData = new JSONObject(); 144 | trackingData.put("build_name", buildName); 145 | trackingData.put("product", product); 146 | trackingData.put("pipeline", pipelineStatus); 147 | trackOperation(PluginsTrackerEvents.CI_PLUGIN_REPORT_GENERATION_STARTED, trackingData); 148 | } 149 | 150 | public void reportGenerationCompleted(String status, String product, boolean pipelineStatus, String buildName, String buildId) { 151 | JSONObject dataToTrack = new JSONObject(); 152 | dataToTrack.put("status", status); 153 | dataToTrack.put("product", product); 154 | dataToTrack.put("pipeline", pipelineStatus); 155 | dataToTrack.put("build_name", buildName); 156 | dataToTrack.put("build_id", buildId); 157 | trackOperation(PluginsTrackerEvents.CI_PLUGIN_REPORT_PUBLISHED, dataToTrack); 158 | } 159 | 160 | public void setCredentials(String username, String accessKey) { 161 | this.username = Optional.ofNullable(this.username) 162 | .orElse(username); 163 | this.accessKey = Optional.ofNullable(this.accessKey) 164 | .orElse(accessKey); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/tracking/PluginsTrackerEvents.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.tracking; 2 | 3 | public class PluginsTrackerEvents { 4 | public static final String CI_PLUGIN_INITIALIZED = "CI_PLUGIN_INITIALIZED"; 5 | public static final String CI_PLUGIN_REPORT_GENERATION_STARTED = "CI_PLUGIN_REPORT_GENERATION_STARTED"; 6 | public static final String CI_PLUGIN_REPORT_PUBLISHED = "CI_PLUGIN_REPORT_PUBLISHED"; 7 | public static final String CI_PLUGIN_ERROR = "CI_PLUGIN_ERROR"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/uploader/AppUploader.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.uploader; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.PrintStream; 5 | 6 | import com.browserstack.appautomate.AppAutomateClient; 7 | import com.browserstack.automate.ci.common.proxysettings.JenkinsProxySettings; 8 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 9 | import com.browserstack.automate.exception.AppAutomateException; 10 | import com.browserstack.automate.exception.InvalidFileExtensionException; 11 | 12 | public class AppUploader { 13 | 14 | String appPath; 15 | BrowserStackCredentials credentials; 16 | private final transient PrintStream logger; 17 | private final String customProxy; 18 | 19 | public AppUploader(String appPath, BrowserStackCredentials credentials, final String customProxy, final PrintStream logger) { 20 | this.appPath = appPath; 21 | this.credentials = credentials; 22 | this.logger = logger; 23 | this.customProxy = customProxy; 24 | } 25 | 26 | public String uploadFile() 27 | throws AppAutomateException, FileNotFoundException, InvalidFileExtensionException { 28 | AppAutomateClient appAutomateClient = 29 | new AppAutomateClient(credentials.getUsername(), credentials.getDecryptedAccesskey()); 30 | 31 | JenkinsProxySettings proxy; 32 | if (customProxy != null) { 33 | proxy = new JenkinsProxySettings(customProxy, logger); 34 | } else { 35 | proxy = new JenkinsProxySettings(logger); 36 | } 37 | 38 | if (proxy.hasProxy()) { 39 | appAutomateClient.setProxy(proxy.getHost(), proxy.getPort(), proxy.getUsername(), proxy.getPassword()); 40 | } 41 | return appAutomateClient.uploadApp(this.appPath).getAppUrl(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/common/uploader/AppUploaderHelper.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.common.uploader; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.PrintStream; 6 | import com.browserstack.automate.ci.common.logger.PluginLogger; 7 | import com.browserstack.automate.ci.jenkins.BrowserStackBuildAction; 8 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 9 | import com.browserstack.automate.exception.AppAutomateException; 10 | import com.browserstack.automate.exception.InvalidFileExtensionException; 11 | import hudson.model.Actionable; 12 | import hudson.util.FormValidation; 13 | 14 | public class AppUploaderHelper { 15 | private static final int MAX_RETRY_ATTEMPTS = 2; 16 | private static final long RETRY_DELAY_MS = 1000; 17 | 18 | public static FormValidation validateAppPath(String appPath) { 19 | if (appPath == null || appPath.isEmpty()) { 20 | return FormValidation.error("Please enter absolute path to your app."); 21 | } 22 | File file = new File(appPath); 23 | if (!file.exists()) { 24 | return FormValidation.error("File not found : " + appPath); 25 | } 26 | 27 | if (!appPath.endsWith(".apk") && !appPath.endsWith(".ipa")) { 28 | return FormValidation.error("File extension should be only .apk or .ipa."); 29 | } 30 | 31 | return FormValidation.ok(); 32 | } 33 | 34 | public static String uploadApp(Actionable build, PrintStream logger, String appPath, final String customProxy) { 35 | PluginLogger.log(logger, "Starting upload process."); 36 | 37 | BrowserStackBuildAction browserStackBuildAction = 38 | build.getAction(BrowserStackBuildAction.class); 39 | if (browserStackBuildAction == null) { 40 | PluginLogger.logError(logger, "Error in fetching browserStackBuildAction."); 41 | return null; 42 | } 43 | 44 | BrowserStackCredentials credentials = browserStackBuildAction.getBrowserStackCredentials(); 45 | if (credentials == null) { 46 | PluginLogger.logError(logger, "Was not able to fetch credentials in AppUploadBuilder."); 47 | return null; 48 | } 49 | 50 | AppUploader appUploader = new AppUploader(appPath, credentials, customProxy, logger); 51 | String appId = ""; 52 | for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { 53 | try { 54 | PluginLogger.log(logger, String.format("Uploading app %s to Browserstack. Attempt %d of %d", appPath, attempt, MAX_RETRY_ATTEMPTS)); 55 | appId = appUploader.uploadFile(); 56 | PluginLogger.log(logger, 57 | String.format("%s uploaded successfully to Browserstack with app_url : %s", appPath, appId)); 58 | return appId; 59 | } catch (AppAutomateException e) { 60 | int statusCode = e.getStatusCode(); 61 | PluginLogger.logError(logger, String.format("App upload failed with status code: %d. Attempt %d of %d", statusCode, attempt, MAX_RETRY_ATTEMPTS)); 62 | PluginLogger.logError(logger, e.getMessage()); 63 | if ((statusCode >= 500 || statusCode == 0) && attempt < MAX_RETRY_ATTEMPTS) { 64 | PluginLogger.log(logger, String.format("Retrying in %d seconds...", RETRY_DELAY_MS / 1000)); 65 | try { 66 | Thread.sleep(RETRY_DELAY_MS); 67 | } catch (InterruptedException ie) { 68 | Thread.currentThread().interrupt(); 69 | PluginLogger.log(logger, "Upload retry interrupted. Error: " + ie.getMessage()); 70 | return null; 71 | } 72 | } else { 73 | return null; 74 | } 75 | } catch (InvalidFileExtensionException | FileNotFoundException e) { 76 | PluginLogger.logError(logger, e.getMessage()); 77 | return null; 78 | } 79 | } 80 | PluginLogger.logError(logger, String.format("App upload failed after %d attempts", MAX_RETRY_ATTEMPTS)); 81 | return null; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AbstractBrowserStackCypressReportForBuild.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import hudson.model.Action; 5 | import hudson.model.Run; 6 | 7 | public abstract class AbstractBrowserStackCypressReportForBuild implements Action { 8 | private Run build; 9 | 10 | @Override 11 | public String getIconFileName() { 12 | return Constants.BROWSERSTACK_LOGO; 13 | } 14 | 15 | @Override 16 | public String getDisplayName() { 17 | return Constants.BROWSERSTACK_CYPRESS_REPORT_DISPLAY_NAME; 18 | } 19 | 20 | @Override 21 | public String getUrlName() { 22 | return Constants.BROWSERSTACK_CYPRESS_REPORT_URL; 23 | } 24 | 25 | public Run getBuild() { 26 | return build; 27 | } 28 | 29 | public void setBuild(Run build) { 30 | this.build = build; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AbstractBrowserStackReportForBuild.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import hudson.model.Run; 5 | import hudson.tasks.test.AbstractTestResultAction; 6 | public abstract class AbstractBrowserStackReportForBuild extends AbstractTestResultAction { 7 | private Run build; 8 | 9 | 10 | @Override 11 | public String getIconFileName() { 12 | return Constants.BROWSERSTACK_LOGO; 13 | } 14 | 15 | @Override 16 | public String getDisplayName() { 17 | return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME; 18 | } 19 | 20 | @Override 21 | public String getUrlName() { 22 | return Constants.BROWSERSTACK_REPORT_URL; 23 | } 24 | 25 | public Run getBuild() { 26 | return build; 27 | } 28 | 29 | public void setBuild(Run build) { 30 | this.build = build; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AppUploaderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.logger.PluginLogger; 5 | import com.browserstack.automate.ci.common.uploader.AppUploaderHelper; 6 | import hudson.Extension; 7 | import hudson.Launcher; 8 | import hudson.model.AbstractBuild; 9 | import hudson.model.AbstractProject; 10 | import hudson.model.BuildListener; 11 | import hudson.tasks.BuildStepDescriptor; 12 | import hudson.tasks.Builder; 13 | import hudson.util.FormValidation; 14 | import org.apache.commons.lang.StringUtils; 15 | import org.kohsuke.stapler.DataBoundConstructor; 16 | import org.kohsuke.stapler.QueryParameter; 17 | 18 | import javax.annotation.Nonnull; 19 | import java.io.IOException; 20 | import java.io.PrintStream; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | public class AppUploaderBuilder extends Builder { 25 | 26 | private final String buildFilePath; 27 | 28 | @DataBoundConstructor 29 | public AppUploaderBuilder(String buildFilePath) { 30 | this.buildFilePath = buildFilePath; 31 | } 32 | 33 | public String getBuildFilePath() { 34 | return buildFilePath; 35 | } 36 | 37 | @Override 38 | public boolean perform(@Nonnull AbstractBuild build, @Nonnull Launcher launcher, 39 | @Nonnull BuildListener listener) throws InterruptedException, IOException { 40 | PrintStream logger = listener.getLogger(); 41 | 42 | String appId = AppUploaderHelper.uploadApp(build, logger, this.buildFilePath, null); 43 | 44 | if (StringUtils.isEmpty(appId)) { 45 | return false; 46 | } else { 47 | addAppIdToEnvironment(build, appId); 48 | PluginLogger.log(logger, 49 | "Environment variable BROWSERSTACK_APP_ID set with value : " + appId); 50 | return true; 51 | } 52 | } 53 | 54 | // This method is for injecting appId so that next build step can use it. 55 | private void addAppIdToEnvironment(AbstractBuild build, String appId) { 56 | VariableInjectorAction variableInjectorAction = build.getAction(VariableInjectorAction.class); 57 | if (variableInjectorAction == null) { 58 | variableInjectorAction = new VariableInjectorAction(new HashMap()); 59 | build.addAction(variableInjectorAction); 60 | } 61 | Map envVariables = new HashMap(); 62 | envVariables.put(BrowserStackEnvVars.BROWSERSTACK_APP_ID, appId); 63 | variableInjectorAction.overrideAll(envVariables); 64 | } 65 | 66 | @Extension 67 | public static class AppUploaderDescriptor extends BuildStepDescriptor { 68 | 69 | @Override 70 | public boolean isApplicable(Class jobType) { 71 | return true; 72 | } 73 | 74 | @Override 75 | public String getDisplayName() { 76 | return "Upload App to BrowserStack"; 77 | } 78 | 79 | // Check if buildFilePath and its extension is valid or not. 80 | public FormValidation doCheckBuildFilePath(@QueryParameter String value) { 81 | return AppUploaderHelper.validateAppPath(value); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AutomateActionData.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import hudson.tasks.junit.CaseResult; 4 | import hudson.tasks.junit.TestAction; 5 | import hudson.tasks.junit.TestObject; 6 | import hudson.tasks.junit.TestResultAction; 7 | 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class AutomateActionData extends TestResultAction.Data { 14 | 15 | private final Map testActionMap; 16 | 17 | public AutomateActionData() { 18 | this.testActionMap = new HashMap(); 19 | } 20 | 21 | public void registerTestAction(final String testCaseId, final TestAction testAction) { 22 | testActionMap.put(testCaseId, testAction); 23 | } 24 | 25 | @Override 26 | public List getTestAction(TestObject testObject) { 27 | if (testObject instanceof CaseResult) { 28 | String caseResultId = testObject.getId(); 29 | if (testActionMap.containsKey(caseResultId)) { 30 | return Collections.singletonList(testActionMap.get(caseResultId)); 31 | } 32 | } 33 | 34 | return Collections.emptyList(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AutomateTestAction.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.analytics.Analytics; 4 | import com.browserstack.automate.ci.common.clienthandler.ClientHandler; 5 | import com.browserstack.automate.ci.common.enums.ProjectType; 6 | import com.browserstack.automate.ci.common.model.BrowserStackSession; 7 | import com.browserstack.automate.ci.jenkins.BrowserStackBuildWrapper.BuildWrapperItem; 8 | import com.browserstack.automate.exception.AppAutomateException; 9 | import com.browserstack.automate.exception.AutomateException; 10 | import com.browserstack.automate.exception.SessionNotFound; 11 | import com.browserstack.automate.model.Session; 12 | import com.browserstack.client.BrowserStackClient; 13 | import com.browserstack.client.exception.BrowserStackException; 14 | import com.google.gson.Gson; 15 | import com.google.gson.GsonBuilder; 16 | import hudson.model.Run; 17 | import hudson.tasks.junit.CaseResult; 18 | import hudson.tasks.junit.TestAction; 19 | import org.kohsuke.stapler.bind.JavaScriptMethod; 20 | import org.kohsuke.stapler.export.Exported; 21 | 22 | /** 23 | * A {@link TestAction} extension to display the BrowserStack Automate video for the session. 24 | * 25 | * @author Shirish Kamath 26 | * @author Anirudha Khanna 27 | */ 28 | public class AutomateTestAction extends TestAction { 29 | 30 | private final CaseResult caseResult; 31 | private final BrowserStackSession browserStackSession; 32 | private final Run run; 33 | private transient BrowserStackException lastException; 34 | 35 | public AutomateTestAction(Run run, CaseResult caseResult, String sessionStr) { 36 | this.run = run; 37 | this.caseResult = caseResult; 38 | 39 | // Generate BrowserStackSession object from jsonobject 40 | Gson gson = new GsonBuilder().create(); 41 | this.browserStackSession = gson.fromJson(sessionStr, BrowserStackSession.class); 42 | } 43 | 44 | @Exported 45 | public String getLastError() { 46 | return (lastException != null) ? lastException.getMessage() : null; 47 | } 48 | 49 | // For testing only. 50 | BrowserStackException getLastException() { 51 | return this.lastException; 52 | } 53 | 54 | @Exported 55 | public Session getSession() { 56 | if (this.browserStackSession.getSessionId() == null 57 | || this.browserStackSession.getSessionId().isEmpty() || run == null) { 58 | return null; 59 | } 60 | 61 | BrowserStackCredentials credentials = null; 62 | BrowserStackBuildAction buildAction = run.getAction(BrowserStackBuildAction.class); 63 | if (buildAction != null) { 64 | credentials = buildAction.getBrowserStackCredentials(); 65 | } else { 66 | BuildWrapperItem wrapperItem = 67 | BrowserStackBuildWrapper.findBrowserStackBuildWrapper(run.getParent()); 68 | if (wrapperItem == null || wrapperItem.buildWrapper == null) { 69 | return null; 70 | } 71 | credentials = BrowserStackCredentials.getCredentials(wrapperItem.buildItem, 72 | wrapperItem.buildWrapper.getCredentialsId()); 73 | } 74 | 75 | if (credentials == null) { 76 | return null; 77 | } 78 | 79 | Session activeSession = getSession(credentials, this.browserStackSession.getProjectType()); 80 | return activeSession; 81 | } 82 | 83 | @JavaScriptMethod 84 | public void iframeLoadTime(int time) { 85 | Analytics.trackIframeLoad(time); 86 | } 87 | 88 | @Override 89 | public String annotate(String text) { 90 | return text; 91 | } 92 | 93 | public String getIconFileName() { 94 | return null; 95 | } 96 | 97 | public String getDisplayName() { 98 | return null; 99 | } 100 | 101 | public String getUrlName() { 102 | return null; 103 | } 104 | 105 | private Session getSession(BrowserStackCredentials credentials, ProjectType projectType) { 106 | Session activeSession = null; 107 | BrowserStackClient client = ClientHandler.getBrowserStackClient(projectType, credentials.getUsername(), 108 | credentials.getDecryptedAccesskey(), null, null); 109 | try { 110 | activeSession = client.getSession(this.browserStackSession.getSessionId()); 111 | Analytics.trackIframeRequest(); 112 | } catch (SessionNotFound snfEx) { 113 | lastException = snfEx; 114 | return null; 115 | } catch (BrowserStackException aex) { 116 | if (aex instanceof AppAutomateException) { 117 | lastException = new AppAutomateException(aex); 118 | } else { 119 | lastException = new AutomateException(aex); 120 | } 121 | return null; 122 | } 123 | return activeSession; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/AutomateTestDataPublisher.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.AutomateTestCase; 4 | import com.browserstack.automate.ci.common.analytics.Analytics; 5 | import hudson.Extension; 6 | import hudson.FilePath; 7 | import hudson.Launcher; 8 | import hudson.model.AbstractBuild; 9 | import hudson.model.BuildListener; 10 | import hudson.model.Descriptor; 11 | import hudson.model.Run; 12 | import hudson.model.TaskListener; 13 | import hudson.tasks.junit.CaseResult; 14 | import hudson.tasks.junit.SuiteResult; 15 | import hudson.tasks.junit.TestDataPublisher; 16 | import hudson.tasks.junit.TestResult; 17 | import hudson.tasks.junit.TestResultAction; 18 | import org.kohsuke.stapler.DataBoundConstructor; 19 | 20 | import java.io.IOException; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 26 | import static com.browserstack.automate.ci.common.logger.PluginLogger.logDebug; 27 | 28 | public class AutomateTestDataPublisher extends TestDataPublisher { 29 | @Extension(ordinal = 1000) // JENKINS-12161 30 | public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); 31 | private static final String TAG = "[BrowserStack]"; 32 | private static final String REPORT_FILE_PATTERN = "**/browserstack-reports/REPORT-*.xml"; 33 | 34 | @DataBoundConstructor 35 | public AutomateTestDataPublisher() { 36 | // This constructor is only called when the TestDataPublisher is created. 37 | // This is only when the user explicitly chooses to enable BrowserStack as an additional Test report. 38 | Analytics.trackReportingEvent(true); 39 | } 40 | 41 | public static String getTestCaseName(CaseResult caseResult) { 42 | return caseResult.getClassName() + "." + AutomateTestCase.stripTestParams(caseResult.getDisplayName()); 43 | } 44 | 45 | @Override 46 | public TestResultAction.Data getTestData(AbstractBuild abstractBuild, Launcher launcher, BuildListener buildListener, TestResult testResult) throws IOException, InterruptedException { 47 | FilePath filePath = abstractBuild.getWorkspace(); 48 | if (filePath == null) { 49 | return null; 50 | } else { 51 | return contributeTestData(abstractBuild, filePath, launcher, buildListener, testResult); 52 | } 53 | } 54 | 55 | @Override 56 | public TestResultAction.Data contributeTestData(Run run, FilePath workspace, 57 | Launcher launcher, TaskListener listener, 58 | TestResult testResult) throws IOException, InterruptedException { 59 | log(listener.getLogger(), "Publishing test results"); 60 | Map testSessionMap = workspace.act(new BrowserStackReportFileCallable(REPORT_FILE_PATTERN, run.getTimeInMillis())); 61 | AutomateActionData automateActionData = new AutomateActionData(); 62 | Map testCaseIndices = new HashMap(); 63 | 64 | int testCount = 0; 65 | int sessionCount = 0; 66 | 67 | for (SuiteResult suiteResult : testResult.getSuites()) { 68 | List cases = suiteResult.getCases(); 69 | testCount += cases.size(); 70 | logDebug(listener.getLogger(), suiteResult.getName() + ": " + cases.size() + " test cases found."); 71 | 72 | for (CaseResult caseResult : cases) { 73 | String testCaseName = getTestCaseName(caseResult); 74 | 75 | Long testIndex = testCaseIndices.containsKey(testCaseName) ? testCaseIndices.get(testCaseName) : -1L; 76 | testCaseIndices.put(testCaseName, ++testIndex); 77 | logDebug(listener.getLogger(), testCaseName + " / " + testCaseName + " <=> " + testIndex); 78 | 79 | String testId = String.format("%s{%d}", testCaseName, testIndex); 80 | if (testSessionMap.containsKey(testId)) { 81 | AutomateTestAction automateTestAction = new AutomateTestAction(run, caseResult, testSessionMap.get(testId)); 82 | automateActionData.registerTestAction(caseResult.getId(), automateTestAction); 83 | logDebug(listener.getLogger(), "registerTestAction: " + testId + " => " + automateTestAction); 84 | sessionCount++; 85 | } 86 | } 87 | } 88 | 89 | testCaseIndices.clear(); 90 | log(listener.getLogger(), testCount + " tests recorded"); 91 | log(listener.getLogger(), sessionCount + " sessions captured"); 92 | log(listener.getLogger(), "Publishing test results: SUCCESS"); 93 | return automateActionData; 94 | } 95 | 96 | private static class DescriptorImpl extends Descriptor { 97 | 98 | @Override 99 | public String getDisplayName() { 100 | return "Embed BrowserStack Report"; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildAction.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import hudson.model.Action; 4 | 5 | public class BrowserStackBuildAction implements Action { 6 | 7 | private BrowserStackCredentials browserStackCredentials; 8 | 9 | public BrowserStackBuildAction(BrowserStackCredentials browserStackCredentials) { 10 | super(); 11 | this.browserStackCredentials = browserStackCredentials; 12 | } 13 | 14 | public BrowserStackCredentials getBrowserStackCredentials() { 15 | return browserStackCredentials; 16 | } 17 | 18 | public void setBrowserStackCredentials(BrowserStackCredentials browserStackCredentials) { 19 | this.browserStackCredentials = browserStackCredentials; 20 | } 21 | 22 | @Override 23 | public String getIconFileName() { 24 | // TODO Auto-generated method stub 25 | return null; 26 | } 27 | 28 | @Override 29 | public String getDisplayName() { 30 | // TODO Auto-generated method stub 31 | return null; 32 | } 33 | 34 | @Override 35 | public String getUrlName() { 36 | // TODO Auto-generated method stub 37 | return null; 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackBuildWrapperDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackBuildWrapperOperations; 4 | import com.browserstack.automate.ci.common.analytics.Analytics; 5 | import com.browserstack.automate.ci.jenkins.local.LocalConfig; 6 | import com.browserstack.automate.ci.jenkins.qualityDashboard.QualityDashboardInit; 7 | import hudson.Extension; 8 | import hudson.model.AbstractProject; 9 | import hudson.model.Item; 10 | import hudson.tasks.BuildWrapperDescriptor; 11 | import hudson.util.FormValidation; 12 | import hudson.util.ListBoxModel; 13 | import net.sf.json.JSONObject; 14 | import org.kohsuke.stapler.AncestorInPath; 15 | import org.kohsuke.stapler.QueryParameter; 16 | import org.kohsuke.stapler.StaplerRequest; 17 | 18 | @Extension 19 | public final class BrowserStackBuildWrapperDescriptor extends BuildWrapperDescriptor { 20 | private static final String NAMESPACE = "browserStack"; 21 | 22 | private String credentialsId; 23 | private LocalConfig localConfig; 24 | // By default usage stats are enabled. But user's can choose to disable through Jenkin's 25 | // configuration. 26 | private boolean usageStatsEnabled = true; 27 | 28 | public BrowserStackBuildWrapperDescriptor() { 29 | super(BrowserStackBuildWrapper.class); 30 | load(); 31 | 32 | if (usageStatsEnabled) { 33 | Analytics.trackInstall(); 34 | } 35 | } 36 | 37 | private static int compareIntegers(int x, int y) { 38 | return (x == y) ? 0 : (x < y) ? -1 : 1; 39 | } 40 | 41 | @Override 42 | public String getDisplayName() { 43 | return "BrowserStack"; 44 | } 45 | 46 | @Override 47 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 48 | if (formData.has(NAMESPACE)) { 49 | JSONObject config = formData.getJSONObject(NAMESPACE); 50 | req.bindJSON(this, config); 51 | save(); 52 | if (config.has("usageStatsEnabled")) { 53 | setEnableUsageStats(config.getBoolean("usageStatsEnabled")); 54 | } 55 | if (config.has("credentialsId")){ 56 | QualityDashboardInit qdInit = new QualityDashboardInit(); 57 | qdInit.pluginConfiguredNotif(); 58 | } 59 | } 60 | return true; 61 | } 62 | 63 | @Override 64 | public boolean isApplicable(AbstractProject item) { 65 | return true; 66 | } 67 | 68 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath final Item context) { 69 | return BrowserStackBuildWrapperOperations.doFillCredentialsIdItems(context); 70 | } 71 | 72 | public FormValidation doCheckLocalPath(@AncestorInPath final AbstractProject project, 73 | @QueryParameter final String localPath) { 74 | return BrowserStackBuildWrapperOperations.doCheckLocalPath(project, localPath); 75 | } 76 | 77 | public String getCredentialsId() { 78 | return credentialsId; 79 | } 80 | 81 | public void setCredentialsId(String credentialsId) { 82 | this.credentialsId = credentialsId; 83 | } 84 | 85 | public LocalConfig getLocalConfig() { 86 | return localConfig; 87 | } 88 | 89 | public void setLocalConfig(LocalConfig localConfig) { 90 | this.localConfig = localConfig; 91 | } 92 | 93 | public boolean getEnableUsageStats() { 94 | return usageStatsEnabled; 95 | } 96 | 97 | public void setEnableUsageStats(boolean usageStatsEnabled) { 98 | this.usageStatsEnabled = usageStatsEnabled; 99 | Analytics.setEnabled(this.usageStatsEnabled); 100 | // We track an install if one has not been done before. 101 | // Since a user could have chosen to disable the plugin and then chosen to re-enable it, 102 | // before installing a newer version of the plugin. 103 | if (this.usageStatsEnabled) { 104 | Analytics.trackInstall(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportFileCallable.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import hudson.remoting.VirtualChannel; 4 | import jenkins.MasterToSlaveFileCallable; 5 | import org.apache.commons.io.IOUtils; 6 | 7 | import java.io.*; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | public class BrowserStackCypressReportFileCallable extends MasterToSlaveFileCallable { 11 | 12 | private final String filepath; 13 | 14 | public BrowserStackCypressReportFileCallable(String filepath) { 15 | this.filepath = filepath; 16 | } 17 | 18 | @Override 19 | public String invoke(File workspace, VirtualChannel channel) throws IOException, InterruptedException { 20 | String jsonTxt = null; 21 | final String reportJSONPath = filepath + "/results/browserstack-cypress-report.json"; 22 | try { 23 | InputStream is = new FileInputStream(reportJSONPath); 24 | jsonTxt = IOUtils.toString(is, StandardCharsets.UTF_8); 25 | } catch (FileNotFoundException e) { 26 | throw e; 27 | } catch (IOException e) { 28 | throw e; 29 | } 30 | return jsonTxt; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportForBuild.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import com.browserstack.automate.ci.common.enums.ProjectType; 5 | import com.browserstack.automate.ci.common.tracking.PluginsTracker; 6 | import hudson.FilePath; 7 | import hudson.model.Run; 8 | import org.apache.commons.io.IOUtils; 9 | import org.json.JSONArray; 10 | import org.json.JSONObject; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.io.*; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static com.browserstack.automate.ci.common.logger.PluginLogger.logError; 19 | 20 | public class BrowserStackCypressReportForBuild extends AbstractBrowserStackCypressReportForBuild { 21 | private static PrintStream logger; 22 | private final String buildName; 23 | private final transient JSONObject result; 24 | private final Map resultAggregation; 25 | private final ProjectType projectType; 26 | // to make them available in jelly 27 | private final String passedConst = Constants.SessionStatus.PASSED; 28 | private final String failedConst = Constants.SessionStatus.FAILED; 29 | private final transient PluginsTracker tracker; 30 | private final boolean pipelineStatus; 31 | 32 | public BrowserStackCypressReportForBuild(final Run build, 33 | final ProjectType projectType, 34 | final String buildName, 35 | final PrintStream logger, 36 | final PluginsTracker tracker, 37 | final boolean pipelineStatus) { 38 | super(); 39 | setBuild(build); 40 | this.buildName = buildName; 41 | this.result = new JSONObject(); 42 | this.resultAggregation = new HashMap<>(); 43 | this.projectType = projectType; 44 | BrowserStackCypressReportForBuild.logger = logger; 45 | this.tracker = tracker; 46 | this.pipelineStatus = pipelineStatus; 47 | } 48 | 49 | public boolean generateBrowserStackCypressReport(@Nonnull FilePath workspace, String jenkinsFolder) { 50 | if (result.length() == 0) { 51 | 52 | JSONObject matrix = null; 53 | try { 54 | String fileContent = workspace.act(new BrowserStackCypressReportFileCallable(jenkinsFolder)); 55 | matrix = new JSONObject(fileContent); 56 | } catch (FileNotFoundException e) { 57 | logError(logger, "Cypress report not found at " + jenkinsFolder); 58 | tracker.sendError("BrowserStack Cypress Report Not Found", pipelineStatus, "CypressReportGeneration"); 59 | } catch (IOException e) { 60 | logError(logger, "There was a problem while reading report files"); 61 | tracker.sendError(e.toString(), pipelineStatus, "CypressReportGeneration"); 62 | } catch (InterruptedException e) { 63 | logError(logger, "Process was interrupted while retrieving the report"); 64 | tracker.sendError(e.toString(), pipelineStatus, "CypressReportGeneration"); 65 | } 66 | 67 | if (matrix == null) { 68 | return false; 69 | } 70 | 71 | String buildNameWithBuildNumber = matrix.optString("build_name"); 72 | int indexOfBuildNumberSeparator = buildNameWithBuildNumber.lastIndexOf(": ") == -1 ? buildNameWithBuildNumber.length() 73 | : buildNameWithBuildNumber.lastIndexOf(": "); 74 | String buildNameWithoutBuildNumber = buildNameWithBuildNumber.substring(0, indexOfBuildNumberSeparator); 75 | 76 | if (buildNameWithoutBuildNumber == null) { 77 | logError(logger, "BrowserStack Cypress Report not generated, result json may have been corrupted. Please retry."); 78 | tracker.sendError("Report not generated", pipelineStatus, "CypressReportGeneration"); 79 | return false; 80 | } 81 | 82 | if (!buildNameWithoutBuildNumber.equalsIgnoreCase(this.buildName)) { 83 | logError(logger, "BrowserStack Cypress Report not generated, build name mismatch. Expected build name:" + this.buildName + ", got:" + buildNameWithBuildNumber); 84 | tracker.sendError("Report not generated", pipelineStatus, "CypressReportGeneration"); 85 | return false; 86 | } 87 | 88 | generateResult(matrix); 89 | 90 | if (result.length() > 0) { 91 | generateAggregationInfo(); 92 | return true; 93 | } 94 | return false; 95 | } 96 | return true; 97 | } 98 | 99 | private void generateResult(JSONObject matrix) { 100 | result.put("buildName", matrix.getString("build_name")); 101 | result.put("buildId", matrix.getString("build_id")); 102 | result.put("projectName", matrix.getString("project_name")); 103 | result.put("buildUrl", matrix.getString("build_url")); 104 | 105 | JSONArray specs = new JSONArray(); 106 | JSONObject rows = matrix.getJSONObject("rows"); 107 | rows.keySet().forEach(specName -> 108 | { 109 | JSONObject spec = new JSONObject(); 110 | JSONObject specData = rows.getJSONObject(specName); 111 | JSONObject specMeta = specData.getJSONObject("meta"); 112 | 113 | spec.put("name", specName); 114 | spec.put("path", specData.getString("path")); 115 | 116 | // Meta 117 | spec.put("total", specMeta.getInt("total")); 118 | spec.put("failed", specMeta.getInt("failed")); 119 | spec.put("passed", specMeta.getInt("passed")); 120 | 121 | // Sessions 122 | JSONArray sessions = specData.getJSONArray("sessions"); 123 | spec.put("sessions", sessions); 124 | 125 | specs.put(spec); 126 | }); 127 | 128 | result.put("specs", specs); 129 | } 130 | 131 | private void generateAggregationInfo() { 132 | int totalSpecs = 0, totalErrors = 0; 133 | 134 | JSONArray specs = result.getJSONArray("specs"); 135 | 136 | for (int i = 0; i < specs.length(); i++) { 137 | JSONObject spec = specs.getJSONObject(i); 138 | totalSpecs += spec.getInt("total"); 139 | totalErrors += spec.getInt("failed"); 140 | } 141 | 142 | resultAggregation.put("totalSpecs", String.valueOf(totalSpecs)); 143 | resultAggregation.put("totalErrors", String.valueOf(totalErrors)); 144 | } 145 | 146 | public JSONObject getResult() { 147 | return result; 148 | } 149 | 150 | public Map getResultAggregation() { 151 | return resultAggregation; 152 | } 153 | 154 | public String getBuildName() { 155 | return buildName; 156 | } 157 | 158 | public ProjectType getProjectType() { 159 | return projectType; 160 | } 161 | 162 | public String getPassedConst() { 163 | return passedConst; 164 | } 165 | 166 | public String getFailedConst() { 167 | return failedConst; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackCypressReportPublisher.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.constants.Constants; 5 | import com.browserstack.automate.ci.common.enums.ProjectType; 6 | import com.browserstack.automate.ci.common.tracking.PluginsTracker; 7 | import hudson.EnvVars; 8 | import hudson.Extension; 9 | import hudson.FilePath; 10 | import hudson.Launcher; 11 | import hudson.model.AbstractProject; 12 | import hudson.model.Run; 13 | import hudson.model.TaskListener; 14 | import hudson.tasks.BuildStepDescriptor; 15 | import hudson.tasks.BuildStepMonitor; 16 | import hudson.tasks.Publisher; 17 | import hudson.tasks.Recorder; 18 | import jenkins.tasks.SimpleBuildStep; 19 | import org.kohsuke.stapler.DataBoundConstructor; 20 | 21 | import javax.annotation.Nonnull; 22 | import java.io.IOException; 23 | import java.io.PrintStream; 24 | import java.util.Optional; 25 | 26 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 27 | 28 | public class BrowserStackCypressReportPublisher extends Recorder implements SimpleBuildStep { 29 | 30 | @DataBoundConstructor 31 | public BrowserStackCypressReportPublisher() { 32 | } 33 | 34 | public BuildStepMonitor getRequiredMonitorService() { 35 | return BuildStepMonitor.NONE; 36 | } 37 | 38 | 39 | @Override 40 | public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { 41 | final PrintStream logger = listener.getLogger(); 42 | final PluginsTracker tracker = new PluginsTracker(); 43 | final boolean pipelineStatus = false; 44 | 45 | log(logger, "Generating BrowserStack Cypress Test Report"); 46 | 47 | final EnvVars parentEnvs = build.getEnvironment(listener); 48 | String browserStackBuildName = parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME); 49 | browserStackBuildName = Optional.ofNullable(browserStackBuildName).orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG)); 50 | final String projectFolder = parentEnvs.get("BROWSERSTACK_CYPRESS_PROJECT_ROOT") != null ? parentEnvs.get("BROWSERSTACK_CYPRESS_PROJECT_ROOT") : parentEnvs.get("WORKSPACE"); 51 | ProjectType product = ProjectType.AUTOMATE; 52 | 53 | tracker.reportGenerationInitialized(browserStackBuildName, product.name(), pipelineStatus); 54 | log(logger, "BrowserStack Cypress Project identified as : " + product.name()); 55 | 56 | final BrowserStackCypressReportForBuild bstackReportAction = 57 | new BrowserStackCypressReportForBuild(build, product, browserStackBuildName, logger, tracker, pipelineStatus); 58 | final boolean reportResult = bstackReportAction.generateBrowserStackCypressReport(workspace, projectFolder); 59 | build.addAction(bstackReportAction); 60 | 61 | String reportStatus = reportResult ? Constants.ReportStatus.SUCCESS : Constants.ReportStatus.FAILED; 62 | log(logger, "BrowserStack Cypress Report Status: " + reportStatus); 63 | } 64 | 65 | @Extension 66 | public static final class DescriptorImpl extends BuildStepDescriptor { 67 | 68 | @Override 69 | @SuppressWarnings("rawtypes") 70 | public boolean isApplicable(Class aClass) { 71 | // indicates that this builder can be used with all kinds of project types 72 | return true; 73 | } 74 | 75 | /** 76 | * This human readable name is used in the configuration screen. 77 | */ 78 | @Override 79 | public String getDisplayName() { 80 | return Constants.BROWSERSTACK_CYPRESS_REPORT_DISPLAY_NAME; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackReportFileCallable.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.report.XmlReporter; 4 | import hudson.AbortException; 5 | import hudson.Util; 6 | import hudson.remoting.VirtualChannel; 7 | import jenkins.MasterToSlaveFileCallable; 8 | import org.apache.tools.ant.DirectoryScanner; 9 | import org.apache.tools.ant.types.FileSet; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | /** 17 | * Finds XML files in the workspace that match the supplied Ant pattern, 18 | * and parses them for test case - session mapping. 19 | *

20 | * Logic for detection of stale report files is from Jenkins JUnit plugin. 21 | * 22 | * @author Shirish Kamath 23 | * @author Anirudha Khanna 24 | */ 25 | public class BrowserStackReportFileCallable extends MasterToSlaveFileCallable> { 26 | 27 | private final String filePattern; 28 | 29 | private final long buildTime; 30 | 31 | private final long masterTime; 32 | 33 | public BrowserStackReportFileCallable(String filePattern, long buildTime) { 34 | this.filePattern = filePattern; 35 | this.buildTime = buildTime; 36 | this.masterTime = System.currentTimeMillis(); 37 | } 38 | 39 | @Override 40 | public Map invoke(File workspace, VirtualChannel channel) throws IOException, InterruptedException { 41 | long slaveTime = System.currentTimeMillis(); 42 | 43 | FileSet fs = Util.createFileSet(workspace, filePattern); 44 | DirectoryScanner ds = fs.getDirectoryScanner(); 45 | ds.scan(); 46 | 47 | Map testSessionMap = new HashMap(); 48 | String[] filePaths = ds.getIncludedFiles(); 49 | if (filePaths.length == 0) { 50 | return testSessionMap; 51 | } 52 | 53 | boolean parsed = false; 54 | long buildTime = this.buildTime + (slaveTime - masterTime); 55 | 56 | for (String filePath : filePaths) { 57 | File f = new File(workspace, filePath); 58 | if (!f.exists()) { 59 | continue; 60 | } 61 | 62 | if (buildTime - 3000 <= f.lastModified()) { 63 | Map results = XmlReporter.parse(f); 64 | if (!results.isEmpty()) { 65 | testSessionMap.putAll(results); 66 | parsed = true; 67 | } 68 | } 69 | } 70 | 71 | if (!parsed) { 72 | if (slaveTime < buildTime - 1000) { 73 | // build time is in the the future. clock on this slave must be running behind 74 | throw new AbortException( 75 | "Clock on this slave is out of sync with the master, and therefore \n" + 76 | "I can't figure out what test results are new and what are old.\n" + 77 | "Please keep the slave clock in sync with the master."); 78 | } 79 | 80 | File f = new File(workspace, filePaths[0]); 81 | throw new AbortException( 82 | String.format( 83 | "Test reports were found but none of them are new. Did tests run? %n" + 84 | "For example, %s is %s old%n", f, 85 | Util.getTimeSpanString(buildTime - f.lastModified()))); 86 | } 87 | 88 | return testSessionMap; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackReportPublisher.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.constants.Constants; 5 | import com.browserstack.automate.ci.common.enums.ProjectType; 6 | import com.browserstack.automate.ci.common.tracking.PluginsTracker; 7 | import hudson.EnvVars; 8 | import hudson.Extension; 9 | import hudson.FilePath; 10 | import hudson.Launcher; 11 | import hudson.model.AbstractProject; 12 | import hudson.model.Run; 13 | import hudson.model.TaskListener; 14 | import hudson.tasks.ArtifactArchiver; 15 | import hudson.tasks.BuildStepDescriptor; 16 | import hudson.tasks.BuildStepMonitor; 17 | import hudson.tasks.Publisher; 18 | import hudson.tasks.Recorder; 19 | import jenkins.tasks.SimpleBuildStep; 20 | import org.kohsuke.stapler.DataBoundConstructor; 21 | 22 | import javax.annotation.Nonnull; 23 | import java.io.IOException; 24 | import java.io.PrintStream; 25 | import java.util.Optional; 26 | import java.util.logging.Logger; 27 | 28 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 29 | 30 | public class BrowserStackReportPublisher extends Recorder implements SimpleBuildStep { 31 | private static final Logger LOGGER = Logger.getLogger(BrowserStackReportPublisher.class.getName()); 32 | 33 | @DataBoundConstructor 34 | public BrowserStackReportPublisher() { 35 | } 36 | 37 | public BuildStepMonitor getRequiredMonitorService() { 38 | return BuildStepMonitor.NONE; 39 | } 40 | 41 | 42 | @Override 43 | public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { 44 | final PrintStream logger = listener.getLogger(); 45 | final PluginsTracker tracker = new PluginsTracker(); 46 | final boolean pipelineStatus = false; 47 | 48 | log(logger, "Generating BrowserStack Test Report"); 49 | 50 | final EnvVars parentEnvs = build.getEnvironment(listener); 51 | String browserStackBuildName = parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME); 52 | final String browserStackAppID = parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_APP_ID); 53 | browserStackBuildName = Optional.ofNullable(browserStackBuildName).orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG)); 54 | 55 | ProjectType product = ProjectType.AUTOMATE; 56 | if (browserStackAppID != null && !browserStackAppID.isEmpty()) { 57 | product = ProjectType.APP_AUTOMATE; 58 | } 59 | 60 | tracker.reportGenerationInitialized(browserStackBuildName, product.name(), pipelineStatus); 61 | log(logger, "BrowserStack Project identified as : " + product.name()); 62 | 63 | final BrowserStackReportForBuild bstackReportAction = 64 | new BrowserStackReportForBuild(build, product, browserStackBuildName, logger, tracker, pipelineStatus, null); 65 | final boolean reportResult = bstackReportAction.generateBrowserStackReport(); 66 | build.addAction(bstackReportAction); 67 | 68 | String reportStatus = reportResult ? Constants.ReportStatus.SUCCESS : Constants.ReportStatus.FAILED; 69 | log(logger, "BrowserStack Report Status: " + reportStatus); 70 | 71 | LOGGER.info(String.format("Archiving artifacts for pattern %s", Constants.BROWSERSTACK_REPORT_PATH_PATTERN)); 72 | ArtifactArchiver artifactArchiver = new ArtifactArchiver(Constants.BROWSERSTACK_REPORT_PATH_PATTERN); 73 | artifactArchiver.setAllowEmptyArchive(true); 74 | artifactArchiver.perform(build, workspace, parentEnvs, launcher, listener); 75 | LOGGER.info(String.format("Succesfully archived artifacts for pattern %s", artifactArchiver.getArtifacts())); 76 | 77 | tracker.reportGenerationCompleted(reportStatus, product.name(), pipelineStatus, 78 | browserStackBuildName, bstackReportAction.getBrowserStackBuildID()); 79 | } 80 | 81 | @Extension 82 | public static final class DescriptorImpl extends BuildStepDescriptor { 83 | 84 | @Override 85 | @SuppressWarnings("rawtypes") 86 | public boolean isApplicable(Class aClass) { 87 | // indicates that this builder can be used with all kinds of project types 88 | return true; 89 | } 90 | 91 | /** 92 | * This human readable name is used in the configuration screen. 93 | */ 94 | @Override 95 | public String getDisplayName() { 96 | return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/BrowserStackResult.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import hudson.model.Run; 7 | import org.json.JSONObject; 8 | 9 | import com.browserstack.automate.ci.common.constants.Constants; 10 | import hudson.tasks.test.TestObject; 11 | import hudson.tasks.test.TestResult; 12 | 13 | public class BrowserStackResult extends TestResult { 14 | // owner of this build 15 | protected Run build; 16 | private String buildName; 17 | private String browserStackBuildBrowserUrl; 18 | private final transient List result; 19 | private final Map resultAggregation; 20 | private final String errorConst = Constants.SessionStatus.ERROR; 21 | private final String failedConst = Constants.SessionStatus.FAILED; 22 | 23 | public BrowserStackResult(String buildName, String browserStackBuildBrowserUrl, List resultList, Map resultAggregation) { 24 | this.buildName = buildName; 25 | this.browserStackBuildBrowserUrl = browserStackBuildBrowserUrl; 26 | this.result = resultList; 27 | this.resultAggregation = resultAggregation; 28 | } 29 | 30 | @Override 31 | public TestResult findCorrespondingResult(String id) { 32 | if (id.equals(getId())) { 33 | return this; 34 | } 35 | return null; 36 | } 37 | 38 | @Override 39 | public String getDisplayName() { 40 | return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME; 41 | } 42 | 43 | @Override 44 | public Run getRun() { 45 | return build; 46 | } 47 | 48 | @Override 49 | public TestObject getParent() { 50 | return null; 51 | } 52 | 53 | public List getResult() { 54 | return result; 55 | } 56 | 57 | public Map getResultAggregation() { 58 | return resultAggregation; 59 | } 60 | 61 | public String getErrorConst() { 62 | return errorConst; 63 | } 64 | 65 | public String getFailedConst() { 66 | return failedConst; 67 | } 68 | 69 | public void setRun(Run build) { 70 | this.build = build; 71 | } 72 | 73 | public String getBuildName() { 74 | return buildName; 75 | } 76 | 77 | public String getBrowserStackBuildBrowserUrl() { 78 | return browserStackBuildBrowserUrl; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/VariableInjectorAction.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins; 2 | 3 | import hudson.EnvVars; 4 | import hudson.model.AbstractBuild; 5 | import hudson.model.EnvironmentContributingAction; 6 | import hudson.model.Run; 7 | 8 | import javax.annotation.CheckForNull; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | /** 14 | * Description : This class is for injecting environment variables. 15 | */ 16 | public class 17 | VariableInjectorAction implements EnvironmentContributingAction { 18 | 19 | protected transient @CheckForNull 20 | Map envMap = new HashMap(); 21 | ; 22 | private transient @CheckForNull 23 | Run build; 24 | 25 | public VariableInjectorAction(Map envMap) { 26 | this.envMap = envMap; 27 | } 28 | 29 | @Override 30 | public String getIconFileName() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public String getDisplayName() { 36 | return null; 37 | } 38 | 39 | @Override 40 | public String getUrlName() { 41 | return null; 42 | } 43 | 44 | public void overrideAll(Map newEnvMap) { 45 | if (envMap == null) { 46 | envMap = new HashMap(); 47 | } 48 | envMap.putAll(newEnvMap); 49 | } 50 | 51 | public Set getEnvMapKeys() { 52 | if (envMap == null) { 53 | return null; 54 | } 55 | return envMap.keySet(); 56 | } 57 | 58 | @Override 59 | public void buildEnvVars(AbstractBuild build, EnvVars env) { 60 | if (this.envMap != null && env != null) { 61 | env.putAll(this.envMap); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackReportStatus.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.integrationService; 2 | 3 | public enum BrowserStackReportStatus { 4 | IN_PROGRESS, 5 | COMPLETED, 6 | TEST_AVAILABLE, 7 | NOT_AVAILABLE 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/integrationService/BrowserStackTestReportPublisher.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.integrationService; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.constants.Constants; 5 | import com.browserstack.automate.ci.jenkins.BrowserStackBuildAction; 6 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 7 | import edu.umd.cs.findbugs.annotations.NonNull; 8 | import hudson.EnvVars; 9 | import hudson.Extension; 10 | import hudson.model.AbstractProject; 11 | import hudson.model.Run; 12 | import hudson.model.TaskListener; 13 | import hudson.tasks.BuildStepDescriptor; 14 | import hudson.tasks.BuildStepMonitor; 15 | import hudson.tasks.Publisher; 16 | import hudson.tasks.Recorder; 17 | import hudson.FilePath; 18 | import hudson.Launcher; 19 | import jenkins.tasks.SimpleBuildStep; 20 | import org.jenkinsci.Symbol; 21 | import org.kohsuke.stapler.DataBoundConstructor; 22 | 23 | import javax.annotation.CheckForNull; 24 | import java.io.IOException; 25 | import java.io.PrintStream; 26 | import java.util.*; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import java.util.logging.Logger; 29 | 30 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 31 | import static com.browserstack.automate.ci.common.logger.PluginLogger.logError; 32 | 33 | public class BrowserStackTestReportPublisher extends Recorder implements SimpleBuildStep { 34 | private static final Logger LOGGER = Logger.getLogger(BrowserStackTestReportPublisher.class.getName()); 35 | private Map customEnvVars; 36 | 37 | @DataBoundConstructor 38 | public BrowserStackTestReportPublisher(@CheckForNull String product) { 39 | this.customEnvVars = new ConcurrentHashMap<>(); 40 | } 41 | 42 | @Override 43 | public void perform(Run build, @NonNull FilePath workspace, @NonNull Launcher launcher, TaskListener listener) throws IOException, InterruptedException { 44 | final PrintStream logger = listener.getLogger(); 45 | log(logger, "Adding BrowserStack Report"); 46 | 47 | EnvVars parentEnvs = build.getEnvironment(listener); 48 | parentEnvs.putAll(getCustomEnvVars()); 49 | 50 | String browserStackBuildName = Optional.ofNullable(parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME)) 51 | .orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG)); 52 | 53 | BrowserStackBuildAction buildAction = build.getAction(BrowserStackBuildAction.class); 54 | if (buildAction == null || buildAction.getBrowserStackCredentials() == null) { 55 | logError(logger, "No BrowserStackBuildAction or credentials found"); 56 | return; 57 | } 58 | 59 | BrowserStackCredentials credentials = buildAction.getBrowserStackCredentials(); 60 | 61 | LOGGER.info("Adding BrowserStack Report Action"); 62 | 63 | 64 | Date buildTimestamp = new Date(build.getStartTimeInMillis()); 65 | 66 | // Format the timestamp (e.g., YYYY-MM-DD HH:MM:SS) 67 | long unixTimestamp = buildTimestamp.getTime() / 1000; 68 | 69 | String buildCreatedAt = String.valueOf(unixTimestamp); 70 | 71 | build.addAction(new BrowserStackTestReportAction(build, credentials, browserStackBuildName,buildCreatedAt)); 72 | 73 | } 74 | 75 | 76 | public Map getCustomEnvVars() { 77 | return customEnvVars; 78 | } 79 | 80 | @Override 81 | public BuildStepMonitor getRequiredMonitorService() { 82 | return BuildStepMonitor.NONE; 83 | } 84 | 85 | @Symbol(Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION) 86 | @Extension 87 | public static final class DescriptorImpl extends BuildStepDescriptor { 88 | 89 | @Override 90 | @SuppressWarnings("rawtypes") 91 | public boolean isApplicable(Class aClass) { 92 | // indicates that this builder can be used with all kinds of project types 93 | return true; 94 | } 95 | @Override 96 | public String getDisplayName() { 97 | return Constants.BROWSERSTACK_CAD_REPORT_DISPLAY_NAME; 98 | } 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/integrationService/RequestsUtil.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.integrationService; 2 | 3 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 4 | import okhttp3.*; 5 | import org.apache.http.client.utils.URIBuilder; 6 | 7 | import java.io.IOException; 8 | import java.net.URISyntaxException; 9 | import java.util.Map; 10 | 11 | public class RequestsUtil { 12 | private transient OkHttpClient client; 13 | 14 | 15 | public Response makeRequest(String url, BrowserStackCredentials browserStackCredentials, RequestBody body) throws Exception { 16 | try { 17 | Request request = new Request.Builder() 18 | .url(url) 19 | .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) 20 | .post(body) 21 | .build(); 22 | return getClient().newCall(request).execute(); 23 | } catch (IOException e) { 24 | e.printStackTrace(); 25 | throw e; 26 | } 27 | } 28 | 29 | public String buildQueryParams(String url, Map params) throws URISyntaxException { 30 | try { 31 | URIBuilder builder = new URIBuilder(url); 32 | for (String key : params.keySet()) { 33 | builder.addParameter(key, params.get(key)); 34 | } 35 | String fullUrl = builder.build().toString(); 36 | return fullUrl; 37 | } catch (URISyntaxException uriSyntaxException) { 38 | uriSyntaxException.printStackTrace(); 39 | throw uriSyntaxException; 40 | } 41 | } 42 | 43 | private OkHttpClient getClient() { 44 | if (client == null) { 45 | client = new OkHttpClient(); 46 | } 47 | return client; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/local/BrowserStackLocalUtils.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.local; 2 | 3 | import hudson.Launcher; 4 | 5 | import java.io.PrintStream; 6 | 7 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 8 | 9 | public class BrowserStackLocalUtils { 10 | 11 | public static void stopBrowserStackLocal(JenkinsBrowserStackLocal browserStackLocal, 12 | Launcher launcher, PrintStream logger) throws Exception { 13 | if (browserStackLocal != null) { 14 | log(logger, "Local: Stopping BrowserStack Local..."); 15 | try { 16 | browserStackLocal.stop(launcher); 17 | log(logger, "Local: Stopped"); 18 | } catch (Exception e) { 19 | log(logger, "Local: ERROR: " + e.getMessage()); 20 | } 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/local/JenkinsBrowserStackLocal.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.local; 2 | 3 | import com.browserstack.automate.ci.common.logger.PluginLogger; 4 | import com.browserstack.local.Local; 5 | import hudson.EnvVars; 6 | import hudson.Launcher; 7 | import jenkins.security.MasterToSlaveCallable; 8 | import org.apache.commons.lang.StringUtils; 9 | 10 | import java.io.IOException; 11 | import java.io.PrintStream; 12 | import java.io.Serializable; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.UUID; 18 | 19 | public class JenkinsBrowserStackLocal extends Local implements Serializable { 20 | private static final long serialVersionUID = 1830651088511115761L; 21 | private static final String OPTION_LOCAL_IDENTIFIER = "localIdentifier"; 22 | // local identifier doesn't override when user passes --local-identifier 23 | // Not replacing existing localIdentifier because of legacy reason 24 | private static final String OPTION_LOCAL_IDENTIFIER_2 = "--local-identifier"; 25 | 26 | private final String accesskey; 27 | private final String binarypath; 28 | private final String[] arguments; 29 | private String localIdentifier; 30 | private EnvVars envVars; 31 | private transient PrintStream logger; // transient since PrintStream is no serializable, it breaks in PipeLine tests 32 | 33 | public JenkinsBrowserStackLocal(String accesskey, LocalConfig localConfig, String buildTag, EnvVars envVars, PrintStream logger) { 34 | this.accesskey = accesskey; 35 | this.binarypath = localConfig.getLocalPath(); 36 | this.envVars = envVars; 37 | this.logger = logger; 38 | String localOptions = localConfig.getLocalOptions(); 39 | this.arguments = processLocalArguments((localOptions != null) ? localOptions.trim() : "", buildTag); 40 | } 41 | 42 | private static DaemonAction detectDaemonAction(List command) { 43 | if (command.size() > 2) { 44 | String action = command.get(2).toLowerCase(); 45 | if (action.equals("start")) { 46 | return DaemonAction.START; 47 | } else if (action.equals("stop")) { 48 | return DaemonAction.STOP; 49 | } 50 | } 51 | 52 | return null; 53 | } 54 | 55 | private String[] processLocalArguments(final String argString, String buildTag) { 56 | String[] args = argString.split("\\s+"); 57 | int localIdPos = 0; 58 | boolean localIdentifierOverriden = false; 59 | List arguments = new ArrayList(); 60 | for (int i = 0; i < args.length; i++) { 61 | if (args[i].contains(OPTION_LOCAL_IDENTIFIER) || args[i].contains(OPTION_LOCAL_IDENTIFIER_2)) { 62 | localIdPos = i; 63 | if (i < args.length - 1 && args[i + 1] != null && !args[i + 1].startsWith("-")) { 64 | localIdentifier = args[i + 1]; 65 | if (StringUtils.isNotBlank(localIdentifier)) { 66 | localIdentifierOverriden = true; 67 | } 68 | 69 | // skip next, since already processed 70 | i += 1; 71 | } 72 | 73 | continue; 74 | } 75 | 76 | // inject from environment variable if variable starts with $ 77 | if (args[i].startsWith("$")) { 78 | String envVarName = args[i].substring(1); 79 | PluginLogger.log(logger, 80 | "Local: Replacing " + args[i] + " in local options with Environment variable " + envVarName); 81 | args[i] = envVars.get(envVarName); 82 | } 83 | 84 | arguments.add(args[i]); 85 | } 86 | 87 | if (!localIdentifierOverriden) { 88 | localIdentifier = UUID.randomUUID().toString() + "-" + buildTag.replaceAll("[^\\w\\-\\.]", "_"); 89 | } 90 | 91 | arguments.add(localIdPos, localIdentifier); 92 | arguments.add(localIdPos, "-" + OPTION_LOCAL_IDENTIFIER); 93 | 94 | return arguments.toArray(new String[]{}); 95 | } 96 | 97 | public void start() throws Exception { 98 | Map localOptions = new HashMap(); 99 | localOptions.put("key", accesskey); 100 | if (binarypath != null && binarypath.length() > 0) localOptions.put("binarypath", binarypath); 101 | super.start(localOptions); 102 | } 103 | 104 | public void stop() throws Exception { 105 | Map localOptions = new HashMap(); 106 | localOptions.put("key", accesskey); 107 | if (binarypath != null && binarypath.length() > 0) localOptions.put("binarypath", binarypath); 108 | super.stop(localOptions); 109 | } 110 | 111 | public void start(Launcher launcher) throws Exception { 112 | launcher.getChannel().call(new MasterToSlaveCallable() { 113 | @Override 114 | public Void call() throws Exception { 115 | JenkinsBrowserStackLocal.this.start(); 116 | return null; 117 | } 118 | }); 119 | } 120 | 121 | public void stop(Launcher launcher) throws Exception { 122 | launcher.getChannel().call(new MasterToSlaveCallable() { 123 | @Override 124 | public Void call() throws Exception { 125 | JenkinsBrowserStackLocal.this.stop(); 126 | return null; 127 | } 128 | }); 129 | } 130 | 131 | @Override 132 | protected LocalProcess runCommand(List command) throws IOException { 133 | DaemonAction daemonAction = detectDaemonAction(command); 134 | if (daemonAction != null) { 135 | for (String arg : arguments) { 136 | if (StringUtils.isNotBlank(arg)) { 137 | command.add(arg.trim()); 138 | } 139 | } 140 | } 141 | 142 | return super.runCommand(command); 143 | } 144 | 145 | public String getLocalIdentifier() { 146 | return localIdentifier; 147 | } 148 | 149 | public String[] getArguments() { 150 | // using clone() here because without it findbugs raises EI_EXPOSE_REP. 151 | // https://stackoverflow.com/a/1732803/2577465 152 | return arguments.clone(); 153 | } 154 | 155 | private enum DaemonAction { 156 | START, STOP 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/local/LocalConfig.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.local; 2 | 3 | import org.kohsuke.stapler.DataBoundConstructor; 4 | 5 | import java.io.Serializable; 6 | 7 | public class LocalConfig implements Serializable { 8 | private String localPath; 9 | private String localOptions; 10 | 11 | public LocalConfig() { 12 | } 13 | 14 | @DataBoundConstructor 15 | public LocalConfig(String localPath, String localOptions) { 16 | this.localPath = localPath; 17 | this.localOptions = localOptions; 18 | } 19 | 20 | public String getLocalPath() { 21 | return this.localPath; 22 | } 23 | 24 | public void setLocalPath(String localPath) { 25 | this.localPath = localPath; 26 | } 27 | 28 | public String getLocalOptions() { 29 | return this.localOptions; 30 | } 31 | 32 | public void setLocalOptions(String localOptions) { 33 | this.localOptions = localOptions; 34 | } 35 | 36 | public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if ((o == null) || (getClass() != o.getClass())) { 39 | return false; 40 | } 41 | LocalConfig that = (LocalConfig) o; 42 | if (this.localPath != null ? !this.localPath.equals(that.localPath) : that.localPath != null) return false; 43 | return this.localOptions != null ? this.localOptions.equals(that.localOptions) : that.localOptions == null; 44 | } 45 | 46 | public int hashCode() { 47 | int result = this.localPath != null ? this.localPath.hashCode() : 0; 48 | result = 31 * result + (this.localOptions != null ? this.localOptions.hashCode() : 0); 49 | return result; 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/AccessControlsFilter.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import com.google.inject.Injector; 4 | import hudson.Extension; 5 | import hudson.init.InitMilestone; 6 | import hudson.init.Initializer; 7 | import hudson.model.Describable; 8 | import hudson.model.Descriptor; 9 | import hudson.util.PluginServletFilter; 10 | import jenkins.model.Jenkins; 11 | import net.sf.json.JSONObject; 12 | import org.kohsuke.stapler.StaplerRequest; 13 | 14 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 15 | 16 | import javax.servlet.Filter; 17 | import javax.servlet.FilterChain; 18 | import javax.servlet.FilterConfig; 19 | import javax.servlet.ServletException; 20 | import javax.servlet.ServletRequest; 21 | import javax.servlet.ServletResponse; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | import java.io.IOException; 25 | import java.io.ObjectStreamException; 26 | import java.util.*; 27 | import java.util.logging.Logger; 28 | 29 | /** 30 | * Filter to support 31 | * CORS 32 | * to access Jenkins API's from a dynamic web application using frameworks like 33 | * AngularJS 34 | * 35 | * @author Udaypal Aarkoti 36 | * @author Steven Christou 37 | */ 38 | @Extension 39 | public class AccessControlsFilter implements Filter { 40 | 41 | private static final Logger LOGGER = Logger.getLogger(AccessControlsFilter.class.getCanonicalName()); 42 | private static final String PREFLIGHT_REQUEST = "OPTIONS"; 43 | 44 | @Initializer(after = InitMilestone.JOB_LOADED) 45 | public static void init() throws ServletException { 46 | Injector inj = Jenkins.getInstance().getInjector(); 47 | if (inj == null) { 48 | return; 49 | } 50 | PluginServletFilter.addFilter(inj.getInstance(AccessControlsFilter.class)); 51 | } 52 | 53 | @Override 54 | public void init(FilterConfig filterConfig) throws ServletException { 55 | 56 | } 57 | 58 | /** 59 | * Handle CORS Access Controls 60 | */ 61 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 62 | throws IOException, ServletException { 63 | HttpServletRequest req = (HttpServletRequest) request; 64 | HttpServletResponse resp = (HttpServletResponse) response; 65 | 66 | Set allowedOrigins = new HashSet(Arrays.asList( 67 | "https://observability.browserstack.com", 68 | "https://automation.browserstack.com", 69 | "https://automate.browserstack.com", 70 | "https://app-automate.browserstack.com", 71 | "https://test-management.browserstack.com" 72 | )); 73 | 74 | String origin = req.getHeader("Origin"); 75 | if (origin != null && allowedOrigins.contains(origin)) { 76 | resp.addHeader("Access-Control-Allow-Credentials", "true"); 77 | resp.addHeader("Access-Control-Allow-Origin", origin); 78 | resp.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT"); 79 | resp.addHeader("Access-Control-Allow-Headers", "*"); 80 | resp.addHeader("Access-Control-Expose-Headers", "*"); 81 | resp.addHeader("Access-Control-Max-Age", "999"); 82 | } 83 | 84 | if (req.getMethod().equals(PREFLIGHT_REQUEST)) { 85 | resp.setStatus(200); 86 | return; 87 | } 88 | 89 | chain.doFilter(request, response); 90 | } 91 | 92 | @Override 93 | public void destroy() { 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/BuildWithObservabilityConfigActionFreeStyleFactory.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Action; 5 | import hudson.model.FreeStyleProject; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import javax.annotation.Nonnull; 9 | import jenkins.model.TransientActionFactory; 10 | 11 | /** 12 | * Attaches the {@link BuildWithObservabilityConfigAction} action to all {@link FreeStyleProject} instances. 13 | */ 14 | @Extension 15 | public class BuildWithObservabilityConfigActionFreeStyleFactory extends TransientActionFactory { 16 | /** {@inheritDoc} */ 17 | @Override 18 | public Class type() { 19 | return FreeStyleProject.class; 20 | } 21 | 22 | /** {@inheritDoc} */ 23 | @Nonnull 24 | @Override 25 | public Collection createFor(@Nonnull FreeStyleProject job) { 26 | return Collections.singleton(new BuildWithObservabilityConfigAction(job)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/BuildWithObservabilityConfigActionWorkflowFactory.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Action; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import javax.annotation.Nonnull; 9 | import jenkins.model.TransientActionFactory; 10 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 11 | 12 | /** 13 | * Attaches the {@link BuildWithObservabilityConfigAction} action to all {@link WorkflowJob} instances. 14 | */ 15 | @Extension(optional = true) 16 | public class BuildWithObservabilityConfigActionWorkflowFactory extends TransientActionFactory { 17 | @Override 18 | public Class type() { 19 | return WorkflowJob.class; 20 | } 21 | 22 | @Nonnull 23 | @Override 24 | public Collection createFor(@Nonnull WorkflowJob job) { 25 | return Collections.singleton(new BuildWithObservabilityConfigAction(job)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/ObservabilityCause.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import hudson.model.Cause; 4 | import net.sf.json.JSONObject; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | import static com.browserstack.automate.ci.common.BrowserStackEnvVars.BROWSERSTACK_RERUN; 9 | import static com.browserstack.automate.ci.common.BrowserStackEnvVars.BROWSERSTACK_RERUN_TESTS; 10 | 11 | /** 12 | * Indicates that a build was started because of one or more Observability params. 13 | */ 14 | public class ObservabilityCause extends Cause { 15 | private JSONObject params; 16 | private String tests; 17 | private String reRun; 18 | 19 | public ObservabilityCause(@Nonnull JSONObject params) { 20 | this.tests = params.getString(BROWSERSTACK_RERUN_TESTS); 21 | this.reRun = params.getString(BROWSERSTACK_RERUN); 22 | this.params = params; 23 | } 24 | 25 | @Nonnull 26 | public JSONObject getParams() { 27 | return params; 28 | } 29 | 30 | public String getTests() { 31 | return tests; 32 | } 33 | 34 | public String getReRun() { 35 | return reRun; 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public String getShortDescription() { 41 | return "Observability Params: " + params.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/ObservabilityConfig.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import org.kohsuke.stapler.DataBoundConstructor; 4 | 5 | import java.io.Serializable; 6 | 7 | public class ObservabilityConfig implements Serializable { 8 | private String tests; 9 | private String reRun; 10 | 11 | public ObservabilityConfig() { 12 | } 13 | 14 | @DataBoundConstructor 15 | public ObservabilityConfig(String tests, String reRun) { 16 | this.tests = tests; 17 | this.reRun = reRun; 18 | } 19 | 20 | public String getTests() { 21 | return this.tests; 22 | } 23 | 24 | public void setTests(String tests) { 25 | this.tests = tests; 26 | } 27 | 28 | public String getReRun() { 29 | return this.reRun; 30 | } 31 | 32 | public void setReRun(String reRun) { 33 | this.reRun = reRun; 34 | } 35 | 36 | public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if ((o == null) || (getClass() != o.getClass())) { 39 | return false; 40 | } 41 | ObservabilityConfig that = (ObservabilityConfig) o; 42 | if (this.tests != null ? !this.tests.equals(that.tests) : that.tests != null) return false; 43 | return this.reRun != null ? this.reRun.equals(that.reRun) : that.reRun == null; 44 | } 45 | 46 | public int hashCode() { 47 | int result = this.tests != null ? this.tests.hashCode() : 0; 48 | result = 31 * result + (this.reRun != null ? this.reRun.hashCode() : 0); 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/observability/ObservabilityEnvironmentContributor.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.observability; 2 | 3 | import hudson.EnvVars; 4 | import hudson.Extension; 5 | import hudson.model.EnvironmentContributor; 6 | import hudson.model.Run; 7 | import hudson.model.TaskListener; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.io.IOException; 11 | 12 | import static com.browserstack.automate.ci.common.BrowserStackEnvVars.BROWSERSTACK_RERUN; 13 | import static com.browserstack.automate.ci.common.BrowserStackEnvVars.BROWSERSTACK_RERUN_TESTS; 14 | 15 | @Extension 16 | public class ObservabilityEnvironmentContributor extends EnvironmentContributor { 17 | 18 | @Override 19 | public void buildEnvironmentFor(@Nonnull Run r, @Nonnull EnvVars envs, @Nonnull TaskListener listener) 20 | throws IOException, InterruptedException { 21 | 22 | ObservabilityCause cause = (ObservabilityCause) r.getCause(ObservabilityCause.class); 23 | if (cause != null) { 24 | envs.put(BROWSERSTACK_RERUN_TESTS, cause.getTests()); 25 | envs.put(BROWSERSTACK_RERUN, cause.getReRun()); 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/AppUploadStepExecution.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.logger.PluginLogger; 5 | import com.browserstack.automate.ci.common.uploader.AppUploaderHelper; 6 | import hudson.EnvVars; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.jenkinsci.plugins.workflow.steps.BodyExecution; 11 | import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; 12 | import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; 13 | import org.jenkinsci.plugins.workflow.steps.StepContext; 14 | import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; 15 | 16 | import java.io.PrintStream; 17 | import java.util.HashMap; 18 | import java.util.Optional; 19 | 20 | public class AppUploadStepExecution extends SynchronousNonBlockingStepExecution { 21 | 22 | private StepContext context; 23 | private String appPath; 24 | private BodyExecution body; 25 | 26 | 27 | protected AppUploadStepExecution(StepContext context, String appPath) { 28 | super(context); 29 | this.context = context; 30 | this.appPath = appPath; 31 | } 32 | 33 | @Override 34 | protected Void run() throws Exception { 35 | Run run = context.get(Run.class); 36 | TaskListener taskListener = context.get(TaskListener.class); 37 | PrintStream logger = taskListener.getLogger(); 38 | EnvVars parentContextEnvVars = context.get(EnvVars.class); 39 | 40 | String customProxy = parentContextEnvVars.get("https_proxy"); 41 | customProxy = Optional.ofNullable(customProxy).orElse(parentContextEnvVars.get("http_proxy")); 42 | 43 | String appId = AppUploaderHelper.uploadApp(run, logger, this.appPath, customProxy); 44 | 45 | if (StringUtils.isEmpty(appId)) { 46 | PluginLogger.log(logger, "ERROR : App Id empty. ABORTING!!!"); 47 | return null; 48 | } 49 | 50 | HashMap overridesMap = new HashMap(); 51 | overridesMap.put(BrowserStackEnvVars.BROWSERSTACK_APP_ID, appId); 52 | 53 | body = getContext().newBodyInvoker() 54 | .withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), 55 | new ExpanderImpl(overridesMap))) 56 | .withCallback(BodyExecutionCallback.wrap(getContext())).start(); 57 | PluginLogger.log(logger, "Environment variable BROWSERSTACK_APP_ID set with value : " + appId); 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/AppUploaderStep.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import com.browserstack.automate.ci.common.uploader.AppUploaderHelper; 4 | import com.google.common.collect.ImmutableSet; 5 | import hudson.Extension; 6 | import hudson.model.Run; 7 | import hudson.model.TaskListener; 8 | import hudson.util.FormValidation; 9 | import org.jenkinsci.plugins.workflow.steps.Step; 10 | import org.jenkinsci.plugins.workflow.steps.StepContext; 11 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 12 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 13 | import org.kohsuke.stapler.DataBoundConstructor; 14 | import org.kohsuke.stapler.QueryParameter; 15 | 16 | import java.util.Set; 17 | 18 | public class AppUploaderStep extends Step { 19 | 20 | public String appPath; 21 | 22 | @DataBoundConstructor 23 | public AppUploaderStep(String appPath) throws Exception { 24 | this.appPath = appPath; 25 | } 26 | 27 | @Override 28 | public StepExecution start(StepContext context) throws Exception { 29 | return new AppUploadStepExecution(context, appPath); 30 | } 31 | 32 | @Extension 33 | public static final class StepDescriptorImpl extends StepDescriptor { 34 | 35 | @Override 36 | public Set> getRequiredContext() { 37 | return ImmutableSet.of(Run.class, TaskListener.class); 38 | } 39 | 40 | @Override 41 | public String getFunctionName() { 42 | return "browserstackAppUploader"; 43 | } 44 | 45 | @Override 46 | public String getDisplayName() { 47 | return "BrowserStack App Uploader"; 48 | } 49 | 50 | @Override 51 | public boolean takesImplicitBlockArgument() { 52 | return true; 53 | } 54 | 55 | public FormValidation doCheckAppPath(@QueryParameter String value) { 56 | return AppUploaderHelper.validateAppPath(value); 57 | } 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackPipelineStep.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackBuildWrapperOperations; 4 | import com.browserstack.automate.ci.jenkins.local.LocalConfig; 5 | import com.browserstack.automate.ci.jenkins.observability.ObservabilityConfig; 6 | import com.google.common.collect.ImmutableSet; 7 | import hudson.Extension; 8 | import hudson.Launcher; 9 | import hudson.model.AbstractProject; 10 | import hudson.model.Item; 11 | import hudson.model.Run; 12 | import hudson.model.TaskListener; 13 | import hudson.util.FormValidation; 14 | import hudson.util.ListBoxModel; 15 | import org.jenkinsci.plugins.workflow.steps.Step; 16 | import org.jenkinsci.plugins.workflow.steps.StepContext; 17 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 18 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 19 | import org.kohsuke.stapler.AncestorInPath; 20 | import org.kohsuke.stapler.DataBoundConstructor; 21 | import org.kohsuke.stapler.DataBoundSetter; 22 | import org.kohsuke.stapler.QueryParameter; 23 | 24 | import java.util.Set; 25 | 26 | public class BrowserStackPipelineStep extends Step { 27 | 28 | public String credentialsId; 29 | public LocalConfig localConfig; 30 | public ObservabilityConfig observabilityConfig; 31 | 32 | @DataBoundConstructor 33 | public BrowserStackPipelineStep(String credentialsId) { 34 | this.credentialsId = credentialsId; 35 | } 36 | 37 | @DataBoundSetter 38 | public void setLocalConfig(LocalConfig localConfig) { 39 | this.localConfig = localConfig; 40 | } 41 | 42 | @DataBoundSetter 43 | public void setObservabilityConfig(ObservabilityConfig observabilityConfig) { 44 | this.observabilityConfig = observabilityConfig; 45 | } 46 | 47 | @Override 48 | public StepExecution start(StepContext context) throws Exception { 49 | return new BrowserStackPipelineStepExecution(context, credentialsId, localConfig, observabilityConfig); 50 | } 51 | 52 | @Extension 53 | public static final class StepDescriptorImpl extends StepDescriptor { 54 | 55 | @Override 56 | public Set> getRequiredContext() { 57 | return ImmutableSet.of(Run.class, TaskListener.class, Launcher.class); 58 | } 59 | 60 | @Override 61 | public String getFunctionName() { 62 | return "browserstack"; 63 | } 64 | 65 | @Override 66 | public String getDisplayName() { 67 | return "BrowserStack"; 68 | } 69 | 70 | @Override 71 | public boolean takesImplicitBlockArgument() { 72 | return true; 73 | } 74 | 75 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath final Item context) { 76 | return BrowserStackBuildWrapperOperations.doFillCredentialsIdItems(context); 77 | } 78 | 79 | public FormValidation doCheckLocalPath(@AncestorInPath final AbstractProject project, 80 | @QueryParameter final String localPath) { 81 | return BrowserStackBuildWrapperOperations.doCheckLocalPath(project, localPath); 82 | } 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStep.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import com.browserstack.automate.ci.common.enums.ProjectType; 5 | import com.google.common.collect.ImmutableSet; 6 | import hudson.Extension; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import hudson.util.FormValidation; 10 | import hudson.util.ListBoxModel; 11 | import org.jenkinsci.plugins.workflow.steps.Step; 12 | import org.jenkinsci.plugins.workflow.steps.StepContext; 13 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 14 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 15 | import org.kohsuke.stapler.DataBoundConstructor; 16 | import org.kohsuke.stapler.QueryParameter; 17 | 18 | import java.util.Set; 19 | 20 | public class BrowserStackReportStep extends Step { 21 | public ProjectType project; 22 | public String product; 23 | 24 | @DataBoundConstructor 25 | public BrowserStackReportStep(String product) { 26 | if (Constants.APP_AUTOMATE.equalsIgnoreCase(product)) { 27 | this.project = ProjectType.APP_AUTOMATE; 28 | this.product = Constants.APP_AUTOMATE; 29 | } else { 30 | this.project = ProjectType.AUTOMATE; 31 | this.product = Constants.AUTOMATE; 32 | } 33 | } 34 | 35 | @Override 36 | public StepExecution start(StepContext stepContext) throws Exception { 37 | return new BrowserStackReportStepExecution(stepContext, project); 38 | } 39 | 40 | @Extension 41 | public static class DescriptorImpl extends StepDescriptor { 42 | 43 | @Override 44 | public Set> getRequiredContext() { 45 | return ImmutableSet.of(Run.class, TaskListener.class); 46 | } 47 | 48 | @Override 49 | public String getFunctionName() { 50 | return Constants.BROWSERSTACK_REPORT_AUT_PIPELINE_FUNCTION ; // deprecated Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION; 51 | } 52 | 53 | @Override 54 | public String getDisplayName() { 55 | return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME; 56 | } 57 | 58 | public ListBoxModel doFillProductItems() { 59 | ListBoxModel items = new ListBoxModel(); 60 | items.add("Automate", Constants.AUTOMATE); 61 | items.add("App Automate", Constants.APP_AUTOMATE); 62 | return items; 63 | } 64 | 65 | public FormValidation doCheckProduct(@QueryParameter String product) { 66 | return FormValidation.ok(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/BrowserStackReportStepExecution.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import com.browserstack.automate.ci.common.BrowserStackEnvVars; 4 | import com.browserstack.automate.ci.common.constants.Constants; 5 | import com.browserstack.automate.ci.common.enums.ProjectType; 6 | import com.browserstack.automate.ci.common.tracking.PluginsTracker; 7 | import com.browserstack.automate.ci.jenkins.BrowserStackReportForBuild; 8 | import hudson.EnvVars; 9 | import hudson.model.Run; 10 | import hudson.model.TaskListener; 11 | import org.jenkinsci.plugins.workflow.steps.StepContext; 12 | import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; 13 | 14 | import java.io.PrintStream; 15 | import java.util.Optional; 16 | 17 | import static com.browserstack.automate.ci.common.logger.PluginLogger.log; 18 | 19 | public class BrowserStackReportStepExecution extends SynchronousNonBlockingStepExecution { 20 | 21 | private final ProjectType product; 22 | 23 | public BrowserStackReportStepExecution(StepContext context, final ProjectType product) { 24 | super(context); 25 | this.product = product; 26 | } 27 | 28 | @Override 29 | protected Void run() throws Exception { 30 | Run run = getContext().get(Run.class); 31 | TaskListener taskListener = getContext().get(TaskListener.class); 32 | final PrintStream logger = taskListener.getLogger(); 33 | final EnvVars parentContextEnvVars = getContext().get(EnvVars.class); 34 | final EnvVars parentEnvs = run.getEnvironment(taskListener); 35 | 36 | String customProxy = parentContextEnvVars.get("https_proxy"); 37 | customProxy = Optional.ofNullable(customProxy).orElse(parentContextEnvVars.get("http_proxy")); 38 | 39 | final PluginsTracker tracker = new PluginsTracker(customProxy); 40 | 41 | log(logger, "Generating BrowserStack Test Report via Pipeline for : " + product.name()); 42 | 43 | String browserStackBuildName = parentContextEnvVars.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME); 44 | browserStackBuildName = Optional.ofNullable(browserStackBuildName).orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG)); 45 | 46 | tracker.reportGenerationInitialized(browserStackBuildName, product.name(), true); 47 | 48 | final BrowserStackReportForBuild bstackReportAction = 49 | new BrowserStackReportForBuild(run, product, browserStackBuildName, logger, tracker, true, customProxy); 50 | final boolean reportResult = bstackReportAction.generateBrowserStackReport(); 51 | run.addAction(bstackReportAction); 52 | 53 | String reportStatus = reportResult ? Constants.ReportStatus.SUCCESS : Constants.ReportStatus.FAILED; 54 | log(logger, "BrowserStack Report Status via Pipeline: " + reportStatus); 55 | 56 | tracker.reportGenerationCompleted(reportStatus, product.name(), true, 57 | browserStackBuildName, bstackReportAction.getBrowserStackBuildID()); 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/pipeline/ExpanderImpl.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.pipeline; 2 | 3 | import hudson.EnvVars; 4 | import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.io.IOException; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | public class ExpanderImpl extends EnvironmentExpander { 12 | 13 | private static final long serialVersionUID = 1; 14 | private final Map overrides; 15 | 16 | ExpanderImpl(HashMap overrides) { 17 | this.overrides = overrides; 18 | } 19 | 20 | @Override 21 | public void expand(@Nonnull EnvVars env) throws IOException, InterruptedException { 22 | env.overrideAll(overrides); 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardAPIUtil.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.qualityDashboard; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import okhttp3.*; 9 | import java.io.IOException; 10 | import java.io.Serializable; 11 | 12 | public class QualityDashboardAPIUtil { 13 | 14 | OkHttpClient client = new OkHttpClient(); 15 | 16 | public Response makeGetRequestToQd(String getUrl, BrowserStackCredentials browserStackCredentials) { 17 | try { 18 | Request request = new Request.Builder() 19 | .url(getUrl) 20 | .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) 21 | .build(); 22 | Response response = client.newCall(request).execute(); 23 | return response; 24 | } catch(IOException e) { 25 | e.printStackTrace(); 26 | } 27 | return null; 28 | } 29 | 30 | public Response makePostRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) { 31 | try { 32 | Request request = new Request.Builder() 33 | .url(postUrl) 34 | .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) 35 | .post(requestBody) 36 | .build(); 37 | Response response = client.newCall(request).execute(); 38 | return response; 39 | } catch(IOException e) { 40 | e.printStackTrace(); 41 | } 42 | return null; 43 | } 44 | 45 | public Response makePutRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) { 46 | try { 47 | Request request = new Request.Builder() 48 | .url(postUrl) 49 | .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) 50 | .put(requestBody) 51 | .build(); 52 | Response response = client.newCall(request).execute(); 53 | return response; 54 | } catch(IOException e) { 55 | e.printStackTrace(); 56 | } 57 | return null; 58 | } 59 | 60 | public Response makeDeleteRequestToQd(String postUrl, BrowserStackCredentials browserStackCredentials, RequestBody requestBody) { 61 | try { 62 | Request request = new Request.Builder() 63 | .url(postUrl) 64 | .header("Authorization", Credentials.basic(browserStackCredentials.getUsername(), browserStackCredentials.getDecryptedAccesskey())) 65 | .delete(requestBody) 66 | .build(); 67 | Response response = client.newCall(request).execute(); 68 | return response; 69 | } catch(IOException e) { 70 | e.printStackTrace(); 71 | } 72 | return null; 73 | } 74 | 75 | public void logToQD(BrowserStackCredentials browserStackCredentials, String logMessage) throws JsonProcessingException { 76 | LogMessage logMessageObj = new LogMessage(logMessage); 77 | ObjectMapper objectMapper = new ObjectMapper(); 78 | String jsonBody = objectMapper.writeValueAsString(logMessageObj); 79 | RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody); 80 | makePostRequestToQd(Constants.QualityDashboardAPI.getLogMessageEndpoint(), browserStackCredentials, requestBody); 81 | } 82 | } 83 | 84 | class LogMessage implements Serializable { 85 | 86 | @JsonProperty("message") 87 | private String message; 88 | 89 | public LogMessage(String message) { 90 | this.message = message; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardInitItemListener.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.qualityDashboard; 2 | 3 | import com.browserstack.automate.ci.common.constants.Constants; 4 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import hudson.Extension; 9 | import hudson.model.Item; 10 | import hudson.model.listeners.ItemListener; 11 | import okhttp3.MediaType; 12 | import okhttp3.RequestBody; 13 | import okhttp3.Response; 14 | import java.io.Serializable; 15 | import java.util.logging.Logger; 16 | 17 | @Extension 18 | public class QualityDashboardInitItemListener extends ItemListener { 19 | 20 | private static final Logger LOGGER = Logger.getLogger(QualityDashboardInitItemListener.class.getName()); 21 | 22 | @Override 23 | public void onCreated(Item job) { 24 | try { 25 | BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds(); 26 | if(browserStackCredentials == null) { 27 | LOGGER.warning("BrowserStackCredentials not found. Please ensure they are configured correctly."); 28 | return; 29 | } 30 | QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); 31 | 32 | String itemName = job.getFullName(); 33 | String itemType = QualityDashboardUtil.getItemTypeModified(job); 34 | 35 | apiUtil.logToQD(browserStackCredentials, "Item Created : " + itemName + " - " + "Item Type : " + itemType); 36 | if(itemType != null && !itemType.equals("FOLDER")) { 37 | try { 38 | String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType)); 39 | syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "POST"); 40 | 41 | } catch(Exception e) { 42 | LOGGER.warning("Error syncing item creation to Quality Dashboard: " + e.getMessage()); 43 | e.printStackTrace(); 44 | } 45 | } else { 46 | apiUtil.logToQD(browserStackCredentials, "Skipping item creation sync: " + itemName); 47 | } 48 | } catch(Exception e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | 53 | @Override 54 | public void onDeleted(Item job) { 55 | String itemName = job.getFullName(); 56 | String itemType = QualityDashboardUtil.getItemTypeModified(job); 57 | if(itemType != null) { 58 | try { 59 | String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType)); 60 | syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "DELETE"); 61 | } catch(Exception e) { 62 | LOGGER.warning("Error syncing item deletion to Quality Dashboard: " + e.getMessage()); 63 | e.printStackTrace(); 64 | } 65 | } 66 | } 67 | 68 | @Override 69 | public void onRenamed(Item job, String oldName, String newName) { 70 | String itemType = QualityDashboardUtil.getItemTypeModified(job); 71 | if(itemType != null) { 72 | try { 73 | oldName = job.getParent().getFullName() + "/" + oldName; 74 | newName = job.getParent().getFullName() + "/" + newName; 75 | String jsonBody = getJsonReqBody(new ItemRename(oldName, newName, itemType)); 76 | syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "PUT"); 77 | } catch(Exception e) { 78 | LOGGER.warning("Error syncing item rename to Quality Dashboard: " + e.getMessage()); 79 | e.printStackTrace(); 80 | } 81 | } 82 | } 83 | 84 | private String getJsonReqBody( T item) throws JsonProcessingException { 85 | ObjectMapper objectMapper = new ObjectMapper(); 86 | String jsonBody = objectMapper.writeValueAsString(item); 87 | return jsonBody; 88 | } 89 | 90 | private Response syncItemListToQD(String jsonBody, String url, String typeOfRequest) throws JsonProcessingException { 91 | RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody); 92 | QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil(); 93 | BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds(); 94 | if(browserStackCredentials == null) { 95 | LOGGER.warning("BrowserStack credentials not found. Please ensure they are configured correctly."); 96 | return null; 97 | } 98 | if(typeOfRequest.equals("PUT")) { 99 | apiUtil.logToQD(browserStackCredentials, "Syncing Item Update - PUT"); 100 | return apiUtil.makePutRequestToQd(url, browserStackCredentials, requestBody); 101 | } else if(typeOfRequest.equals("DELETE")) { 102 | apiUtil.logToQD(browserStackCredentials, "Syncing Item Deleted - DELETE"); 103 | return apiUtil.makeDeleteRequestToQd(url, browserStackCredentials, requestBody); 104 | } else { 105 | apiUtil.logToQD(browserStackCredentials, "Syncing Item Added - POST"); 106 | return apiUtil.makePostRequestToQd(url, browserStackCredentials, requestBody); 107 | } 108 | } 109 | } 110 | 111 | class ItemUpdate implements Serializable { 112 | @JsonProperty("item") 113 | private String itemName; 114 | 115 | @JsonProperty("itemType") 116 | private String itemType; 117 | 118 | public ItemUpdate(String itemName, String itemType) { 119 | this.itemName = itemName; 120 | this.itemType = itemType; 121 | } 122 | } 123 | 124 | class ItemRename implements Serializable { 125 | @JsonProperty("fromName") 126 | private String fromItemName; 127 | 128 | @JsonProperty("toName") 129 | private String toItemName; 130 | 131 | @JsonProperty("itemType") 132 | private String itemType; 133 | 134 | public ItemRename(String fromItemName, String toItemName, String itemType) { 135 | this.fromItemName = fromItemName; 136 | this.toItemName = toItemName; 137 | this.itemType = itemType; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/browserstack/automate/ci/jenkins/qualityDashboard/QualityDashboardUtil.java: -------------------------------------------------------------------------------- 1 | package com.browserstack.automate.ci.jenkins.qualityDashboard; 2 | 3 | import com.browserstack.automate.ci.jenkins.BrowserStackCredentials; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.common.StandardCredentials; 6 | import hudson.model.Item; 7 | import hudson.model.Job; 8 | import jenkins.model.Jenkins; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class QualityDashboardUtil { 14 | public static BrowserStackCredentials getBrowserStackCreds() { 15 | Jenkins jenkins = Jenkins.getInstanceOrNull(); 16 | List creds = CredentialsProvider.lookupCredentials(StandardCredentials.class,jenkins,null,new ArrayList<>()); 17 | BrowserStackCredentials browserStackCredentials = (BrowserStackCredentials) creds.stream().filter(c -> c instanceof BrowserStackCredentials).findFirst().orElse(null); 18 | return browserStackCredentials; 19 | } 20 | 21 | public static String getItemTypeModified(Item job) { 22 | try { 23 | if (job instanceof Job) { 24 | return job.getClass().getSimpleName().toUpperCase(); 25 | } 26 | else if (job.getClass().getName().contains("Folder")) { 27 | return "FOLDER"; 28 | } 29 | else { 30 | return null; 31 | } 32 | } catch (Exception e) { 33 | // Return null in case of any error during job type determination 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/hudson.remoting.ClassFilter: -------------------------------------------------------------------------------- 1 | net.sf.json.JSONObject 2 | org.apache.commons.collections.map.ListOrderedMap 3 | net.sf.json.JSONNull 4 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AbstractBrowserStackCypressReportForBuild/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 80 | 81 | 82 |

83 |
84 |

No BrowserStack Cypress Report Available

85 |
86 |

BrowserStack Cypress test report could not be generated for this build. Please ensure that:

87 |
88 |
    89 |
  • You have set valid BrowserStack credentials via BrowserStack Plugin.
  • 90 |
  • You are using ‘--sync’ while running cli.
  • 91 |
  • You are using ‘--build-name` or `-b’ to set the correct build name while running cli.
  • 92 |
  • Refer to the Automate Jenkins documentation for more details.
  • 93 |
94 |
95 |
96 | 97 | 98 |
99 |
100 | BUILD 101 |

${it.result.buildName}

102 |
103 | 104 |
105 | TOTAL SPECS 106 |

${it.resultAggregation.totalSpecs}${it.resultAggregation.totalErrors} ERRORS

107 |
108 | 111 |
112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AbstractBrowserStackReportForBuild/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 87 | 88 | 89 | 90 | 91 |
92 |
93 |

No BrowserStack Report Available

94 |
95 |

BrowserStack test report could not be generated for this build. Please ensure that:

96 |
97 |
    98 |
  • You have set valid BrowserStack credentials via BrowserStack Plugin.
  • 99 |
  • You are using ‘BROWSERSTACK_BUILD_NAME’ environment variable to set Automate build name capability[‘build’] in test cases.
  • 100 |
  • Refer to the Automate Jenkins documentation for more details.
  • 101 |
102 |
103 |
104 |
105 | 106 | 107 |
108 |
109 |

110 | Build Name: ${it.buildName} 111 |

112 |
113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 136 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |
NameStatusREST APIBrowser/DeviceOSDurationCreated At
${i.name} 129 | 130 | ${i.status} 131 | 132 | 133 | ${i.status} 134 | 135 | 137 | 138 | ${i.userMarked} 139 | 140 | 141 | ${i.userMarked} 142 | 143 | ${i.browser}${i.os}${i.duration}${i.createdAtReadable}
151 |
152 |
153 |
154 |
155 |
156 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AbstractBrowserStackReportForBuild/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 80 | 81 | 82 |
83 |
84 |

No BrowserStack Report Available

85 |
86 |

BrowserStack test report could not be generated for this build. Please ensure that:

87 |
88 |
    89 |
  • You have set valid BrowserStack credentials via BrowserStack Plugin.
  • 90 |
  • You are using ‘BROWSERSTACK_BUILD_NAME’ environment variable to set Automate build name capability[‘build’] in test cases.
  • 91 |
  • Refer to the Automate Jenkins documentation for more details.
  • 92 |
93 |
94 |
95 |
96 | 97 |
98 |
99 | BUILD 100 |

${it.buildName}${it.result.resultAggregation.buildDuration}

101 |
102 | 103 |
104 | TOTAL SESSIONS 105 |

${it.result.resultAggregation.totalSessions}${it.result.resultAggregation.totalErrors} ERRORS

106 |
107 | 110 |
111 |
112 |
113 |
114 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/help-allowedHeaders.html: -------------------------------------------------------------------------------- 1 |
2 |

Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request.

3 | 4 |

For example: <field-name>[, <field-name>]*

5 |
-------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/help-allowedMethods.html: -------------------------------------------------------------------------------- 1 |
2 |

Specifies the method or methods allowed when accessing the Jenkins resources. This is used in response to a pre-flight 3 | request. For example: GET, PUT, OPTIONS

4 | 5 |

Supported methods include GET, PUT, OPTIONS, DELETE, POST

6 |
-------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/help-allowedOrigins.html: -------------------------------------------------------------------------------- 1 |
2 |

This parameter specifies a URI (origin) that may access the Jenkins resource(s). The browser must enforce this. 3 | Asterix (*) means that the resource can be accessed by any domain in a cross-site manner. Should be used with caution.

4 | 5 |

For example: http://foo.a, http://bar.b, http://acme.org

6 |
-------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/help-exposedHeaders.html: -------------------------------------------------------------------------------- 1 |
2 |

This header lets a server whitelist headers that browsers are allowed to access.

3 | 4 |

For example: X-My-Custom-Header, X-Another-Custom-Header
5 | This allows the X-My-Custom-Header and X-Another-Custom-Header headers to be exposed to the browser.

6 |
-------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AccessControlsFilter/help-maxAge.html: -------------------------------------------------------------------------------- 1 |
2 |

This header indicates how long the results of a preflight request can be cached.

3 | 4 |

For example: <delta-seconds>
5 | The delta-seconds parameter indicates the number of seconds the results can be cached.

6 |
-------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AppUploaderBuilder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AppUploaderBuilder/help-buildFilePath.html: -------------------------------------------------------------------------------- 1 |
2 | Please specify the absolute path or relative to the app(.apk or .ipa) file to 3 | be uploaded. 4 |
5 | The app_url of the uploaded app can be accessed from the environment variable 6 | BROWSERSTACK_APP_ID. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/com/browserstack/automate/ci/jenkins/AutomateTestAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 29 |
30 |

${%title}

31 | 32 | ${%linkText} 33 | 34 |
35 | 36 |