Skip to content

Instantly share code, notes, and snippets.

@Szandor72
Forked from mattandneil/build.xml
Created August 10, 2017 11:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Szandor72/eea87973555eb2295442be39881427bb to your computer and use it in GitHub Desktop.
Save Szandor72/eea87973555eb2295442be39881427bb to your computer and use it in GitHub Desktop.
Salesforce Organization Destroy - Ant Script

Salesforce Organization Destroy - Ant Script

This script searches and destroys (most) metadata in an organization. Use cases include ISV package development and testing within Developer Edition and Sandbox environments.

Usage: (paste the macrodef XML into your build file)

<target name="destroy">
    <destroy
        username="${sf.username}"
        password="${sf.password}"
        serverurl="${sf.serverurl}"
    />
</target>

It forces an input prompt for the URL and username:

THIS TASK IRREVERSIBLY DESTROYS ALL METADATA. ARE YOU SURE?
(https://test.salesforce.com/?un=username@organization...)

How does it work?

First sf:describeMetadata determines the "shape" of the organization. Then, all components are listed in turn from each metadata type. The results are appended in a destructiveChangesPost.xml manifest which gets deployed back over the org.

Does it unlink dependencies?

Yes. Dependencies are cleared in one metadata deployment. It takes advantage of destructive changes POST-deploy, which clears everything in a single transaction. Specifically:

  • Page Layouts are cleared of custom buttons and custom links
  • Sites are cleared of static resources and visualforce pages
  • Profiles are cleared of default application visibilities
  • Roles are cleared of their parent hierarchy association

What cannot be destroyed?

Some metadata types such as Assignment Rules act as permanent containers for the individual rules. Or others such as Record Types do not support delete operation. These fall into a few different categories.

Rules whose child types CAN be destroyed independently:

  • Assignment Rules
  • AutoResponse Rules
  • Escalation Rules
  • Matching Rules
  • Sharing Rules
  • Workflow Rules

Standard component types which cannot be destroyed:

  • Field
  • Object
  • Layout
  • Profile
  • App Menu
  • Matching Rule
  • Clean Data Service
  • Business Process as in Case record type
  • Community as in Idea Zone, not to be confused with "Network"

Others:

  • Force.com Sites (permanent and cannot be destroyed)
  • Flow (the flow definitions CAN be destroyed independently)
  • Certificate (left intact to avoid interfering with Environment Hub)
  • CustomObjectTranslation (each entry is maintained when the object)
  • InstalledPackage (best uninstalled via UI which nags about extensions etc)
  • RecordType (can be destroyed via UI which involves an asynchronous background process)

The other unsupported metadata types are well documented in the Metadata API guide

What about Custom Metadata Types?

This script destroys both the named metadata records, and also the custom definition.

<macrodef name="destroy" description="Destroys all metadata in an organization - Revision 16">
<attribute name="username" />
<attribute name="password" />
<attribute name="serverurl" default="https://login.salesforce.com" />
<attribute name="tempDir" default="temp/destroy" description="Directory to write metadata." />
<attribute name="apiVersion" default="40.0" />
<sequential>
<!-- prompt user to confirm -->
<input message="THIS TASK IRREVERSIBLY DESTROYS ALL METADATA. ARE YOU SURE?" validargs="@{serverurl}/?un=@{username}" />
<!-- http api helper -->
<macrodef name="soapcall">
<text name="request" />
<attribute name="endpoint" />
<attribute name="tempfile" default="" />
<attribute name="soapaction" default="&quot;&quot;" />
<sequential>
<local name="request" />
<property name="request" value="@{request}" />
<script language="javascript">with (new JavaImporter(java.net, java.io)) {
var line, result = '', connection = new URL('@{endpoint}').openConnection();
connection.setDoOutput(true);
connection.setRequestMethod('POST');
connection.setRequestProperty('Content-Type', 'text/xml');
connection.setRequestProperty('SOAPAction', '@{soapaction}');
var writer = new OutputStreamWriter(connection.getOutputStream());
writer.write(project.getProperty('request')); writer.flush(); //request
var reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while ((line = reader.readLine()) != null) result += line + '\n'; reader.close(); //response
var echo = project.createTask('echo');
if ('@{tempfile}') echo.setFile(new File('@{tempfile}'));
echo.setMessage(result);
echo.perform();
}</script>
</sequential>
</macrodef>
<!-- http soap login -->
<local name="loginResponse.tmp" />
<tempfile property="loginResponse.tmp" prefix="loginResponse" suffix=".tmp" createfile="true" deleteonexit="true" />
<soapcall tempfile="${loginResponse.tmp}" endpoint="@{serverurl}/services/Soap/u/@{apiVersion}" soapaction="login"><![CDATA[
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<login xmlns="urn:partner.soap.sforce.com">
<username>@{username}</username>
<password>@{password}</password>
</login>
</Body>
</Envelope>
]]></soapcall>
<!-- parse endpoint -->
<local name="loginUrl" />
<loadfile property="loginUrl" srcFile="${loginResponse.tmp}">
<filterchain><tokenfilter><filetokenizer/><replaceregex flags="gs" pattern=".*(https://[^/]+).*" replace="\1" /></tokenfilter></filterchain>
</loadfile>
<!-- parse session -->
<local name="sessionId" />
<loadfile property="sessionId" srcFile="${loginResponse.tmp}">
<filterchain><tokenfilter><filetokenizer/><replaceregex flags="gs" pattern=".*&lt;sessionId&gt;([^&lt;]+)&lt;/sessionId&gt;.*" replace="\1" /></tokenfilter></filterchain>
</loadfile>
<!-- clear role and permission set dependencies with execanon -->
<soapcall endpoint="${loginUrl}/services/Soap/T/@{apiVersion}"><![CDATA[
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Header>
<SessionHeader xmlns="urn:tooling.soap.sforce.com">
<sessionId>${sessionId}</sessionId>
</SessionHeader>
</Header>
<Body>
<executeAnonymous xmlns="urn:tooling.soap.sforce.com">
<String>
delete [SELECT Id FROM PermissionSetAssignment WHERE PermissionSet.ProfileId = null];
User[] users = [SELECT Id FROM User WHERE UserRoleId != null];
for (User user : users) user.UserRoleId = null;
update users;
</String>
</executeAnonymous>
</Body>
</Envelope>
]]></soapcall>
<!-- reset working directory -->
<delete dir="@{tempDir}" />
<mkdir dir="@{tempDir}" />
<!-- determines org shape -->
<local name="describeMetadataResult.tmp" />
<tempfile property="describeMetadataResult.tmp" prefix="describeMetadataResult" suffix=".tmp" createfile="true" deleteonexit="true" />
<sf:describeMetadata serverurl="${loginUrl}" sessionid="${sessionId}" resultFilePath="${describeMetadataResult.tmp}" />
<!-- clean metadata descriptions -->
<local name="metadataTypes.tmp" />
<tempfile property="metadataTypes.tmp" prefix="metadataTypes" suffix=".tmp" createfile="true" deleteonexit="true" />
<concat destFile="${metadataTypes.tmp}">
<fileset file="${describeMetadataResult.tmp}" />
<filterchain>
<linecontainsregexp><regexp pattern="ChildObjects|XMLName" /></linecontainsregexp>
<tokenfilter><replacestring from="," to="${line.separator}"/></tokenfilter>
<tokenfilter><replacestring from="ChildObjects: " to=""/></tokenfilter>
<tokenfilter><replacestring from="XMLName: " to=""/></tokenfilter>
<tokenfilter><replacestring from="*" to=""/></tokenfilter>
<tokenfilter><ignoreblank/></tokenfilter>
<sortfilter/>
</filterchain>
</concat>
<!-- lists by type with regex filter -->
<macrodef name="listMetadataForDestroy">
<attribute name="negate" />
<attribute name="pattern" />
<attribute name="metadataType" />
<sequential>
<echo>Preparing destructiveChangesPost.xml - @{metadataType}</echo>
<local name="listMetadataResult.tmp" />
<tempfile property="listMetadataResult.tmp" prefix="@{metadataType}" suffix=".tmp" createfile="true" deleteonexit="true" />
<sf:listMetadata serverurl="${loginUrl}" sessionid="${sessionId}" metadataType="@{metadataType}" resultFilePath="${listMetadataResult.tmp}" />
<concat destFile="@{tempDir}/destructiveChangesPost.xml" append="true">
<fileset file="${listMetadataResult.tmp}" />
<header filtering="false"><![CDATA[${line.separator}<types>${line.separator} <name>@{metadataType}</name>${line.separator}]]></header>
<filterchain>
<linecontains><contains value="FullName/Id" /></linecontains>
<replaceregex pattern="FullName/Id: (.+)/.*" replace="&lt;members&gt;\1&lt;/members&gt;" />
<linecontainsregexp negate="@{negate}"><regexp pattern="@{pattern}" /></linecontainsregexp>
</filterchain>
<footer filtering="false"><![CDATA[</types>]]></footer>
</concat>
</sequential>
</macrodef>
<!-- open destructive changes definition -->
<echo file="@{tempDir}/destructiveChangesPost.xml"><![CDATA[<Package>]]></echo>
<!-- iterates over (most) metadata types -->
<loadfile property="" srcFile="${metadataTypes.tmp}">
<filterchain>
<!-- AppMenu - AppSwitcher.appmenu - Error: The AppMenu called 'AppSwitcher' is standard and cannot be deleted -->
<!-- AssignmentRules - Case.assignmentRules - Error: The AssignmentRules called 'Case' is standard and cannot be deleted -->
<!-- AutoResponseRules - Lead.autoResponseRules - Error: The AutoResponseRules called 'Lead' is standard and cannot be deleted -->
<!-- Certificate - SelfSignedCert.crt - Error: We can't delete this certificate because your Identity Provider is using it -->
<!-- CleanDataService - DataCloudCompanyMatch - Error: You can't delete default data integration rule -->
<!-- Community - Zone.community - Error: invalid parameter value -->
<!-- CustomSite - BigAss.site - Error: insufficient access rights on cross-reference id -->
<!-- CustomObjectTranslation - MyMeta__mdt-en_US - Error: The CustomObjectTranslation called 'MyMeta__mdt-en_US' is standard and cannot be deleted -->
<!-- EscalationRules - Case.escalationRules: - Error: The EscalationRules called 'Case' is standard and cannot be deleted -->
<!-- Flow - TaskNotify.flowDefinition - Error: insufficient access rights on cross-reference id -->
<!-- InstalledPackage - Error: cannot modify managed object: state=installed -->
<!-- MatchingRules - Account.matchingRule: - Error: Matching Rules have to be deleted individually -->
<!-- RecordType - Metric.Completion - Error: Cannot delete record type through API -->
<!-- SharingRules - Account.sharingRules - Error: The SharingRules called 'Account' is standard and cannot be deleted -->
<!-- Workflow - Account.workflow - Error: Cannot delete a workflow object; Workflow Rules and Actions must be deleted individually -->
<linecontainsregexp negate="true">
<regexp pattern="AppMenu|AssignmentRules|AutoResponseRules|Certificate|CleanDataService|Community|CustomSite|CustomObjectTranslation|EscalationRules|Flow|InstalledPackage|MatchingRules|RecordType|SharingRules|Workflow" />
</linecontainsregexp>
<!-- Layout - Remove only: Account-Account %28Marketing%29 and WorkFeedback-Feedback Layout - Summer %2715 etc -->
<!-- Profile - Remove only: Custom: Marketing Profile and Custom: Support Profile and Custom: Sales Profile etc -->
<!-- ListView - Leaves behind: Activity.All, Asset.All, Campaign.All, Contract.All, Product2.All, User.All etc -->
<!-- ApexPage - Leaves behind: SiteHome Visualforce Page which is required in orgs containing Force.com Sites -->
<!-- CustomField - Leaves behind: BigObject Customer_Interaction__b.Score_This_Game__c etc -->
<!-- MatchingRule - Leaves behind: Lead.Standard_Lead_Match_Rule_v1_0 and Account.Standard_Account_Match_Rule_v1_0 etc -->
<!-- CustomObject - Remove only: Big Objects, Custom Objects, Platform Events, External Objects etc -->
<!-- BusinessProcess - Leaves behind: Case.master etc -->
<!-- CustomApplication - Leaves behind: standard__AppLauncher etc -->
<scriptfilter language="javascript">
var negate = false, pattern = '.*', metadataType = self.getToken();
if ('Layout' == metadataType) (negate = false) | (pattern = '%27|%28|%29');
if ('Profile' == metadataType) (negate = false) | (pattern = 'Custom%3A');
if ('ListView' == metadataType) (negate = true) | (pattern = '\\.All&lt;/members&gt;');
if ('ApexPage' == metadataType) (negate = true) | (pattern = '&gt;SiteHome&lt;');
if ('CustomField' == metadataType) (negate = true) | (pattern = '__b\\.');
if ('MatchingRule' == metadataType) (negate = true) | (pattern = 'Standard_');
if ('CustomObject' == metadataType) (negate = false) | (pattern = '__b|__c|__e|__x|__mdt');
if ('BusinessProcess' == metadataType) (negate = true) | (pattern = 'master');
if ('CustomApplication' == metadataType) (negate = true) | (pattern = 'standard__');
var macro = project.createTask('listMetadataForDestroy');
macro.setDynamicAttribute('negate', negate);
macro.setDynamicAttribute('pattern', pattern);
macro.setDynamicAttribute('metadatatype', metadataType);
macro.execute(); //dynamic attributes are lowercase insistent
</scriptfilter>
</filterchain>
</loadfile>
<!-- close destructive changes definition -->
<echo append="true" file="@{tempDir}/destructiveChangesPost.xml"><![CDATA[</Package>]]></echo>
<!-- retrieves by type and regex replaces -->
<macrodef name="bulkRetrieveForDestroy">
<attribute name="metadataType" />
<attribute name="directoryName" />
<attribute name="pattern" />
<attribute name="expression" />
<sequential>
<echo>Preparing package.xml - @{metadataType}</echo>
<mkdir dir="@{tempDir}/@{directoryName}" />
<sf:bulkRetrieve serverurl="${loginUrl}" sessionid="${sessionId}" retrieveTarget="@{tempDir}" metadataType="@{metadataType}" batchSize="10000" />
<replaceregexp flags="gs">
<fileset dir="@{tempDir}/@{directoryName}" />
<regexp pattern="@{pattern}" />
<substitution expression="@{expression}" />
</replaceregexp>
</sequential>
</macrodef>
<!-- fix layout custom links - Error: This WebLink is referenced elsewhere in salesforce.com -->
<bulkRetrieveForDestroy
metadataType="Layout"
directoryName="layouts"
pattern="&lt;layoutItems&gt;\s+&lt;customLink&gt;[^&lt;]+&lt;/customLink&gt;\s+&lt;/layoutItems&gt;"
expression="&lt;!--\0--&gt;"
/>
<!-- fix layout custom buttons - Error: This WebLink is referenced elsewhere in salesforce.com - Order-Order Layout -->
<replaceregexp flags="gs">
<fileset dir="@{tempDir}/layouts" />
<regexp pattern="&lt;customButtons&gt;[^&lt;]+&lt;/customButtons&gt;" />
<substitution expression="&lt;!--\0--&gt;" />
</replaceregexp>
<!-- fix profile default apps - Error: Unable to delete custom app. Profiles are using this custom app as default -->
<bulkRetrieveForDestroy
metadataType="Profile"
directoryName="profiles"
pattern="&lt;userPermissions&gt;.*&lt;/userPermissions&gt;"
expression="&lt;applicationVisibilities&gt;&lt;application&gt;standard__AppLauncher&lt;/application&gt;&lt;default&gt;true&lt;/default&gt;&lt;visible&gt;true&lt;/visible&gt;&lt;/applicationVisibilities&gt;"
/>
<!-- fix role parents - Error: Your attempt to delete the role could not be completed because at least one role reports to that role -->
<bulkRetrieveForDestroy
metadataType="Role"
directoryName="roles"
pattern="&lt;parentRole&gt;[^&lt;]+&lt;/parentRole&gt;"
expression="&lt;!--\0--&gt;"
/>
<!-- fix object listviews - Error: cannot delete last filter -->
<bulkRetrieveForDestroy
metadataType="ListView"
directoryName="objects"
pattern="&lt;listViews&gt;.*&lt;/listViews&gt;"
expression="&lt;listViews&gt;&lt;fullName&gt;All&lt;/fullName&gt;&lt;filterScope&gt;Everything&lt;/filterScope&gt;&lt;label&gt;All&lt;/label&gt;&lt;/listViews&gt;"
/>
<!-- fix site dependencies - Error: This static resource is referenced elsewhere in salesforce.com. Remove the usage and try again -->
<bulkRetrieveForDestroy
metadataType="CustomSite"
directoryName="sites"
pattern="&lt;CustomSite xmlns=&quot;http://soap.sforce.com/2006/04/metadata&quot;&gt;.*&lt;active&gt;([^&lt;]+)&lt;/active&gt;.*&lt;allowStandardPortalPages&gt;([^&lt;]+)&lt;/allowStandardPortalPages&gt;.*&lt;clickjackProtectionLevel&gt;([^&lt;]+)&lt;/clickjackProtectionLevel&gt;.*&lt;indexPage&gt;([^&lt;]+)&lt;/indexPage&gt;.*&lt;masterLabel&gt;([^&lt;]+)&lt;/masterLabel&gt;.*&lt;requireHttps&gt;([^&lt;]+)&lt;/requireHttps&gt;.*&lt;siteType&gt;([^&lt;]+)&lt;/siteType&gt;.*&lt;subdomain&gt;([^&lt;]+)&lt;/subdomain&gt;.*&lt;/CustomSite&gt;"
expression="&lt;CustomSite xmlns=&quot;http://soap.sforce.com/2006/04/metadata&quot;&gt;${line.separator}&lt;active&gt;\1&lt;/active&gt;${line.separator}&lt;allowStandardPortalPages&gt;\2&lt;/allowStandardPortalPages&gt;${line.separator}&lt;clickjackProtectionLevel&gt;\3&lt;/clickjackProtectionLevel&gt;${line.separator}&lt;indexPage&gt;SiteHome&lt;/indexPage&gt;${line.separator}&lt;masterLabel&gt;\5&lt;/masterLabel&gt;${line.separator}&lt;requireHttps&gt;\6&lt;/requireHttps&gt;${line.separator}&lt;siteType&gt;\7&lt;/siteType&gt;${line.separator}&lt;subdomain&gt;\8&lt;/subdomain&gt;${line.separator}&lt;urlPathPrefix&gt;\5&lt;/urlPathPrefix&gt;${line.separator}&lt;/CustomSite&gt;"
/>
<!-- fix site index pages - Error: Required field is missing: indexPage -->
<local name="NumberOfSites.tmp" />
<condition property="NumberOfSites.tmp" else=""><resourcecount when="ne" count="0"><fileset dir="@{tempDir}/sites" /></resourcecount></condition>
<resourcecount property="NumberOfSites.tmp"><fileset dir="@{tempDir}/sites" /></resourcecount>
<script language="javascript">with (new JavaImporter(java.io)) {
if ('${NumberOfSites.tmp}') {
var page = project.createTask('echo');
page.setFile(new File('@{tempDir}/pages/SiteHome.page'));
page.setMessage('&lt;apex:page/&gt;');
page.perform();
var meta = project.createTask('echo');
meta.setFile(new File('@{tempDir}/pages/SiteHome.page-meta.xml'));
meta.setMessage('&lt;ApexPage&gt;&lt;label&gt;SiteHome&lt;/label&gt;&lt;/ApexPage&gt;');
meta.perform();
}
}</script>
<!-- fix support setting queue dependencies - Error: cannot delete queue that is in use -->
<mkdir dir="@{tempDir}/settings" />
<echoxml file="@{tempDir}/settings/Case.settings" namespacePolicy="all">
<CaseSettings>
<defaultCaseOwner>@{username}</defaultCaseOwner>
<defaultCaseOwnerType>User</defaultCaseOwnerType>
</CaseSettings>
</echoxml>
<!-- fix big objects - Error: Custom BigObjects do not support layouts -->
<delete><fileset dir="@{tempDir}" includes="**/*__b*" /></delete>
<!-- MANUAL COMPONENTS -->
<!-- CustomSite URL Rewriter is not available through Metadata API -->
<!-- Inbound Email Services are not available through Metadata API -->
<!-- Lead Settings behaviour is not available through Metadata API -->
<!-- strips namespaced components -->
<macrodef name="unspecifyForDestroy">
<attribute name="namespacePrefix" />
<sequential>
<echo>Stripping namespaced components: @{namespacePrefix}</echo>
<replaceregexp
flags="gm"
file="@{tempDir}/destructiveChangesPost.xml"
match="&lt;members&gt;[^&lt;]*@{namespacePrefix}__[^&lt;]+&lt;/members&gt;"
replace="&lt;!--\0--&gt;"
/>
<delete><fileset dir="@{tempDir}" includes="**/@{namespacePrefix}__*" /></delete>
</sequential>
</macrodef>
<!-- iterates over all namespace prefixes -->
<echo>Listing installed packages...</echo>
<local name="InstalledPackage.tmp" />
<tempfile property="InstalledPackage.tmp" prefix="InstalledPackage" suffix=".tmp" createfile="true" deleteonexit="true" />
<sf:listMetadata serverurl="${loginUrl}" sessionid="${sessionId}" metadataType="InstalledPackage" resultFilePath="${InstalledPackage.tmp}" />
<loadfile property="" srcFile="${InstalledPackage.tmp}">
<filterchain>
<linecontains><contains value="FullName/Id" /></linecontains>
<replaceregex pattern="FullName/Id: (.+)/.*" replace="\1" />
<sortfilter />
<uniqfilter />
<scriptfilter language="javascript">
var macro = project.createTask('unspecifyForDestroy');
macro.setDynamicAttribute('namespaceprefix', self.getToken());
macro.execute(); //dynamic attributes are lowercase insistent
</scriptfilter>
</filterchain>
</loadfile>
<!-- create package definition for fixes -->
<echoxml file="@{tempDir}/package.xml" namespacePolicy="all">
<Package>
<version>40.0</version>
<types>
<name>ApexPage</name>
<members>*</members>
</types>
<types>
<name>CustomSite</name>
<members>*</members>
</types>
<types>
<name>Layout</name>
<members>*</members>
</types>
<types>
<name>ListView</name>
<members>*</members>
</types>
<types>
<name>Profile</name>
<members>*</members>
</types>
<types>
<name>Role</name>
<members>*</members>
</types>
<types>
<name>Settings</name>
<members>*</members>
</types>
</Package>
</echoxml>
<!-- destroy! -->
<sf:deploy
serverurl="${loginUrl}"
sessionid="${sessionId}"
deployRoot="@{tempDir}"
ignoreWarnings="true"
singlePackage="true"
purgeOnDelete="true"
/>
</sequential>
</macrodef>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment