Last active
November 14, 2022 18:18
-
-
Save mattandneil/cc1e4b6bfe70e36bd3efd92952b95402 to your computer and use it in GitHub Desktop.
Salesforce Organization Destroy - Ant Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<macrodef name="destroy" description="Destroys all metadata in an organization - Revision 23"> | |
<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="43.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="""" /> | |
<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=".*<sessionId>([^<]+)</sessionId>.*" replace="\1" /></tokenfilter></filterchain> | |
</loadfile> | |
<!-- stop all running jobs and clear roles 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]; | |
for (SObject cron : [SELECT Id FROM CronTrigger]) System.abortJob(cron.Id); | |
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="<members>\1</members>" /> | |
<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 --> | |
<!-- ConnectedApp - Connected Apps may be used in use by other orgs so don't trash them --> | |
<!-- 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 --> | |
<!-- TopicsForObjects - Error: Entity type 'TopicsForObjects' is not available for delete in this api version --> | |
<!-- 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|ConnectedApp|CustomSite|CustomObjectTranslation|EscalationRules|Flow|InstalledPackage|MatchingRules|RecordType|SharingRules|TopicsForObjects|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</members>'); | |
if ('ApexPage' == metadataType) (negate = true) | (pattern = '>SiteHome<'); | |
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="<layoutItems>\s+<customLink>[^<]+</customLink>\s+</layoutItems>" | |
expression="<!--\0-->" | |
/> | |
<!-- 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="<customButtons>[^<]+</customButtons>" /> | |
<substitution expression="<!--\0-->" /> | |
</replaceregexp> | |
<!-- fix layout custom actions - Error: Cannot delete action EditDescription. The following layout is referencing this. : Task Layout. --> | |
<replaceregexp flags="gs"> | |
<fileset dir="@{tempDir}/layouts" /> | |
<regexp pattern="<quickActionList>.+</quickActionList>" /> | |
<substitution expression="<!--\0-->" /> | |
</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="<userPermissions>.*</userPermissions>" | |
expression="<applicationVisibilities><application>standard__AppLauncher</application><default>true</default><visible>true</visible></applicationVisibilities>" | |
/> | |
<!-- fix apex class dependencies - Parent__c - Error: This custom field is referenced elsewhere in salesforce.com --> | |
<bulkRetrieveForDestroy | |
metadataType="ApexClass" | |
directoryName="classes" | |
pattern="(/\*.*?\*/)?.+?\s+class\s+([A-Za-z0-9_]+)\s*([A-Za-z0-9<. ,>]*)?\s*\{.*\}" | |
expression="public class \2 {public \2(){} public \2(ApexPages.StandardController c){} public \2(ApexPages.StandardSetController c){}}" | |
/> | |
<!-- fix visualforce page dependencies - Parent__c - Error: This custom field is referenced elsewhere in salesforce.com --> | |
<bulkRetrieveForDestroy | |
metadataType="ApexPage" | |
directoryName="pages" | |
pattern=".*apex:page.*" | |
expression="<apex:page \1/>" | |
/> | |
<!-- fix trigger dependencies --> | |
<bulkRetrieveForDestroy | |
metadataType="ApexTrigger" | |
directoryName="triggers" | |
pattern="(/\*.*?\*/)?.*?\s*?trigger\s+([A-Za-z0-9_]+)\s+on\s+([A-Za-z0-9_]*)?.*" | |
expression="trigger \2 on \3 (after insert) {}" | |
/> | |
<!-- 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="<parentRole>[^<]+</parentRole>" | |
expression="<!--\0-->" | |
/> | |
<!-- fix object listviews - Error: cannot delete last filter --> | |
<bulkRetrieveForDestroy | |
metadataType="ListView" | |
directoryName="objects" | |
pattern="<listViews>.*</listViews>" | |
expression="<listViews><fullName>All</fullName><filterScope>Everything</filterScope><label>All</label></listViews>" | |
/> | |
<!-- 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="<CustomSite xmlns="http://soap.sforce.com/2006/04/metadata">.*<active>([^<]+)</active>.*<allowStandardPortalPages>([^<]+)</allowStandardPortalPages>.*<clickjackProtectionLevel>([^<]+)</clickjackProtectionLevel>.*<indexPage>([^<]+)</indexPage>.*<masterLabel>([^<]+)</masterLabel>.*<requireHttps>([^<]+)</requireHttps>.*<siteType>([^<]+)</siteType>.*<subdomain>([^<]+)</subdomain>.*</CustomSite>" | |
expression="<CustomSite xmlns="http://soap.sforce.com/2006/04/metadata">${line.separator}<active>\1</active>${line.separator}<allowStandardPortalPages>\2</allowStandardPortalPages>${line.separator}<clickjackProtectionLevel>\3</clickjackProtectionLevel>${line.separator}<indexPage>SiteHome</indexPage>${line.separator}<masterLabel>\5</masterLabel>${line.separator}<requireHttps>\6</requireHttps>${line.separator}<siteType>\7</siteType>${line.separator}<subdomain>\8</subdomain>${line.separator}<urlPathPrefix>\5</urlPathPrefix>${line.separator}</CustomSite>" | |
/> | |
<!-- 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 (project.getProperty('NumberOfSites.tmp')) { | |
var page = project.createTask('echo'); | |
page.setFile(new File('@{tempDir}/pages/SiteHome.page')); | |
page.setMessage('<apex:page/>'); | |
page.perform(); | |
var meta = project.createTask('echo'); | |
meta.setFile(new File('@{tempDir}/pages/SiteHome.page-meta.xml')); | |
meta.setMessage('<ApexPage><label>SiteHome</label></ApexPage>'); | |
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="<members>[^<]*@{namespacePrefix}__[^<]+</members>" | |
replace="<!--\0-->" | |
/> | |
<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>43.0</version> | |
<types> | |
<name>ApexClass</name> | |
<members>*</members> | |
</types> | |
<types> | |
<name>ApexPage</name> | |
<members>*</members> | |
</types> | |
<types> | |
<name>ApexTrigger</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