├── .gitignore
├── src
├── main
│ ├── resources
│ │ └── org
│ │ │ └── jenkinsci
│ │ │ └── plugins
│ │ │ └── sma
│ │ │ ├── SMABuilder
│ │ │ ├── help-password.html
│ │ │ ├── help-proxyPass.html
│ │ │ ├── help-proxyPort.html
│ │ │ ├── help-username.html
│ │ │ ├── help-proxyUser.html
│ │ │ ├── help-serverType.html
│ │ │ ├── help-validateEnabled.html
│ │ │ ├── help-proxyServer.html
│ │ │ ├── help-runTestRegex.html
│ │ │ ├── help-maxPoll.html
│ │ │ ├── help-pollWait.html
│ │ │ ├── help-securityToken.html
│ │ │ ├── help-prTargetBranch.html
│ │ │ ├── help-testLevel.html
│ │ │ ├── config.jelly
│ │ │ └── global.jelly
│ │ │ ├── index.jelly
│ │ │ └── salesforceMetadata.xml
│ └── java
│ │ └── org
│ │ └── jenkinsci
│ │ └── plugins
│ │ └── sma
│ │ ├── SMAMetadataTypes.java
│ │ ├── SMAPackage.java
│ │ ├── SMAUtility.java
│ │ ├── SMAMetadata.java
│ │ ├── SMARunner.java
│ │ ├── SMABuilder.java
│ │ ├── SMAGit.java
│ │ └── SMAConnection.java
└── test
│ ├── resources
│ ├── testAddsMods.txt
│ ├── testDeletes.txt
│ └── testPackage.xml
│ └── java
│ └── org
│ └── jenkinsci
│ └── plugins
│ └── sma
│ ├── SMAMetadataTest.java
│ ├── SMAPackageTest.java
│ ├── SMAUtilityTest.java
│ ├── SMAConnectionTest.java
│ └── SMAGitTest.java
├── README.md
├── LICENSE.txt
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea/
3 | work/
4 | target/
5 | wiki/
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-password.html:
--------------------------------------------------------------------------------
1 |
2 | The password for the user you provided above.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-proxyPass.html:
--------------------------------------------------------------------------------
1 |
2 | The password for the proxy user entered above.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-proxyPort.html:
--------------------------------------------------------------------------------
1 |
2 | The port needed for proxy server defined above.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-username.html:
--------------------------------------------------------------------------------
1 |
2 | The Salesforce user that will perform the deployment.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-proxyUser.html:
--------------------------------------------------------------------------------
1 |
2 | If your proxy requires authentication, enter the username here.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-serverType.html:
--------------------------------------------------------------------------------
1 |
2 | The instance type of Salesforce that you are deploying against.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-validateEnabled.html:
--------------------------------------------------------------------------------
1 |
2 | Indicate whether you would like to perform a test deployment only.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-proxyServer.html:
--------------------------------------------------------------------------------
1 |
2 | If you're behind a proxy, use this field to configure your proxy server.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-runTestRegex.html:
--------------------------------------------------------------------------------
1 |
2 | Enter a valid Java regular expression to enable SMA to find your unmanaged package unit tests.
3 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-maxPoll.html:
--------------------------------------------------------------------------------
1 |
2 | The number of times to poll the server for the results of the deploy
3 | request. Note that deployment may succeed even if you stop waiting.
4 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-pollWait.html:
--------------------------------------------------------------------------------
1 |
2 | The number of milliseconds to wait when polling for results of
3 | the deployment. Note that deployment may succeed even if you stop waiting.
4 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-securityToken.html:
--------------------------------------------------------------------------------
1 |
2 | The security token for the user.
3 |
4 | You can leave this field blank if you do not use security tokens in your organization.
5 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-prTargetBranch.html:
--------------------------------------------------------------------------------
1 |
2 | If this job is configured to deploy or validate pull requests, specify what the target branch will be (e.g. "develop").
3 |
4 | SMA will use this information to generate the appropriate delta as pull requests must be handled differently.
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Salesforce Migration Assistant
2 | This Jenkins plugin automatically deploys metadata changes to a Salesforce organization based on differences between two commits in Git.
3 |
4 | See the [wiki](https://github.com/jenkinsci/salesforce-migration-assistant-plugin/wiki) for more information.
5 |
6 | ### Licensing
7 | This software is licensed under the terms you may find in the file name "LICENSE.txt" in this directory.
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/index.jelly:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | This Jenkins plugin generates automatically deploys metadata changes to a Salesforce organization based on differences between two commits in Git. Instead of deploying a repository's contents every time a change is made, the plugin can determine what metadata needs to be deployed and deleted and coordinate only those changes. This has the benefit of drastically reducing deployment times and uncoupling the reliance on the package manifest file (package.xml).
7 |
8 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-testLevel.html:
--------------------------------------------------------------------------------
1 |
2 | The test level you wish to perform this deployment at.
3 |
4 | - None: No tests will be run during this deployment.
5 | - Relevant: the RunSpecifiedTests level. Jenkins will use the information provided in the Run Test Regex field under the System Configuration section to determine which set of tests need to be run for this particular deployment. A warning will be generated in Jenkins log if no relevant test is found for a particular ApexClass.
6 | - Local: All unit tests are run, excluding those found in managed packages.
7 | - All: All unit tests are run, including those found in managed packages.
8 |
--------------------------------------------------------------------------------
/src/test/resources/testAddsMods.txt:
--------------------------------------------------------------------------------
1 | src/classes/Test.cls
2 | src/classes/Test.cls-meta.xml
3 | src/components/Test.component
4 | src/components/Test.component-meta.xml
5 | src/pages/Test.page
6 | src/pages/Test.page-meta.xml
7 | src/triggers/Test.trigger
8 | src/triggers/Test.trigger-meta.xml
9 | src/staticresources/Test.resource
10 | src/staticresources/Test.resource-meta.xml
11 | src/classes/Test2.cls
12 | src/classes/Test2.cls-meta.xml
13 | src/components/Test2.component
14 | src/components/Test2.component-meta.xml
15 | src/pages/Test2.page
16 | src/pages/Test2.page-meta.xml
17 | src/triggers/Test2.trigger
18 | src/triggers/Test2.trigger-meta.xml
19 | src/staticresources/Test2.resource
20 | src/staticresources/Test2.resource-meta.xml
21 |
--------------------------------------------------------------------------------
/src/test/resources/testDeletes.txt:
--------------------------------------------------------------------------------
1 | src/classes/Test.cls
2 | src/classes/Test.cls-meta.xml
3 | src/components/Test.component
4 | src/components/Test.component-meta.xml
5 | src/pages/Test.page
6 | src/pages/Test.page-meta.xml
7 | src/triggers/Test.trigger
8 | src/triggers/Test.trigger-meta.xml
9 | src/staticresources/Test.resource
10 | src/staticresources/Test.resource-meta.xml
11 | src/classes/Test2.cls
12 | src/classes/Test2.cls-meta.xml
13 | src/components/Test2.component
14 | src/components/Test2.component-meta.xml
15 | src/pages/Test2.page
16 | src/pages/Test2.page-meta.xml
17 | src/triggers/Test2.trigger
18 | src/triggers/Test2.trigger-meta.xml
19 | src/staticresources/Test2.resource
20 | src/staticresources/Test2.resource-meta.xml
21 | src/appMenus/Test.appMenu
22 | src/samlssoconfigs/Test.samlssoconfig
23 | src/workflows/Test.workflow
--------------------------------------------------------------------------------
/src/test/resources/testPackage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ApexClass
5 | Test
6 | Test2
7 |
8 |
9 | ApexComponent
10 | Test
11 | Test2
12 |
13 |
14 | ApexPage
15 | Test
16 | Test2
17 |
18 |
19 | ApexTrigger
20 | Test
21 | Test2
22 |
23 |
24 | StaticResource
25 | Test
26 | Test2
27 |
28 | 32.0
29 |
30 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/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 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Anthony Sanchez
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.
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/global.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/sma/SMAMetadataTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import static org.junit.Assert.assertEquals;
7 | import static org.junit.Assert.assertTrue;
8 |
9 | public class SMAMetadataTest {
10 |
11 | private SMAMetadata metadataObject;
12 | String extension = ".ext";
13 | String container = "container";
14 | String member = "Member";
15 | String metadataType = "MDType";
16 | String path = "src/container/";
17 | boolean destructible = true;
18 | boolean valid = true;
19 | boolean metaxml = true;
20 | String body = "";
21 |
22 | @Before
23 | public void setUp() throws Exception {
24 | metadataObject = new SMAMetadata(extension, container, member, metadataType,
25 | path, destructible, valid, metaxml, body.getBytes());
26 | }
27 |
28 | @Test
29 | public void testGetExtension() throws Exception {
30 | assertEquals(extension, metadataObject.getExtension());
31 | }
32 |
33 | @Test
34 | public void testGetContainer() throws Exception {
35 | assertEquals(container, metadataObject.getContainer());
36 | }
37 |
38 | @Test
39 | public void testGetPath() throws Exception {
40 | assertEquals(path, metadataObject.getPath());
41 | }
42 |
43 | @Test
44 | public void testGetMember() throws Exception {
45 | assertEquals(member, metadataObject.getMember());
46 | }
47 |
48 | @Test
49 | public void testGetMetadataType() throws Exception {
50 | assertEquals(metadataType, metadataObject.getMetadataType());
51 | }
52 |
53 | @Test
54 | public void testIsDestructible() throws Exception {
55 | if (destructible){
56 | assertTrue(metadataObject.isDestructible());
57 | }else{
58 | assertTrue(!metadataObject.isDestructible());
59 | }
60 | }
61 |
62 | @Test
63 | public void testHasMetaxml() throws Exception {
64 | if (metaxml){
65 | assertTrue(metadataObject.hasMetaxml());
66 | }else{
67 | assertTrue(!metadataObject.hasMetaxml());
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/sma/SMAPackageTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import org.apache.commons.io.FileUtils;
4 | import org.junit.After;
5 | import org.junit.Assert;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 |
9 | import java.io.File;
10 | import java.util.Arrays;
11 | import java.util.List;
12 |
13 | public class SMAPackageTest
14 | {
15 | private String jenkinsHome;
16 | private String runTestRegex;
17 | private String pollWait;
18 | private String maxPoll;
19 | private List contents;
20 | private File testWorkspace;
21 | private String testWorkspacePath;
22 |
23 | @Before
24 | public void setUp() throws Exception
25 | {
26 | //Setup the fake workspace and package manifest
27 | testWorkspace = File.createTempFile("TestWorkspace", "");
28 | testWorkspace.delete();
29 | testWorkspace.mkdirs();
30 | testWorkspacePath = testWorkspace.getPath();
31 |
32 | String emptyString = "";
33 |
34 | SMAMetadata apex = SMAMetadataTypes.createMetadataObject("/src/classes/TestApex.cls", emptyString.getBytes());
35 | SMAMetadata trigger = SMAMetadataTypes.createMetadataObject("/src/triggers/TestTrigger.trigger", emptyString.getBytes());
36 | SMAMetadata page = SMAMetadataTypes.createMetadataObject("/src/pages/TestPage.page", emptyString.getBytes());
37 | SMAMetadata workflow = SMAMetadataTypes.createMetadataObject("/src/workflows/TestWorkflow.workflow", emptyString.getBytes());
38 |
39 | contents = Arrays.asList(apex, trigger, page, workflow);
40 | }
41 |
42 | @Test
43 | public void testPackage() throws Exception
44 | {
45 | SMAPackage testPackage = new SMAPackage(contents, false);
46 |
47 | System.out.println(testPackage.getPackage());
48 |
49 | Assert.assertTrue(testPackage.getPackage().contains("Workflow"));
50 | }
51 |
52 | @Test
53 | public void testDestructiveChange() throws Exception
54 | {
55 | SMAPackage testPackage = new SMAPackage(contents, true);
56 |
57 | System.out.println(testPackage.getPackage());
58 |
59 | Assert.assertTrue(!testPackage.getPackage().contains("Workflow"));
60 | }
61 |
62 | @After
63 | public void tearDown() throws Exception
64 | {
65 | FileUtils.deleteDirectory(testWorkspace);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/sma/SMAUtilityTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import com.google.common.io.Files;
4 | import org.junit.After;
5 | import org.junit.Assert;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 |
9 | import java.io.ByteArrayOutputStream;
10 | import java.io.File;
11 | import java.util.ArrayList;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | public class SMAUtilityTest
17 | {
18 | File localPath;
19 | Map metadata;
20 | SMAPackage packageManifest;
21 | SMAPackage destructiveChange;
22 |
23 | @Before
24 | public void setUp() throws Exception
25 | {
26 | localPath = Files.createTempDir();
27 |
28 | String[] strings = {"TestContents", "TestXML"};
29 |
30 | metadata = new HashMap();
31 | metadata.put("classes/TestApex.cls", strings[0].getBytes());
32 | metadata.put("classes/TestApex.cls-meta.xml", strings[1].getBytes());
33 | metadata.put("pages/TestPages.page", strings[0].getBytes());
34 | metadata.put("pages/TestPages.page-meta.xml", strings[1].getBytes());
35 | metadata.put("triggers/TestTrigger.trigger", strings[0].getBytes());
36 | metadata.put("triggers/TestTrigger.trigger-meta.xml", strings[1].getBytes());
37 |
38 | List metadataList = new ArrayList();
39 |
40 | for (String s : metadata.keySet())
41 | {
42 | if (!s.contains("-meta.xml"))
43 | {
44 | metadataList.add(SMAMetadataTypes.createMetadataObject(s, metadata.get(s)));
45 | }
46 | }
47 |
48 | packageManifest = new SMAPackage(metadataList, false);
49 | destructiveChange = new SMAPackage(metadataList, true);
50 | }
51 |
52 | @After
53 | public void tearDown() throws Exception
54 | {
55 | localPath.delete();
56 | }
57 |
58 | @Test
59 | public void testZipPackage() throws Exception
60 | {
61 | ByteArrayOutputStream testStream = new ByteArrayOutputStream();
62 |
63 | testStream = SMAUtility.zipPackage(metadata, packageManifest, destructiveChange);
64 |
65 | System.out.println(testStream);
66 |
67 | Assert.assertNotNull(testStream);
68 | }
69 |
70 | @Test
71 | public void testWriteZip() throws Exception
72 | {
73 | ByteArrayOutputStream testStream = new ByteArrayOutputStream();
74 |
75 | testStream = SMAUtility.zipPackage(metadata, packageManifest, destructiveChange);
76 |
77 | SMAUtility.writeZip(testStream, localPath.getPath() + "/streamToZip.zip");
78 |
79 | File zipFile = new File(localPath.getPath() + "/streamToZip.zip");
80 |
81 | Assert.assertTrue(zipFile.exists());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAMetadataTypes.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import org.apache.commons.io.FilenameUtils;
4 | import org.w3c.dom.Document;
5 | import org.w3c.dom.Element;
6 | import org.w3c.dom.Node;
7 | import org.w3c.dom.NodeList;
8 |
9 | import javax.xml.parsers.DocumentBuilder;
10 | import javax.xml.parsers.DocumentBuilderFactory;
11 | import java.io.File;
12 | import java.util.logging.Logger;
13 |
14 | /**
15 | * Class for the salesforceMetadata.xml document that contains Salesforce Metadata API information.
16 | *
17 | */
18 | public class SMAMetadataTypes
19 | {
20 | private static final Logger LOG = Logger.getLogger(SMAMetadataTypes.class.getName());
21 |
22 | private static final ClassLoader loader = SMAMetadataTypes.class.getClassLoader();
23 | private static String pathToResource = loader.getResource("org/jenkinsci/plugins/sma/salesforceMetadata.xml").toString();
24 | private static Document doc;
25 | private static Boolean docAlive = false;
26 |
27 | /**
28 | * Initializes the Document representation of the salesforceMetadata.xml file
29 | *
30 | * @throws Exception
31 | */
32 | private static void initDocument() throws Exception
33 | {
34 | DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
35 | DocumentBuilder dbBuilder = dbFactory.newDocumentBuilder();
36 | doc = dbBuilder.parse(pathToResource);
37 | docAlive = true;
38 | }
39 |
40 | /**
41 | * Returns the Salesforce Metadata API Version
42 | *
43 | * @return version
44 | */
45 | public static String getAPIVersion() throws Exception
46 | {
47 | if (!docAlive)
48 | {
49 | initDocument();
50 | }
51 |
52 | String version = null;
53 |
54 | doc.getDocumentElement().normalize();
55 |
56 | NodeList verNodes = doc.getElementsByTagName("version");
57 |
58 | //There should only be one node in this list
59 | for (int iterator = 0; iterator < verNodes.getLength(); iterator++)
60 | {
61 | Node curNode = verNodes.item(iterator);
62 | Element verElement = (Element) curNode;
63 | //If for some reason there is more than one, get the first one
64 | version = verElement.getAttribute("API");
65 | }
66 |
67 | return version;
68 | }
69 |
70 | /**
71 | * Creates an SMAMetadata object from a string representation of a file's path and filename.
72 | *
73 | * @param filepath
74 | * @return SMAMetadata
75 | * @throws Exception
76 | */
77 | public static SMAMetadata createMetadataObject(String filepath, byte[] data) throws Exception
78 | {
79 | if (!docAlive)
80 | {
81 | initDocument();
82 | }
83 |
84 | String container = "empty";
85 | String metadataType = "Invalid";
86 | boolean destructible = false;
87 | boolean valid = false;
88 | boolean metaxml = false;
89 |
90 | File file = new File(filepath);
91 | String object = file.getName();
92 | String member = FilenameUtils.removeExtension(object);
93 | String extension = FilenameUtils.getExtension(filepath);
94 | String path = FilenameUtils.getFullPath(filepath);
95 |
96 | //Normalize the salesforceMetadata.xml configuration file
97 | doc.getDocumentElement().normalize();
98 |
99 | NodeList extNodes = doc.getElementsByTagName("extension");
100 |
101 | //Get the node with the corresponding extension and get the relevant information for
102 | //creating the SMAMetadata object
103 | for (int iterator = 0; iterator < extNodes.getLength(); iterator++)
104 | {
105 | Node curNode = extNodes.item(iterator);
106 |
107 | Element element = (Element) curNode;
108 | if (element.getAttribute("name").equals(extension))
109 | {
110 | container = element.getElementsByTagName("container").item(0).getTextContent();
111 | metadataType = element.getElementsByTagName("metadata").item(0).getTextContent();
112 | destructible = Boolean.parseBoolean(element.getElementsByTagName("destructible").item(0).
113 | getTextContent());
114 | valid = true;
115 | metaxml = Boolean.parseBoolean(element.getElementsByTagName("metaxml").item(0).getTextContent());
116 | break;
117 | }
118 | }
119 |
120 | return new SMAMetadata(extension, container, member, metadataType,
121 | path, destructible, valid, metaxml, data);
122 | }
123 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 | org.jenkins-ci.plugins
5 | plugin
6 | 1.642.4
7 |
8 |
9 | salesforce-migration-assistant-plugin
10 | Salesforce Migration Assistant
11 | 2.2.2-SNAPSHOT
12 | hpi
13 |
14 |
15 |
16 | aesanch2
17 | Anthony Sanchez
18 | senninha09@gmail.com
19 |
20 |
21 |
22 |
23 | scm:git:ssh://github.com/jenkinsci/salesforce-migration-assistant-plugin.git
24 | scm:git:ssh://git@github.com/jenkinsci/salesforce-migration-assistant-plugin.git
25 | https://github.com/jenkinsci/salesforce-migration-assistant-plugin
26 | HEAD
27 |
28 |
29 |
30 | GitHub
31 | https://github.com/jenkinsci/salesforce-migration-assistant-plugin/issues
32 |
33 |
34 | https://wiki.jenkins-ci.org/display/JENKINS/Salesforce+Migration+Assistant+Plugin
35 |
36 |
37 |
38 | MIT License
39 | http://opensource.org/licenses/MIT
40 |
41 |
42 |
43 |
44 |
45 | repo.jenkins-ci.org
46 | http://repo.jenkins-ci.org/public/
47 |
48 |
49 | jgit-repository
50 | Eclipse JGit Repository
51 | https://repo.eclipse.org/content/groups/releases/
52 |
53 |
54 |
55 |
56 |
57 | repo.jenkins-ci.org
58 | http://repo.jenkins-ci.org/public/
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | org.jenkins-ci.tools
67 | maven-hpi-plugin
68 |
69 | true
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | org.jenkins-ci.plugins
79 | git
80 | 2.3.1
81 |
82 |
83 | org.eclipse.jgit
84 | org.eclipse.jgit
85 | 3.5.1.201410131835-r
86 |
87 |
88 | junit
89 | junit
90 | 4.11
91 | test
92 |
93 |
94 | com.sun.xml.ws
95 | jaxws-rt
96 | 2.2
97 |
98 |
99 | com.sun.xml.ws
100 | policy
101 | 2.2.1
102 |
103 |
104 | org.json
105 | org.json
106 | chargebee-1.0
107 |
108 |
109 | com.force.api
110 | force-wsc
111 | 35.2.0
112 |
113 |
114 | com.force.api
115 | force-partner-api
116 | 35.0.0
117 |
118 |
119 | com.force.api
120 | force-metadata-api
121 | 35.0.0
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAPackage.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import com.sforce.soap.metadata.Package;
4 | import com.sforce.soap.metadata.PackageTypeMembers;
5 | import com.sforce.ws.bind.TypeMapper;
6 | import com.sforce.ws.parser.XmlOutputStream;
7 |
8 | import javax.xml.namespace.QName;
9 | import java.io.ByteArrayOutputStream;
10 | import java.util.ArrayList;
11 | import java.util.HashMap;
12 | import java.util.List;
13 | import java.util.Map;
14 |
15 | /**
16 | * Wrapper for com.sforce.soap.metadata.Package.
17 | *
18 | */
19 | public class SMAPackage
20 | {
21 | private List contents;
22 | private boolean destructiveChange;
23 | private Package packageManifest;
24 |
25 | /**
26 | * Constructor for SMAPackage
27 | * Takes the SMAMetdata contents that are to be represented by the manifest file and generates a Package for deployment
28 | *
29 | * @param contents
30 | * @param destructiveChange
31 | */
32 | public SMAPackage(List contents,
33 | boolean destructiveChange) throws Exception
34 | {
35 | this.contents = contents;
36 | this.destructiveChange = destructiveChange;
37 |
38 | packageManifest = new Package();
39 | packageManifest.setVersion(SMAMetadataTypes.getAPIVersion());
40 | packageManifest.setTypes((PackageTypeMembers[]) determinePackageTypes().toArray(new PackageTypeMembers[0]));
41 | }
42 |
43 | public List getContents()
44 | {
45 | return contents;
46 | }
47 |
48 | /**
49 | * Returns the name of the manifest file for this SMAPackage
50 | * @return
51 | */
52 | public String getName()
53 | {
54 | String name;
55 |
56 | if (destructiveChange)
57 | {
58 | name = "destructiveChanges.xml";
59 | } else
60 | {
61 | name = "package.xml";
62 | }
63 |
64 | return name;
65 | }
66 |
67 | /**
68 | * Transforms the Package into a ByteArray
69 | *
70 | * @return String(packageStream.toByteArray())
71 | * @throws Exception
72 | */
73 | public String getPackage() throws Exception
74 | {
75 | TypeMapper typeMapper = new TypeMapper();
76 | ByteArrayOutputStream packageStream = new ByteArrayOutputStream();
77 | QName packageQName = new QName("http://soap.sforce.com/2006/04/metadata", "Package");
78 | XmlOutputStream xmlOutputStream = new XmlOutputStream(packageStream, true);
79 | xmlOutputStream.setPrefix("", "http://soap.sforce.com/2006/04/metadata");
80 | xmlOutputStream.setPrefix("xsi", "http://www.w3.org/2001/XMLSchema-instance");
81 | packageManifest.write(packageQName, xmlOutputStream, typeMapper);
82 | xmlOutputStream.close();
83 |
84 | return new String(packageStream.toByteArray());
85 | }
86 |
87 | /**
88 | * Returns whether or not this package contains Apex components
89 | *
90 | * @return containsApex
91 | */
92 | public boolean containsApex() {
93 | boolean containsApex = false;
94 |
95 | for (SMAMetadata thisMetadata : contents) {
96 | if (thisMetadata.getMetadataType().equals("ApexClass")
97 | || thisMetadata.getMetadataType().equals("ApexTrigger"))
98 | {
99 | containsApex = true;
100 | break;
101 | }
102 | }
103 |
104 | return containsApex;
105 | }
106 |
107 | /**
108 | * Sorts the metadata into types and members for the manifest
109 | *
110 | * @return
111 | */
112 | private List determinePackageTypes()
113 | {
114 | List types = new ArrayList();
115 | Map> contentsByType = new HashMap>();
116 |
117 | // Sort the metadata objects by metadata type
118 | for (SMAMetadata mdObject : contents)
119 | {
120 | if (destructiveChange && !mdObject.isDestructible())
121 | {
122 | // Don't include non destructible metadata in destructiveChanges
123 | continue;
124 | }
125 | else if (contentsByType.containsKey(mdObject.getMetadataType()))
126 | {
127 | contentsByType.get(mdObject.getMetadataType()).add(mdObject.getMember());
128 | } else
129 | {
130 | List memberList = new ArrayList();
131 | memberList.add(mdObject.getMember());
132 | contentsByType.put(mdObject.getMetadataType(), memberList);
133 | }
134 | }
135 |
136 | // Put the members into list of PackageTypeMembers
137 | for (String metadataType : contentsByType.keySet())
138 | {
139 | PackageTypeMembers members = new PackageTypeMembers();
140 | members.setName(metadataType);
141 | members.setMembers((String[]) contentsByType.get(metadataType).toArray(new String[0]));
142 | types.add(members);
143 | }
144 |
145 | return types;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAUtility.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import hudson.model.BuildListener;
4 |
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.File;
7 | import java.io.FileOutputStream;
8 | import java.util.Collections;
9 | import java.util.List;
10 | import java.util.Map;
11 | import java.util.logging.Logger;
12 | import java.util.regex.Matcher;
13 | import java.util.regex.Pattern;
14 | import java.util.zip.ZipEntry;
15 | import java.util.zip.ZipOutputStream;
16 |
17 | /**
18 | * Utility class for performing a variety of tasks in SMA.
19 | *
20 | * @author aesanch2
21 | */
22 | public class SMAUtility
23 | {
24 |
25 | private static final Logger LOG = Logger.getLogger(SMAUtility.class.getName());
26 |
27 |
28 | /**
29 | * Creates a zipped byte array of the deployment or rollback package
30 | *
31 | * @param deployData
32 | * @param packageManifest
33 | * @param destructiveChange
34 | * @return
35 | * @throws Exception
36 | */
37 | public static ByteArrayOutputStream zipPackage(Map deployData,
38 | SMAPackage packageManifest,
39 | SMAPackage destructiveChange) throws Exception
40 | {
41 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
42 | ZipOutputStream zos = new ZipOutputStream(baos);
43 |
44 | ZipEntry manifestFile = new ZipEntry(packageManifest.getName());
45 | zos.putNextEntry(manifestFile);
46 | zos.write(packageManifest.getPackage().getBytes());
47 | zos.closeEntry();
48 |
49 | ZipEntry destructiveChanges = new ZipEntry(destructiveChange.getName());
50 | zos.putNextEntry(destructiveChanges);
51 | zos.write(destructiveChange.getPackage().getBytes());
52 | zos.closeEntry();
53 |
54 | for (String metadata : deployData.keySet())
55 | {
56 | ZipEntry metadataEntry = new ZipEntry(metadata);
57 | zos.putNextEntry(metadataEntry);
58 | zos.write(deployData.get(metadata));
59 | zos.closeEntry();
60 | }
61 |
62 | zos.close();
63 |
64 | return baos;
65 | }
66 |
67 | /**
68 | * Helper to write the zip to a file location
69 | *
70 | * @param zipBytes
71 | * @param location
72 | * @throws Exception
73 | */
74 | public static void writeZip(ByteArrayOutputStream zipBytes, String location) throws Exception
75 | {
76 | FileOutputStream fos = new FileOutputStream(location);
77 | fos.write(zipBytes.toByteArray());
78 | fos.close();
79 | }
80 |
81 | /**
82 | * Helper to find an existing package.xml file in the provided repository
83 | *
84 | * @param directory
85 | * @return
86 | */
87 | public static String findPackage(File directory)
88 | {
89 | String location = "";
90 |
91 | File[] filesInDir = directory.listFiles();
92 |
93 | for (File f : filesInDir)
94 | {
95 | if (f.isDirectory())
96 | {
97 | location = findPackage(f);
98 | }
99 | else if (f.getName().equals("package.xml"))
100 | {
101 | location = f.getPath();
102 | }
103 |
104 | if (!location.isEmpty())
105 | {
106 | break;
107 | }
108 | }
109 |
110 | return location;
111 | }
112 |
113 | /**
114 | * We don't actually want to load the -meta.xml files, so we use this to get the real item and handle the -metas
115 | * elsewhere since both components are required for deployment.
116 | *
117 | * @param repoItem
118 | * @return
119 | */
120 | public static String checkMeta(String repoItem)
121 | {
122 | String actualItem = repoItem;
123 |
124 | if (repoItem.contains("-meta"))
125 | {
126 | actualItem = repoItem.substring(0, repoItem.length() - 9);
127 | }
128 |
129 | return actualItem;
130 | }
131 |
132 | /**
133 | * Prints a set of metadata names to the Jenkins console
134 | *
135 | * @param listener
136 | * @param metadataList
137 | */
138 | public static void printMetadataToConsole(BuildListener listener, List metadataList)
139 | {
140 | // Sorts by extension, then by member name
141 | Collections.sort(metadataList);
142 |
143 | for (SMAMetadata metadata : metadataList)
144 | {
145 | listener.getLogger().println("- " + metadata.getFullName());
146 | }
147 |
148 | listener.getLogger().println();
149 | }
150 |
151 | /**
152 | * Searches for a possible unit tests in the repository for a given set of metadata
153 | *
154 | * @param allMetadata
155 | * @param testClassRegex
156 | * @return
157 | */
158 | public static String searchForTestClass(List allMetadata, String testClassRegex)
159 | {
160 | String match = "noneFound";
161 | Matcher matcher;
162 |
163 | for (String s : allMetadata)
164 | {
165 | matcher = Pattern.compile(testClassRegex).matcher(s);
166 | if (matcher.find())
167 | {
168 | match = s;
169 | break;
170 | }
171 | }
172 |
173 | return match;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAMetadata.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.logging.Logger;
6 |
7 | /**
8 | * Creates an object representation of a Salesforce Metadata file.
9 | *
10 | */
11 | public class SMAMetadata implements Comparable
12 | {
13 | private static final Logger LOG = Logger.getLogger(SMAMetadata.class.getName());
14 |
15 | private String extension;
16 | private String container;
17 | private String member;
18 | private String metadataType;
19 | private String path;
20 | private boolean destructible;
21 | private boolean valid;
22 | private boolean metaxml;
23 | private byte[] body;
24 |
25 | /**
26 | * Constructor for SMAMetadata object
27 | *
28 | * @param extension
29 | * @param container
30 | * @param member
31 | * @param metadataType
32 | * @param path
33 | * @param destructible
34 | * @param valid
35 | * @param metaxml
36 | * @param body
37 | */
38 | public SMAMetadata(String extension,
39 | String container,
40 | String member,
41 | String metadataType,
42 | String path,
43 | boolean destructible,
44 | boolean valid,
45 | boolean metaxml,
46 | byte[] body)
47 | {
48 | this.extension = extension;
49 | this.container = container;
50 | this.member = member;
51 | this.metadataType = metadataType;
52 | this.path = path;
53 | this.destructible = destructible;
54 | this.valid = valid;
55 | this.metaxml = metaxml;
56 | this.body = body;
57 | }
58 |
59 | /**
60 | * Returns the extension for this metadata file.
61 | *
62 | * @return A string representation of the extension type of the metadata file.
63 | */
64 | public String getExtension()
65 | {
66 | return extension;
67 | }
68 |
69 | /**
70 | * Returns the parent container for this metadata file.
71 | *
72 | * @return A string representation of the parent container for this metadata file.
73 | */
74 | public String getContainer()
75 | {
76 | return container;
77 | }
78 |
79 | /**
80 | * Returns the path of the metadata file.
81 | *
82 | * @return A string representation of the path of the metadata file.
83 | */
84 | public String getPath()
85 | {
86 | return path;
87 | }
88 |
89 | /**
90 | * Returns the name of the metadata file.
91 | *
92 | * @return A string representation of the metadata file's name.
93 | */
94 | public String getMember()
95 | {
96 | return member;
97 | }
98 |
99 | /**
100 | * Returns the metadata type of this metadata file.
101 | *
102 | * @return A string representation of the metadata file's type.
103 | */
104 | public String getMetadataType()
105 | {
106 | return metadataType;
107 | }
108 |
109 | /**
110 | * Returns whether or not this metadata object can be deleted using the Salesforce API.
111 | *
112 | * @return A boolean that describes whether or not this metadata object can be deleted using the Salesforce API.
113 | */
114 | public boolean isDestructible()
115 | {
116 | return destructible;
117 | }
118 |
119 | /**
120 | * Returns whether or not this metadata object is a valid member of the Salesforce API.
121 | *
122 | * @return A boolean that describes wheter or not this metadata object is a valid member of the Salesforce API.
123 | */
124 | public boolean isValid()
125 | {
126 | return valid;
127 | }
128 |
129 | /**
130 | * Returns whether or not this metadata object has an accompanying -meta.xml file.
131 | *
132 | * @return
133 | */
134 | public boolean hasMetaxml()
135 | {
136 | return metaxml;
137 | }
138 |
139 | /**
140 | * A toString() like method that returns a concatenation of the name and extension of the metadata object.
141 | *
142 | * @return A string of the name and extension of the metadata object.
143 | */
144 | public String getFullName()
145 | {
146 | return member + "." + extension;
147 | }
148 |
149 | public String toString()
150 | {
151 | return container + "/" + getFullName();
152 | }
153 |
154 | /**
155 | * The blob data in String format of the metadata's content.
156 | *
157 | * @return
158 | */
159 | public byte[] getBody() { return body; }
160 |
161 | /**
162 | * For sorting metadata by extension followed by member
163 | *
164 | * @param comparison
165 | * @return
166 | */
167 | @Override
168 | public int compareTo(SMAMetadata comparison)
169 | {
170 | int extCompare = this.extension.compareToIgnoreCase(comparison.extension);
171 | return extCompare == 0 ? this.member.compareToIgnoreCase(comparison.member) : extCompare;
172 | }
173 |
174 | /**
175 | * Get all apex files in the provided list
176 | *
177 | * @param contents
178 | * @return
179 | */
180 | public static List getApexClasses(List contents)
181 | {
182 | List allApex = new ArrayList();
183 |
184 | for (SMAMetadata md : contents)
185 | {
186 | if (md.getMetadataType().equals("ApexClass"))
187 | {
188 | allApex.add(md.getMember());
189 | }
190 | }
191 |
192 | return allApex;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/sma/SMAConnectionTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import com.google.common.io.Files;
4 | import com.sforce.soap.metadata.CodeCoverageResult;
5 | import com.sforce.soap.metadata.DeployDetails;
6 | import com.sforce.soap.metadata.RunTestsResult;
7 | import com.sforce.soap.metadata.TestLevel;
8 | import org.junit.After;
9 | import org.junit.Assert;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 |
13 | import java.io.ByteArrayOutputStream;
14 | import java.io.File;
15 | import java.util.ArrayList;
16 | import java.util.HashMap;
17 | import java.util.List;
18 | import java.util.Map;
19 |
20 | public class SMAConnectionTest
21 | {
22 | //TODO: need to mock this configuration
23 | SMAConnection sfConnection;
24 | String username = "";
25 | String password = "";
26 | String securityToken = "";
27 | String server = "";
28 | String proxyServer = "";
29 | String proxyUser = "";
30 | String proxyPass = "";
31 | Integer proxyPort;
32 | File localPath;
33 | ByteArrayOutputStream boas;
34 |
35 | @Before
36 | public void setUp() throws Exception
37 | {
38 | localPath = Files.createTempDir();
39 |
40 | String apex = "public class TestApex {}";
41 | StringBuilder sb = new StringBuilder();
42 | sb.append("");
43 | sb.append("34.0");
44 | sb.append("Active");
45 | sb.append("");
46 |
47 | Map metadata = new HashMap();
48 | metadata.put("classes/TestApex.cls", apex.getBytes());
49 | metadata.put("classes/TestApex.cls-meta.xml", sb.toString().getBytes());
50 |
51 | List metadataList = new ArrayList();
52 |
53 | for (String s : metadata.keySet())
54 | {
55 | if (!s.contains("-meta.xml"))
56 | {
57 | metadataList.add(SMAMetadataTypes.createMetadataObject(s, metadata.get(s)));
58 | }
59 | }
60 |
61 | SMAPackage packageManifest = new SMAPackage(metadataList, false);
62 | SMAPackage destructiveChange = new SMAPackage(new ArrayList(), true);
63 |
64 | boas = SMAUtility.zipPackage(metadata, packageManifest, destructiveChange);
65 |
66 | SMAUtility.writeZip(boas, localPath.getPath() + "/testDeploy.zip");
67 |
68 |
69 | }
70 |
71 | @Test
72 | public void testDeployment() throws Exception
73 | {
74 | boolean success;
75 |
76 | if (username.isEmpty() || password.isEmpty() || securityToken.isEmpty())
77 | {
78 | success = true;
79 | }
80 | else
81 | {
82 | sfConnection = new SMAConnection(
83 | username,
84 | password,
85 | securityToken,
86 | server,
87 | "30000",
88 | "200",
89 | proxyServer,
90 | proxyUser,
91 | proxyPass,
92 | proxyPort
93 | );
94 |
95 | success = sfConnection.deployToServer(
96 | boas,
97 | TestLevel.NoTestRun,
98 | null,
99 | true,
100 | true
101 | );
102 | }
103 |
104 | Assert.assertTrue(success);
105 | }
106 |
107 | @Test
108 | public void testGetCodeCoverageResults() throws Exception
109 | {
110 | if (username.isEmpty() || password.isEmpty() || securityToken.isEmpty())
111 | {
112 | Assert.assertTrue(true);
113 | }
114 | else
115 | {
116 | sfConnection = new SMAConnection(
117 | username,
118 | password,
119 | securityToken,
120 | server,
121 | "30000",
122 | "200",
123 | proxyServer,
124 | proxyUser,
125 | proxyPass,
126 | proxyPort
127 | );
128 |
129 | StringBuilder sb = new StringBuilder();
130 | sb.append(
131 | "[SMA] Code Coverage Results\n" +
132 | "1st Test.cls -- 80%\n" +
133 | "2nd Test.cls -- 80%\n" +
134 | "\n" +
135 | "Total code coverage for this deployment -- 80%" +
136 | "\n"
137 | );
138 | String expectedCoverage = sb.toString();
139 | DeployDetails details = new DeployDetails();
140 | RunTestsResult testsResult = new RunTestsResult();
141 |
142 | CodeCoverageResult testCCR1 = new CodeCoverageResult();
143 | testCCR1.setName("1st Test");
144 | testCCR1.setNumLocations(10);
145 | testCCR1.setNumLocationsNotCovered(2);
146 | CodeCoverageResult testCCR2 = new CodeCoverageResult();
147 | testCCR2.setName("2nd Test");
148 | testCCR2.setNumLocations(20);
149 | testCCR2.setNumLocationsNotCovered(4);
150 |
151 | CodeCoverageResult[] expectedCCR = new CodeCoverageResult[]{testCCR1, testCCR2};
152 |
153 | testsResult.setCodeCoverage(expectedCCR);
154 | details.setRunTestResult(testsResult);
155 |
156 | sfConnection.setDeployDetails(details);
157 |
158 | String actualCoverage = sfConnection.getCodeCoverage();
159 | Assert.assertEquals(expectedCoverage, actualCoverage);
160 | }
161 | }
162 |
163 | @After
164 | public void tearDown() throws Exception
165 | {
166 | localPath.delete();
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMARunner.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import hudson.EnvVars;
4 |
5 | import java.io.File;
6 | import java.util.ArrayList;
7 | import java.util.HashMap;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.logging.Logger;
11 |
12 | /**
13 | * Class that contains all of the configuration pertinent to the running job
14 | *
15 | */
16 | public class SMARunner
17 | {
18 | private static final Logger LOG = Logger.getLogger(SMARunner.class.getName());
19 |
20 | private Boolean deployAll = false;
21 | private String currentCommit;
22 | private String previousCommit;
23 | private String rollbackLocation;
24 | private SMAGit git;
25 | private List deployMetadata = new ArrayList();
26 | private List deleteMetadata = new ArrayList();
27 | private List rollbackMetadata = new ArrayList();
28 | private List rollbackAdditions = new ArrayList();
29 |
30 | /**
31 | * Wrapper for coordinating the configuration of the running job
32 | *
33 | * @param jobVariables
34 | * @throws Exception
35 | */
36 | public SMARunner(EnvVars jobVariables, String prTargetBranch) throws Exception
37 | {
38 | // Get envvars to initialize SMAGit
39 | Boolean shaOverride = false;
40 | currentCommit = jobVariables.get("GIT_COMMIT");
41 | Boolean ghprbJob = (jobVariables.get("ghprbSourceBranch") != null);
42 | String buildCause = jobVariables.get("BUILD_CAUSE");
43 | String pathToWorkspace = jobVariables.get("WORKSPACE");
44 | String jobName = jobVariables.get("JOB_NAME");
45 | String buildNumber = jobVariables.get("BUILD_NUMBER");
46 |
47 | if (jobVariables.containsKey("GIT_PREVIOUS_SUCCESSFUL_COMMIT"))
48 | {
49 | previousCommit = jobVariables.get("GIT_PREVIOUS_SUCCESSFUL_COMMIT");
50 | }
51 | else
52 | {
53 | deployAll = true;
54 | }
55 |
56 | if (jobVariables.containsKey("SMA_DEPLOY_ALL_METADATA"))
57 | {
58 | deployAll = Boolean.valueOf(jobVariables.get("SMA_DEPLOY_ALL_METADATA"));
59 | }
60 |
61 | if (jobVariables.containsKey("SMA_PREVIOUS_COMMIT_OVERRIDE"))
62 | {
63 | if (!jobVariables.get("SMA_PREVIOUS_COMMIT_OVERRIDE").isEmpty())
64 | {
65 | shaOverride = true;
66 | previousCommit = jobVariables.get("SMA_PREVIOUS_COMMIT_OVERRIDE");
67 | }
68 | }
69 |
70 | // Configure using pull request logic
71 | if (ghprbJob || buildCause.equals("GITHUBPULLREQUESTCAUSE") && !shaOverride)
72 | {
73 | deployAll = false;
74 | git = new SMAGit(pathToWorkspace, currentCommit, prTargetBranch, SMAGit.Mode.PRB);
75 | }
76 | // Configure for all the metadata
77 | else if (deployAll)
78 | {
79 | git = new SMAGit(pathToWorkspace, currentCommit, null, SMAGit.Mode.INI);
80 | }
81 | // Configure using the previous successful commit for this job
82 | else
83 | {
84 | git = new SMAGit(pathToWorkspace, currentCommit, previousCommit, SMAGit.Mode.STD);
85 | }
86 |
87 | rollbackLocation = pathToWorkspace + "/sma/rollback" + jobName + buildNumber + ".zip";
88 | }
89 |
90 | /**
91 | * Returns whether the current job is set to deploy all the metadata in the repository
92 | *
93 | * @return deployAll
94 | */
95 | public Boolean getDeployAll()
96 | {
97 | return deployAll;
98 | }
99 |
100 | /**
101 | * Returns the SMAMetadata that is going to be deployed in this job
102 | *
103 | * @return
104 | * @throws Exception
105 | */
106 | public List getPackageMembers() throws Exception
107 | {
108 | if (deployAll)
109 | {
110 | deployMetadata = buildMetadataList(git.getAllMetadata());
111 | }
112 | else if (deployMetadata.isEmpty())
113 | {
114 | Map positiveChanges = git.getNewMetadata();
115 | positiveChanges.putAll(git.getUpdatedMetadata());
116 |
117 | deployMetadata = buildMetadataList(positiveChanges);
118 | }
119 |
120 | return deployMetadata;
121 | }
122 |
123 | /**
124 | * Returns the SMAMetadata that is going to be deleted in this job
125 | *
126 | * @return deleteMetadata
127 | * @throws Exception
128 | */
129 | public List getDestructionMembers() throws Exception
130 | {
131 | if (deleteMetadata.isEmpty())
132 | {
133 | Map negativeChanges = git.getDeletedMetadata();
134 |
135 | deleteMetadata = buildMetadataList(negativeChanges);
136 | }
137 |
138 | return deleteMetadata;
139 | }
140 |
141 | public List getRollbackMetadata() throws Exception
142 | {
143 | if (deleteMetadata.isEmpty())
144 | {
145 | getDestructionMembers();
146 | }
147 |
148 | rollbackMetadata = new ArrayList();
149 | rollbackMetadata.addAll(deleteMetadata);
150 | rollbackMetadata.addAll(buildMetadataList(git.getOriginalMetadata()));
151 |
152 | return rollbackMetadata;
153 | }
154 |
155 | public List getRollbackAdditions() throws Exception
156 | {
157 | rollbackAdditions = new ArrayList();
158 | rollbackAdditions.addAll(buildMetadataList(git.getNewMetadata()));
159 |
160 | return rollbackAdditions;
161 | }
162 |
163 | /**
164 | * Returns a map with the file name mapped to the byte contents of the metadata
165 | *
166 | * @return deploymentData
167 | * @throws Exception
168 | */
169 | public Map getDeploymentData() throws Exception
170 | {
171 | if (deployMetadata.isEmpty())
172 | {
173 | getPackageMembers();
174 | }
175 |
176 | return getData(deployMetadata, currentCommit);
177 | }
178 |
179 | public Map getRollbackData() throws Exception
180 | {
181 | if (rollbackMetadata.isEmpty())
182 | {
183 | getRollbackMetadata();
184 | }
185 |
186 | return getData(rollbackMetadata, previousCommit);
187 | }
188 |
189 | /**
190 | * Helper method to find the byte[] contents of given metadata
191 | *
192 | * @param metadatas
193 | * @param commit
194 | * @return
195 | * @throws Exception
196 | */
197 | private Map getData(List metadatas, String commit) throws Exception
198 | {
199 | Map data = new HashMap();
200 |
201 | for (SMAMetadata metadata : metadatas)
202 | {
203 | data.put(metadata.toString(), metadata.getBody());
204 |
205 | if (metadata.hasMetaxml())
206 | {
207 | String metaXml = metadata.toString() + "-meta.xml";
208 | String pathToXml = metadata.getPath() + metadata.getFullName() + "-meta.xml";
209 | data.put(metaXml, git.getBlob(pathToXml, commit));
210 | }
211 | }
212 |
213 | return data;
214 | }
215 |
216 | /**
217 | * Constructs a list of SMAMetadata objects from a Map of files and their byte[] contents
218 | *
219 | * @param repoItems
220 | * @return
221 | * @throws Exception
222 | */
223 | private List buildMetadataList(Map repoItems) throws Exception
224 | {
225 | List thisMetadata = new ArrayList();
226 |
227 | for (String repoItem : repoItems.keySet())
228 | {
229 | SMAMetadata mdObject = SMAMetadataTypes.createMetadataObject(repoItem, repoItems.get(repoItem));
230 | if (mdObject.isValid())
231 | {
232 | thisMetadata.add(mdObject);
233 | }
234 | }
235 |
236 | return thisMetadata;
237 | }
238 |
239 | /**
240 | * Returns a String array of all the unit tests that should be run in this job
241 | *
242 | * @param testRegex
243 | * @return
244 | * @throws Exception
245 | */
246 | public String[] getSpecifiedTests(String testRegex) throws Exception
247 | {
248 | List specifiedTestsList = new ArrayList();
249 | List deployApex = SMAMetadata.getApexClasses(deployMetadata);
250 | List allApex = SMAMetadata.getApexClasses(
251 | buildMetadataList(
252 | git.getAllMetadata()
253 | )
254 | );
255 |
256 | for (String md : deployApex)
257 | {
258 | if (md.matches(testRegex))
259 | {
260 | if (!specifiedTestsList.contains(md))
261 | {
262 | specifiedTestsList.add(md);
263 | }
264 | }
265 | else
266 | {
267 | // Go find the test class we need to add
268 | String testClass = SMAUtility.searchForTestClass(allApex, md + testRegex);
269 |
270 | if (testClass.equals("noneFound"))
271 | {
272 | testClass = SMAUtility.searchForTestClass(allApex, testRegex + md);
273 |
274 | if (testClass.equals("noneFound"))
275 | {
276 | LOG.warning("No test class for " + md + " found");
277 | continue;
278 | }
279 | }
280 |
281 | if (!specifiedTestsList.contains(testClass))
282 | {
283 | specifiedTestsList.add(testClass);
284 | }
285 |
286 | }
287 | }
288 |
289 | String[] specifiedTests = new String[specifiedTestsList.size()];
290 |
291 | specifiedTestsList.toArray(specifiedTests);
292 |
293 | return specifiedTests;
294 | }
295 |
296 | public String getRollbackLocation()
297 | {
298 | File rollbackLocationFile = new File(rollbackLocation);
299 |
300 | if (!rollbackLocationFile.getParentFile().exists())
301 | {
302 | rollbackLocationFile.getParentFile().mkdirs();
303 | }
304 |
305 | return rollbackLocation;
306 | }
307 | }
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/sma/SMAGitTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import org.apache.commons.io.FileUtils;
4 | import org.eclipse.jgit.api.CreateBranchCommand;
5 | import org.eclipse.jgit.api.Git;
6 | import org.eclipse.jgit.lib.Repository;
7 | import org.eclipse.jgit.revwalk.RevCommit;
8 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
9 | import org.junit.After;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 |
13 | import java.io.File;
14 | import java.io.PrintWriter;
15 | import java.util.ArrayList;
16 | import java.util.HashMap;
17 | import java.util.List;
18 | import java.util.Map;
19 |
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.assertTrue;
22 |
23 | public class SMAGitTest
24 | {
25 |
26 | private Repository repository;
27 | private SMAGit git;
28 | private File addition, addMeta;
29 | private File modification, modifyMeta;
30 | private File deletion, deleteMeta;
31 | private File localPath;
32 | private String oldSha, newSha, gitDir;
33 | private final String contents = "\n";
34 |
35 | /**
36 | * Before to setup the test.
37 | *
38 | * @throws Exception
39 | */
40 | @Before
41 | public void setUp() throws Exception
42 | {
43 | //Setup the fake repository
44 | localPath = File.createTempFile("TestGitRepository", "");
45 | localPath.delete();
46 | repository = FileRepositoryBuilder.create(new File(localPath, ".git"));
47 | repository.create();
48 |
49 | File classesPath = new File(repository.getDirectory().getParent() + "/src/classes");
50 | classesPath.mkdirs();
51 | File pagesPath = new File(repository.getDirectory().getParent() + "/src/pages");
52 | pagesPath.mkdirs();
53 | File triggersPath = new File(repository.getDirectory().getParent() + "/src/triggers");
54 | triggersPath.mkdirs();
55 |
56 |
57 | //Add the first collection of files
58 | deletion = createFile("deleteThis.cls", classesPath);
59 | deleteMeta = createFile("deleteThis.cls-meta.xml", classesPath);
60 | modification = createFile("modifyThis.page", pagesPath);
61 | modifyMeta = createFile("modifyThis.page-meta.xml", pagesPath);
62 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls").call();
63 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls-meta.xml").call();
64 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page").call();
65 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page-meta.xml").call();
66 |
67 | //Create the first commit
68 | RevCommit firstCommit = new Git(repository).commit().setMessage("Add deleteThis and modifyThis").call();
69 | oldSha = firstCommit.getName();
70 |
71 |
72 | //Delete the deletion file, modify the modification file, and add the addition file
73 | new Git(repository).rm().addFilepattern("src/classes/deleteThis.cls").call();
74 | new Git(repository).rm().addFilepattern("src/classes/deleteThis.cls-meta.xml").call();
75 | modification.setExecutable(true);
76 | addition = createFile("addThis.trigger", triggersPath);
77 | addMeta = createFile("addThis.trigger-meta.xml", triggersPath);
78 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page").call();
79 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page-meta.xml").call();
80 | new Git(repository).add().addFilepattern("src/triggers/addThis.trigger").call();
81 | new Git(repository).add().addFilepattern("src/triggers/addThis.trigger-meta.xml").call();
82 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls").call();
83 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls-meta.xml").call();
84 |
85 | //Create the second commit
86 | RevCommit secondCommit = new Git(repository).commit().setMessage("Remove deleteThis. Modify " +
87 | "modifyThis. Add addThis.").call();
88 | newSha = secondCommit.getName();
89 |
90 | gitDir = localPath.getPath();
91 | }
92 |
93 | /**
94 | * After to tear down the test.
95 | *
96 | * @throws Exception
97 | */
98 | @After
99 | public void tearDown() throws Exception
100 | {
101 | repository.close();
102 | FileUtils.deleteDirectory(localPath);
103 | }
104 |
105 | /**
106 | * Test the diff capability of the wrapper.
107 | *
108 | * @throws Exception
109 | */
110 | @Test
111 | public void testDiff() throws Exception
112 | {
113 | Map expectedDelete = new HashMap();
114 | expectedDelete.put("src/classes/deleteThis.cls", contents.getBytes());
115 |
116 | Map expectedMods = new HashMap();
117 | expectedMods.put("src/pages/modifyThis.page", contents.getBytes());
118 |
119 | Map expectedAdds = new HashMap();
120 | expectedAdds.put("src/triggers/addThis.trigger", contents.getBytes());
121 |
122 | git = new SMAGit(gitDir, newSha, oldSha, SMAGit.Mode.STD);
123 |
124 | Map deletedContents = git.getDeletedMetadata();
125 | Map modifiedContents = git.getUpdatedMetadata();
126 | Map addedContents = git.getNewMetadata();
127 |
128 | assertEquals(expectedAdds.size(), addedContents.size());
129 | assertEquals(expectedMods.size(), modifiedContents.size());
130 | assertEquals(expectedDelete.size(), deletedContents.size());
131 | }
132 |
133 | /**
134 | * Test the overloaded constructors.
135 | *
136 | * @throws Exception
137 | */
138 | @Test
139 | public void testInitialCommit() throws Exception
140 | {
141 | Map expectedContents = new HashMap();
142 | expectedContents.put("src/pages/modifyThis.page", contents.getBytes());
143 | expectedContents.put("src/pages/modifyThis.page-meta.xml", contents.getBytes());
144 | expectedContents.put("src/triggers/addThis.trigger", contents.getBytes());
145 | expectedContents.put("src/triggers/addThis.trigger-meta.xml", contents.getBytes());
146 |
147 | git = new SMAGit(gitDir, newSha, null, SMAGit.Mode.INI);
148 |
149 | Map allMetadata = git.getAllMetadata();
150 |
151 | assertEquals(expectedContents.size(), allMetadata.size());
152 | }
153 |
154 | /**
155 | * Test the ghprb constructor.
156 | *
157 | * @throws Exception
158 | */
159 | @Test
160 | public void testPullRequest() throws Exception
161 | {
162 | Map expectedContents = new HashMap();
163 | expectedContents.put("src/pages/modifyThis.page", contents.getBytes());
164 | expectedContents.put("src/pages/modifyThis.page-meta.xml", contents.getBytes());
165 | expectedContents.put("src/triggers/addThis.trigger", contents.getBytes());
166 | expectedContents.put("src/triggers/addThis.trigger-meta.xml", contents.getBytes());
167 |
168 | String oldBranch = "refs/remotes/origin/oldBranch";
169 | CreateBranchCommand cbc = new Git(repository).branchCreate();
170 | cbc.setName(oldBranch);
171 | cbc.setStartPoint(oldSha);
172 | cbc.call();
173 |
174 | git = new SMAGit(gitDir, newSha, "oldBranch", SMAGit.Mode.PRB);
175 |
176 | Map allMetadata = git.getAllMetadata();
177 |
178 | assertEquals(expectedContents.size(), allMetadata.size());
179 | }
180 |
181 | /**
182 | * Test the ability to update the package manifest.
183 | *
184 | * @throws Exception
185 | */
186 | @Test
187 | public void testCommitPackageXML() throws Exception
188 | {
189 | Map metadataContents = new HashMap();
190 | List metadata = new ArrayList();
191 |
192 | git = new SMAGit(gitDir, newSha, oldSha, SMAGit.Mode.STD);
193 | metadataContents = git.getUpdatedMetadata();
194 | metadataContents.putAll(git.getNewMetadata());
195 |
196 | for (String s : metadataContents.keySet())
197 | {
198 | metadata.add(SMAMetadataTypes.createMetadataObject(s, metadataContents.get(s)));
199 | }
200 |
201 | SMAPackage manifest = new SMAPackage(metadata, false);
202 |
203 | Boolean createdManifest = git.updatePackageXML(
204 | localPath.getPath(),
205 | "Test Guy",
206 | "testguy@example.net",
207 | manifest
208 | );
209 |
210 | assertTrue(createdManifest);
211 | }
212 |
213 | /**
214 | * Test the ability to update the package manifest.
215 | *
216 | * @throws Exception
217 | */
218 | @Test
219 | public void testCommitExistingPackage() throws Exception
220 | {
221 | File sourceDir = new File(localPath.getPath() + "/src");
222 | File existingPackage = createFile("package.xml", sourceDir);
223 |
224 | new Git(repository).add().addFilepattern("src/package.xml").call();
225 | new Git(repository).commit().setMessage("Add package.xml").call();
226 |
227 | Map metadataContents = new HashMap();
228 | List metadata = new ArrayList();
229 |
230 | git = new SMAGit(gitDir, newSha, oldSha, SMAGit.Mode.STD);
231 | metadataContents = git.getUpdatedMetadata();
232 | metadataContents.putAll(git.getNewMetadata());
233 |
234 | for (String s : metadataContents.keySet())
235 | {
236 | metadata.add(SMAMetadataTypes.createMetadataObject(s, metadataContents.get(s)));
237 | }
238 |
239 | SMAPackage manifest = new SMAPackage(metadata, false);
240 |
241 | Boolean createdManifest = git.updatePackageXML(
242 | localPath.getPath(),
243 | "Test Guy",
244 | "testguy@example.net",
245 | manifest
246 | );
247 |
248 | assertTrue(createdManifest);
249 |
250 | // Also check to make sure we didn't create the default package
251 | File unexpectedPackage = new File(localPath.getPath() + "/unpackaged/package.xml");
252 | assertTrue(!unexpectedPackage.exists());
253 | }
254 |
255 | private File createFile(String name, File path) throws Exception
256 | {
257 | File thisFile;
258 |
259 | thisFile = new File(path, name);
260 | thisFile.createNewFile();
261 |
262 | PrintWriter print = new PrintWriter(thisFile);
263 | print.println(contents);
264 | print.close();
265 |
266 | return thisFile;
267 | }
268 | }
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMABuilder.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import hudson.Extension;
4 | import hudson.Launcher;
5 | import hudson.model.*;
6 | import hudson.tasks.BuildStepDescriptor;
7 | import hudson.tasks.Builder;
8 | import hudson.util.ListBoxModel;
9 | import org.kohsuke.stapler.DataBoundConstructor;
10 | import org.kohsuke.stapler.StaplerRequest;
11 |
12 | import java.io.ByteArrayOutputStream;
13 | import java.io.PrintStream;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | import com.sforce.soap.metadata.TestLevel;
18 | import net.sf.json.JSONObject;
19 |
20 | /**
21 | * @author Anthony Sanchez
22 | */
23 | public class SMABuilder extends Builder
24 | {
25 | private boolean validateEnabled;
26 | private String username;
27 | private String password;
28 | private String securityToken;
29 | private String serverType;
30 | private String testLevel;
31 | private String prTargetBranch;
32 |
33 | @DataBoundConstructor
34 | public SMABuilder(Boolean validateEnabled,
35 | String username,
36 | String password,
37 | String securityToken,
38 | String serverType,
39 | String testLevel,
40 | String prTargetBranch)
41 | {
42 | this.username = username;
43 | this.password = password;
44 | this.securityToken = securityToken;
45 | this.serverType = serverType;
46 | this.validateEnabled = validateEnabled;
47 | this.testLevel = testLevel;
48 | this.prTargetBranch = prTargetBranch;
49 | }
50 |
51 | @Override
52 | public boolean perform(AbstractBuild build,
53 | Launcher launcher,
54 | BuildListener listener)
55 | {
56 | String smaDeployResult = "";
57 | boolean JOB_SUCCESS = false;
58 |
59 | PrintStream writeToConsole = listener.getLogger();
60 | List parameterValues = new ArrayList();
61 |
62 | try
63 | {
64 | // Initialize the runner for this job
65 | SMARunner currentJob = new SMARunner(build.getEnvironment(listener), prTargetBranch);
66 |
67 | // Build the package and destructiveChanges manifests
68 | SMAPackage packageXml = new SMAPackage(currentJob.getPackageMembers(), false);
69 | writeToConsole.println("[SMA] Deploying the following metadata:");
70 | SMAUtility.printMetadataToConsole(listener, currentJob.getPackageMembers());
71 | SMAPackage destructiveChanges;
72 |
73 | if (currentJob.getDeployAll() || currentJob.getDestructionMembers().isEmpty())
74 | {
75 | destructiveChanges = new SMAPackage(new ArrayList(), true);
76 | }
77 | else
78 | {
79 | destructiveChanges = new SMAPackage(currentJob.getDestructionMembers(), true);
80 | writeToConsole.println("[SMA] Deleting the following metadata:");
81 | SMAUtility.printMetadataToConsole(listener, currentJob.getDestructionMembers());
82 | }
83 |
84 | // Build the zipped deployment package
85 | ByteArrayOutputStream deploymentPackage = SMAUtility.zipPackage(
86 | currentJob.getDeploymentData(),
87 | packageXml,
88 | destructiveChanges
89 | );
90 |
91 | // Initialize the connection to Salesforce for this job
92 | SMAConnection sfConnection = new SMAConnection(
93 | getUsername(),
94 | getPassword(),
95 | getSecurityToken(),
96 | getServerType(),
97 | getDescriptor().getPollWait(),
98 | getDescriptor().getMaxPoll(),
99 | getDescriptor().getProxyServer(),
100 | getDescriptor().getProxyUser(),
101 | getDescriptor().getProxyPass(),
102 | getDescriptor().getProxyPort()
103 | );
104 |
105 | // Deploy to the server
106 | String[] specifiedTests = null;
107 | TestLevel testLevel = TestLevel.valueOf(getTestLevel());
108 |
109 | if (testLevel.equals(TestLevel.RunSpecifiedTests))
110 | {
111 | specifiedTests = currentJob.getSpecifiedTests(getDescriptor().getRunTestRegex());
112 | }
113 |
114 | JOB_SUCCESS = sfConnection.deployToServer(
115 | deploymentPackage,
116 | testLevel,
117 | specifiedTests,
118 | getValidateEnabled(),
119 | packageXml.containsApex()
120 | );
121 |
122 | if (JOB_SUCCESS)
123 | {
124 | if (!testLevel.equals(TestLevel.NoTestRun))
125 | {
126 | smaDeployResult = sfConnection.getCodeCoverage();
127 | }
128 |
129 | smaDeployResult = smaDeployResult + "\n[SMA] Deployment Succeeded";
130 |
131 | if (!currentJob.getDeployAll())
132 | {
133 | SMAPackage rollbackPackageXml = new SMAPackage(
134 | currentJob.getRollbackMetadata(),
135 | false
136 | );
137 |
138 | SMAPackage rollbackDestructiveXml = new SMAPackage(
139 | currentJob.getRollbackAdditions(),
140 | true
141 | );
142 |
143 | ByteArrayOutputStream rollbackPackage = SMAUtility.zipPackage(
144 | currentJob.getRollbackData(),
145 | rollbackPackageXml,
146 | rollbackDestructiveXml
147 | );
148 |
149 | SMAUtility.writeZip(rollbackPackage, currentJob.getRollbackLocation());
150 | }
151 | }
152 | else
153 | {
154 | smaDeployResult = sfConnection.getComponentFailures();
155 |
156 | if (!TestLevel.valueOf(getTestLevel()).equals(TestLevel.NoTestRun))
157 | {
158 | smaDeployResult = smaDeployResult + sfConnection.getTestFailures();
159 | smaDeployResult = smaDeployResult + sfConnection.getCodeCoverageWarnings();
160 | }
161 |
162 | smaDeployResult = smaDeployResult + "\n[SMA] Deployment Failed";
163 | }
164 | } catch (Exception e)
165 | {
166 | e.printStackTrace(writeToConsole);
167 | }
168 |
169 | parameterValues.add(new StringParameterValue("smaDeployResult", smaDeployResult));
170 | build.addAction(new ParametersAction(parameterValues));
171 |
172 | writeToConsole.println(smaDeployResult);
173 |
174 | return JOB_SUCCESS;
175 | }
176 |
177 | public boolean getValidateEnabled()
178 | {
179 | return validateEnabled;
180 | }
181 |
182 | public String getUsername()
183 | {
184 | return username;
185 | }
186 |
187 | public String getSecurityToken()
188 | {
189 | return securityToken;
190 | }
191 |
192 | public String getPassword()
193 | {
194 | return password;
195 | }
196 |
197 | public String getServerType()
198 | {
199 | return serverType;
200 | }
201 |
202 | public String getTestLevel()
203 | {
204 | return testLevel;
205 | }
206 |
207 | public String getPrTargetBranch()
208 | {
209 | return prTargetBranch;
210 | }
211 |
212 | @Override
213 | public DescriptorImpl getDescriptor()
214 | {
215 | return (DescriptorImpl) super.getDescriptor();
216 | }
217 |
218 | @Extension
219 | public static final class DescriptorImpl extends BuildStepDescriptor
220 | {
221 |
222 | private String maxPoll = "200";
223 | private String pollWait = "30000";
224 | private String runTestRegex = ".*[T|t]est.*";
225 | private String proxyServer;
226 | private String proxyUser;
227 | private String proxyPass;
228 | private Integer proxyPort;
229 |
230 |
231 | public DescriptorImpl()
232 | {
233 | load();
234 | }
235 |
236 | public boolean isApplicable(Class extends AbstractProject> aClass)
237 | {
238 | // Indicates that this builder can be used with all kinds of project types
239 | return true;
240 | }
241 |
242 | public String getDisplayName()
243 | {
244 | return "Salesforce Migration Assistant";
245 | }
246 |
247 | public String getMaxPoll()
248 | {
249 | return maxPoll;
250 | }
251 |
252 | public String getPollWait()
253 | {
254 | return pollWait;
255 | }
256 |
257 | public String getRunTestRegex()
258 | {
259 | return runTestRegex;
260 | }
261 |
262 | public String getProxyServer() { return proxyServer; }
263 |
264 | public String getProxyUser() { return proxyUser; }
265 |
266 | public String getProxyPass() { return proxyPass; }
267 |
268 | public Integer getProxyPort() { return proxyPort; }
269 |
270 | public ListBoxModel doFillServerTypeItems()
271 | {
272 | return new ListBoxModel(
273 | new ListBoxModel.Option("Production (https://login.salesforce.com)", "https://login.salesforce.com"),
274 | new ListBoxModel.Option("Sandbox (https://test.salesforce.com)", "https://test.salesforce.com")
275 | );
276 | }
277 |
278 | public ListBoxModel doFillTestLevelItems()
279 | {
280 | return new ListBoxModel(
281 | new ListBoxModel.Option("None", "NoTestRun"),
282 | new ListBoxModel.Option("Relevant", "RunSpecifiedTests"),
283 | new ListBoxModel.Option("Local", "RunLocalTests"),
284 | new ListBoxModel.Option("All", "RunAllTestsInOrg")
285 | );
286 | }
287 |
288 | public boolean configure(StaplerRequest request, JSONObject formData) throws FormException
289 | {
290 | maxPoll = formData.getString("maxPoll");
291 | pollWait = formData.getString("pollWait");
292 | proxyServer = formData.getString("proxyServer");
293 | proxyUser = formData.getString("proxyUser");
294 | proxyPass = formData.getString("proxyPass");
295 | proxyPort = formData.optInt("proxyPort");
296 |
297 | save();
298 | return false;
299 | }
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAGit.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import org.eclipse.jgit.api.DiffCommand;
4 | import org.eclipse.jgit.api.Git;
5 | import org.eclipse.jgit.diff.DiffEntry;
6 | import org.eclipse.jgit.lib.ObjectId;
7 | import org.eclipse.jgit.lib.ObjectReader;
8 | import org.eclipse.jgit.lib.Repository;
9 | import org.eclipse.jgit.revwalk.RevCommit;
10 | import org.eclipse.jgit.revwalk.RevTree;
11 | import org.eclipse.jgit.revwalk.RevWalk;
12 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
13 | import org.eclipse.jgit.treewalk.CanonicalTreeParser;
14 | import org.eclipse.jgit.treewalk.TreeWalk;
15 |
16 | import java.io.*;
17 | import java.util.HashMap;
18 | import java.util.List;
19 | import java.util.Map;
20 | import java.util.logging.Logger;
21 |
22 | /**
23 | * Wrapper for git interactions using jGit.
24 | *
25 | */
26 | public class SMAGit
27 | {
28 | public enum Mode { STD, INI, PRB }
29 |
30 | private final String SOURCEDIR = "src/";
31 |
32 | private Git git;
33 | private Repository repository;
34 | private List diffs;
35 | private String prevCommit, curCommit;
36 |
37 | private static final Logger LOG = Logger.getLogger(SMAGit.class.getName());
38 |
39 | /**
40 | * Creates an SMAGit instance
41 | *
42 | * @param pathToWorkspace
43 | * @param curCommit
44 | * @param diffAgainst
45 | * @param smaMode
46 | * @throws Exception
47 | */
48 | public SMAGit(String pathToWorkspace,
49 | String curCommit,
50 | String diffAgainst,
51 | Mode smaMode) throws Exception
52 | {
53 | String pathToRepo = pathToWorkspace + "/.git";
54 | File repoDir = new File(pathToRepo);
55 | FileRepositoryBuilder builder = new FileRepositoryBuilder();
56 | repository = builder.setGitDir(repoDir).readEnvironment().build();
57 | git = new Git(repository);
58 | this.curCommit = curCommit;
59 |
60 | if (smaMode == Mode.PRB)
61 | {
62 | ObjectId branchId = repository.resolve("refs/remotes/origin/" + diffAgainst);
63 | RevCommit targetCommit = new RevWalk(repository).parseCommit(branchId);
64 |
65 | this.prevCommit = targetCommit.getName();
66 | }
67 | else if (smaMode == Mode.STD)
68 | {
69 | this.prevCommit = diffAgainst;
70 | }
71 |
72 | if (smaMode != Mode.INI)
73 | {
74 | getDiffs();
75 | }
76 | }
77 |
78 | /**
79 | * Returns all of the items that were added in the current commit.
80 | *
81 | * @return The ArrayList containing all of the additions in the current commit.
82 | * @throws IOException
83 | */
84 | public Map getNewMetadata() throws Exception
85 | {
86 | Map additions = new HashMap();
87 |
88 | for (DiffEntry diff : diffs)
89 | {
90 | if (diff.getChangeType().toString().equals("ADD"))
91 | {
92 | String item = SMAUtility.checkMeta(diff.getNewPath());
93 | if (!additions.containsKey(item) && item.contains(SOURCEDIR))
94 | {
95 | additions.put(diff.getNewPath(), getBlob(diff.getNewPath(), curCommit));
96 | }
97 | }
98 | }
99 |
100 | return additions;
101 | }
102 |
103 | /**
104 | * Returns all of the items that were deleted in the current commit.
105 | *
106 | * @return The ArrayList containing all of the items that were deleted in the current commit.
107 | */
108 | public Map getDeletedMetadata() throws Exception
109 | {
110 | Map deletions = new HashMap();
111 |
112 | for (DiffEntry diff : diffs)
113 | {
114 | if (diff.getChangeType().toString().equals("DELETE"))
115 | {
116 | String item = SMAUtility.checkMeta(diff.getOldPath());
117 | if (!deletions.containsKey(item) && item.contains(SOURCEDIR))
118 | {
119 | deletions.put(diff.getOldPath(), getBlob(diff.getOldPath(), prevCommit));
120 | }
121 | }
122 | }
123 |
124 | return deletions;
125 | }
126 |
127 | /**
128 | * Returns all of the updated changes in the current commit.
129 | *
130 | * @return The ArrayList containing the items that were modified (new paths) and added to the repository.
131 | * @throws IOException
132 | */
133 | public Map getUpdatedMetadata() throws Exception
134 | {
135 | Map modifiedMetadata = new HashMap();
136 |
137 | for (DiffEntry diff : diffs)
138 | {
139 | if (diff.getChangeType().toString().equals("MODIFY"))
140 | {
141 | String item = SMAUtility.checkMeta(diff.getNewPath());
142 | if (!modifiedMetadata.containsKey(item) && item.contains(SOURCEDIR))
143 | {
144 | modifiedMetadata.put(diff.getNewPath(), getBlob(diff.getNewPath(), curCommit));
145 | }
146 | }
147 | }
148 | return modifiedMetadata;
149 | }
150 |
151 | /**
152 | * Returns all of the modified (old paths) changes in the current commit.
153 | *
154 | * @return ArrayList containing the items that were modified (old paths).
155 | */
156 | public Map getOriginalMetadata() throws Exception
157 | {
158 | Map originalMetadata = new HashMap();
159 |
160 | for (DiffEntry diff : diffs)
161 | {
162 | if (diff.getChangeType().toString().equals("MODIFY"))
163 | {
164 | String item = SMAUtility.checkMeta(diff.getOldPath());
165 | if (!originalMetadata.containsKey(item) && item.contains(SOURCEDIR))
166 | {
167 | originalMetadata.put(diff.getOldPath(), getBlob(diff.getOldPath(), prevCommit));
168 | }
169 | }
170 | }
171 |
172 | return originalMetadata;
173 | }
174 |
175 | /**
176 | * Returns the blob information for the file at the specified path and commit
177 | *
178 | * @param repoItem
179 | * @param commit
180 | * @return
181 | * @throws Exception
182 | */
183 | public byte[] getBlob(String repoItem, String commit) throws Exception
184 | {
185 | byte[] data;
186 |
187 | String parentPath = repository.getDirectory().getParent();
188 |
189 | ObjectId commitId = repository.resolve(commit);
190 |
191 | ObjectReader reader = repository.newObjectReader();
192 | RevWalk revWalk = new RevWalk(reader);
193 | RevCommit revCommit = revWalk.parseCommit(commitId);
194 | RevTree tree = revCommit.getTree();
195 | TreeWalk treeWalk = TreeWalk.forPath(reader, repoItem, tree);
196 |
197 | if (treeWalk != null)
198 | {
199 | data = reader.open(treeWalk.getObjectId(0)).getBytes();
200 | }
201 | else
202 | {
203 | throw new IllegalStateException("Did not find expected file '" + repoItem + "'");
204 | }
205 |
206 | reader.release();
207 |
208 | return data;
209 | }
210 |
211 | /**
212 | * Replicates ls-tree for the current commit.
213 | *
214 | * @return Map containing the full path and the data for all items in the repository.
215 | * @throws IOException
216 | */
217 | public Map getAllMetadata() throws Exception
218 | {
219 | Map contents = new HashMap();
220 | ObjectReader reader = repository.newObjectReader();
221 | ObjectId commitId = repository.resolve(curCommit);
222 | RevWalk revWalk = new RevWalk(reader);
223 | RevCommit commit = revWalk.parseCommit(commitId);
224 | RevTree tree = commit.getTree();
225 | TreeWalk treeWalk = new TreeWalk(reader);
226 | treeWalk.addTree(tree);
227 | treeWalk.setRecursive(false);
228 |
229 | while (treeWalk.next())
230 | {
231 | if (treeWalk.isSubtree())
232 | {
233 | treeWalk.enterSubtree();
234 | }
235 | else
236 | {
237 | String member = treeWalk.getPathString();
238 | if (member.contains(SOURCEDIR))
239 | {
240 | byte[] data = getBlob(member, curCommit);
241 | contents.put(member, data);
242 | }
243 | }
244 | }
245 |
246 | reader.release();
247 |
248 | return contents;
249 | }
250 |
251 | /**
252 | * Creates an updated package.xml file and commits it to the repository
253 | *
254 | * @param workspace The workspace.
255 | * @param userName The user name of the committer.
256 | * @param userEmail The email of the committer.
257 | * @param manifest The SMAPackage representation of a package manifest
258 | * @return A boolean value indicating whether an update was required or not.
259 | * @throws Exception
260 | */
261 | public boolean updatePackageXML(String workspace,
262 | String userName,
263 | String userEmail,
264 | SMAPackage manifest) throws Exception
265 | {
266 | File packageXml;
267 |
268 | // Only need to update the manifest if we have additions or deletions
269 | if (!getNewMetadata().isEmpty() || !getDeletedMetadata().isEmpty())
270 | {
271 | // Fine the existing package.xml file in the repository
272 | String packageLocation = SMAUtility.findPackage(new File(workspace));
273 |
274 | if (!packageLocation.isEmpty())
275 | {
276 | packageXml = new File(packageLocation);
277 | }
278 | else
279 | {
280 | // We couldn't find one, so just create one.
281 | packageXml = new File(workspace + "/unpackaged/package.xml");
282 | packageXml.getParentFile().mkdirs();
283 | packageXml.createNewFile();
284 | }
285 |
286 | // Write the manifest to the location of the package.xml in the fs
287 | FileOutputStream fos = new FileOutputStream(packageXml, false);
288 | fos.write(manifest.getPackage().getBytes());
289 | fos.close();
290 |
291 | String path = packageXml.getPath();
292 |
293 | // Commit the updated package.xml file to the repository
294 | git.add().addFilepattern(path).call();
295 | git.commit().setCommitter(userName, userEmail).setMessage("Jenkins updated package.xml").call();
296 |
297 | return true;
298 | }
299 |
300 | return false;
301 | }
302 |
303 | public Git getRepo()
304 | {
305 | return git;
306 | }
307 |
308 | public String getPrevCommit()
309 | {
310 | return prevCommit;
311 | }
312 |
313 | public String getCurCommit()
314 | {
315 | return curCommit;
316 | }
317 |
318 | /**
319 | * Returns the diff between two commits.
320 | *
321 | * @return List that contains DiffEntry objects of the changes made between the previous and current commits.
322 | * @throws Exception
323 | */
324 | private void getDiffs() throws Exception
325 | {
326 | OutputStream out = new ByteArrayOutputStream();
327 | CanonicalTreeParser oldTree = getTree(prevCommit);
328 | CanonicalTreeParser newTree = getTree(curCommit);
329 | DiffCommand diff = git.diff().setOutputStream(out).setOldTree(oldTree).setNewTree(newTree);
330 | diffs = diff.call();
331 | }
332 |
333 | /**
334 | * Returns the Canonical Tree Parser representation of a commit.
335 | *
336 | * @param commit Commit in the repository.
337 | * @return CanonicalTreeParser representing the tree for the commit.
338 | * @throws IOException
339 | */
340 | private CanonicalTreeParser getTree(String commit) throws IOException
341 | {
342 | CanonicalTreeParser tree = new CanonicalTreeParser();
343 | ObjectReader reader = repository.newObjectReader();
344 | ObjectId head = repository.resolve(commit + "^{tree}");
345 | tree.reset(reader, head);
346 | return tree;
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/salesforceMetadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CustomApplication
5 | applications
6 | true
7 | false
8 |
9 |
10 | AppMenu
11 | appMenus
12 | false
13 | false
14 |
15 |
16 | ApprovalProcess
17 | approvalProcesses
18 | true
19 | false
20 |
21 |
22 | AssignmentRule
23 | assignmentRules
24 | true
25 | false
26 |
27 |
28 | AuthProvider
29 | authproviders
30 | true
31 | false
32 |
33 |
34 | AutoResponseRule
35 | autoResponseRules
36 | true
37 | false
38 |
39 |
40 | CallCenter
41 | callCenters
42 | true
43 | false
44 |
45 |
46 | Community
47 | communities
48 | true
49 | false
50 |
51 |
52 | ApexClass
53 | classes
54 | true
55 | true
56 |
57 |
58 | CustomPermission
59 | customPermissions
60 | true
61 | false
62 |
63 |
64 | ApexComponent
65 | components
66 | true
67 | true
68 |
69 |
70 | ConnectedApp
71 | connectedapps
72 | true
73 | false
74 |
75 |
76 | CustomApplicationComponent
77 | customApplicationComponents
78 | true
79 | false
80 |
81 |
82 | Dashboard
83 | dashboards
84 | true
85 | false
86 |
87 |
88 | DataCategoryGroups
89 | datacategorygroups
90 | true
91 | false
92 |
93 |
94 | Document
95 | documents
96 | true
97 | false
98 |
99 |
100 | EmailTemplate
101 | emails
102 | true
103 | false
104 |
105 |
106 | EntitlementProcess
107 | entitlementProcesses
108 | true
109 | false
110 |
111 |
112 | EscalationRules
113 | escalationRules
114 | true
115 | false
116 |
117 |
118 | FlexiPage
119 | flexipages
120 | true
121 | false
122 |
123 |
124 | Flow
125 | flow
126 |
127 |
128 | ExternalDataSource
129 | dataSources
130 | true
131 | false
132 |
133 |
134 | Group
135 | groups
136 | true
137 | false
138 |
139 |
140 | HomePageComponent
141 | homepagecomponents
142 | true
143 | false
144 |
145 |
146 | HomePageLayout
147 | homepageLayouts
148 | true
149 | false
150 |
151 |
152 | CustomLabels
153 | labels
154 | true
155 | false
156 |
157 |
158 | Layout
159 | layouts
160 | true
161 | false
162 |
163 |
164 | Letterhead
165 | letterhead
166 | true
167 | false
168 |
169 |
170 | LiveChatAgentConfig
171 | liveChatAgentConfigs
172 | true
173 | false
174 |
175 |
176 | LiveChatButton
177 | liveChatButtons
178 | true
179 | false
180 |
181 |
182 | LiveChatDeployment
183 | liveChatDeployments
184 | true
185 | false
186 |
187 |
188 | Milestonetype
189 | milestonetypes
190 | true
191 | false
192 |
193 |
194 | Network
195 | networks
196 | true
197 | false
198 |
199 |
200 | CustomObject
201 | objects
202 | true
203 | false
204 |
205 |
206 | CustomObjectTranslation
207 | objectTranslations
208 | true
209 | false
210 |
211 |
212 | ApexPage
213 | pages
214 | true
215 | true
216 |
217 |
218 | PermissionSet
219 | permissionsets
220 | true
221 | false
222 |
223 |
224 | Portal
225 | portals
226 | true
227 | false
228 |
229 |
230 | PostTemplate
231 | postTemplates
232 | true
233 | false
234 |
235 |
236 | Profile
237 | profiles
238 | true
239 | false
240 |
241 |
242 | Queue
243 | queues
244 | true
245 | false
246 |
247 |
248 | QuickAction
249 | quickActions
250 | true
251 | false
252 |
253 |
254 | RemoteSite
255 | remoteSiteSettings
256 | true
257 | false
258 |
259 |
260 | Reports
261 | reports
262 | true
263 | false
264 |
265 |
266 | ReportType
267 | reporttype
268 | true
269 | false
270 |
271 |
272 | Role
273 | roles
274 | true
275 | false
276 |
277 |
278 | SamlSsoConfig
279 | samlssoconfigs
280 | false
281 | false
282 |
283 |
284 | Settings
285 | settings
286 | true
287 | false
288 |
289 |
290 | SharingSet
291 | sharingSets
292 | true
293 | false
294 |
295 |
296 | CustomSite
297 | sites
298 | true
299 | false
300 |
301 |
302 | Skill
303 | skills
304 | true
305 | false
306 |
307 |
308 | Territory
309 | territories
310 | true
311 | false
312 |
313 |
314 | Translation
315 | translations
316 | true
317 | false
318 |
319 |
320 | ApexTrigger
321 | triggers
322 | true
323 | true
324 |
325 |
326 | CustomTab
327 | tabs
328 | true
329 | false
330 |
331 |
332 | StaticResource
333 | staticresources
334 | true
335 | true
336 |
337 |
338 | CustomPageWeblink
339 | weblinks
340 | true
341 | false
342 |
343 |
344 | Workflow
345 | workflows
346 | false
347 | false
348 |
349 |
355 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMAConnection.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import com.sforce.soap.metadata.*;
4 | import com.sforce.soap.partner.Connector;
5 | import com.sforce.soap.partner.LoginResult;
6 | import com.sforce.soap.partner.PartnerConnection;
7 | import com.sforce.ws.ConnectorConfig;
8 |
9 | import java.io.ByteArrayOutputStream;
10 | import java.text.DecimalFormat;
11 | import java.util.logging.Level;
12 | import java.util.logging.Logger;
13 |
14 | /**
15 | * This class handles the API connection and actions against the Salesforce instance
16 | *
17 | */
18 | public class SMAConnection
19 | {
20 | private static final Logger LOG = Logger.getLogger(SMAConnection.class.getName());
21 |
22 | private final ConnectorConfig initConfig = new ConnectorConfig();
23 | private final ConnectorConfig metadataConfig = new ConnectorConfig();
24 |
25 | private final MetadataConnection metadataConnection;
26 | private final PartnerConnection partnerConnection;
27 |
28 | private final String pollWaitString;
29 | private final String maxPollString;
30 |
31 | private DeployResult deployResult;
32 | private DeployDetails deployDetails;
33 | private double API_VERSION;
34 |
35 | /**
36 | * Constructor that sets up the connection to a Salesforce organization
37 | *
38 | * @param username
39 | * @param password
40 | * @param securityToken
41 | * @param server
42 | * @param pollWaitString
43 | * @param maxPollString
44 | * @param proxyServer
45 | * @param proxyUser
46 | * @param proxyPort
47 | * @param proxyPass
48 | * @throws Exception
49 | */
50 | public SMAConnection(String username,
51 | String password,
52 | String securityToken,
53 | String server,
54 | String pollWaitString,
55 | String maxPollString,
56 | String proxyServer,
57 | String proxyUser,
58 | String proxyPass,
59 | Integer proxyPort) throws Exception
60 | {
61 | System.setProperty("https.protocols", "TLSv1,TLSv1.1,TLSv1.2");
62 |
63 | API_VERSION = Double.valueOf(SMAMetadataTypes.getAPIVersion());
64 | this.pollWaitString = pollWaitString;
65 | this.maxPollString = maxPollString;
66 |
67 | String endpoint = server + "/services/Soap/u/" + String.valueOf(API_VERSION);
68 |
69 | initConfig.setUsername(username);
70 | initConfig.setPassword(password + securityToken);
71 | initConfig.setAuthEndpoint(endpoint);
72 | initConfig.setServiceEndpoint(endpoint);
73 | initConfig.setManualLogin(true);
74 |
75 | //Proxy support
76 | if (!proxyServer.isEmpty()) {
77 | initConfig.setProxy(proxyServer, proxyPort);
78 | if (!proxyPass.isEmpty()) {
79 | initConfig.setProxyUsername(proxyUser);
80 | initConfig.setProxyPassword(proxyPass);
81 | }
82 | }
83 |
84 |
85 |
86 | partnerConnection = Connector.newConnection(initConfig);
87 |
88 | LoginResult loginResult = new LoginResult();
89 |
90 | loginResult = partnerConnection.login(initConfig.getUsername(), initConfig.getPassword());
91 | metadataConfig.setServiceEndpoint(loginResult.getMetadataServerUrl());
92 | metadataConfig.setSessionId(loginResult.getSessionId());
93 | metadataConfig.setProxy(initConfig.getProxy());
94 | metadataConfig.setProxyUsername(initConfig.getProxyUsername());
95 | metadataConfig.setProxyPassword(initConfig.getProxyPassword());
96 |
97 | metadataConnection = new MetadataConnection(metadataConfig);
98 | }
99 |
100 | /**
101 | * Sets configuration and performs the deployment of metadata to a Salesforce organization
102 | *
103 | * @param bytes
104 | * @param validateOnly
105 | * @param testLevel
106 | * @param specifiedTests
107 | * @param containsApex
108 | * @return
109 | * @throws Exception
110 | */
111 | public boolean deployToServer(ByteArrayOutputStream bytes,
112 | TestLevel testLevel,
113 | String[] specifiedTests,
114 | boolean validateOnly,
115 | boolean containsApex) throws Exception
116 | {
117 | DeployOptions deployOptions = new DeployOptions();
118 | deployOptions.setPerformRetrieve(false);
119 | deployOptions.setRollbackOnError(true);
120 | deployOptions.setSinglePackage(true);
121 | deployOptions.setCheckOnly(validateOnly);
122 |
123 | // We need to make sure there are actually tests supplied for RunSpecifiedTests...
124 | if (testLevel.equals(TestLevel.RunSpecifiedTests))
125 | {
126 | if (specifiedTests.length > 0)
127 | {
128 | deployOptions.setTestLevel(testLevel);
129 | deployOptions.setRunTests(specifiedTests);
130 | }
131 | else
132 | {
133 | deployOptions.setTestLevel(TestLevel.NoTestRun);
134 | }
135 | }
136 | // And that we should even set a TestLevel
137 | else if (containsApex)
138 | {
139 | deployOptions.setTestLevel(testLevel);
140 | }
141 |
142 | AsyncResult asyncResult = metadataConnection.deploy(bytes.toByteArray(), deployOptions);
143 | String asyncResultId = asyncResult.getId();
144 |
145 | int poll = 0;
146 | int maxPoll = Integer.valueOf(maxPollString);
147 | long pollWait = Long.valueOf(pollWaitString);
148 | boolean fetchDetails;
149 | do
150 | {
151 | Thread.sleep(pollWait);
152 |
153 | if (poll++ > maxPoll)
154 | {
155 | throw new Exception("[SMA] Request timed out. You can check the results later by using this AsyncResult Id: " + asyncResultId);
156 | }
157 |
158 | // Only fetch the details every three poll attempts
159 | fetchDetails = (poll % 3 == 0);
160 | deployResult = metadataConnection.checkDeployStatus(asyncResultId, fetchDetails);
161 | }
162 | while (!deployResult.isDone());
163 |
164 | // This is more to do with errors related to Salesforce. Actual deployment failures are not returned as error codes.
165 | if (!deployResult.isSuccess() && deployResult.getErrorStatusCode() != null)
166 | {
167 | throw new Exception(deployResult.getErrorStatusCode() + " msg:" + deployResult.getErrorMessage());
168 | }
169 |
170 | if (!fetchDetails)
171 | {
172 | // Get the final result with details if we didn't do it in the last attempt.
173 | deployResult = metadataConnection.checkDeployStatus(asyncResultId, true);
174 | }
175 |
176 | deployDetails = deployResult.getDetails();
177 |
178 | return deployResult.isSuccess();
179 | }
180 |
181 | /**
182 | * Returns a formatted string of test failures for printing to the Jenkins console
183 | *
184 | * @return
185 | */
186 | public String getTestFailures()
187 | {
188 | RunTestsResult rtr = deployDetails.getRunTestResult();
189 | StringBuilder buf = new StringBuilder();
190 | if (rtr.getFailures().length > 0)
191 | {
192 | buf.append("[SMA] Test Failures\n");
193 | for (RunTestFailure failure : rtr.getFailures())
194 | {
195 | String n = (failure.getNamespace() == null ? "" :
196 | (failure.getNamespace() + ".")) + failure.getName();
197 | buf.append("Test failure, method: " + n + "." +
198 | failure.getMethodName() + " -- " +
199 | failure.getMessage() + " stack " +
200 | failure.getStackTrace() + "\n\n");
201 | }
202 | }
203 |
204 | return buf.toString();
205 | }
206 |
207 | /**
208 | * Returns a formatted string of component failures for printing to the Jenkins console
209 | *
210 | * @return
211 | */
212 | public String getComponentFailures()
213 | {
214 | DeployMessage messages[] = deployDetails.getComponentFailures();
215 | StringBuilder buf = new StringBuilder();
216 | for (DeployMessage message : messages)
217 | {
218 | if (!message.isSuccess())
219 | {
220 | buf.append("[SMA] Component Failures\n");
221 | if (buf.length() == 0)
222 | {
223 | buf = new StringBuilder("\nFailures:\n");
224 | }
225 |
226 | String loc = (message.getLineNumber() == 0 ? "" :
227 | ("(" + message.getLineNumber() + "," +
228 | message.getColumnNumber() + ")"));
229 |
230 | if (loc.length() == 0
231 | && !message.getFileName().equals(message.getFullName()))
232 | {
233 | loc = "(" + message.getFullName() + ")";
234 | }
235 | buf.append(message.getFileName() + loc + ":" +
236 | message.getProblem()).append('\n');
237 | }
238 | }
239 |
240 | return buf.toString();
241 | }
242 |
243 | /**
244 | * Returns a formatted string of the code coverage information for this deployment
245 | *
246 | * @return
247 | */
248 | public String getCodeCoverage()
249 | {
250 | RunTestsResult rtr = deployDetails.getRunTestResult();
251 | StringBuilder buf = new StringBuilder();
252 | DecimalFormat df = new DecimalFormat("#.##");
253 |
254 | //Get the individual coverage results
255 | CodeCoverageResult[] ccresult = rtr.getCodeCoverage();
256 | if (ccresult.length > 0);
257 | {
258 | buf.append("[SMA] Code Coverage Results\n");
259 |
260 | double loc = 0;
261 | double locUncovered = 0;
262 | for (CodeCoverageResult ccr : ccresult)
263 | {
264 | buf.append(ccr.getName() + ".cls");
265 | buf.append(" -- ");
266 | loc = ccr.getNumLocations();
267 | locUncovered = ccr.getNumLocationsNotCovered();
268 |
269 | double coverage = 0;
270 | if (loc > 0)
271 | {
272 | coverage = calculateCoverage(locUncovered, loc);
273 | }
274 |
275 | buf.append(df.format(coverage) + "%\n");
276 | }
277 |
278 | // Get the total code coverage for this deployment
279 | double totalCoverage = getTotalCodeCoverage(ccresult);
280 | buf.append("\nTotal code coverage for this deployment -- ");
281 | buf.append(df.format(totalCoverage) + "%\n");
282 | }
283 |
284 | return buf.toString();
285 | }
286 |
287 | /**
288 | * Returns a formatted string of code coverage warnings for printing to the Jenkins console
289 | *
290 | * @return
291 | */
292 | public String getCodeCoverageWarnings()
293 | {
294 | RunTestsResult rtr = deployDetails.getRunTestResult();
295 | StringBuilder buf = new StringBuilder();
296 | CodeCoverageWarning[] ccwarn = rtr.getCodeCoverageWarnings();
297 | if (ccwarn.length > 0);
298 | {
299 | buf.append("[SMA] Code Coverage Warnings\n");
300 | for (CodeCoverageWarning ccw : ccwarn)
301 | {
302 | buf.append("Code coverage issue");
303 | if (ccw.getName() != null)
304 | {
305 | String n = (ccw.getNamespace() == null ? "" :
306 | (ccw.getNamespace() + ".")) + ccw.getName();
307 | buf.append(", class: " + n);
308 | }
309 | buf.append(" -- " + ccw.getMessage() + "\n");
310 | }
311 | }
312 |
313 | return buf.toString();
314 | }
315 |
316 | /**
317 | * Returns the DeployDetails from this deployment
318 | *
319 | * @return
320 | */
321 | public DeployDetails getDeployDetails()
322 | {
323 | return deployDetails;
324 | }
325 |
326 | /**
327 | * Sets the DeployDetails for this deployment. For unit tests
328 | *
329 | * @param deployDetails
330 | */
331 | public void setDeployDetails(DeployDetails deployDetails)
332 | {
333 | this.deployDetails = deployDetails;
334 | }
335 |
336 | /**
337 | * Helper method to calculate the total code coverage in this deployment
338 | *
339 | * @param ccresult
340 | * @return
341 | */
342 | private Double getTotalCodeCoverage(CodeCoverageResult[] ccresult)
343 | {
344 | double totalLoc = 0;
345 | double totalLocUncovered = 0;
346 |
347 | if (ccresult.length > 0);
348 | {
349 | for (CodeCoverageResult ccr : ccresult)
350 | {
351 | totalLoc += ccr.getNumLocations();
352 | totalLocUncovered += ccr.getNumLocationsNotCovered();
353 | }
354 | }
355 |
356 | // Determine the coverage
357 | double coverage = 0;
358 | if (totalLoc > 0)
359 | {
360 | coverage = calculateCoverage(totalLocUncovered, totalLoc);
361 | }
362 |
363 | return coverage;
364 | }
365 |
366 | /**
367 | * Helper method to calculate the double for the coverage
368 | *
369 | * @param totalLocUncovered
370 | * @param totalLoc
371 | * @return
372 | */
373 | private double calculateCoverage(double totalLocUncovered, double totalLoc)
374 | {
375 | return (1 - (totalLocUncovered / totalLoc)) * 100;
376 | }
377 | }
--------------------------------------------------------------------------------