├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ └── cd.yaml
├── .vscode
└── settings.json
├── .mvn
├── maven.config
└── extensions.xml
├── .gitignore
├── src
└── main
│ ├── resources
│ ├── com
│ │ └── applitools
│ │ │ └── jenkins
│ │ │ ├── ApplitoolsBuildWrapper
│ │ │ ├── help.html
│ │ │ ├── help-applitoolsApiKey.html
│ │ │ ├── help-serverURL.html
│ │ │ ├── help-eyesScmIntegrationEnabled.html
│ │ │ ├── config.properties
│ │ │ └── config.jelly
│ │ │ ├── ApplitoolsStep
│ │ │ ├── help-eyesScmIntegrationEnabled.html
│ │ │ ├── config.properties
│ │ │ └── config.jelly
│ │ │ ├── config.jelly
│ │ │ └── AbstractApplitoolsStatusDisplayAction
│ │ │ └── summary.jelly
│ └── index.jelly
│ ├── java
│ └── com
│ │ └── applitools
│ │ └── jenkins
│ │ ├── ApplitoolsEnv.java
│ │ ├── AbstractApplitoolsStatusDisplayAction.java
│ │ ├── ApplitoolsEnvironmentExpander.java
│ │ ├── ApplitoolsJobDsl.java
│ │ ├── ApplitoolsEnvironmentUtil.java
│ │ ├── ApplitoolsProjectConfigProperty.java
│ │ ├── ApplitoolsStatusDisplayAction.java
│ │ ├── ApplitoolsStep.java
│ │ ├── ApplitoolsBuildWrapper.java
│ │ └── ApplitoolsCommon.java
│ └── test
│ └── UrlValidatorTests.java
├── Jenkinsfile
├── LICENSE
├── Deployment.md
├── CHANGELOG.md
├── .run
└── Jenkins Debug.run.xml
├── README.md
└── pom.xml
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @jenkinsci/applitools-eyes-plugin-developers
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "interactive"
3 | }
--------------------------------------------------------------------------------
/.mvn/maven.config:
--------------------------------------------------------------------------------
1 | -Pconsume-incrementals
2 | -Pmight-produce-incrementals
3 | -Dchangelist.format=%d.v%s
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea/
3 | *.iml
4 | *~
5 | .DS_Store
6 | work/
7 | *.releaseBackup
8 | release.properties
9 | /.java-version
10 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/help.html:
--------------------------------------------------------------------------------
1 |
2 | Enable the Applitools Eyes Jenkins plugin for this project.
3 |
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 | This plugin adds Applitools Eyes test results to your Jenkins build report.
4 |
5 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/help-applitoolsApiKey.html:
--------------------------------------------------------------------------------
1 |
2 | You can get your API Key from the Applitools Dashboard User Menu.
3 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/help-serverURL.html:
--------------------------------------------------------------------------------
1 |
2 | If you're using a private Applitools server, specify its URL (e.g., https://privateyes.applitools.com)
3 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsStep/help-eyesScmIntegrationEnabled.html:
--------------------------------------------------------------------------------
1 |
2 | Check this box if you've enabled SCM Integration in the Applitools Eyes Dashboard for this project.
3 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/help-eyesScmIntegrationEnabled.html:
--------------------------------------------------------------------------------
1 |
2 | Check this box if you've enabled SCM Integration in the Applitools Eyes Dashboard for this project.
3 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | buildPlugin(
2 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests
3 | configurations: [
4 | [platform: 'linux', jdk: 21],
5 | [platform: 'windows', jdk: 17],
6 | ])
7 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsStep/config.properties:
--------------------------------------------------------------------------------
1 | url.text.text=Applitools URL
2 | notifyOnCompletion.text.text=Notify on completion
3 | applitoolsApiKey.text.text=Applitools API key
4 | dontCloseBatches.text.text=Don''t Close Batches
5 | eyesScmIntegrationEnabled.text.text=Eyes SCM Integration Enabled
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/config.properties:
--------------------------------------------------------------------------------
1 | url.text.text=Applitools URL
2 | notifyOnCompletion.text.text=Notify On completion
3 | applitoolsApiKey.text.text=Applitools API key
4 | dontCloseBatches.text.text=Don''t Close Batches
5 | eyesScmIntegrationEnabled.text.text=Eyes SCM Integration Enabled
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: maven
6 | directory: /
7 | schedule:
8 | interval: monthly
9 | - package-ecosystem: github-actions
10 | directory: /
11 | schedule:
12 | interval: monthly
13 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/AbstractApplitoolsStatusDisplayAction/summary.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
|
4 |
5 |
--------------------------------------------------------------------------------
/.mvn/extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | io.jenkins.tools.incrementals
4 | git-changelist-maven-extension
5 | 1.6
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yaml:
--------------------------------------------------------------------------------
1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins
2 |
3 | name: cd
4 | on:
5 | workflow_dispatch:
6 | check_run:
7 | types:
8 | - completed
9 |
10 | permissions:
11 | checks: read
12 | contents: write
13 |
14 | jobs:
15 | maven-cd:
16 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1
17 | secrets:
18 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
19 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }}
20 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsEnv.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 | import java.io.Serializable;
3 | /**
4 | * Created by addihorowitz on 5/20/17.
5 | */
6 | public class ApplitoolsEnv implements Serializable{
7 | public String serverURL = ApplitoolsCommon.APPLITOOLS_DEFAULT_URL;
8 |
9 | public String getServerURL() {
10 | return serverURL;
11 | }
12 |
13 | public void setServerURL(String serverURL) {
14 | if (serverURL != null && !serverURL.isEmpty())
15 | this.serverURL = serverURL;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Applitools
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/AbstractApplitoolsStatusDisplayAction.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import hudson.model.Action;
4 |
5 | /**
6 | * Base class for Applitools status display action.
7 | */
8 | public abstract class AbstractApplitoolsStatusDisplayAction implements Action {
9 |
10 | public abstract String getIframeText();
11 |
12 | public String getIconFileName() {
13 | return null;
14 | }
15 |
16 | public String getDisplayName() {
17 | return null;
18 | }
19 |
20 | public String getUrlName() {
21 | return null;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Deployment.md:
--------------------------------------------------------------------------------
1 | # Jenkins Plugin Deployment
2 |
3 | 1. In `pom.xml` make sure you update the `version` tag.
4 | 2. In `ApplitoolsStatusDisplayAction` make sure to update the
5 | version sent in the `agentId` query part of the url to the same version.
6 | 3. Make sure you have `~/.m2/settings.xml` filled with the content you get from
7 | the `curl` command:
8 | ```
9 | curl -u applitools: https://repo.jenkins-ci.org/setup/settings.xml
10 | ```
11 | If you don't have the password, ask someone who have it, or look for it in the
12 | vault in Azure: *JenkinsPluginKV :: Secrets :: jenkins-plugin-password*
13 | 4. Run `mvn clean install deploy` (or click *deploy* in the maven lifecycle menu in IntelliJ IDEA IDE)
14 | 5. Notice it might take a couple of hours until you see the updated plugin in the plugins page.
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsEnvironmentExpander.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 | import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander;
3 | import java.util.Map;
4 | import java.util.HashMap;
5 | import edu.umd.cs.findbugs.annotations.NonNull;
6 | import hudson.EnvVars;
7 | import java.io.IOException;
8 |
9 | /**
10 | * Created by addihorowitz on 5/7/17.
11 | */
12 | public class ApplitoolsEnvironmentExpander extends EnvironmentExpander {
13 | private static final long serialVersionUID = 1;
14 | private final Map overrides;
15 |
16 | ApplitoolsEnvironmentExpander(HashMap overrides) {
17 | this.overrides = overrides;
18 |
19 | }
20 |
21 | @Override
22 | public void expand(@NonNull EnvVars env) throws IOException, InterruptedException {
23 | env.overrideAll(overrides);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsStep/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/main/resources/com/applitools/jenkins/ApplitoolsBuildWrapper/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.16.3] - 2024-04-25
2 | ### Fixed
3 | - Fixed incorrect batch id when starting a new run and looking at older runs [Trello 3273](https://trello.com/c/GFvXy9jP)
4 |
5 | ## [1.16] - 2024-03-07
6 | ### Added
7 | - Added support for Eyes SCM integration [Trello 3273](https://trello.com/c/GFvXy9jP)
8 |
9 | ## [1.15] - 2024-01-28
10 | ### Updated
11 | - Check if using a custom batch id before archiving it. [Trello 2201](https://trello.com/c/B8KrpLpF)
12 | - Update `jenkins.version` to version `2.361.4` as [recommended here](https://www.jenkins.io/doc/developer/plugin-development/choosing-jenkins-baseline/).
13 | - Update parent pom to `4.51`.
14 | - Remove deprecated `java.level` property.
15 | - Bump JobDSL to `1.72` to rely on non-vulnerable version.
16 | - Rely on Jenkins core BOM for workflow plugins versions.
17 |
18 | ## [1.14] - 2023-02-27
19 | ### Fixed
20 | - Fixed crash on newer Jenkins versions.
21 |
22 | ## [1.12] - 2019-11-13
23 | ### Added
24 | - Allow setting Batch ID explicitly.
25 |
26 | ## [1.11] - 2019-11-13
27 | ### Added
28 | - This CHANGELOG file.
29 | - Batch close & notification support.
30 |
--------------------------------------------------------------------------------
/.run/Jenkins Debug.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/main/test/UrlValidatorTests.java:
--------------------------------------------------------------------------------
1 | import com.applitools.jenkins.ApplitoolsBuildWrapper;
2 | import org.junit.Assert;
3 | import org.junit.Test;
4 |
5 | public class UrlValidatorTests {
6 |
7 | @Test
8 | public void TesEmptyUrlExpectFalse()
9 | {
10 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL("");
11 | Assert.assertFalse(result);
12 | }
13 |
14 |
15 | @Test
16 | public void TesNullUrlExpectFalse()
17 | {
18 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL(null);
19 | Assert.assertFalse(result);
20 | }
21 |
22 | @Test
23 | public void TestInvalidUrlExpectFalse()
24 | {
25 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL("https://");
26 | Assert.assertFalse(result);
27 | }
28 |
29 | @Test
30 | public void TestValidUrlExpectTrue()
31 | {
32 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL("https://eyes.applitools.com");
33 | Assert.assertTrue(result);
34 | }
35 |
36 | @Test
37 | public void TestValidUrlWithQueryExpectFalse()
38 | {
39 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL("https://eyes.applitools.com?a=b");
40 | Assert.assertFalse(result);
41 | }
42 |
43 | @Test
44 | public void TestInvalidUrlWithXssInjectionExpectFalse()
45 | {
46 | boolean result = ApplitoolsBuildWrapper.DescriptorImpl.validURL("https://eyes.applitools.com/\"> build,
23 | Map
env, String serverURL, String batchName,
24 | String batchId, String projectName, Secret applitoolsApiKey) {
25 | listener.getLogger().println("Creating Applitools environment variables:");
26 |
27 | outputEnvironmentVariable(listener, env, APPLITOOLS_DONT_CLOSE_BATCHES, TRUE_VALUE, true);
28 |
29 | if (serverURL != null && !serverURL.isEmpty()) {
30 | outputEnvironmentVariable(listener, env, APPLITOOLS_PROJECT_SERVER_URL, serverURL, true);
31 | }
32 |
33 | if (batchId != null && !batchId.isEmpty()) {
34 | outputEnvironmentVariable(listener, env, APPLITOOLS_BATCH_ID, batchId, true);
35 | }
36 |
37 | if (batchName != null && !batchName.isEmpty()) {
38 | // String customersBatchId = process.env
39 | outputEnvironmentVariable(listener, env, APPLITOOLS_BATCH_NAME, batchName, true);
40 | }
41 |
42 | if (projectName != null) {
43 | outputEnvironmentVariable(listener, env, APPLITOOLS_BATCH_SEQUENCE, projectName, true);
44 | }
45 |
46 | if (applitoolsApiKey != null) {
47 | String decryptedApiKey = applitoolsApiKey.getPlainText();
48 | if (!decryptedApiKey.isEmpty()) {
49 | outputEnvironmentVariable(listener, env, APPLITOOLS_API_KEY, decryptedApiKey, true);
50 | }
51 | }
52 | }
53 |
54 | public static void outputEnvironmentVariable(final TaskListener listener, Map env, String key, String value, boolean overwrite) {
55 | String prefix = "APPLITOOLS_";
56 |
57 | if (env.get(key) == null || overwrite) {
58 | String keyName = prefix + key;
59 | env.put(keyName, value);
60 | listener.getLogger().println(keyName + " = '" + value + "'");
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsProjectConfigProperty.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import edu.umd.cs.findbugs.annotations.NonNull;
4 | import hudson.model.JobProperty;
5 | import hudson.model.JobPropertyDescriptor;
6 | import hudson.Extension;
7 | import hudson.model.AbstractProject;
8 | import hudson.model.Job;
9 | import hudson.util.Secret;
10 |
11 | import java.io.Serializable;
12 |
13 | /**
14 | * Encapsulates Applitools plugin configuration.
15 | */
16 | public class ApplitoolsProjectConfigProperty extends JobProperty> implements Serializable{
17 | private String serverURL;
18 | private boolean notifyOnCompletion;
19 | private Secret applitoolsApiKey;
20 | private boolean dontCloseBatches;
21 | private boolean eyesScmIntegrationEnabled;
22 |
23 | public ApplitoolsProjectConfigProperty(String serverURL, boolean notifyOnCompletion, Secret applitoolsApiKey,
24 | boolean dontCloseBatches, boolean eyesScmIntegrationEnabled) {
25 | this.setServerURL(serverURL);
26 | this.notifyOnCompletion = notifyOnCompletion;
27 | this.applitoolsApiKey = applitoolsApiKey;
28 | this.dontCloseBatches = dontCloseBatches;
29 | this.eyesScmIntegrationEnabled = eyesScmIntegrationEnabled;
30 | }
31 |
32 | public Secret getApplitoolsApiKey() {
33 | return applitoolsApiKey;
34 | }
35 |
36 | public void setApplitoolsApiKey(Secret value) {
37 | this.applitoolsApiKey = value;
38 | }
39 |
40 | public String getServerURL()
41 | {
42 | return this.serverURL;
43 | }
44 |
45 | public void setServerURL(String serverURL)
46 | {
47 | if (ApplitoolsBuildWrapper.DescriptorImpl.validURL(serverURL)) {
48 | this.serverURL = serverURL.trim();
49 | } else {
50 | this.serverURL = ApplitoolsCommon.APPLITOOLS_DEFAULT_URL;
51 | }
52 | }
53 |
54 | public boolean getNotifyOnCompletion() { return this.notifyOnCompletion; }
55 |
56 | public void setNotifyOnCompletion(boolean value) { this.notifyOnCompletion = value; }
57 |
58 | public boolean getDontCloseBatches() {
59 | return dontCloseBatches;
60 | }
61 |
62 | public void setDontCloseBatches(boolean dontCloseBatches) {
63 | this.dontCloseBatches = dontCloseBatches;
64 | }
65 |
66 | public boolean getEyesScmIntegrationEnabled() {
67 | return eyesScmIntegrationEnabled;
68 | }
69 |
70 | public void setEyesScmIntegrationEnabled(boolean eyesScmIntegrationEnabled) {
71 | this.eyesScmIntegrationEnabled = eyesScmIntegrationEnabled;
72 | }
73 |
74 | @Override
75 | public JobPropertyDescriptor getDescriptor() {
76 | return DESCRIPTOR;
77 | }
78 |
79 | /* even though we are not displaying it, it should be there*/
80 | @Extension
81 | public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
82 | public static class DescriptorImpl extends JobPropertyDescriptor {
83 |
84 | public DescriptorImpl() {
85 | super(ApplitoolsProjectConfigProperty.class);
86 | load();
87 | }
88 |
89 | @Override
90 | @NonNull
91 | public String getDisplayName() {
92 | return "Set Applitools URL";
93 | }
94 |
95 | @Override
96 | public boolean isApplicable(Class extends Job> jobType) {
97 | return AbstractProject.class.isAssignableFrom(jobType);
98 | }
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Applitools Eyes Plugin for Jenkins
2 |
3 | This plugin adds [Applitools Eyes](https://applitools.com) test results to your Jenkins build report.
4 |
5 | ### Adding the Applitools Eyes plugin to your Jenkins CI:
6 | To add the Applitools plugin to Jenkins, use menu
7 |
8 | *Jenkins* => *Manage Jenkins* => *Manage Plugins*
9 |
10 | then choose "Available Plugins" and search for "Applitools".
11 |
12 | For more details see the [guide](https://www.jenkins.io/doc/book/managing/plugins/).
13 |
14 | ### In case of a freestyle project
15 | To enable Applitools support for a freestyle project, check the 'Applitools support'
16 | on the **Build Environment** section on the projects' configuration page.
17 |
18 | [More details are available here](https://plugins.jenkins.io/applitools-eyes/)
19 |
20 | If you are using a dedicated Applitools Eyes server, update the Applitools URL accordingly
21 | (If you don't know if you're using a dedicated server, you are not using one).
22 |
23 | ### In case of a pipeline project
24 |
25 | To use the Applitools plugin in a pipeline project, you need to add the Applitools() directive and put your run code in a block. Following is a script example:
26 | ```
27 | node {
28 | stage('Applitools build') {
29 | Applitools() {
30 | sh 'mvn clean test'
31 | }
32 | }
33 | }
34 | ```
35 | If you are using a dedicated Applitools Eyes server, you should update the Applitools URL accordingly inside the Applitools directive. For example:
36 |
37 | ```
38 | node {
39 | stage('Applitools build') {
40 | Applitools('https://myprivateserver.com') {
41 | sh 'mvn clean test'
42 | }
43 | }
44 | }
45 | ```
46 |
47 | ### Special Arguments
48 |
49 | * **`serverUrl`**: Defines a custom Eyes server URL (in case of a private cloud or on-prem Eyes)
50 | * **`applitoolsApiKey`**: Define the API key for the Eyes account. Best practice is to use an env-var with `credentials` like this:
51 | ```
52 | environment{
53 | APPLITOOLS_API_KEY = credentials('APPLITOOLS_API_KEY')
54 | }
55 | ...
56 | stage{
57 | Applitools(applitoolsApiKey: "$APPLITOOLS_API_KEY")
58 | }
59 |
60 | ```
61 | You can find more info about setting Jenkins credentials [here](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#secret-text)
62 | * **`dontCloseBatches`**: If set to `true` the batch will not be closed, allowing other tests to be added to the same batch. If set to `false` the batch will be closed, and any other test with the same batch ID will be added to a new batch.
63 | * **`eyesScmIntegrationEnabled`**: If set to `true`, the desired batch ID will be used internally, but a generated batch ID will be used externally. Use this if you want to use the SCM Integration feature in Eyes, which means you'll need to set your batch ID to the commit SHA. In a Jenkins Pipeline this can be done like this:
64 | ```
65 | environment {
66 | APPLITOOLS_BATCH_ID = "${env.GIT_COMMIT}"
67 | }
68 | ```
69 | or leave it unset alltogether which will look for the `GIT_COMMIT` env var for you.
70 | * **`notifyOnCompletion`**: If set to `true`, will tell Eyes server that the batch was closed and it can send notifications about it as configured in the Eyes Dashboard Admin Panel.
71 |
72 | ### Updating Your Tests Code
73 | Jenkins exports the batch ID to the APPLITOOLS_BATCH_ID environment variable. You need to update your tests code to use this ID in order for your tests to appear in the Applitools window report in Jenkins.
74 |
75 | In addition, Jenkins exports a suggested batch name to the APPLITOOLS_BATCH_NAME environment variable. Using this batch name is optional (the batch name is used for display purposes only).
76 |
77 | Following is a Java code example:
78 | ```
79 | BatchInfo batchInfo = new BatchInfo(System.getenv("APPLITOOLS_BATCH_NAME"));
80 |
81 | // If the test runs via Jenkins, set the batch ID accordingly.
82 | String batchId = System.getenv("APPLITOOLS_BATCH_ID");
83 | if (batchId != null) {
84 | batchInfo.setId(batchId);
85 | }
86 | eyes.setBatch(batchInfo);
87 | ```
88 |
89 | If you have any questions or need any assistance in using the plugin, feel free to contact Applitools support at: support [at] applitools dot com.
90 |
91 |
92 | ### Explicitly setting the BATCH ID / Name
93 | The plugin exports a set of environment variables which are used by the Applitools SDK when a test is run,
94 | and later by the plugin itself to present the Applitools results in the build status page.
95 |
96 | These environment variables can overriden during the build.
97 | For example, to override the APPLITOOLS_BATCH_ID environment variable, place a value to the file ./.applitools/BATCH_ID (in the build root folder).
98 | If the file ./.applitools/BATCH_ID exists, the module will read it and export its value as APPLITOOLS_BATCH_ID environment variable.
99 | Also, it will be stored as a build artifact, which will later be will be used to display the Applitools test results.
100 |
101 | The same goes for any other Applitools environment variable.
102 |
103 |
104 | ### Building the plugin from source
105 |
106 | To build the version, execute
107 |
108 | ```
109 | mvn clean; mvn package
110 | ```
111 |
112 | The built module appears in the ./target/applitools-eyes.hpi
113 |
114 | To load this module go to the "Manage Plugins" page, then to "Advanced" and select this file.
115 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | org.jenkins-ci.plugins
7 | plugin
8 | 4.76
9 |
10 |
11 |
12 | applitools-eyes
13 | 1.16.6
14 | hpi
15 | Applitools Eyes Plugin
16 | https://github.com/jenkinsci/applitools-eyes-plugin
17 |
18 |
19 | Apache License, Version 2.0
20 | http://www.apache.org/licenses/LICENSE-2.0
21 | repo
22 |
23 |
24 |
25 |
26 | scm:git:https://github.com/${gitHubRepo}.git
27 | scm:git:https://github.com:${gitHubRepo}.git
28 | https://github.com/${gitHubRepo}
29 | applitools-eyes-${version}
30 |
31 |
32 |
33 | jenkinsci/applitools-eyes-plugin
34 |
35 |
36 | 2.440.2
37 |
38 | 1.72
39 | ${project.version}
40 |
41 |
42 |
43 |
44 | applitools
45 | Applitools Team
46 | team@applitools.com
47 |
48 |
49 |
50 |
51 |
52 |
53 | org.codehaus.mojo
54 | properties-maven-plugin
55 | 1.1.0
56 |
57 |
58 | generate-resources
59 |
60 | write-project-properties
61 |
62 |
63 | ${project.build.outputDirectory}/my.properties
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | io.jenkins.tools.bom
74 | bom-2.361.x
75 | 2102.v854b_fec19c92
76 | pom
77 | import
78 |
79 |
80 |
81 |
82 |
83 |
84 | org.jenkins-ci.plugins.workflow
85 | workflow-step-api
86 |
87 |
88 | org.jenkins-ci.plugins.workflow
89 | workflow-job
90 |
91 |
92 | org.jenkins-ci.plugins
93 | job-dsl
94 | ${plugins.job-dsl.version}
95 | true
96 |
97 |
98 | org.jenkins-ci.plugins
99 | apache-httpcomponents-client-4-api
100 |
101 |
102 | org.owasp.encoder
103 | encoder
104 | 1.3.1
105 |
106 |
107 |
108 |
109 | repo.jenkins-ci.org
110 | https://repo.jenkins-ci.org/public/
111 |
112 |
113 |
114 |
115 | repo.jenkins-ci.org
116 | https://repo.jenkins-ci.org/public/
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsStatusDisplayAction.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import hudson.model.Run;
4 | import org.apache.commons.lang.mutable.MutableBoolean;
5 |
6 | import java.io.PrintWriter;
7 | import java.io.StringWriter;
8 | import java.text.SimpleDateFormat;
9 | import java.util.Calendar;
10 | import java.util.HashMap;
11 | import java.util.Map;
12 | import java.util.logging.Logger;
13 |
14 |
15 | /**
16 | * Encapsulates the Applitools' status display action.
17 | */
18 | public class ApplitoolsStatusDisplayAction extends AbstractApplitoolsStatusDisplayAction {
19 | private static final String TIMESTAMP_PATTERN = "yyyyMMddHHmmss";
20 | private final String projectName;
21 | private final int buildNumber;
22 | private final Calendar buildTimestamp;
23 | private String serverURL;
24 | @SuppressWarnings("rawtypes")
25 | private final Run build;
26 | private Map applitoolsValuesFromArtifacts;
27 | private static final Logger logger = Logger.getLogger(ApplitoolsStatusDisplayAction.class.getName());
28 | private boolean scmIntegrationEnabled;
29 |
30 | @SuppressWarnings("rawtypes")
31 | public ApplitoolsStatusDisplayAction(Run build) {
32 | this.projectName = build.getParent().getDisplayName();
33 | this.buildNumber = build.getNumber();
34 | this.buildTimestamp = build.getTimestamp();
35 | this.serverURL = null;
36 | this.build = build;
37 | this.applitoolsValuesFromArtifacts = new HashMap<>();
38 | for (Object property : build.getParent().getAllProperties()) {
39 | if (property instanceof ApplitoolsProjectConfigProperty) {
40 | this.serverURL = ((ApplitoolsProjectConfigProperty) property).getServerURL();
41 | this.scmIntegrationEnabled = ((ApplitoolsProjectConfigProperty) property).getEyesScmIntegrationEnabled();
42 | break;
43 | }
44 | }
45 | }
46 |
47 |
48 | @Override
49 | public String getIframeText() {
50 | this.applitoolsValuesFromArtifacts =
51 | ApplitoolsCommon.checkApplitoolsArtifacts(
52 | this.build.getArtifacts(),
53 | this.build.getArtifactManager().root());
54 | try {
55 | String iframeURL = generateIframeURL();
56 | if (iframeURL == null) {
57 | // In case Applitools support has been removed from the project,
58 | // remove iframes from old reports
59 | return "";
60 | }
61 |
62 | return "\n";
64 | } catch (Exception ex) {
65 | StringWriter sw = new StringWriter();
66 | PrintWriter pw = new PrintWriter(sw);
67 | ex.printStackTrace(pw);
68 | logger.warning(sw.toString());
69 | return "";
70 | }
71 | }
72 |
73 | private String generateBatchId() {
74 | return generateBatchId(ApplitoolsCommon.getEnv(), this.projectName, this.buildNumber,
75 | this.buildTimestamp, this.applitoolsValuesFromArtifacts, null, this.scmIntegrationEnabled);
76 | }
77 |
78 | private String generateIframeURL() {
79 | if (serverURL == null || serverURL.isEmpty()) {
80 | // In case Applitools support has been removed from the project
81 | return null;
82 | }
83 |
84 | return serverURL + "/app/batchesnoauth/?startInfoBatchId=" + generateBatchId() + "&hideBatchList=true&intercom=false&agentId=eyes-jenkins-" + ApplitoolsCommon.getPluginVersion();
85 | }
86 |
87 | public static String generateBatchId(Map env, String projectName, int buildNumber,
88 | Calendar buildTimestamp, Map applitoolsValuesFromArtifacts,
89 | MutableBoolean isCustom, boolean scmIntegrationEnabled) {
90 | if (!scmIntegrationEnabled) {
91 | if (applitoolsValuesFromArtifacts != null &&
92 | applitoolsValuesFromArtifacts.containsKey(ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID)) {
93 | return applitoolsValuesFromArtifacts.get(ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID);
94 | } else if (env != null) {
95 | String buildNumberFromEnv = env.get("BUILD_NUMBER");
96 | if (("" + buildNumber).equals(buildNumberFromEnv)) {
97 | String batchId = env.get("APPLITOOLS_BATCH_ID");
98 | if (batchId != null) {
99 | ApplitoolsBuildWrapper.isCustomBatchId = true;
100 | if (isCustom != null) {
101 | isCustom.setValue(true);
102 | if (applitoolsValuesFromArtifacts != null) {
103 | applitoolsValuesFromArtifacts.put(ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID, batchId);
104 | }
105 | }
106 | return batchId;
107 | }
108 | }
109 | }
110 | }
111 |
112 | final String BATCH_ID_PREFIX = "jenkins";
113 | SimpleDateFormat buildDate = new SimpleDateFormat(TIMESTAMP_PATTERN);
114 | buildDate.setTimeZone(buildTimestamp.getTimeZone());
115 |
116 | return BATCH_ID_PREFIX + "-" + projectName + "-" + buildNumber + "-" + buildDate.format(buildTimestamp.getTime());
117 | }
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsStep.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import com.google.inject.Inject;
4 | import edu.umd.cs.findbugs.annotations.NonNull;
5 | import hudson.EnvVars;
6 | import hudson.Extension;
7 | import hudson.FilePath;
8 | import hudson.Launcher;
9 | import hudson.model.*;
10 | import java.io.IOException;
11 | import java.util.Collections;
12 | import java.util.HashMap;
13 | import java.util.Map;
14 | import java.util.Set;
15 |
16 | import hudson.util.Secret;
17 | import net.sf.json.JSONObject;
18 | import org.jenkinsci.plugins.workflow.steps.*;
19 | import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
20 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
21 | import org.kohsuke.stapler.DataBoundConstructor;
22 | import org.kohsuke.stapler.StaplerRequest;
23 |
24 | import static com.applitools.jenkins.ApplitoolsBuildWrapper.isCustomBatchId;
25 |
26 | /**
27 | * Created by addihorowitz on 5/7/17.
28 | */
29 | public class ApplitoolsStep extends AbstractStepImpl {
30 | private String serverURL;
31 | private final boolean notifyOnCompletion;
32 | private final Secret applitoolsApiKey;
33 | private final boolean dontCloseBatches;
34 | private final boolean eyesScmIntegrationEnabled;
35 |
36 | @DataBoundConstructor
37 | public ApplitoolsStep(String serverURL,
38 | boolean notifyOnCompletion,
39 | Secret applitoolsApiKey,
40 | boolean dontCloseBatches,
41 | boolean eyesScmIntegrationEnabled)
42 | {
43 | super(true);
44 | this.notifyOnCompletion = notifyOnCompletion;
45 | this.applitoolsApiKey = applitoolsApiKey;
46 | this.dontCloseBatches = dontCloseBatches;
47 | this.eyesScmIntegrationEnabled = eyesScmIntegrationEnabled;
48 | if (serverURL != null && !serverURL.isEmpty())
49 | this.serverURL = serverURL;
50 | }
51 |
52 | public String getServerURL() {
53 | if (serverURL != null && !serverURL.isEmpty())
54 | return serverURL;
55 | return ApplitoolsCommon.APPLITOOLS_DEFAULT_URL;
56 | }
57 |
58 | public Secret getApplitoolsApiKey() {
59 | return this.applitoolsApiKey;
60 | }
61 |
62 | public boolean getNotifyOnCompletion() {
63 | return this.notifyOnCompletion;
64 | }
65 |
66 | public boolean getDontCloseBatches() {
67 | return this.dontCloseBatches;
68 | }
69 | public boolean getEyesScmIntegrationEnabled() {
70 | return this.eyesScmIntegrationEnabled;
71 | }
72 |
73 | public static class ApplitoolsStepExecution extends AbstractStepExecutionImpl {
74 | private static final long serialVersionUID = 1;
75 | @Inject(optional=true) private transient ApplitoolsStep step;
76 | private transient Run,?> run;
77 | private transient TaskListener listener;
78 | private transient FilePath workspace;
79 | private transient Launcher launcher;
80 |
81 | private BodyExecution body;
82 |
83 | @Override
84 | public boolean start() throws Exception {
85 | run = getContext().get(Run.class);
86 | listener = getContext().get(TaskListener.class);
87 | workspace = getContext().get(FilePath.class);
88 | launcher = getContext().get(Launcher.class);
89 | EnvVars env = getContext().get(EnvVars.class);
90 |
91 | Job,?> job = run.getParent();
92 | if (!(job instanceof TopLevelItem)) {
93 | throw new Exception("should be top level job " + job);
94 | }
95 |
96 | HashMap overrides = new HashMap<>();
97 | if (env != null) {
98 | overrides.putAll(env);
99 | }
100 | final Map applitoolsArtifacts = ApplitoolsBuildWrapper.getApplitoolsArtifactList(
101 | getContext().get(FilePath.class), listener);
102 |
103 | ApplitoolsCommon.buildEnvVariablesForExternalUsage(overrides, run, listener, workspace, launcher,
104 | step.getServerURL(), step.getApplitoolsApiKey(), applitoolsArtifacts,
105 | step.getEyesScmIntegrationEnabled());
106 |
107 | body = getContext().newBodyInvoker()
108 | .withContext(
109 | EnvironmentExpander.merge(
110 | getContext().get(EnvironmentExpander.class),
111 | new ApplitoolsEnvironmentExpander(overrides)
112 | )
113 | ).withCallback(new BodyExecutionCallback() {
114 | @Override
115 | public void onStart(StepContext context) {
116 | try {
117 | ApplitoolsCommon.integrateWithApplitools(run,
118 | step.getServerURL(),
119 | step.getNotifyOnCompletion(),
120 | step.getApplitoolsApiKey(),
121 | step.getDontCloseBatches(),
122 | step.getEyesScmIntegrationEnabled());
123 | } catch (Exception ex) {
124 | listener.getLogger().println("Failed to update properties");
125 | }
126 | }
127 |
128 | @Override
129 | public void onSuccess(StepContext context, Object result) {
130 | closeBatch();
131 | context.onSuccess(result);
132 | }
133 |
134 | @Override
135 | public void onFailure(StepContext context, Throwable t) {
136 | closeBatch();
137 | context.onFailure(t);
138 | }
139 |
140 | public void closeBatch() {
141 | try {
142 | if (isCustomBatchId) {
143 | ApplitoolsCommon.archiveArtifacts(run, workspace, launcher, listener);
144 | }
145 |
146 | if (!step.dontCloseBatches){
147 | ApplitoolsCommon.closeBatch(
148 | run, listener,
149 | step.getServerURL(),
150 | step.getNotifyOnCompletion(),
151 | step.getApplitoolsApiKey(),
152 | step.getEyesScmIntegrationEnabled());
153 | }
154 | }
155 | catch (IOException ex) {
156 | listener.getLogger().println("Error closing batch: " + ex.getMessage());
157 | }
158 | }
159 | })
160 | .withCallback(BodyExecutionCallback.wrap(getContext()))
161 | .start();
162 |
163 | return false;
164 | }
165 |
166 | @Override
167 | public void stop(@NonNull Throwable cause) {
168 | if (body!=null) {
169 | body.cancel(cause);
170 | }
171 | }
172 | }
173 |
174 | @Extension
175 | public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
176 | public DescriptorImpl() {
177 | super(ApplitoolsStepExecution.class);
178 | }
179 |
180 | @NonNull
181 | @Override
182 | public String getDisplayName() {
183 | return "Applitools Support";
184 | }
185 |
186 | @Override public String getFunctionName() {
187 | return "Applitools";
188 | }
189 |
190 | @Override public boolean takesImplicitBlockArgument() {
191 | return true;
192 | }
193 |
194 |
195 | @Override
196 | public Set> getProvidedContext() {
197 | return Collections.>singleton(ApplitoolsEnv.class);
198 | }
199 |
200 | @Override
201 | public ApplitoolsStep newInstance(StaplerRequest req, JSONObject formData) {
202 | return new ApplitoolsStep(
203 | formData.getString("serverURL"),
204 | formData.getBoolean("notifyOnCompletion"),
205 | Secret.fromString(formData.getString("applitoolsApiKey")),
206 | formData.getBoolean("dontCloseBatches"),
207 | formData.getBoolean("eyesScmIntegrationEnabled")
208 | );
209 | }
210 |
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsBuildWrapper.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import org.owasp.encoder.Encode;
4 | import edu.umd.cs.findbugs.annotations.NonNull;
5 | import hudson.Extension;
6 | import hudson.FilePath;
7 | import hudson.Launcher;
8 | import hudson.model.*;
9 | import hudson.tasks.BuildWrapper;
10 | import hudson.util.FormValidation;
11 | import java.io.*;
12 | import java.net.MalformedURLException;
13 | import java.net.URL;
14 | import java.nio.charset.StandardCharsets;
15 | import java.util.*;
16 | import java.util.regex.Matcher;
17 |
18 | import hudson.util.Secret;
19 | import jenkins.util.VirtualFile;
20 | import net.sf.json.JSONObject;
21 | import org.apache.commons.io.IOUtils;
22 | import org.kohsuke.stapler.DataBoundConstructor;
23 | import org.kohsuke.stapler.QueryParameter;
24 | import org.kohsuke.stapler.StaplerRequest;
25 |
26 | /**
27 | * Code for the build page.
28 | */
29 | public class ApplitoolsBuildWrapper extends BuildWrapper implements Serializable {
30 | public final static String BATCH_NOTIFICATION_PATH = "/api/sessions/batches/%s/close/bypointerid";
31 | public String serverURL;
32 | public boolean notifyOnCompletion;
33 | public Secret applitoolsApiKey;
34 | public boolean dontCloseBatches;
35 | public boolean eyesScmIntegrationEnabled;
36 | static boolean isCustomBatchId = false;
37 |
38 | static final Map ARTIFACT_PATHS = new HashMap<>();
39 |
40 | static {
41 | ARTIFACT_PATHS.put(
42 | ApplitoolsCommon.APPLITOOLS_ARTIFACT_PREFIX +
43 | "_" +
44 | ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID,
45 | ApplitoolsCommon.APPLITOOLS_ARTIFACT_FOLDER +
46 | "/" +
47 | ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID
48 | );
49 | }
50 |
51 | @DataBoundConstructor
52 | public ApplitoolsBuildWrapper(String serverURL, boolean notifyOnCompletion,
53 | Secret applitoolsApiKey, boolean dontCloseBatches, boolean eyesScmIntegrationEnabled) {
54 | this.applitoolsApiKey = applitoolsApiKey;
55 | this.notifyOnCompletion = notifyOnCompletion;
56 | this.dontCloseBatches = dontCloseBatches;
57 | this.eyesScmIntegrationEnabled = eyesScmIntegrationEnabled;
58 | if (serverURL != null && !serverURL.isEmpty())
59 | {
60 | if (DescriptorImpl.validURL(serverURL))
61 | {
62 | this.serverURL = serverURL.trim();
63 | }
64 | } else {
65 | this.serverURL = ApplitoolsCommon.APPLITOOLS_DEFAULT_URL;
66 | }
67 | }
68 |
69 | @Override
70 | public Environment setUp(final AbstractBuild build, final Launcher launcher, final BuildListener listener) throws IOException {
71 |
72 | runPreBuildActions(build, listener);
73 |
74 | return new Environment() {
75 | @Override
76 | public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
77 | if (isCustomBatchId) {
78 | build.pickArtifactManager().archive(build.getWorkspace(), launcher, listener, ARTIFACT_PATHS);
79 | }
80 | if (!dontCloseBatches) {
81 | ApplitoolsCommon.closeBatch(
82 | build, listener, serverURL, notifyOnCompletion, applitoolsApiKey, eyesScmIntegrationEnabled);
83 | }
84 | return true;
85 | }
86 |
87 | @Override
88 | public void buildEnvVars(Map env) {
89 | FilePath workspace = build.getWorkspace();
90 | if (workspace != null) {
91 | Map applitoolsArtifacts = getApplitoolsArtifactList(workspace, listener);
92 | ApplitoolsCommon.buildEnvVariablesForExternalUsage(env, build, listener, workspace, launcher,
93 | serverURL, applitoolsApiKey, applitoolsArtifacts, eyesScmIntegrationEnabled);
94 | }
95 | }
96 | };
97 | }
98 |
99 | public static Map getApplitoolsArtifactList(FilePath workspace, TaskListener listener) {
100 | Map applitoolsArtifacts = new HashMap<>();
101 | if (workspace != null) {
102 | for (Map.Entry apath : ARTIFACT_PATHS.entrySet()) {
103 | try {
104 | VirtualFile rootDir = workspace.absolutize().toVirtualFile();
105 | listener.getLogger().println("Workspace absolute path: " + workspace.absolutize());
106 |
107 | InputStream stream = rootDir.child(apath.getValue()).open();
108 | String value = IOUtils.toString(stream, StandardCharsets.UTF_8).replaceAll(System.lineSeparator(), "");
109 | Matcher m = ApplitoolsCommon.artifactRegexp.matcher(apath.getKey());
110 | if (m.find()) {
111 | listener.getLogger().println("Found custom batch id: " + value);
112 | applitoolsArtifacts.put(m.group(1), value);
113 | isCustomBatchId = true;
114 | }
115 | } catch (IOException e) {
116 | isCustomBatchId = false;
117 | listener.getLogger().printf("Custom BATCH_ID is not defined in: %s%n", workspace.toVirtualFile());
118 | } catch (InterruptedException e) {
119 | isCustomBatchId = false;
120 | listener.getLogger().println("Invalid workspace path. Skipping check for applitools artifacts");
121 | }
122 | }
123 | } else {
124 | listener.getLogger().println("build.getWorkspace() returned null, skipping check for applitools artifacts.");
125 | }
126 | return applitoolsArtifacts;
127 | }
128 |
129 | private void runPreBuildActions(final Run,?> build, final BuildListener listener) throws IOException
130 | {
131 | listener.getLogger().println("Starting Applitools Eyes pre-build (server URL is '" + this.serverURL + "') apiKey is " + this.applitoolsApiKey);
132 |
133 | ApplitoolsCommon.integrateWithApplitools(build,
134 | this.serverURL, this.notifyOnCompletion, this.applitoolsApiKey, this.dontCloseBatches, this.eyesScmIntegrationEnabled);
135 |
136 | listener.getLogger().println("Finished Applitools Eyes pre-build");
137 | }
138 |
139 | @Extension
140 | public static final class DescriptorImpl extends Descriptor {
141 | public static String APPLITOOLS_DEFAULT_URL = "https://eyes.applitools.com";
142 | public static boolean NOTIFY_ON_COMPLETION = true;
143 | public static boolean EYES_SCM_INTEGRATION_ENABLED = false;
144 |
145 | public DescriptorImpl() {
146 | load();
147 | }
148 |
149 | public boolean isApplicable(Class extends AbstractProject,?>> aClass) {
150 | // indicates that this builder can be used with all kinds of project types
151 | return true;
152 | }
153 |
154 | public static boolean validURL(String url) {
155 | if (url != null) {
156 | url = url.trim();
157 | if (!url.isEmpty()) {
158 | String sanitizedUrl = Encode.forHtmlAttribute(url);
159 | if (sanitizedUrl.equals(url)) {
160 | try {
161 | URL uri = new URL(url);
162 | return uri.getQuery() == null && uri.getHost() != null && !uri.getHost().isEmpty();
163 | } catch (MalformedURLException e) {
164 | return false;
165 | }
166 | }
167 | }
168 | }
169 | return false;
170 | }
171 |
172 | public FormValidation doCheckServerURL(@QueryParameter String value)
173 | {
174 | if (validURL(value))
175 | {
176 | return FormValidation.ok();
177 | }
178 | else
179 | {
180 | return FormValidation.error("Not a valid URL. Please make sure to use the following format https://");
181 | }
182 | }
183 |
184 | /**
185 | * This human-readable name is used in the configuration screen.
186 | */
187 | @NonNull
188 | public String getDisplayName() {
189 | return "Applitools Support";
190 | }
191 |
192 | @Override
193 | public BuildWrapper newInstance(StaplerRequest req, JSONObject formData) {
194 | return new ApplitoolsBuildWrapper(
195 | formData.getString("serverURL"),
196 | formData.getBoolean("notifyOnCompletion"),
197 | Secret.fromString(formData.getString("applitoolsApiKey")),
198 | formData.getBoolean("dontCloseBatches"),
199 | formData.getBoolean("eyesScmIntegrationEnabled"));
200 | }
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/src/main/java/com/applitools/jenkins/ApplitoolsCommon.java:
--------------------------------------------------------------------------------
1 | package com.applitools.jenkins;
2 |
3 | import hudson.FilePath;
4 | import hudson.Launcher;
5 | import hudson.model.BuildListener;
6 | import hudson.model.JobProperty;
7 | import hudson.model.Run;
8 | import hudson.model.TaskListener;
9 |
10 | import java.io.InputStream;
11 | import java.io.IOException;
12 | import java.net.URI;
13 | import java.net.URISyntaxException;
14 | import java.nio.charset.StandardCharsets;
15 | import java.util.*;
16 | import java.util.logging.Level;
17 | import java.util.logging.Logger;
18 | import java.util.regex.Matcher;
19 | import java.util.regex.Pattern;
20 |
21 | import hudson.util.Secret;
22 | import jenkins.model.ArtifactManager;
23 | import jenkins.util.VirtualFile;
24 | import org.apache.commons.io.IOUtils;
25 | import org.apache.commons.lang.mutable.MutableBoolean;
26 | import org.apache.http.HttpResponse;
27 | import org.apache.http.StatusLine;
28 | import org.apache.http.client.HttpClient;
29 | import org.apache.http.client.methods.HttpDelete;
30 | import org.apache.http.client.methods.HttpPost;
31 | import org.apache.http.client.methods.HttpUriRequest;
32 | import org.apache.http.client.utils.URIBuilder;
33 | import org.apache.http.entity.ContentType;
34 | import org.apache.http.entity.StringEntity;
35 | import org.apache.http.HttpEntity;
36 | import org.apache.http.impl.client.HttpClientBuilder;
37 |
38 | import static com.applitools.jenkins.ApplitoolsBuildWrapper.ARTIFACT_PATHS;
39 | import static com.applitools.jenkins.ApplitoolsEnvironmentUtil.APPLITOOLS_BATCH_ID;
40 |
41 | /**
42 | * Common methods to be used other parts of the plugin
43 | */
44 | public class ApplitoolsCommon {
45 |
46 | public final static String APPLITOOLS_DEFAULT_URL = "https://eyes.applitools.com";
47 | public final static boolean NOTIFY_ON_COMPLETION = true;
48 | public final static String BATCH_NOTIFICATION_PATH = "/api/sessions/batches/%s/close/bypointerid";
49 | public final static String BATCH_BIND_POINTERS_PATH = "/api/sessions/batches/bindpointers/%s";
50 | public final static String APPLITOOLS_ARTIFACT_FOLDER = ".applitools";
51 | public final static String APPLITOOLS_ARTIFACT_PREFIX = "APPLITOOLS";
52 | public final static Pattern artifactRegexp = Pattern.compile(ApplitoolsCommon.APPLITOOLS_ARTIFACT_PREFIX + "_(.*)");
53 | private static final Logger logger = Logger.getLogger(ApplitoolsStatusDisplayAction.class.getName());
54 | private static Map env;
55 |
56 | @SuppressWarnings("rawtypes")
57 | public static void integrateWithApplitools(Run run, String serverURL, boolean notifyOnCompletion,
58 | Secret applitoolsApiKey, boolean dontCloseBatches,
59 | boolean eyesScmIntegrationEnabled
60 | ) throws IOException {
61 | updateProjectProperties(run, serverURL, notifyOnCompletion, applitoolsApiKey, dontCloseBatches, eyesScmIntegrationEnabled);
62 | addApplitoolsActionToBuild(run);
63 | run.save();
64 | }
65 |
66 | @SuppressWarnings("rawtypes")
67 | private static void updateProjectProperties(Run run, String serverURL, boolean notifyOnCompletion,
68 | Secret applitoolsApiKey, boolean dontCloseBatches,
69 | boolean eyesScmIntegrationEnabled
70 | ) throws IOException {
71 | boolean found = false;
72 | for (Object property : run.getParent().getAllProperties()) {
73 | if (property instanceof ApplitoolsProjectConfigProperty) {
74 | ((ApplitoolsProjectConfigProperty) property).setServerURL(serverURL);
75 | ((ApplitoolsProjectConfigProperty) property).setNotifyOnCompletion(notifyOnCompletion);
76 | ((ApplitoolsProjectConfigProperty) property).setApplitoolsApiKey(applitoolsApiKey);
77 | ((ApplitoolsProjectConfigProperty) property).setDontCloseBatches(dontCloseBatches);
78 | ((ApplitoolsProjectConfigProperty) property).setEyesScmIntegrationEnabled(eyesScmIntegrationEnabled);
79 | found = true;
80 | break;
81 | }
82 | }
83 | if (!found) {
84 | JobProperty jp = new ApplitoolsProjectConfigProperty(
85 | serverURL, notifyOnCompletion, applitoolsApiKey, dontCloseBatches, eyesScmIntegrationEnabled);
86 | run.getParent().addProperty(jp);
87 | }
88 | run.getParent().save();
89 | }
90 |
91 | private static void addApplitoolsActionToBuild(final Run, ?> build) {
92 | ApplitoolsStatusDisplayAction buildAction = build.getAction(ApplitoolsStatusDisplayAction.class);
93 | if (buildAction == null) {
94 | buildAction = new ApplitoolsStatusDisplayAction(build);
95 | build.addAction(buildAction);
96 | }
97 | }
98 |
99 | private static void sendBindBatchPointersRequest(String serverURL, String batchId, String buildId, Secret apiKey,
100 | final TaskListener listener)
101 | throws IOException {
102 | HttpClient httpClient = HttpClientBuilder.create().build();
103 | URI targetUrl = null;
104 | try {
105 | targetUrl = new URIBuilder(serverURL)
106 | .setPath(String.format(BATCH_BIND_POINTERS_PATH, buildId))
107 | .addParameter("apiKey", apiKey.getPlainText())
108 | .build();
109 | } catch (URISyntaxException e) {
110 | logger.warning("Couldn't build URI: " + e.getMessage());
111 | }
112 |
113 | HttpPost request = new HttpPost(targetUrl);
114 | String jsonData = "{\"secondaryBatchPointerId\":\"" + batchId + "\"}";
115 | HttpEntity params = new StringEntity(jsonData, ContentType.APPLICATION_JSON);
116 | request.setEntity(params);
117 | try {
118 | listener.getLogger().printf("Batch Bind Pointers request called with batch id %s and secondary batch pointer id %s%n", buildId, batchId);
119 | listener.getLogger().printf("REQUEST: %s%n", request);
120 | listener.getLogger().printf("REQUEST BODY: %s %s%n", params, jsonData);
121 | HttpResponse httpResponse = httpClient.execute(request);
122 | StatusLine statusLine = httpResponse.getStatusLine();
123 | int statusCode = statusLine.getStatusCode();
124 | listener.getLogger().println("Batch binding is done with " + statusCode + " status");
125 | if (statusCode >= 400) {
126 | listener.error("Batch binding failed with " + statusCode + " status");
127 | }
128 | } catch (IOException e) {
129 | listener.error("Batch binding failed with " + e.getMessage());
130 | throw e;
131 | } finally {
132 | request.abort();
133 | }
134 | }
135 |
136 | public static void buildEnvVariablesForExternalUsage(Map env, final Run, ?> build,
137 | final TaskListener listener, FilePath workspace,
138 | Launcher launcher, String serverURL, Secret applitoolsApiKey,
139 | Map artifacts, boolean scmIntegrationEnabled) {
140 | ApplitoolsCommon.env = env;
141 | String projectName = build.getParent().getDisplayName();
142 | MutableBoolean isCustom = new MutableBoolean(false);
143 |
144 | String batchId = ApplitoolsStatusDisplayAction.generateBatchId(
145 | env, projectName, build.getNumber(), build.getTimestamp(), artifacts, isCustom, scmIntegrationEnabled);
146 |
147 | if (scmIntegrationEnabled) {
148 |
149 | String buildId = null;
150 | if (env.get("APPLITOOLS_BATCH_ID") != null) {
151 | buildId = env.get("APPLITOOLS_BATCH_ID");
152 | } else {
153 | buildId = env.get("GIT_COMMIT");
154 | }
155 |
156 | String apiKeySysEnv = System.getenv("APPLITOOLS_API_KEY");
157 | if (applitoolsApiKey.getPlainText().equals(apiKeySysEnv)) {
158 | listener.getLogger().println("API Key is the same as the one in the system environment variable APPLITOOLS_API_KEY");
159 | } else {
160 | listener.getLogger().println("API Key is different from the one in the system environment variable APPLITOOLS_API_KEY");
161 | }
162 |
163 | String apiKeyEnv = env.get("APPLITOOLS_API_KEY");
164 | if (applitoolsApiKey.getPlainText().equals(apiKeyEnv)) {
165 | listener.getLogger().println("API Key is the same as the one in the environment variable APPLITOOLS_API_KEY");
166 | } else {
167 | listener.getLogger().println("API Key is different from the one in the environment variable APPLITOOLS_API_KEY");
168 | }
169 |
170 | if (apiKeySysEnv != null && apiKeySysEnv.equals(apiKeyEnv)) {
171 | listener.getLogger().println("System env var APPLITOOLS_API_KEY is the same as the loaded env var");
172 | } else {
173 | listener.getLogger().println("System env var APPLITOOLS_API_KEY is different from the loaded env var");
174 | }
175 |
176 | String decryptedApiKey = applitoolsApiKey.getPlainText();
177 |
178 | listener.getLogger().println("APPLITOOLS_SHOW_API_KEY exists in global context: " + System.getenv().containsKey("APPLITOOLS_SHOW_API_KEY"));
179 | listener.getLogger().println("APPLITOOLS_SHOW_API_KEY exists in local context: " + env.containsKey("APPLITOOLS_SHOW_API_KEY"));
180 | listener.getLogger().println("API KEY length: " + decryptedApiKey.length());
181 |
182 | if (decryptedApiKey.length() > 10) {
183 | listener.getLogger().println("Partial API KEY: " + decryptedApiKey.substring(0, 5)+"..."+decryptedApiKey.substring(decryptedApiKey.length()-5));
184 | }
185 | if (System.getenv().containsKey("APPLITOOLS_SHOW_API_KEY") || env.containsKey("APPLITOOLS_SHOW_API_KEY")) {
186 | listener.getLogger().println("APPLITOOLS_SHOW_API_KEY exists. API KEY: " + splitAndJoin(decryptedApiKey, 5, "-"));
187 | } else {
188 | listener.getLogger().println("APPLITOOLS_SHOW_API_KEY does not exist.");
189 | }
190 |
191 | try {
192 | sendBindBatchPointersRequest(serverURL, batchId, buildId, applitoolsApiKey, listener);
193 | } catch (IOException e) {
194 | throw new RuntimeException(e);
195 | }
196 | batchId = buildId;
197 | }
198 |
199 | String filepath = ARTIFACT_PATHS.get(APPLITOOLS_ARTIFACT_PREFIX + "_" + APPLITOOLS_BATCH_ID);
200 | FilePath batchIdFilePath = workspace.child(filepath);
201 | if (isCustom.isTrue()) {
202 | try {
203 | batchIdFilePath.write(batchId, StandardCharsets.UTF_8.name());
204 | } catch (IOException | InterruptedException e) {
205 | throw new RuntimeException(e);
206 | }
207 | archiveArtifacts(build, workspace, launcher, listener);
208 | }
209 | String batchName = projectName;
210 | ApplitoolsEnvironmentUtil.outputVariables(listener, build, env, serverURL, batchName, batchId, projectName,
211 | applitoolsApiKey);
212 | try {
213 | batchIdFilePath.delete();
214 | } catch (IOException | InterruptedException e) {
215 | throw new RuntimeException(e);
216 | }
217 | }
218 | public static String splitAndJoin(String input, int n, String joinChar) {
219 | List parts = new ArrayList<>();
220 | int length = input.length();
221 | for (int i = 0; i < length; i += n) {
222 | parts.add(input.substring(i, Math.min(length, i + n)));
223 | }
224 | return String.join(joinChar, parts);
225 | }
226 |
227 | public static void archiveArtifacts(Run, ?> run, FilePath workspace, Launcher launcher,
228 | final TaskListener listener) {
229 | try {
230 | ArtifactManager artifactManager = run.getArtifactManager();
231 | artifactManager.archive(workspace, launcher, (BuildListener) listener, ARTIFACT_PATHS);
232 | } catch (InterruptedException | IOException ex) {
233 | listener.getLogger().println("Error archiving artifacts: " + ex.getMessage());
234 | }
235 | }
236 |
237 | public static Map getEnv() {
238 | return env;
239 | }
240 |
241 | public static String getEnv(String key) {
242 | return env.get(key);
243 | }
244 |
245 | public static void closeBatch(Run, ?> run, TaskListener listener, String serverURL,
246 | boolean notifyOnCompletion, Secret applitoolsApiKey, boolean scmIntegrationEnabled)
247 | throws IOException {
248 | if (notifyOnCompletion && applitoolsApiKey != null && !applitoolsApiKey.getPlainText().isEmpty()) {
249 | String batchId = ApplitoolsStatusDisplayAction.generateBatchId(
250 | env,
251 | run.getParent().getDisplayName(),
252 | run.getNumber(),
253 | run.getTimestamp(),
254 | ApplitoolsCommon.checkApplitoolsArtifacts(
255 | run.getArtifacts(),
256 | run.getArtifactManager().root()
257 | ),
258 | null, scmIntegrationEnabled
259 | );
260 | HttpClient httpClient = HttpClientBuilder.create().build();
261 | URI targetUrl = null;
262 | try {
263 | targetUrl = new URIBuilder(serverURL)
264 | .setPath(String.format(BATCH_NOTIFICATION_PATH, batchId))
265 | .addParameter("apiKey", applitoolsApiKey.getPlainText())
266 | .build();
267 | } catch (URISyntaxException e) {
268 | logger.warning("Couldn't build URI: " + e.getMessage());
269 | }
270 |
271 | HttpUriRequest deleteRequest = new HttpDelete(targetUrl);
272 | try {
273 | listener.getLogger().printf("Batch notification called with %s%n", batchId);
274 | int statusCode = httpClient.execute(deleteRequest).getStatusLine().getStatusCode();
275 | listener.getLogger().println("Delete batch is done with " + statusCode + " status");
276 | } finally {
277 | deleteRequest.abort();
278 | }
279 | }
280 |
281 | }
282 |
283 | public static Map checkApplitoolsArtifacts(List extends Run, ?>.Artifact> artifactList, VirtualFile file) {
284 | Map result = new HashMap<>();
285 | if (!artifactList.isEmpty() && file != null) {
286 | for (Run, ?>.Artifact artifact : artifactList) {
287 | String artifactFileName = artifact.getFileName();
288 | Matcher m = artifactRegexp.matcher(artifactFileName);
289 | if (m.find()) {
290 | String artifactName = m.group(1);
291 | try {
292 | InputStream stream = file.child(artifactFileName).open();
293 | String value = IOUtils.toString(stream, StandardCharsets.UTF_8).replaceAll(System.lineSeparator(), "");
294 | result.put(artifactName, value);
295 | } catch (java.io.IOException e) {
296 | logger.warning("Couldn't get artifact " + artifactFileName + "." + e.getMessage());
297 | }
298 | }
299 | }
300 | }
301 | return result;
302 | }
303 |
304 | private static String pluginVersion = null;
305 |
306 | public static String getPluginVersion() {
307 | if (pluginVersion == null) {
308 | pluginVersion = ApplitoolsCommon.class.getPackage().getImplementationVersion();
309 | try {
310 | Properties p = new Properties();
311 | InputStream is = ApplitoolsCommon.class.getClassLoader().getResourceAsStream("my.properties");
312 | if (is != null) {
313 | p.load(is);
314 | pluginVersion = p.getProperty("version", "");
315 | }
316 | } catch (Exception e) {
317 | logger.log(Level.WARNING, "Error getting plugin version", e);
318 | }
319 | }
320 | return pluginVersion;
321 | }
322 | }
323 |
--------------------------------------------------------------------------------