├── .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 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 | } --------------------------------------------------------------------------------