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