Tuesday, May 5, 2015

Working with Salesforce's destructiveChanges.xml

If you've ever had a need to remove a bunch of custom objects, fields, pages, classes, etc. from an org, or from multiple orgs you've probably come across documentation about destructiveChanges.xml.  If you're familiar with developing on the Salesforce platform using Maven's Mate or Eclipse, you're probably already familiar with package.xml.  Both files have nearly identically formats.  The difference between them is package.xml enumerates the stuff you want to synchronize between your org and your development environment and destructiveChanges.xml enumerates the items you want to obliterate (or delete) from whatever org you point it at.


The easiest way to see how they're identical is to look at what each of them looks like empty.

package.xml
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <version>29.0</version>
</Package>

destructiveChanges.xml
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
</Package>

The only difference between them is destructiveChanges doesn't have a <version> tag.

Let's look again after we add a class to each.  In package.xml we're synchronizing a class and in destructiveChanges.xml its a class we want to remove from our org.

package.xml
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <version>29.0</version>
    <types>
        <members>TomTest</members>
        <name>ApexClass</name>
    <types>
</Package>

destructiveChanges.xml
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>TomTest</members>
        <name>ApexClass</name>
    <types>
</Package>

As a percentage, the two files are more similar now than they were before. The only difference between them is still the <version> tag.

Executing destructive changes

So how do we execute destructive changes?  The short answer is using Salesforce's migration tool.  In a few minutes we'll execute "ant undeployCode," but we've a few items to take care of first.

For me, the first problem was where to put the files destructiveChanges.xml and package.xml. The former is new and the latter is NOT the same file that usually appears in the src/ directory.

At Xede, we create git repositories for our projects.  Each repository is forked from xede-sf-template.

DrozBook:git tgagne$ ls -lR xede-sf-template
total 16
-rw-r--r--  1 tgagne  staff   684 Aug 21  2014 README.md
-rwxr-xr-x  1 tgagne  staff  1430 Sep 17  2014 build.xml
drwxr-xr-x  4 tgagne  staff   136 Aug 21  2014 del

xede-sf-template//del:
total 16
-rwxr-xr-x  1 tgagne  staff  563 Jan 17  2014 destructiveChanges.xml
-rw-r--r--  1 tgagne  staff  136 Aug 21  2014 package.xml

The repo includes a directory named "del" (not very imaginative) and inside it are the files destructiveChanges.xml and package.xml.  It seems odd to me, but the migration tool requires both the destructiveChanges.xml AND a package.xml to reside there.

The package.xml file is the same empty version as before.  But the template's destructiveChanges.xml contains placeholders--but still basically does nothing.

DrozBook:xede-sf-template tgagne$ cat del/package.xml
<package xmlns="http://soap.sforce.com/2006/04/metadata">
    <version>29.0</version>
</package>

DrozBook:xede-sf-template tgagne$ cat del/destructiveChanges.xml 
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <name>ApexClass</name>
    </types>
    <types>
        <name>ApexComponent</name>
    </types>
    <types>
        <name>ApexPage</name>
    </types>
    <types>
        <name>ApexTrigger</name>
    </types>
    <types>
        <name>CustomObject</name>
    </types>
    <types>
        <name>Flow</name>
    </types>
    <types>
        <name>StaticResource</name>
    </types>
    <types>
        <name>Workflow</name>
    </types>
</Package>

Now that we have a directory with both files in it, and we have versions of those files that basically do nothing, let's get ready to run the tool.

There's one more file we need to create that's required by the tool, build.xml.  If you're not already using it for deployments you're likely not using it at all.  My version of build.xml is in the parent of del/.  You can see it above in the directory listing of xede-sf-template.

DrozBook:xede-sf-template tgagne$ cat build.xml
<project name="xede-sf-template" default="usage" basedir="." xmlns:sf="antlib:com.salesforce">

    <property environment="env"/>

    <target name="undeployCode">
      <sf:deploy 
 username="${env.SFUSER}" 
 password="${env.SFPASS}" 
 serverurl="${env.SFURL}" 
 maxPoll="${env.SFPOLL}" 
 ignoreWarnings="true"
 checkOnly="${env.CHECKONLY}"
 runAllTests="${env.RUNALLTESTS}"
        deployRoot="del"/>
    </target>

</project>
If 
Since build.xml is in the parent directory to del/ the "deployRoot" attribute is "del," the subdirectory.

The environment property (<property environment.../>) allows operating system environment variables to be substituted inside your build.xml.  In the example above, the environment variables are about what you'd expect them to be (using the bash shell):

