-
-
Notifications
You must be signed in to change notification settings - Fork 1
Org Maintenance
Maintenance commands help you keep your Org tidy. The vision for this family of commands is to be the one-stop shop to find components on your org that are safe to delete and aid you analysing and removing them.
Garbage is everything that was once part of an Unlocked Package, got deleted from source, but did not get erased on the target org when the package was upgraded. Garbage is exclusively scoped to Unlocked Packages, so it never collects managed or unpackaged components.
Note
When a package is installed, Package2Member has the information which metadata component belongs to a package. There are several reasons, why components may not be deleted from the target org when a package version is installed that has components removed. The most common causes are: The component was still referenced by low code (LWC or QuickActions on Layouts, invocable ApexClasses referenced by flows). Additionally, some components are never deleted (CustomFields or CustomMetadataRecords). Of course, installing a package with --upgrade-type DeprecateOnly only deprecates components, without even attempting to delete them.
"Garbage commands" only analyse the Package2Member entity. They prepare manifests and delegate actual deployment to sf project deploy start. By design, garbage commands are bound to the scope of the Package2Member and never gather metadata that is not packaged.
The garbage collector (sf jsc maintain garbage collect) finds components that were removed from package source. It exclusively uses data available on the Org, no git diffs are performed. When you run it against an org with no packages installed (or the packages have never been upgraded), the result will be empty:
{
"status": 0,
"result": {
"deprecatedMembers": {},
"unsupported": [],
"totalDeprecatedComponentCount": 0
},
"warnings": []
}Now consider a package upgrade where you remove a CustomLabel and a CustomField. After the new package version is installed, the garbage collector will yield the following result:
{
"status": 0,
"result": {
"deprecatedMembers": {
"ExternalString": {
"metadataType": "CustomLabel",
"componentCount": 1,
"components": [
{
"developerName": "My_Custom_Label",
"fullyQualifiedName": "My_Custom_Label",
"subjectId": "1010X00000AVjnmQAD",
"subscriberPackageId": "033...",
"packageName": "My Test Package",
"deprecatedSinceVersion": "1.2.3"
}
]
},
"CustomField": {
"metadataType": "CustomField",
"componentCount": 1,
"components": [
{
"developerName": "MyCustomField",
"fullyQualifiedName": "MyCustomObject__c.MyCustomField__c",
"subjectId": "00N9Q00000F5jokUAB",
"subscriberPackageId": "033...",
"packageName": "My Test Package",
"deprecatedSinceVersion": "1.2.3"
},
]
}
},
"unsupported": [],
"totalDeprecatedComponentCount": 2
},
"warnings": []
}The --json flag is the best way to inspect the deprecated components that the collector found, as well as an overview of types that are not yet supported. You can also create a manifest directly:
sf jsc maintain garbage collect --target-org MySandbox --output-dir tmp/test-1 --output-format DestructiveChangesXMLWhich will create the following destructiveChanges.xml with an empty package.xml at tmp/test-1:
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>My_Custom_Label</members>
<name>CustomLabel</name>
</types>
<types>
<members>MyCustomObject__c.MyCustomField__c</members>
<name>CustomField</name>
</types>
<version>62.0</version>
</Package>After inspecting the manifest and making sure that you actually want to erase the components, you can get rid of them with a simple sf project deploy start --metadata-dir tmp/test-1.
Reserved. Not implemented yet.
This is an exhaustive list of all supported metadata types. If you feel comfortable reading source code, you can find the implementations here: garbage-collection/entity-handlers/index.ts
| Entity Definition Name | Metadata Alias | Notes |
|---|---|---|
ExternalString |
CustomLabel |
|
ApexClass |
||
BusinessProcess |
||
AuraDefinitionBundle |
||
FlowDefinition |
Flow |
Exports flow versions (1) |
LightningComponentBundle |
||
FlexiPage |
||
CustomMetadataRecord |
CustomMetadata |
Records, not definitions (2) |
Layout |
||
CustomObject |
||
CustomField |
Ignores deleted fields (3) | |
QuickActionDefinition |
QuickAction |
|
WorkflowAlert |
||
WorkflowFieldUpdate |
||
StaticResource |
||
CustomTab |
||
PermissionSet |
||
ValidationRule |
||
EmailTemplate |
||
CompactLayout |
||
GlobalValueSet |
||
FieldSet |
||
CustomApplication |
||
ProcessDefinition |
ApprovalProcess |
Some metadata types are different and require special treatment. This section explains the design decisions behind some not-so-intuitive behavior.
When you create a new flow, it is packaged as a FlowDefinition (prefix 300). When the flow is deployed (as part of a package install), it is actually deployed as a flow version (which is the Flow entity, prefix 301). You cannot change or delete FlowDefinitions directly, therefore the garbage commands retrieve only flow versions.
In source driven development, there is absolutely no reason to keep old flow versions. When you roll back, you either roll back to a previous commit, or you install an older package version. In both cases, a new flow version is created and set to active. Even if it was identical to an older version, you never reactivate the old one. Therefore, all outdated flow versions are treated as garbage, and are safe to delete.
When the FlowDefinition itself is deprecated (removed from the package), this is a different story: Active flow versions cannot be deleted. To delete it, you need to set it to inactive first, then it is collected in garbage. This is not yet automated.
When a custom object or one of its fields is deleted (not purged), it is still on your org for 30 days. Salesforce appends the suffix _del (so MyField__c will become MyField_del__c). Unfortunately, these fields are still returned by the Tooling API and I have not yet found a way to reliably identify them (maybe we cross-reference with describe results in the future? 🤔 ). The collector simply checks for the regex (_del)[\d]*$. So please don't name your fields like this, if you want them to get collected.
In any case, always analyze the manifest content before doing something with it!
Custom Metadata Objects (and Fields) are custom objects. The entity definitions are deployed and retrieved as CustomObject and CustomField. However, records of these entities are also their own types, and you retrieve them separately: You filter for the custom metadata type itself, e.g. MyCustomMetadata__mdt).
The entity definitions are only included in garbage, if the definitions itself were removed from a package. The records, however, are included if the individual records were removed.
Sometimes, you want to exclude only individual deprecated components. Think about layouts or flexi pages that were once part of the package, but got moved to unpackaged metadata. To do this, navigate to Setup > Apps > Packaging > Installed Packages.
Find the package where the component is located, open "Package Details" and click "View Components". Search for the component in the related list and click "Remove". This removes the component from the package (it still stays on the org) and puts it in an unpackaged state.
Unfortunately, there is no automated way to remove deprecated components from an installed package. You have to perform these steps manually.
When deprecated components are garbage, all unpackaged components are clutter. Not happy soup. Not implemented yet.
This section has some recipes how to solve common use cases by combining maintain commands with other standard commands. The example sf jsc maintain commands all export to the same metadata-dir, so you can use this snippet to copy/paste.
sf project deploy validate --metadata-dir tmp/dev-sandbox-garbage -o MyDevSandboxWhen a package removes a flow, Salesforce does not deactivate or even delete the flow on the subscriber org. Instead, it is marked deprecated and stays active. To fetch all versions of a deprecated flow, simply filter for FlowDefinition.
sf jsc maintain garbage collect -o MyDevSandbox -m FlowDefinition -f DestructiveChangesXML -d tmp/dev-sandbox-garbageBy default, the garbage collector gathers all "Obsolete" flow versions, even if the flow itself is not deprecated. To fetch them, filter garbage for FlowDefinition.
sf jsc maintain garbage collect -o MyDevSandbox -m FlowDefinition -f DestructiveChangesXML -d tmp/dev-sandbox-garbageCustom Metadata records are deployed as CustomMetadata, but have to be filtered by their type's API name. For example, filter for MyTriggerHandler__mdt to get all deprecated records of this type.
sf jsc maintain garbage collect -o MyDevSandbox -m MyCustomMetadataType__mdt -f DestructiveChangesXML -d tmp/dev-sandbox-garbageThe data model requires the presence of a DevHub in the context of a command execution to resolve Package2Id (starting with 0Ho) to the universal SubscriberPackageId (starting with 033). Right now, the command accepts a DevHub as parameter, and only tries to resolve Ids with the supplied DevHub. If the DevHub does not know about this package, the Ids cannot be resolved, and therefore not be used to filter garbage results.