├── .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-useCustomSettings.html
│ │ │ ├── help-prTargetBranch.html
│ │ │ ├── help-testLevel.html
│ │ │ ├── help-runTestManifest.html
│ │ │ ├── config.jelly
│ │ │ └── global.jelly
│ │ │ ├── index.jelly
│ │ │ └── salesforceMetadata.xml
│ └── java
│ │ └── org
│ │ └── jenkinsci
│ │ └── plugins
│ │ └── sma
│ │ ├── SMATestManifestReader.java
│ │ ├── SMAJenkinsCIOrgSettings.java
│ │ ├── SMAMetadataTypes.java
│ │ ├── SMAPackage.java
│ │ ├── SMAUtility.java
│ │ ├── SMAMetadata.java
│ │ ├── SMARunner.java
│ │ ├── SMABuilder.java
│ │ ├── SMAGit.java
│ │ └── SMAConnection.java
└── test
│ ├── resources
│ ├── SMAManifest.xml
│ ├── testAddsMods.txt
│ ├── testDeletes.txt
│ └── testPackage.xml
│ └── java
│ └── org
│ └── jenkinsci
│ └── plugins
│ └── sma
│ ├── SMATestManifestReaderTest.java
│ ├── 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-useCustomSettings.html:
--------------------------------------------------------------------------------
1 |
2 | A Custom Setting called JenkinsCISettings__c will be created on the target org to store information about the deployed code. This will improve the stability of the deployment to the org and also offers more advanced options.
3 |
--------------------------------------------------------------------------------
/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/test/resources/SMAManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | test1
5 | TST_test1
6 | TST_test2
7 |
8 |
9 |
10 | test2
11 | TST_test2
12 |
13 |
14 | test3
15 |
16 |
17 |
18 |
19 | test4
20 |
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/test/java/org/jenkinsci/plugins/sma/SMATestManifestReaderTest.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 |
8 | import java.util.Map;
9 | import java.util.Set;
10 |
11 | /**
12 | * Created by ronvelzeboer on 24/01/17.
13 | */
14 | public class SMATestManifestReaderTest {
15 | private Map> mapping;
16 |
17 | @Before
18 | public void setUp() throws Exception {
19 | SMATestManifestReader reader = new SMATestManifestReader("SMAManifest.xml");
20 | this.mapping = reader.getClassMapping();
21 | }
22 |
23 | @Test
24 | public void testClassWithMultipleTests() {
25 | assertEquals(true, mapping.containsKey("test1"));
26 | assertEquals(2, mapping.get("test1").size());
27 | }
28 |
29 | @Test
30 | public void testMappingWithEmptyTestContainers() {
31 | assertEquals(true, mapping.containsKey("test3"));
32 | assertEquals(0, mapping.get("test3").size());
33 | }
34 |
35 | @Test
36 | public void testMappingWithNoTestContainers() {
37 | assertEquals(true, mapping.containsKey("test4"));
38 | assertEquals(0, mapping.get("test4").size());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/sma/SMABuilder/help-runTestManifest.html:
--------------------------------------------------------------------------------
1 |
2 | Enter the Manifest XML filename which resides inside of your repository. This manifest allows for more fine-grained controll of which Apex test classes should be run for a particular class.
3 |
4 | The Manifest XML should be formatted like:
5 |
6 | <?xml version="1.0" ?>
7 | <Config>
8 | <mappings>
9 | <class>HNDL_Account</class>
10 | <tests>TST_HNDL_Account</tests>
11 | <tests>TST_VAT</tests>
12 | <tests>…</tests>
13 | </mappings>
14 | <mappings>
15 | <class>…</class>
16 | <tests>…</tests>
17 | </mappings>
18 | </Config>
19 |
--------------------------------------------------------------------------------
/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 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMATestManifestReader.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 |
4 | import org.apache.commons.configuration.ConfigurationException;
5 | import org.apache.commons.configuration.HierarchicalConfiguration;
6 | import org.apache.commons.configuration.XMLConfiguration;
7 |
8 | import java.util.*;
9 |
10 | /**
11 | * Created by ronvelzeboer on 14/01/17.
12 | */
13 | public class SMATestManifestReader {
14 | private final XMLConfiguration manifest;
15 |
16 | public SMATestManifestReader(String pathToManifest) throws ConfigurationException {
17 | this.manifest = new XMLConfiguration(pathToManifest);
18 | }
19 |
20 | public Map> getClassMapping() {
21 | Map> result = new HashMap>();
22 | List mappingConfig = this.manifest.configurationsAt("mappings");
23 |
24 | if (null == mappingConfig) { return result; }
25 |
26 | for (HierarchicalConfiguration config : mappingConfig) {
27 | String className = config.getString("class");
28 |
29 | if (null == className || className.isEmpty()) { continue; }
30 |
31 | if (!result.containsKey(className)) {
32 | result.put(className, new HashSet());
33 | }
34 | String[] testClasses = config.getStringArray("tests");
35 |
36 | for (String testClass : testClasses) {
37 | if (testClass.isEmpty()) { continue; }
38 |
39 | result.get(className).add(testClass);
40 | }
41 | }
42 | return result;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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/main/java/org/jenkinsci/plugins/sma/SMAJenkinsCIOrgSettings.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import com.sforce.soap.partner.fault.InvalidSObjectFault;
4 | import com.sforce.soap.partner.sobject.SObject;
5 |
6 | import java.util.Calendar;
7 | import java.util.NoSuchElementException;
8 | import java.util.TimeZone;
9 |
10 | /**
11 | * Created by ronvelzeboer on 08/02/17.
12 | */
13 | public class SMAJenkinsCIOrgSettings {
14 | private static final String NAME = "SMA";
15 |
16 | private SMAConnection connection;
17 | private SObject sfCustomSetting;
18 |
19 | private SMAJenkinsCIOrgSettings(SMAConnection connection) throws Exception {
20 | this.connection = connection;
21 | initCustomSetting();
22 | }
23 |
24 | private void initCustomSetting() throws Exception {
25 | connection.createJenkinsCICustomSettingsSObject();
26 | try {
27 | sfCustomSetting = connection.retrieveJenkinsCISettingsFromOrg();
28 | } catch (InvalidSObjectFault e) {
29 | sfCustomSetting = createNewCustomSetting();
30 | } catch (NoSuchElementException e) {
31 | sfCustomSetting = createNewCustomSetting();
32 | }
33 | }
34 |
35 | private SObject createNewCustomSetting() {
36 | SObject so = new SObject();
37 | so.setType("JenkinsCISettings__c");
38 | so.setField("Name", NAME);
39 | so.setField("GitSha1__c", null);
40 | so.setField("GitDeploymentDate__c", null);
41 | so.setField("JenkinsJobName__c", null);
42 | so.setField("JenkinsBuildNumber__c", null);
43 | return so;
44 | }
45 |
46 | public static SMAJenkinsCIOrgSettings getInstance(SMAConnection connection) throws Exception {
47 | return new SMAJenkinsCIOrgSettings(connection);
48 | }
49 |
50 | private SObject getCustomSetting() {
51 | return sfCustomSetting;
52 | }
53 |
54 | public String getGitSha1() {
55 | return null == getCustomSetting().getField("GitSha1__c") ? null : getCustomSetting().getField("GitSha1__c").toString();
56 | }
57 |
58 | public void setGitSha1(String sha1) {
59 | getCustomSetting().setField("GitSha1__c", sha1);
60 | }
61 |
62 | public void setJenkinsJobName(String name) {
63 | getCustomSetting().setField("JenkinsJobName__c", name);
64 | }
65 |
66 | public void setJenkinsBuildNumber(String build) {
67 | getCustomSetting().setField("JenkinsBuildNumber__c", build);
68 | }
69 |
70 | public void save() throws Exception {
71 | getCustomSetting().setField("GitDeploymentDate__c", Calendar.getInstance(TimeZone.getTimeZone("GMT")));
72 | connection.saveJenkinsCISettings(getCustomSetting());
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/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 | private static final Logger LOG = Logger.getLogger(SMAMetadataTypes.class.getName());
20 |
21 | private static final ClassLoader loader = SMAMetadataTypes.class.getClassLoader();
22 | private static String pathToResource = loader.getResource("org/jenkinsci/plugins/sma/salesforceMetadata.xml").toString();
23 | private static Document doc;
24 | private static Boolean docAlive = false;
25 |
26 | /**
27 | * Initializes the Document representation of the salesforceMetadata.xml file
28 | *
29 | * @throws Exception
30 | */
31 | private static void initDocument() throws Exception {
32 | DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
33 | DocumentBuilder dbBuilder = dbFactory.newDocumentBuilder();
34 | doc = dbBuilder.parse(pathToResource);
35 | docAlive = true;
36 | }
37 |
38 | /**
39 | * Returns the Salesforce Metadata API Version
40 | *
41 | * @return version
42 | */
43 | public static String getAPIVersion() throws Exception {
44 | if (!docAlive) {
45 | initDocument();
46 | }
47 | String version = null;
48 |
49 | doc.getDocumentElement().normalize();
50 |
51 | NodeList verNodes = doc.getElementsByTagName("version");
52 |
53 | //There should only be one node in this list
54 | for (int iterator = 0; iterator < verNodes.getLength(); iterator++) {
55 | Node curNode = verNodes.item(iterator);
56 | Element verElement = (Element) curNode;
57 | //If for some reason there is more than one, get the first one
58 | version = verElement.getAttribute("API");
59 | }
60 | return version;
61 | }
62 |
63 | /**
64 | * Creates an SMAMetadata object from a string representation of a file's path and filename.
65 | *
66 | * @param filepath
67 | * @return SMAMetadata
68 | * @throws Exception
69 | */
70 | public static SMAMetadata createMetadataObject(String filepath, byte[] data) throws Exception {
71 | if (!docAlive) {
72 | initDocument();
73 | }
74 | String container = "empty";
75 | String metadataType = "Invalid";
76 | boolean destructible = false;
77 | boolean valid = false;
78 | boolean metaxml = false;
79 |
80 | File file = new File(filepath);
81 | String object = file.getName();
82 | String member = FilenameUtils.removeExtension(object);
83 | String extension = FilenameUtils.getExtension(filepath);
84 | String path = FilenameUtils.getFullPath(filepath);
85 |
86 | //Normalize the salesforceMetadata.xml configuration file
87 | doc.getDocumentElement().normalize();
88 |
89 | NodeList extNodes = doc.getElementsByTagName("extension");
90 |
91 | //Get the node with the corresponding extension and get the relevant information for
92 | //creating the SMAMetadata object
93 | for (int iterator = 0; iterator < extNodes.getLength(); iterator++) {
94 | Node curNode = extNodes.item(iterator);
95 |
96 | Element element = (Element) curNode;
97 | if (element.getAttribute("name").equals(extension)) {
98 | container = element.getElementsByTagName("container").item(0).getTextContent();
99 | metadataType = element.getElementsByTagName("metadata").item(0).getTextContent();
100 | destructible = Boolean.parseBoolean(
101 | element.getElementsByTagName("destructible").item(0).getTextContent()
102 | );
103 | valid = true;
104 | metaxml = Boolean.parseBoolean(element.getElementsByTagName("metaxml").item(0).getTextContent());
105 | break;
106 | }
107 | }
108 | return new SMAMetadata(
109 | extension, container, member, metadataType, path, destructible, valid, metaxml, data
110 | );
111 | }
112 | }
--------------------------------------------------------------------------------
/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 | private final String METADATA_URI = "http://soap.sforce.com/2006/04/metadata";
25 |
26 | /**
27 | * Constructor for SMAPackage
28 | * Takes the SMAMetdata contents that are to be represented by the manifest file and generates a Package for deployment
29 | *
30 | * @param contents
31 | * @param destructiveChange
32 | */
33 | public SMAPackage(List contents,
34 | boolean destructiveChange) throws Exception
35 | {
36 | this.contents = contents;
37 | this.destructiveChange = destructiveChange;
38 |
39 | packageManifest = new Package();
40 | packageManifest.setVersion(SMAMetadataTypes.getAPIVersion());
41 | packageManifest.setTypes(determinePackageTypes().toArray(new PackageTypeMembers[0]));
42 | }
43 |
44 | public List getContents() { return contents; }
45 |
46 | /**
47 | * Returns the name of the manifest file for this SMAPackage
48 | * @return
49 | */
50 | public String getName() {
51 | return destructiveChange ? "destructiveChanges.xml" : "package.xml";
52 | }
53 |
54 | /**
55 | * Transforms the Package into a ByteArray
56 | *
57 | * @return String(packageStream.toByteArray())
58 | * @throws Exception
59 | */
60 | public String getPackage() throws Exception {
61 | TypeMapper typeMapper = new TypeMapper();
62 | ByteArrayOutputStream packageStream = new ByteArrayOutputStream();
63 | QName packageQName = new QName(METADATA_URI, "Package");
64 | XmlOutputStream xmlOutputStream = null;
65 | try {
66 | xmlOutputStream = new XmlOutputStream(packageStream, true);
67 | xmlOutputStream.setPrefix("", METADATA_URI);
68 | xmlOutputStream.setPrefix("xsi", "http://www.w3.org/2001/XMLSchema-instance");
69 | packageManifest.write(packageQName, xmlOutputStream, typeMapper);
70 | } finally {
71 | if (null != xmlOutputStream) { xmlOutputStream.close(); }
72 | }
73 | return new String(packageStream.toByteArray());
74 | }
75 |
76 | /**
77 | * Returns whether or not this package contains Apex components
78 | *
79 | * @return containsApex
80 | */
81 | public boolean containsApex() {
82 | for (SMAMetadata thisMetadata : contents) {
83 | if (thisMetadata.getMetadataType().equals("ApexClass")
84 | || thisMetadata.getMetadataType().equals("ApexTrigger")) {
85 | return true;
86 | }
87 | }
88 | return false;
89 | }
90 |
91 | /**
92 | * Sorts the metadata into types and members for the manifest
93 | *
94 | * @return
95 | */
96 | private List determinePackageTypes() {
97 | List types = new ArrayList();
98 | Map> contentsByType = new HashMap>();
99 |
100 | // Sort the metadata objects by metadata type
101 | for (SMAMetadata mdObject : contents) {
102 | if (destructiveChange && !mdObject.isDestructible()) {
103 | // Don't include non destructible metadata in destructiveChanges
104 | continue;
105 | }
106 | if (!contentsByType.containsKey(mdObject.getMetadataType())) {
107 | contentsByType.put(mdObject.getMetadataType(), new ArrayList());
108 | }
109 | contentsByType.get(mdObject.getMetadataType()).add(mdObject.getMember());
110 | }
111 | // Put the members into list of PackageTypeMembers
112 | for (String metadataType : contentsByType.keySet()) {
113 | PackageTypeMembers members = new PackageTypeMembers();
114 | members.setName(metadataType);
115 | members.setMembers(contentsByType.get(metadataType).toArray(new String[0]));
116 | types.add(members);
117 | }
118 | return types;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/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.*;
9 | import java.util.logging.Logger;
10 | import java.util.regex.Matcher;
11 | import java.util.regex.Pattern;
12 | import java.util.zip.ZipEntry;
13 | import java.util.zip.ZipOutputStream;
14 |
15 | /**
16 | * Utility class for performing a variety of tasks in SMA.
17 | *
18 | * @author aesanch2
19 | */
20 | public class SMAUtility {
21 | private static final Logger LOG = Logger.getLogger(SMAUtility.class.getName());
22 |
23 |
24 | /**
25 | * Creates a zipped byte array of the deployment or rollback package
26 | *
27 | * @param deployData
28 | * @param packageManifest
29 | * @param destructiveChange
30 | * @return
31 | * @throws Exception
32 | */
33 | public static ByteArrayOutputStream zipPackage(Map deployData,
34 | SMAPackage packageManifest,
35 | SMAPackage destructiveChange) throws Exception
36 | {
37 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
38 | ZipOutputStream zos = null;
39 | try {
40 | zos = new ZipOutputStream(baos);
41 |
42 | ZipEntry manifestFile = new ZipEntry(packageManifest.getName());
43 | zos.putNextEntry(manifestFile);
44 | zos.write(packageManifest.getPackage().getBytes());
45 | zos.closeEntry();
46 |
47 | ZipEntry destructiveChanges = new ZipEntry(destructiveChange.getName());
48 | zos.putNextEntry(destructiveChanges);
49 | zos.write(destructiveChange.getPackage().getBytes());
50 | zos.closeEntry();
51 |
52 | for (String metadata : deployData.keySet()) {
53 | ZipEntry metadataEntry = new ZipEntry(metadata);
54 | zos.putNextEntry(metadataEntry);
55 | zos.write(deployData.get(metadata));
56 | zos.closeEntry();
57 | }
58 | } finally {
59 | if (null != zos) { zos.close(); }
60 | }
61 | return baos;
62 | }
63 |
64 | /**
65 | * Helper to write the zip to a file location
66 | *
67 | * @param zipBytes
68 | * @param location
69 | * @throws Exception
70 | */
71 | public static void writeZip(ByteArrayOutputStream zipBytes, String location) throws Exception {
72 | FileOutputStream fos = null;
73 | try {
74 | fos = new FileOutputStream(location);
75 | fos.write(zipBytes.toByteArray());
76 | } finally {
77 | if (null != fos) { fos.close(); }
78 | }
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 | String location = "";
89 | File[] filesInDir = directory.listFiles();
90 |
91 | for (File f : filesInDir) {
92 | if (f.isDirectory()) {
93 | location = findPackage(f);
94 | } else if (f.getName().equals("package.xml")) {
95 | location = f.getPath();
96 | }
97 | if (!location.isEmpty()) {
98 | break;
99 | }
100 | }
101 | return location;
102 | }
103 |
104 | /**
105 | * We don't actually want to load the -meta.xml files, so we use this to get the real item and handle the -metas
106 | * elsewhere since both components are required for deployment.
107 | *
108 | * @param repoItem
109 | * @return
110 | */
111 | public static String checkMeta(String repoItem) {
112 | String actualItem = repoItem;
113 |
114 | if (repoItem.contains("-meta")) {
115 | actualItem = repoItem.substring(0, repoItem.length() - 9);
116 | }
117 | return actualItem;
118 | }
119 |
120 | /**
121 | * Prints a set of metadata names to the Jenkins console
122 | *
123 | * @param listener
124 | * @param metadataList
125 | */
126 | public static void printMetadataToConsole(BuildListener listener, List metadataList) {
127 | // Sorts by extension, then by member name
128 | Collections.sort(metadataList);
129 |
130 | for (SMAMetadata metadata : metadataList) {
131 | listener.getLogger().println("- " + metadata.getFullName());
132 | }
133 | listener.getLogger().println();
134 | }
135 |
136 | /**
137 | * Searches for a possible unit tests in the repository for a given set of metadata
138 | *
139 | * @param allMetadata
140 | * @param testClassRegex
141 | * @return
142 | */
143 | public static String searchForTestClass(Set allMetadata, String testClassRegex) {
144 | String match = null;
145 | Matcher matcher;
146 |
147 | for (String s : allMetadata) {
148 | matcher = Pattern.compile(testClassRegex).matcher(s);
149 | if (matcher.find()) {
150 | match = s;
151 | break;
152 | }
153 | }
154 | return match;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/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.HashSet;
5 | import java.util.List;
6 | import java.util.logging.Logger;
7 |
8 | /**
9 | * Creates an object representation of a Salesforce Metadata file.
10 | *
11 | */
12 | public class SMAMetadata implements Comparable
13 | {
14 | private static final Logger LOG = Logger.getLogger(SMAMetadata.class.getName());
15 |
16 | private String extension;
17 | private String container;
18 | private String member;
19 | private String metadataType;
20 | private String path;
21 | private boolean destructible;
22 | private boolean valid;
23 | private boolean metaxml;
24 | private byte[] body;
25 |
26 | /**
27 | * Constructor for SMAMetadata object
28 | *
29 | * @param extension
30 | * @param container
31 | * @param member
32 | * @param metadataType
33 | * @param path
34 | * @param destructible
35 | * @param valid
36 | * @param metaxml
37 | * @param body
38 | */
39 | public SMAMetadata(String extension,
40 | String container,
41 | String member,
42 | String metadataType,
43 | String path,
44 | boolean destructible,
45 | boolean valid,
46 | boolean metaxml,
47 | byte[] body)
48 | {
49 | this.extension = extension;
50 | this.container = container;
51 | this.member = member;
52 | this.metadataType = metadataType;
53 | this.path = path;
54 | this.destructible = destructible;
55 | this.valid = valid;
56 | this.metaxml = metaxml;
57 | this.body = body;
58 | }
59 |
60 | /**
61 | * Returns the extension for this metadata file.
62 | *
63 | * @return A string representation of the extension type of the metadata file.
64 | */
65 | public String getExtension() { return extension; }
66 |
67 | /**
68 | * Returns the parent container for this metadata file.
69 | *
70 | * @return A string representation of the parent container for this metadata file.
71 | */
72 | public String getContainer() { return container; }
73 |
74 | /**
75 | * Returns the path of the metadata file.
76 | *
77 | * @return A string representation of the path of the metadata file.
78 | */
79 | public String getPath() { return path; }
80 |
81 | /**
82 | * Returns the name of the metadata file.
83 | *
84 | * @return A string representation of the metadata file's name.
85 | */
86 | public String getMember() { return member; }
87 |
88 | /**
89 | * Returns the metadata type of this metadata file.
90 | *
91 | * @return A string representation of the metadata file's type.
92 | */
93 | public String getMetadataType() { return metadataType; }
94 |
95 | /**
96 | * Returns whether or not this metadata object can be deleted using the Salesforce API.
97 | *
98 | * @return A boolean that describes whether or not this metadata object can be deleted using the Salesforce API.
99 | */
100 | public boolean isDestructible() { return destructible; }
101 |
102 | /**
103 | * Returns whether or not this metadata object is a valid member of the Salesforce API.
104 | *
105 | * @return A boolean that describes wheter or not this metadata object is a valid member of the Salesforce API.
106 | */
107 | public boolean isValid() { return valid; }
108 |
109 | /**
110 | * Returns whether or not this metadata object has an accompanying -meta.xml file.
111 | *
112 | * @return
113 | */
114 | public boolean hasMetaxml() { return metaxml; }
115 |
116 | /**
117 | * A toString() like method that returns a concatenation of the name and extension of the metadata object.
118 | *
119 | * @return A string of the name and extension of the metadata object.
120 | */
121 | public String getFullName() {
122 | return member + "." + extension;
123 | }
124 |
125 | public String toString() {
126 | return container + "/" + getFullName();
127 | }
128 |
129 | /**
130 | * The blob data in String format of the metadata's content.
131 | *
132 | * @return
133 | */
134 | public byte[] getBody() { return body; }
135 |
136 | /**
137 | * For sorting metadata by extension followed by member
138 | *
139 | * @param comparison
140 | * @return
141 | */
142 | @Override
143 | public int compareTo(SMAMetadata comparison)
144 | {
145 | int extCompare = this.extension.compareToIgnoreCase(comparison.extension);
146 | return extCompare == 0 ? this.member.compareToIgnoreCase(comparison.member) : extCompare;
147 | }
148 |
149 | /**
150 | * Get all apex files in the provided list
151 | *
152 | * @param contents
153 | * @return
154 | */
155 | public static HashSet getApexClasses(List contents)
156 | {
157 | HashSet allApex = new HashSet();
158 |
159 | for (SMAMetadata md : contents)
160 | {
161 | if (md.getMetadataType().equals("ApexClass"))
162 | {
163 | allApex.add(md.getMember());
164 | }
165 | }
166 |
167 | return allApex;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 | org.jenkins-ci.plugins
5 | plugin
6 | 1.616
7 |
8 |
9 | salesforce-migration-assistant-plugin
10 | Salesforce Migration Assistant
11 | 3.1-SNAPSHOT
12 | hpi
13 |
14 |
15 |
16 | aesanch2
17 | Anthony Sanchez
18 | senninha09@gmail.com
19 |
20 |
21 | rvelzeboer
22 | Ron Velzeboer
23 | r.velzeboer@kelendria.com
24 |
25 |
26 |
27 |
28 | scm:git:ssh://github.com/jenkinsci/salesforce-migration-assistant-plugin.git
29 | scm:git:ssh://git@github.com/jenkinsci/salesforce-migration-assistant-plugin.git
30 | https://github.com/jenkinsci/salesforce-migration-assistant-plugin
31 | HEAD
32 |
33 |
34 |
35 | GitHub
36 | https://github.com/jenkinsci/salesforce-migration-assistant-plugin/issues
37 |
38 |
39 | https://wiki.jenkins-ci.org/display/JENKINS/Salesforce+Migration+Assistant+Plugin
40 |
41 |
42 |
43 | MIT License
44 | http://opensource.org/licenses/MIT
45 |
46 |
47 |
48 |
49 |
50 | repo.jenkins-ci.org
51 | http://repo.jenkins-ci.org/public/
52 |
53 |
54 | jgit-repository
55 | Eclipse JGit Repository
56 | https://repo.eclipse.org/content/groups/releases/
57 |
58 |
59 |
60 |
61 |
62 | repo.jenkins-ci.org
63 | http://repo.jenkins-ci.org/public/
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | org.jenkins-ci.tools
72 | maven-hpi-plugin
73 |
74 | true
75 |
76 |
77 |
78 | org.apache.maven.plugins
79 | maven-release-plugin
80 | 2.5.1
81 |
82 |
83 | org.apache.maven.shared
84 | maven-invoker
85 | 2.2
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | org.jenkins-ci.plugins
96 | git
97 | 2.3.1
98 |
99 |
100 | org.eclipse.jgit
101 | org.eclipse.jgit
102 | 4.5.0.201609210915-r
103 |
104 |
105 | junit
106 | junit
107 | 4.12
108 | test
109 |
110 |
111 | org.json
112 | org.json
113 | 2.0
114 |
115 |
116 | com.force.api
117 | force-wsc
118 | 36.0.0
119 |
120 |
121 | com.force.api
122 | force-partner-api
123 | 36.0.0
124 |
125 |
126 | com.force.api
127 | force-metadata-api
128 | 36.0.0
129 |
130 |
131 | commons-beanutils
132 | commons-beanutils
133 | 1.7.0
134 |
135 |
136 | commons-configuration
137 | commons-configuration
138 | 1.10
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/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/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, 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 | //Delete the deletion file, modify the modification file, and add the addition file
72 | new Git(repository).rm().addFilepattern("src/classes/deleteThis.cls").call();
73 | new Git(repository).rm().addFilepattern("src/classes/deleteThis.cls-meta.xml").call();
74 | PrintWriter out = new PrintWriter(modification.getPath());
75 | out.println("Modified the page");
76 | out.close();
77 | addition = createFile("addThis.trigger", triggersPath);
78 | addMeta = createFile("addThis.trigger-meta.xml", triggersPath);
79 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page").call();
80 | new Git(repository).add().addFilepattern("src/pages/modifyThis.page-meta.xml").call();
81 | new Git(repository).add().addFilepattern("src/triggers/addThis.trigger").call();
82 | new Git(repository).add().addFilepattern("src/triggers/addThis.trigger-meta.xml").call();
83 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls").call();
84 | new Git(repository).add().addFilepattern("src/classes/deleteThis.cls-meta.xml").call();
85 |
86 | //Create the second commit
87 | RevCommit secondCommit = new Git(repository).commit().setMessage("Remove deleteThis. Modify " +
88 | "modifyThis. Add addThis.").call();
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, 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, 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, "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, 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, 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/SMARunner.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import hudson.EnvVars;
4 | import org.apache.commons.configuration.ConfigurationException;
5 |
6 | import java.io.File;
7 | import java.util.*;
8 | import java.util.logging.Logger;
9 |
10 | /**
11 | * Class that contains all of the configuration pertinent to the running job
12 | *
13 | */
14 | public class SMARunner {
15 | private static final Logger LOG = Logger.getLogger(SMARunner.class.getName());
16 |
17 | private Boolean deployAll = false;
18 | private String currentCommit;
19 | private String previousCommit;
20 | private String rollbackLocation;
21 | private SMAGit git;
22 | private String pathToWorkspace;
23 | private List deployMetadata = new ArrayList();
24 | private List deleteMetadata = new ArrayList();
25 | private List rollbackMetadata = new ArrayList();
26 | private List rollbackAdditions = new ArrayList();
27 |
28 | /**
29 | * Wrapper for coordinating the configuration of the running job
30 | *
31 | * @param jobVariables
32 | * @param prTargetBranch
33 | * @throws Exception
34 | */
35 | public SMARunner(EnvVars jobVariables, String prTargetBranch, SMAJenkinsCIOrgSettings orgSettings) throws Exception {
36 | // Get envvars to initialize SMAGit
37 | Boolean shaOverride = false;
38 | this.pathToWorkspace = jobVariables.get("WORKSPACE");
39 | String jobName = jobVariables.get("JOB_NAME");
40 | String buildNumber = jobVariables.get("BUILD_NUMBER");
41 |
42 | if (null != orgSettings && null != orgSettings.getGitSha1()) {
43 | previousCommit = orgSettings.getGitSha1();
44 | } else if (null == orgSettings && jobVariables.containsKey("GIT_PREVIOUS_SUCCESSFUL_COMMIT")) {
45 | previousCommit = jobVariables.get("GIT_PREVIOUS_SUCCESSFUL_COMMIT");
46 | } else {
47 | deployAll = true;
48 | }
49 | if (jobVariables.containsKey("SMA_DEPLOY_ALL_METADATA")) {
50 | deployAll = Boolean.valueOf(jobVariables.get("SMA_DEPLOY_ALL_METADATA"));
51 | }
52 | if (jobVariables.containsKey("SMA_PREVIOUS_COMMIT_OVERRIDE")
53 | && !jobVariables.get("SMA_PREVIOUS_COMMIT_OVERRIDE").isEmpty()
54 | ) {
55 | shaOverride = true;
56 | previousCommit = jobVariables.get("SMA_PREVIOUS_COMMIT_OVERRIDE");
57 | }
58 | // Configure using pull request logic
59 | if (!prTargetBranch.isEmpty() && !shaOverride) {
60 | deployAll = false;
61 | git = new SMAGit(pathToWorkspace, prTargetBranch, SMAGit.Mode.PRB);
62 | previousCommit = git.getPreviousCommit();
63 |
64 | } else if (deployAll) { // Configure for all the metadata
65 | git = new SMAGit(pathToWorkspace, null, SMAGit.Mode.INI);
66 |
67 | } else { // Configure using the previous successful commit for this job
68 | git = new SMAGit(pathToWorkspace, previousCommit, SMAGit.Mode.STD);
69 | }
70 | currentCommit = git.getCurrentCommit();
71 | rollbackLocation = pathToWorkspace + "/sma/rollback" + jobName + buildNumber + ".zip";
72 | }
73 |
74 | /**
75 | * Returns whether the current job is set to deploy all the metadata in the repository
76 | *
77 | * @return deployAll
78 | */
79 | public Boolean getDeployAll() { return deployAll; }
80 |
81 | /**
82 | * Returns the SMAMetadata that is going to be deployed in this job
83 | *
84 | * @return
85 | * @throws Exception
86 | */
87 | public List getPackageMembers() throws Exception {
88 | if (deployAll) {
89 | deployMetadata = buildMetadataList(git.getAllMetadata());
90 | } else if (deployMetadata.isEmpty()) {
91 | Map positiveChanges = git.getNewMetadata();
92 | positiveChanges.putAll(git.getUpdatedMetadata());
93 |
94 | deployMetadata = buildMetadataList(positiveChanges);
95 | }
96 | return deployMetadata;
97 | }
98 |
99 | /**
100 | * Returns the SMAMetadata that is going to be deleted in this job
101 | *
102 | * @return deleteMetadata
103 | * @throws Exception
104 | */
105 | public List getDestructionMembers() throws Exception {
106 | if (deleteMetadata.isEmpty()) {
107 | Map negativeChanges = git.getDeletedMetadata();
108 |
109 | deleteMetadata = buildMetadataList(negativeChanges);
110 | }
111 | return deleteMetadata;
112 | }
113 |
114 | public List getRollbackMetadata() throws Exception {
115 | if (deleteMetadata.isEmpty()) {
116 | getDestructionMembers();
117 | }
118 | rollbackMetadata = new ArrayList();
119 | rollbackMetadata.addAll(deleteMetadata);
120 | rollbackMetadata.addAll(buildMetadataList(git.getOriginalMetadata()));
121 |
122 | return rollbackMetadata;
123 | }
124 |
125 | public List getRollbackAdditions() throws Exception {
126 | rollbackAdditions = new ArrayList();
127 | rollbackAdditions.addAll(buildMetadataList(git.getNewMetadata()));
128 |
129 | return rollbackAdditions;
130 | }
131 |
132 | /**
133 | * Returns a map with the file name mapped to the byte contents of the metadata
134 | *
135 | * @return deploymentData
136 | * @throws Exception
137 | */
138 | public Map getDeploymentData() throws Exception {
139 | if (deployMetadata.isEmpty()) {
140 | getPackageMembers();
141 | }
142 | return getData(deployMetadata, currentCommit);
143 | }
144 |
145 | public Map getRollbackData() throws Exception {
146 | if (rollbackMetadata.isEmpty()) {
147 | getRollbackMetadata();
148 | }
149 | return getData(rollbackMetadata, previousCommit);
150 | }
151 |
152 | /**
153 | * Helper method to find the byte[] contents of given metadata
154 | *
155 | * @param metadatas
156 | * @param commit
157 | * @return
158 | * @throws Exception
159 | */
160 | private Map getData(List metadatas, String commit) throws Exception {
161 | Map data = new HashMap();
162 |
163 | for (SMAMetadata metadata : metadatas) {
164 | data.put(metadata.toString(), metadata.getBody());
165 |
166 | if (metadata.hasMetaxml()) {
167 | String metaXml = metadata.toString() + "-meta.xml";
168 | String pathToXml = metadata.getPath() + metadata.getFullName() + "-meta.xml";
169 | data.put(metaXml, git.getBlob(pathToXml, commit));
170 | }
171 | }
172 | return data;
173 | }
174 |
175 | /**
176 | * Constructs a list of SMAMetadata objects from a Map of files and their byte[] contents
177 | *
178 | * @param repoItems
179 | * @return
180 | * @throws Exception
181 | */
182 | private List buildMetadataList(Map repoItems) throws Exception {
183 | List thisMetadata = new ArrayList();
184 |
185 | for (String repoItem : repoItems.keySet()) {
186 | SMAMetadata mdObject = SMAMetadataTypes.createMetadataObject(repoItem, repoItems.get(repoItem));
187 |
188 | if (mdObject.isValid()) {
189 | thisMetadata.add(mdObject);
190 | }
191 | }
192 | return thisMetadata;
193 | }
194 |
195 | /**
196 | * Returns a String array of all the unit tests that should be run in this job
197 | *
198 | * @param builder
199 | * @return
200 | * @throws Exception
201 | */
202 | public String[] getSpecifiedTests(SMABuilder builder) throws Exception {
203 | Set specifiedTestsList = new HashSet();
204 |
205 | Set apexClassesToDeploy = SMAMetadata.getApexClasses(deployMetadata);
206 | Set allApexClasses = SMAMetadata.getApexClasses(buildMetadataList(git.getAllMetadata()));
207 | Map> classMapping = getManifestClassMapping(builder);
208 |
209 | for (String className : apexClassesToDeploy) {
210 | Set testsForClass = new HashSet();
211 |
212 | String testName = getSpecifiedTestsByRegex(className, allApexClasses, builder);
213 |
214 | if (null != testName) {
215 | testsForClass.add(testName);
216 | }
217 | if (null != classMapping) {
218 | Set manifestTests = getSpecifiedTestsByManifest(className, classMapping);
219 | testsForClass.addAll(manifestTests);
220 | }
221 | if (testsForClass.size() == 0) {
222 | LOG.warning("No test class for " + className + " found");
223 | continue;
224 | }
225 | specifiedTestsList.addAll(testsForClass);
226 | }
227 | specifiedTestsList.retainAll(allApexClasses);
228 |
229 | SortedSet specifiedTestsListSorted = new TreeSet();
230 | specifiedTestsListSorted.addAll(specifiedTestsList);
231 | return specifiedTestsListSorted.toArray(new String[specifiedTestsListSorted.size()]);
232 | }
233 |
234 | private Map> getManifestClassMapping(SMABuilder builder) {
235 | if (!builder.getRunTestManifest().isEmpty()) {
236 | try {
237 | String pathToManifest = this.pathToWorkspace + File.separator + builder.getRunTestManifest();
238 | SMATestManifestReader manifestReader = new SMATestManifestReader(pathToManifest);
239 | return manifestReader.getClassMapping();
240 | } catch (ConfigurationException e) {
241 | LOG.warning("Error found while loading test manifest '" + builder.getRunTestManifest() + "': " + e.getMessage());
242 | } catch (NoSuchElementException e) {
243 | LOG.warning("Error found in the document structure of the test manifest: " + e.getMessage());
244 | }
245 | }
246 | return null;
247 | }
248 |
249 | private Set getSpecifiedTestsByManifest(String className, Map> classMapping) {
250 | return null != classMapping && classMapping.containsKey(className) ? classMapping.get(className) : Collections.emptySet();
251 | }
252 |
253 | private String getSpecifiedTestsByRegex(String className, Set allApexClasses, SMABuilder builder) {
254 | String testRegex = builder.getRunTestRegex();
255 |
256 | if (null == testRegex || testRegex.isEmpty()) { return null; }
257 |
258 | if (className.matches(testRegex)) {
259 | return className;
260 | }
261 | String[] regexs = new String[] { className + testRegex, testRegex + className };
262 |
263 | for (String regex : regexs) {
264 | String testClass = SMAUtility.searchForTestClass(allApexClasses, regex);
265 |
266 | if (null != testClass) {
267 | return testClass;
268 | }
269 | }
270 | return null;
271 | }
272 |
273 | public String getRollbackLocation() {
274 | File rollbackLocationFile = new File(rollbackLocation);
275 |
276 | if (!rollbackLocationFile.getParentFile().exists()) {
277 | rollbackLocationFile.getParentFile().mkdirs();
278 | }
279 | return rollbackLocation;
280 | }
281 |
282 | public String getCurrentCommit() {
283 | return this.currentCommit;
284 | }
285 | }
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/sma/SMABuilder.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.sma;
2 |
3 | import hudson.EnvVars;
4 | import hudson.Extension;
5 | import hudson.Launcher;
6 | import hudson.model.*;
7 | import hudson.tasks.BuildStepDescriptor;
8 | import hudson.tasks.Builder;
9 | import hudson.util.ListBoxModel;
10 | import org.kohsuke.stapler.DataBoundConstructor;
11 | import org.kohsuke.stapler.StaplerRequest;
12 |
13 | import java.io.ByteArrayOutputStream;
14 | import java.io.PrintStream;
15 | import java.util.*;
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 | private boolean validateEnabled;
25 | private String username;
26 | private String password;
27 | private String securityToken;
28 | private String serverType;
29 | private String testLevel;
30 | private String prTargetBranch;
31 | private String runTestRegex;
32 | private String runTestManifest;
33 | private boolean useCustomSettings;
34 |
35 | @DataBoundConstructor
36 | public SMABuilder(Boolean validateEnabled,
37 | String username,
38 | String password,
39 | String securityToken,
40 | String serverType,
41 | String testLevel,
42 | String prTargetBranch,
43 | String runTestRegex,
44 | String runTestManifest,
45 | Boolean useCustomSettings
46 | ) {
47 | this.username = username;
48 | this.password = password;
49 | this.securityToken = securityToken;
50 | this.serverType = serverType;
51 | this.validateEnabled = validateEnabled;
52 | this.testLevel = testLevel;
53 | this.prTargetBranch = prTargetBranch;
54 | this.runTestRegex = runTestRegex;
55 | this.runTestManifest = runTestManifest;
56 | this.useCustomSettings = useCustomSettings;
57 | }
58 |
59 | @Override
60 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
61 | String smaDeployResult = "";
62 | boolean JOB_SUCCESS = false;
63 |
64 | PrintStream writeToConsole = listener.getLogger();
65 | List parameterValues = new ArrayList();
66 |
67 | try {
68 | // Initialize the connection to Salesforce for this job
69 | SMAConnection sfConnection = new SMAConnection(
70 | getUsername(),
71 | getPassword(),
72 | getSecurityToken(),
73 | getServerType(),
74 | getDescriptor().getPollWait(),
75 | getDescriptor().getMaxPoll(),
76 | getDescriptor().getProxyServer(),
77 | getDescriptor().getProxyUser(),
78 | getDescriptor().getProxyPass(),
79 | getDescriptor().getProxyPort()
80 | );
81 |
82 | // Initialize the runner for this job
83 | SMAJenkinsCIOrgSettings orgSettings = null;
84 | if (getUseCustomSettings()) {
85 | orgSettings = SMAJenkinsCIOrgSettings.getInstance(sfConnection);
86 | writeToConsole.println("[SMA] Using Custom Settings on Org. Current settings: ");
87 | writeToConsole.println("- Git SHA1: " + orgSettings.getGitSha1());
88 | writeToConsole.println();
89 | }
90 | EnvVars jobVariables = build.getEnvironment(listener);
91 | SMARunner currentJob = new SMARunner(jobVariables, getPrTargetBranch(), orgSettings);
92 |
93 | // Build the package and destructiveChanges manifests
94 | SMAPackage packageXml = new SMAPackage(currentJob.getPackageMembers(), false);
95 |
96 | writeToConsole.println("[SMA] Deploying the following metadata:");
97 | SMAUtility.printMetadataToConsole(listener, currentJob.getPackageMembers());
98 |
99 | SMAPackage destructiveChanges = buildDestructiveChangesPackage(currentJob);
100 |
101 | if (destructiveChanges.getContents().size() > 0) {
102 | writeToConsole.println("[SMA] Deleting the following metadata:");
103 | SMAUtility.printMetadataToConsole(listener, destructiveChanges.getContents());
104 | }
105 | // Build the zipped deployment package
106 | ByteArrayOutputStream deploymentPackage = SMAUtility.zipPackage(
107 | currentJob.getDeploymentData(),
108 | packageXml,
109 | destructiveChanges
110 | );
111 |
112 | // Deploy to the server
113 | String[] specifiedTests = null;
114 | TestLevel testLevel = TestLevel.valueOf(getTestLevel());
115 |
116 | if (testLevel.equals(TestLevel.RunSpecifiedTests)) {
117 | specifiedTests = currentJob.getSpecifiedTests(this);
118 |
119 | writeToConsole.println("[SMA] Specified Apex tests to run:");
120 | for (String testName : specifiedTests) {
121 | writeToConsole.println("- " + testName);
122 | }
123 | writeToConsole.println("");
124 | }
125 |
126 | JOB_SUCCESS = sfConnection.deployToServer(
127 | deploymentPackage,
128 | testLevel,
129 | specifiedTests,
130 | getValidateEnabled(),
131 | packageXml.containsApex()
132 | );
133 | if (JOB_SUCCESS) {
134 | if (!testLevel.equals(TestLevel.NoTestRun)) {
135 | smaDeployResult = sfConnection.getCodeCoverage();
136 | }
137 | smaDeployResult += "\n[SMA] " + (getValidateEnabled() ? "Validation" : "Deployment") + " Succeeded";
138 |
139 | if (!getValidateEnabled()) {
140 | if (!currentJob.getDeployAll()) {
141 | createRollbackPackageZip(currentJob);
142 | }
143 | if (getUseCustomSettings()) {
144 | orgSettings.setGitSha1(currentJob.getCurrentCommit());
145 | orgSettings.setJenkinsJobName(jobVariables.get("JOB_NAME"));
146 | orgSettings.setJenkinsBuildNumber(jobVariables.get("BUILD_NUMBER"));
147 | orgSettings.save();
148 | }
149 | writeToConsole.println("Setting GitSha1 to: " + currentJob.getCurrentCommit());
150 | }
151 | } else {
152 | smaDeployResult = sfConnection.getComponentFailures();
153 |
154 | if (!testLevel.equals(TestLevel.NoTestRun)) {
155 | smaDeployResult += sfConnection.getTestFailures() + sfConnection.getCodeCoverageWarnings();
156 | }
157 | smaDeployResult += "\n[SMA] " + (getValidateEnabled() ? "Validation" : "Deployment") + " Failed";
158 | }
159 | } catch (Exception e) {
160 | e.printStackTrace(writeToConsole);
161 | }
162 | parameterValues.add(new StringParameterValue("smaDeployResult", smaDeployResult));
163 | build.addAction(new ParametersAction(parameterValues));
164 | writeToConsole.println(smaDeployResult);
165 |
166 | return JOB_SUCCESS;
167 | }
168 |
169 | private void createRollbackPackageZip(SMARunner currentJob) throws Exception {
170 | SMAPackage rollbackPackageXml = new SMAPackage(currentJob.getRollbackMetadata(), false);
171 | SMAPackage rollbackDestructiveXml = new SMAPackage(currentJob.getRollbackAdditions(), true);
172 |
173 | ByteArrayOutputStream rollbackPackage = SMAUtility.zipPackage(
174 | currentJob.getRollbackData(),
175 | rollbackPackageXml,
176 | rollbackDestructiveXml
177 | );
178 | SMAUtility.writeZip(rollbackPackage, currentJob.getRollbackLocation());
179 | }
180 |
181 | private SMAPackage buildDestructiveChangesPackage(SMARunner currentJob) throws Exception {
182 | List destructionMembers = new ArrayList();
183 | if (!currentJob.getDeployAll()) {
184 | destructionMembers = currentJob.getDestructionMembers();
185 | }
186 | return new SMAPackage(destructionMembers, true);
187 | }
188 |
189 | public boolean getValidateEnabled() { return validateEnabled; }
190 |
191 | public String getUsername() { return username; }
192 |
193 | public String getSecurityToken() { return securityToken; }
194 |
195 | public String getPassword() { return password; }
196 |
197 | public String getServerType() { return serverType; }
198 |
199 | public String getTestLevel() { return testLevel; }
200 |
201 | public String getPrTargetBranch() { return prTargetBranch; }
202 |
203 | public String getRunTestRegex() { return runTestRegex; }
204 |
205 | public String getRunTestManifest() { return runTestManifest; }
206 |
207 | public Boolean getUseCustomSettings() { return useCustomSettings; }
208 |
209 | @Override
210 | public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); }
211 |
212 | @Extension
213 | public static final class DescriptorImpl extends BuildStepDescriptor {
214 | private String maxPoll = "200";
215 | private String pollWait = "30000";
216 | private String runTestRegex = ".*[T|t]est.*";
217 | private String proxyServer = "";
218 | private String proxyUser = "";
219 | private String proxyPass = "";
220 | private Integer proxyPort = 0;
221 |
222 |
223 | public DescriptorImpl() {
224 | load();
225 | }
226 |
227 | public boolean isApplicable(Class extends AbstractProject> aClass) {
228 | // Indicates that this builder can be used with all kinds of project types
229 | return true;
230 | }
231 |
232 | public String getDisplayName() { return "Salesforce Migration Assistant"; }
233 |
234 | public String getMaxPoll() { return maxPoll; }
235 |
236 | public String getPollWait() { return pollWait; }
237 |
238 | public String getRunTestRegex() { return runTestRegex; }
239 |
240 | public String getProxyServer() { return proxyServer; }
241 |
242 | public String getProxyUser() { return proxyUser; }
243 |
244 | public String getProxyPass() { return proxyPass; }
245 |
246 | public Integer getProxyPort() { return proxyPort; }
247 |
248 | public ListBoxModel doFillServerTypeItems() {
249 | return new ListBoxModel(
250 | new ListBoxModel.Option("Production (https://login.salesforce.com)", "https://login.salesforce.com"),
251 | new ListBoxModel.Option("Sandbox (https://test.salesforce.com)", "https://test.salesforce.com")
252 | );
253 | }
254 |
255 | public ListBoxModel doFillTestLevelItems() {
256 | return new ListBoxModel(
257 | new ListBoxModel.Option("None", "NoTestRun"),
258 | new ListBoxModel.Option("Relevant", "RunSpecifiedTests"),
259 | new ListBoxModel.Option("Local", "RunLocalTests"),
260 | new ListBoxModel.Option("All", "RunAllTestsInOrg")
261 | );
262 | }
263 |
264 | public boolean configure(StaplerRequest request, JSONObject formData) throws FormException {
265 | maxPoll = formData.getString("maxPoll");
266 | pollWait = formData.getString("pollWait");
267 | proxyServer = formData.getString("proxyServer");
268 | proxyUser = formData.getString("proxyUser");
269 | proxyPass = formData.getString("proxyPass");
270 | proxyPort = formData.optInt("proxyPort");
271 |
272 | save();
273 | return false;
274 | }
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/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.Constants;
7 | import org.eclipse.jgit.lib.ObjectId;
8 | import org.eclipse.jgit.lib.ObjectReader;
9 | import org.eclipse.jgit.lib.Repository;
10 | import org.eclipse.jgit.revwalk.RevCommit;
11 | import org.eclipse.jgit.revwalk.RevTree;
12 | import org.eclipse.jgit.revwalk.RevWalk;
13 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
14 | import org.eclipse.jgit.transport.RefSpec;
15 | import org.eclipse.jgit.treewalk.CanonicalTreeParser;
16 | import org.eclipse.jgit.treewalk.TreeWalk;
17 |
18 | import java.io.*;
19 | import java.util.HashMap;
20 | import java.util.List;
21 | import java.util.Map;
22 | import java.util.logging.Logger;
23 |
24 | /**
25 | * Wrapper for git interactions using jGit.
26 | *
27 | */
28 | public class SMAGit {
29 | public enum Mode { STD, INI, PRB }
30 |
31 | private final String SOURCEDIR = "src/";
32 |
33 | private Git git;
34 | private Repository repository;
35 | private List diffs;
36 | private String previousCommit, currentCommit;
37 |
38 | private static final Logger LOG = Logger.getLogger(SMAGit.class.getName());
39 |
40 | /**
41 | * Creates an SMAGit instance
42 | *
43 | * @param pathToWorkspace
44 | * @param diffAgainst
45 | * @param smaMode
46 | * @throws Exception
47 | */
48 | public SMAGit(String pathToWorkspace,
49 | String diffAgainst,
50 | Mode smaMode) throws Exception
51 | {
52 | File repoDir = new File(pathToWorkspace + "/.git");
53 | FileRepositoryBuilder builder = new FileRepositoryBuilder();
54 | this.repository = builder.setGitDir(repoDir).readEnvironment().build();
55 | this.git = new Git(repository);
56 |
57 | updateLocalRefSpecs(git);
58 | this.currentCommit = retrieveCommitId(repository, Constants.HEAD);
59 |
60 | if (smaMode == Mode.PRB) {
61 | this.previousCommit = retrieveCommitId(repository, "refs/remotes/origin/" + diffAgainst);
62 |
63 | } else if (smaMode == Mode.STD) {
64 | this.previousCommit = diffAgainst;
65 | }
66 | if (smaMode != Mode.INI) {
67 | getDiffs();
68 | }
69 | }
70 |
71 | /**
72 | *
73 | * @param repository
74 | * @param revStr
75 | * @return
76 | * @throws IOException
77 | */
78 | private static String retrieveCommitId(Repository repository, String revStr) throws IOException {
79 | ObjectId id = repository.resolve(revStr);
80 | RevCommit commit = new RevWalk(repository).parseCommit(id);
81 | return commit.getName();
82 | }
83 |
84 | /**
85 | *
86 | * @param git Git
87 | * @throws Exception
88 | */
89 | private static void updateLocalRefSpecs(Git git) throws Exception {
90 | try {
91 | git.fetch().setRefSpecs(new RefSpec("refs/heads/*:refs/remotes/origin/*")).call();
92 | } catch (Exception e) {
93 | LOG.warning("Error while fetching ref heads and remotes: " + e.getMessage());
94 | }
95 | }
96 |
97 | /**
98 | * Returns all of the items that were added in the current commit.
99 | *
100 | * @return The ArrayList containing all of the additions in the current commit.
101 | * @throws IOException
102 | */
103 | public Map getNewMetadata() throws Exception {
104 | Map additions = new HashMap();
105 |
106 | for (DiffEntry diff : diffs) {
107 | if (diff.getChangeType().toString().equals("ADD")) {
108 | String item = SMAUtility.checkMeta(diff.getNewPath());
109 |
110 | if (!additions.containsKey(item) && item.contains(SOURCEDIR)) {
111 | additions.put(diff.getNewPath(), getBlob(diff.getNewPath(), getCurrentCommit()));
112 | }
113 | }
114 | }
115 | return additions;
116 | }
117 |
118 | /**
119 | * Returns all of the items that were deleted in the current commit.
120 | *
121 | * @return The ArrayList containing all of the items that were deleted in the current commit.
122 | */
123 | public Map getDeletedMetadata() throws Exception
124 | {
125 | Map deletions = new HashMap();
126 |
127 | for (DiffEntry diff : diffs) {
128 | if (diff.getChangeType().toString().equals("DELETE")) {
129 | String item = SMAUtility.checkMeta(diff.getOldPath());
130 |
131 | if (!deletions.containsKey(item) && item.contains(SOURCEDIR)) {
132 | deletions.put(diff.getOldPath(), getBlob(diff.getOldPath(), getPreviousCommit()));
133 | }
134 | }
135 | }
136 |
137 | return deletions;
138 | }
139 |
140 | /**
141 | * Returns all of the updated changes in the current commit.
142 | *
143 | * @return The ArrayList containing the items that were modified (new paths) and added to the repository.
144 | * @throws IOException
145 | */
146 | public Map getUpdatedMetadata() throws Exception {
147 | Map modifiedMetadata = new HashMap();
148 |
149 | for (DiffEntry diff : diffs) {
150 | if (diff.getChangeType().toString().equals("MODIFY")) {
151 | String item = SMAUtility.checkMeta(diff.getNewPath());
152 |
153 | if (!modifiedMetadata.containsKey(item) && item.contains(SOURCEDIR)) {
154 | modifiedMetadata.put(diff.getNewPath(), getBlob(diff.getNewPath(), getCurrentCommit()));
155 | }
156 | }
157 | }
158 | return modifiedMetadata;
159 | }
160 |
161 | /**
162 | * Returns all of the modified (old paths) changes in the current commit.
163 | *
164 | * @return ArrayList containing the items that were modified (old paths).
165 | */
166 | public Map getOriginalMetadata() throws Exception {
167 | Map originalMetadata = new HashMap();
168 |
169 | for (DiffEntry diff : diffs) {
170 | if (diff.getChangeType().toString().equals("MODIFY")) {
171 | String item = SMAUtility.checkMeta(diff.getOldPath());
172 |
173 | if (!originalMetadata.containsKey(item) && item.contains(SOURCEDIR)) {
174 | originalMetadata.put(diff.getOldPath(), getBlob(diff.getOldPath(), getPreviousCommit()));
175 | }
176 | }
177 | }
178 | return originalMetadata;
179 | }
180 |
181 | /**
182 | * Returns the blob information for the file at the specified path and commit
183 | *
184 | * @param repoItem
185 | * @param commit
186 | * @return
187 | * @throws Exception
188 | */
189 | public byte[] getBlob(String repoItem, String commit) throws Exception {
190 | byte[] data;
191 |
192 | ObjectId commitId = repository.resolve(commit);
193 | ObjectReader reader = null;
194 | try {
195 | reader = repository.newObjectReader();
196 | RevWalk revWalk = new RevWalk(reader);
197 | RevCommit revCommit = revWalk.parseCommit(commitId);
198 | RevTree tree = revCommit.getTree();
199 | TreeWalk treeWalk = TreeWalk.forPath(reader, repoItem, tree);
200 |
201 | if (treeWalk != null) {
202 | data = reader.open(treeWalk.getObjectId(0)).getBytes();
203 | } else {
204 | throw new IllegalStateException("Did not find expected file '" + repoItem + "'");
205 | }
206 | } finally {
207 | if (null != reader) { reader.close(); }
208 | }
209 | return data;
210 | }
211 |
212 | /**
213 | * Replicates ls-tree for the current commit.
214 | *
215 | * @return Map containing the full path and the data for all items in the repository.
216 | * @throws IOException
217 | */
218 | public Map getAllMetadata() throws Exception {
219 | Map contents = new HashMap();
220 | ObjectReader reader = null;
221 | try {
222 | reader = repository.newObjectReader();
223 | ObjectId commitId = repository.resolve(getCurrentCommit());
224 | RevWalk revWalk = new RevWalk(reader);
225 | RevCommit commit = revWalk.parseCommit(commitId);
226 | RevTree tree = commit.getTree();
227 | TreeWalk treeWalk = new TreeWalk(reader);
228 | treeWalk.addTree(tree);
229 | treeWalk.setRecursive(false);
230 |
231 | while (treeWalk.next()) {
232 | if (treeWalk.isSubtree()) {
233 | treeWalk.enterSubtree();
234 | } else {
235 | String member = treeWalk.getPathString();
236 | if (member.contains(SOURCEDIR)) {
237 | byte[] data = getBlob(member, getCurrentCommit());
238 | contents.put(member, data);
239 | }
240 | }
241 | }
242 | } finally {
243 | if (null != reader) { reader.close(); }
244 | }
245 | return contents;
246 | }
247 |
248 | /**
249 | * Creates an updated package.xml file and commits it to the repository
250 | *
251 | * @param workspace The workspace.
252 | * @param userName The user name of the committer.
253 | * @param userEmail The email of the committer.
254 | * @param manifest The SMAPackage representation of a package manifest
255 | * @return A boolean value indicating whether an update was required or not.
256 | * @throws Exception
257 | */
258 | public boolean updatePackageXML(String workspace,
259 | String userName,
260 | String userEmail,
261 | SMAPackage manifest) throws Exception
262 | {
263 | File packageXml;
264 |
265 | // Only need to update the manifest if we have additions or deletions
266 | if (!getNewMetadata().isEmpty() || !getDeletedMetadata().isEmpty()) {
267 | // Fine the existing package.xml file in the repository
268 | String packageLocation = SMAUtility.findPackage(new File(workspace));
269 |
270 | if (!packageLocation.isEmpty()) {
271 | packageXml = new File(packageLocation);
272 | } else {
273 | // We couldn't find one, so just create one.
274 | packageXml = new File(workspace + "/unpackaged/package.xml");
275 | packageXml.getParentFile().mkdirs();
276 | packageXml.createNewFile();
277 | }
278 | // Write the manifest to the location of the package.xml in the fs
279 | FileOutputStream fos = null;
280 | try {
281 | fos = new FileOutputStream(packageXml, false);
282 | fos.write(manifest.getPackage().getBytes());
283 | } finally {
284 | if (null != fos) { fos.close(); }
285 | }
286 | String path = packageXml.getPath();
287 |
288 | // Commit the updated package.xml file to the repository
289 | git.add().addFilepattern(path).call();
290 | git.commit().setCommitter(userName, userEmail).setMessage("Jenkins updated package.xml").call();
291 |
292 | return true;
293 | }
294 |
295 | return false;
296 | }
297 |
298 | public Git getRepo() {
299 | return git;
300 | }
301 |
302 | public String getPreviousCommit() {
303 | return previousCommit;
304 | }
305 |
306 | public String getCurrentCommit() {
307 | return currentCommit;
308 | }
309 |
310 | /**
311 | * Returns the diff between two commits.
312 | *
313 | * @return List that contains DiffEntry objects of the changes made between the previous and current commits.
314 | * @throws Exception
315 | */
316 | private void getDiffs() throws Exception {
317 | OutputStream out = new ByteArrayOutputStream();
318 | CanonicalTreeParser oldTree = getTree(getPreviousCommit());
319 | CanonicalTreeParser newTree = getTree(getCurrentCommit());
320 | DiffCommand diff = git.diff().setOutputStream(out).setOldTree(oldTree).setNewTree(newTree);
321 | diffs = diff.call();
322 | }
323 |
324 | /**
325 | * Returns the Canonical Tree Parser representation of a commit.
326 | *
327 | * @param commit Commit in the repository.
328 | * @return CanonicalTreeParser representing the tree for the commit.
329 | * @throws IOException
330 | */
331 | private CanonicalTreeParser getTree(String commit) throws IOException {
332 | CanonicalTreeParser tree = new CanonicalTreeParser();
333 | ObjectReader reader = repository.newObjectReader();
334 | ObjectId head = repository.resolve(commit + "^{tree}");
335 | tree.reset(reader, head);
336 | return tree;
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/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.metadata.Error;
5 | import com.sforce.soap.metadata.FieldType;
6 | import com.sforce.soap.partner.*;
7 | import com.sforce.soap.partner.Connector;
8 | import com.sforce.soap.partner.sobject.SObject;
9 | import com.sforce.ws.ConnectionException;
10 | import com.sforce.ws.ConnectorConfig;
11 |
12 | import java.io.ByteArrayOutputStream;
13 | import java.text.DecimalFormat;
14 | import java.util.NoSuchElementException;
15 | import java.util.logging.Logger;
16 |
17 | /**
18 | * This class handles the API connection and actions against the Salesforce instance
19 | *
20 | */
21 | public class SMAConnection {
22 | private static final Logger LOG = Logger.getLogger(SMAConnection.class.getName());
23 |
24 | private final ConnectorConfig initConfig = new ConnectorConfig();
25 | private final ConnectorConfig metadataConfig = new ConnectorConfig();
26 |
27 | private final MetadataConnection metadataConnection;
28 | private final PartnerConnection partnerConnection;
29 |
30 | private final String pollWaitString;
31 | private final String maxPollString;
32 |
33 | private DeployResult deployResult;
34 | private DeployDetails deployDetails;
35 | private double API_VERSION;
36 |
37 | /**
38 | * Constructor that sets up the connection to a Salesforce organization
39 | *
40 | * @param username
41 | * @param password
42 | * @param securityToken
43 | * @param server
44 | * @param pollWaitString
45 | * @param maxPollString
46 | * @param proxyServer
47 | * @param proxyUser
48 | * @param proxyPort
49 | * @param proxyPass
50 | * @throws Exception
51 | */
52 | public SMAConnection(String username,
53 | String password,
54 | String securityToken,
55 | String server,
56 | String pollWaitString,
57 | String maxPollString,
58 | String proxyServer,
59 | String proxyUser,
60 | String proxyPass,
61 | Integer proxyPort) throws Exception
62 | {
63 | System.setProperty("https.protocols", "TLSv1,TLSv1.1,TLSv1.2");
64 |
65 | API_VERSION = Double.valueOf(SMAMetadataTypes.getAPIVersion());
66 | this.pollWaitString = pollWaitString;
67 | this.maxPollString = maxPollString;
68 |
69 | String endpoint = server + "/services/Soap/u/" + String.valueOf(API_VERSION);
70 |
71 | initConfig.setUsername(username);
72 | initConfig.setPassword(password + securityToken);
73 | initConfig.setAuthEndpoint(endpoint);
74 | initConfig.setServiceEndpoint(endpoint);
75 | initConfig.setManualLogin(true);
76 |
77 | //Proxy support
78 | if (!proxyServer.isEmpty()) {
79 | initConfig.setProxy(proxyServer, proxyPort);
80 | if (!proxyPass.isEmpty()) {
81 | initConfig.setProxyUsername(proxyUser);
82 | initConfig.setProxyPassword(proxyPass);
83 | }
84 | }
85 | PartnerConnection partnerTmpConnection = Connector.newConnection(initConfig);
86 |
87 | LoginResult loginResult = partnerTmpConnection.login(initConfig.getUsername(), initConfig.getPassword());
88 | metadataConfig.setServiceEndpoint(loginResult.getMetadataServerUrl());
89 | metadataConfig.setSessionId(loginResult.getSessionId());
90 | metadataConfig.setProxy(initConfig.getProxy());
91 | metadataConfig.setProxyUsername(initConfig.getProxyUsername());
92 | metadataConfig.setProxyPassword(initConfig.getProxyPassword());
93 |
94 | metadataConnection = new MetadataConnection(metadataConfig);
95 |
96 | ConnectorConfig signedInConfig = new ConnectorConfig();
97 | signedInConfig.setSessionId(loginResult.getSessionId());
98 | signedInConfig.setServiceEndpoint(loginResult.getServerUrl());
99 | partnerConnection = Connector.newConnection(signedInConfig);
100 | }
101 |
102 | public PartnerConnection getPartnerConnection() {
103 | return this.partnerConnection;
104 | }
105 |
106 | /**
107 | * Sets configuration and performs the deployment of metadata to a Salesforce organization
108 | *
109 | * @param bytes
110 | * @param validateOnly
111 | * @param testLevel
112 | * @param specifiedTests
113 | * @param containsApex
114 | * @return
115 | * @throws Exception
116 | */
117 | public boolean deployToServer(ByteArrayOutputStream bytes,
118 | TestLevel testLevel,
119 | String[] specifiedTests,
120 | boolean validateOnly,
121 | boolean containsApex) throws Exception
122 | {
123 | DeployOptions deployOptions = new DeployOptions();
124 | deployOptions.setPerformRetrieve(false);
125 | deployOptions.setRollbackOnError(true);
126 | deployOptions.setSinglePackage(true);
127 | deployOptions.setCheckOnly(validateOnly);
128 |
129 | // We need to make sure there are actually tests supplied for RunSpecifiedTests...
130 | if (testLevel.equals(TestLevel.RunSpecifiedTests)) {
131 | if (specifiedTests.length > 0) {
132 | deployOptions.setTestLevel(testLevel);
133 | deployOptions.setRunTests(specifiedTests);
134 | } else {
135 | deployOptions.setTestLevel(TestLevel.NoTestRun);
136 | }
137 | } else if (containsApex) { // And that we should even set a TestLevel
138 | deployOptions.setTestLevel(testLevel);
139 | }
140 |
141 | AsyncResult asyncResult = metadataConnection.deploy(bytes.toByteArray(), deployOptions);
142 | String asyncResultId = asyncResult.getId();
143 |
144 | int poll = 0;
145 | int maxPoll = Integer.valueOf(maxPollString);
146 | long pollWait = Long.valueOf(pollWaitString);
147 | boolean fetchDetails;
148 | do {
149 | Thread.sleep(pollWait);
150 |
151 | if (poll++ > maxPoll) {
152 | throw new Exception("[SMA] Request timed out. You can check the results later by using this AsyncResult Id: " + asyncResultId);
153 | }
154 | // Only fetch the details every three poll attempts
155 | fetchDetails = (poll % 3 == 0);
156 | deployResult = metadataConnection.checkDeployStatus(asyncResultId, fetchDetails);
157 | } while (!deployResult.isDone());
158 |
159 | // This is more to do with errors related to Salesforce. Actual deployment failures are not returned as error codes.
160 | if (!deployResult.isSuccess() && deployResult.getErrorStatusCode() != null) {
161 | throw new Exception(deployResult.getErrorStatusCode() + " msg:" + deployResult.getErrorMessage());
162 | }
163 |
164 | if (!fetchDetails) {
165 | // Get the final result with details if we didn't do it in the last attempt.
166 | deployResult = metadataConnection.checkDeployStatus(asyncResultId, true);
167 | }
168 | deployDetails = deployResult.getDetails();
169 |
170 | return deployResult.isSuccess();
171 | }
172 |
173 | /**
174 | * Returns a formatted string of test failures for printing to the Jenkins console
175 | *
176 | * @return
177 | */
178 | public String getTestFailures() {
179 | RunTestsResult rtr = deployDetails.getRunTestResult();
180 | StringBuilder buf = new StringBuilder();
181 |
182 | if (rtr.getFailures().length > 0) {
183 | buf.append("[SMA] Test Failures\n");
184 |
185 | for (RunTestFailure failure : rtr.getFailures()) {
186 | String n = (failure.getNamespace() == null ? "" :
187 | (failure.getNamespace() + ".")) + failure.getName();
188 | buf.append("Test failure, method: " + n + "." +
189 | failure.getMethodName() + " -- " +
190 | failure.getMessage() + " stack " +
191 | failure.getStackTrace() + "\n\n");
192 | }
193 | }
194 | return buf.toString();
195 | }
196 |
197 | /**
198 | * Returns a formatted string of component failures for printing to the Jenkins console
199 | *
200 | * @return
201 | */
202 | public String getComponentFailures() {
203 | DeployMessage messages[] = deployDetails.getComponentFailures();
204 | StringBuilder buf = new StringBuilder();
205 |
206 | for (DeployMessage message : messages) {
207 | if (!message.isSuccess()) {
208 | buf.append("[SMA] Component Failures\n");
209 |
210 | String loc = null;
211 | if (message.getLineNumber() > 0) {
212 | loc = "(" + message.getLineNumber() + "," + message.getColumnNumber() + ")";
213 | } else if (!message.getFileName().equals(message.getFullName())) {
214 | loc = "(" + message.getFullName() + ")";
215 | }
216 | buf.append(message.getFileName() + loc + ":" + message.getProblem()).append('\n');
217 | }
218 | }
219 | return buf.toString();
220 | }
221 |
222 | /**
223 | * Returns a formatted string of the code coverage information for this deployment
224 | *
225 | * @return
226 | */
227 | public String getCodeCoverage() {
228 | RunTestsResult rtr = deployDetails.getRunTestResult();
229 | StringBuilder buf = new StringBuilder();
230 | DecimalFormat df = new DecimalFormat("#.##");
231 |
232 | //Get the individual coverage results
233 | CodeCoverageResult[] ccresult = rtr.getCodeCoverage();
234 |
235 | if (ccresult.length > 0) {
236 | buf.append("[SMA] Code Coverage Results\n");
237 |
238 | double loc = 0;
239 | double locUncovered = 0;
240 | for (CodeCoverageResult ccr : ccresult) {
241 | buf.append(ccr.getName() + ".cls");
242 | buf.append(" -- ");
243 | loc = ccr.getNumLocations();
244 | locUncovered = ccr.getNumLocationsNotCovered();
245 |
246 | double coverage = 0;
247 | if (loc > 0) {
248 | coverage = calculateCoverage(locUncovered, loc);
249 | }
250 | buf.append(df.format(coverage) + "%\n");
251 | }
252 |
253 | // Get the total code coverage for this deployment
254 | double totalCoverage = getTotalCodeCoverage(ccresult);
255 | buf.append("\nTotal code coverage for this deployment -- ");
256 | buf.append(df.format(totalCoverage) + "%\n");
257 | }
258 | return buf.toString();
259 | }
260 |
261 | /**
262 | * Returns a formatted string of code coverage warnings for printing to the Jenkins console
263 | *
264 | * @return
265 | */
266 | public String getCodeCoverageWarnings() {
267 | RunTestsResult rtr = deployDetails.getRunTestResult();
268 | StringBuilder buf = new StringBuilder();
269 | CodeCoverageWarning[] ccwarn = rtr.getCodeCoverageWarnings();
270 |
271 | if (ccwarn.length > 0) {
272 | buf.append("[SMA] Code Coverage Warnings\n");
273 |
274 | for (CodeCoverageWarning ccw : ccwarn) {
275 | buf.append("Code coverage issue");
276 |
277 | if (ccw.getName() != null) {
278 | String n = (ccw.getNamespace() == null ? "" :
279 | (ccw.getNamespace() + ".")) + ccw.getName();
280 | buf.append(", class: " + n);
281 | }
282 | buf.append(" -- " + ccw.getMessage() + "\n");
283 | }
284 | }
285 | return buf.toString();
286 | }
287 |
288 | /**
289 | * Returns the DeployDetails from this deployment
290 | *
291 | * @return
292 | */
293 | public DeployDetails getDeployDetails() { return deployDetails; }
294 |
295 | /**
296 | * Sets the DeployDetails for this deployment. For unit tests
297 | *
298 | * @param deployDetails
299 | */
300 | public void setDeployDetails(DeployDetails deployDetails) { this.deployDetails = deployDetails; }
301 |
302 | /**
303 | * Helper method to calculate the total code coverage in this deployment
304 | *
305 | * @param ccresult
306 | * @return
307 | */
308 | private Double getTotalCodeCoverage(CodeCoverageResult[] ccresult) {
309 | double zeroCoverage = 0;
310 |
311 | if (ccresult.length == 0) { return zeroCoverage; }
312 |
313 | double totalLoc = 0;
314 | double totalLocUncovered = 0;
315 |
316 | for (CodeCoverageResult ccr : ccresult) {
317 | totalLoc += ccr.getNumLocations();
318 | totalLocUncovered += ccr.getNumLocationsNotCovered();
319 | }
320 | if (totalLoc == 0) { return zeroCoverage; }
321 |
322 | return calculateCoverage(totalLocUncovered, totalLoc);
323 | }
324 |
325 | /**
326 | * Helper method to calculate the double for the coverage
327 | *
328 | * @param totalLocUncovered
329 | * @param totalLoc
330 | * @return
331 | */
332 | private double calculateCoverage(double totalLocUncovered, double totalLoc) {
333 | return (1 - (totalLocUncovered / totalLoc)) * 100;
334 | }
335 |
336 | public void createJenkinsCICustomSettingsSObject() throws ConnectionException {
337 | CustomObject cs = new CustomObject();
338 | cs.setCustomSettingsType(CustomSettingsType.Hierarchy);
339 | String name = "JenkinsCISettings";
340 | cs.setFullName(name + "__c");
341 | cs.setLabel(name);
342 |
343 | String gitSha1FieldDevName = "GitSha1";
344 | CustomField gitSha1Field = new CustomField();
345 | gitSha1Field.setType(FieldType.Text);
346 | gitSha1Field.setLength(255);
347 | gitSha1Field.setLabel("Git SHA1");
348 | gitSha1Field.setFullName(gitSha1FieldDevName + "__c");
349 |
350 | String gitDeploymentDateDevName = "GitDeploymentDate";
351 | CustomField gitDeploymentDateField = new CustomField();
352 | gitDeploymentDateField.setType(FieldType.DateTime);
353 | gitDeploymentDateField.setLabel("Git Deployment Date");
354 | gitDeploymentDateField.setFullName(gitDeploymentDateDevName + "__c");
355 |
356 | String jobNameDevName = "JenkinsJobName";
357 | CustomField jobNameField = new CustomField();
358 | jobNameField.setType(FieldType.Text);
359 | jobNameField.setLength(255);
360 | jobNameField.setLabel("Jenkins Job Name");
361 | jobNameField.setFullName(jobNameDevName + "__c");
362 |
363 | String buildNumberDevName = "JenkinsBuildNumber";
364 | CustomField buildNumberField = new CustomField();
365 | buildNumberField.setType(FieldType.Text);
366 | buildNumberField.setLength(255);
367 | buildNumberField.setLabel("Jenkins Build Number");
368 | buildNumberField.setFullName(buildNumberDevName + "__c");
369 |
370 | cs.setFields(new CustomField[] { gitSha1Field, gitDeploymentDateField, jobNameField, buildNumberField });
371 |
372 | com.sforce.soap.metadata.SaveResult[] results = metadataConnection.createMetadata(new Metadata[] { cs });
373 |
374 | for (com.sforce.soap.metadata.SaveResult r : results) {
375 | if (r.isSuccess()) {
376 | LOG.warning("Component '" + r.getFullName() + "' created successfully!");
377 | } else {
378 | LOG.warning("Could not create component '" + r.getFullName() + "'. Errors: ");
379 | for (Error err : r.getErrors()) {
380 | LOG.warning("- " + err.getMessage());
381 |
382 | }
383 | }
384 | }
385 | }
386 |
387 | public void saveJenkinsCISettings(SObject settings) throws ConnectionException {
388 | com.sforce.soap.partner.UpsertResult[] res = partnerConnection.upsert("Name", new SObject[] { settings });
389 | for (com.sforce.soap.partner.UpsertResult r : res) {
390 | if (r.isSuccess()) {
391 | LOG.warning("Upsert of JenkinsCISettings should have been successful");
392 |
393 | } else {
394 | LOG.warning("Error while saving JenkinsCISettings: ");
395 | for (com.sforce.soap.partner.Error err : r.getErrors()) {
396 | LOG.warning("- " + err.getMessage());
397 | }
398 | }
399 | }
400 | }
401 |
402 | public SObject retrieveJenkinsCISettingsFromOrg() throws Exception {
403 | QueryResult qr = partnerConnection.query("SELECT Name, GitSha1__c, GitDeploymentDate__c FROM JenkinsCISettings__c WHERE Name = 'SMA' LIMIT 1");
404 | SObject[] sobjs = qr.getRecords();
405 |
406 | if (sobjs.length == 0) {
407 | throw new NoSuchElementException("Could not find a JenkinsCISettings record");
408 | }
409 | return sobjs[0];
410 | }
411 | }
--------------------------------------------------------------------------------