export SFUSER=myusername
export SFPASS=mysecretpassword
export SFURL=https://login.salesforce.com (or https://test.salesforce.com)
export SFPOLL=120
export CHECKONLY=false
export RUNALLTESTS=false

Right about now you may be thinking, "Who wants to set all those environment variables?" Truthfully, I don't.  That's why I created a little script to do it for me called "build."  But before we get into that let's just edit our build.xml file so it doesn't need environment variables.

The build.xml below is for a production org.

DrozBook:xede-sf-template tgagne$ cat build.xml
<project name="xede-sf-template" default="usage" basedir="." xmlns:sf="antlib:com.salesforce">

    <target name="undeployCode">
      <sf:deploy 
 username="tgagne+customer@xede.com" 
 password="mysupersecretpassword" 
 serverurl="https://login.salesforce.com" 
 maxPoll="120" 
 ignoreWarnings="true"
 checkOnly="false"
 runAllTests="false"
        deployRoot="del"/>
    </target>

</project>

So now we have our build.xml, our del directory, del/destructiveChanges.xml which lists nothing and an empty del/package.xml file.  Let's run ant.

DrozBook:xede-sf-template tgagne$ ant undeployCode
Buildfile: /Users/tgagne/git/xede-sf-template/build.xml

undeployCode:
[sf:deploy] Request for a deploy submitted successfully.
[sf:deploy] Request ID for the current deploy task: 0AfU00000034k0SKAQ
[sf:deploy] Waiting for server to finish processing the request...
[sf:deploy] Request Status: InProgress
[sf:deploy] Request Status: Succeeded
[sf:deploy] *********** DEPLOYMENT SUCCEEDED ***********
[sf:deploy] Finished request 0AfU00000034k0SKAQ successfully.

BUILD SUCCESSFUL
Total time: 15 seconds

As you can see, it did nothing.  Let's give it something to do, but make it a class that doesn't exist in the target org.

DrozBook:xede-sf-template tgagne$ cat del/destructiveChanges.xml
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>DoesNotExist</members>
        <name>ApexClass</name>
    </types>
    ... same as before ...
</Package>

I've added a single class, DoesNotExist, to the ApexClass types list and we'll run it again.

DrozBook:xede-sf-template tgagne$ ant undeployCode
Buildfile: /Users/tgagne/git/xede-sf-template/build.xml

undeployCode:
[sf:deploy] Request for a deploy submitted successfully.
[sf:deploy] Request ID for the current deploy task: 0AfU00000034k0mKAA
[sf:deploy] Waiting for server to finish processing the request...
[sf:deploy] Request Status: InProgress
[sf:deploy] Request Status: Succeeded
[sf:deploy] *********** DEPLOYMENT SUCCEEDED ***********
[sf:deploy] All warnings:
[sf:deploy] 1.  destructiveChanges.xml -- Warning: No ApexClass named: DoesNotExist found
[sf:deploy] *********** DEPLOYMENT SUCCEEDED ***********
[sf:deploy] Finished request 0AfU00000034k0mKAA successfully.

BUILD SUCCESSFUL
Total time: 15 seconds

Ant (with the migration tool plugin) is telling us it tried removing the Apex class "DoesNotExist" but it didn't exist.  If the class had existed before but had already been removed this is the message it would display.

As a reader exercise, go ahead and create a class "DoesNotExist" in your org.  I went into Setup->Classes->New and entered "public class DoesNotExist{}". It's about as useless a class as you can create, though I've seen and perhaps written worse.

If you run ant again you'll see it doesn't report an error.
DrozBook:xede-sf-template tgagne$ ant undeployCode
Buildfile: /Users/tgagne/git/xede-sf-template/build.xml

undeployCode:
[sf:deploy] Request for a deploy submitted successfully.
[sf:deploy] Request ID for the current deploy task: 0AfU00000034k11KAA
[sf:deploy] Waiting for server to finish processing the request...
[sf:deploy] Request Status: InProgress
[sf:deploy] Request Status: Succeeded
[sf:deploy] *********** DEPLOYMENT SUCCEEDED ***********
[sf:deploy] Finished request 0AfU00000034k11KAA successfully.

BUILD SUCCESSFUL
Total time: 15 seconds

And there you have it!  For a little extra I'll share my "build" script which makes it pretty easy to extract, undeploy (what we just did) and deploy code with or without tests or verification-only.

No comments:

Post a Comment

Follow @TomGagne