6

It is possible using the details from Salesforce Metadata Api/ Tooling Api to build changesets to retrieve and modify the contents of a change set via the Metadata API.

However, the whole process depends on knowing the "Change Set Name" that can then be used as the Package Name against the Metadata API.

Is there a programmatic way to discover the package names of the changesets that exist in an org?

Ideally this would be an official API.

See also, Salesforce Idea: Expose the native change sets via an API

4
  • 3
    I did , changeSetId.getSObjectType().getDescribe().getName(); , it seems the API name of changeset is DevelopmentPackageVersion and keyPrefix is `0A2'. I tried using SOQL in rest as well as tooling API, it seems SF has not exposed that object yet. Surprisingly if you try instancesf.com/0A2 , it opes up packages tab. Commented Mar 11, 2019 at 20:07
  • good to know .I wish SFDC had exposed this one as well .Makes it easier to build some cool tools . Commented Mar 11, 2019 at 20:19
  • @PranayJaiswal Sorry, I should have linked to the existing idea I had for this to expose DevelopmentPackageVersion and PackageMember. I've mostly posted this Q as it came up on the GoodDaySir Slack channel and I think it would be useful information to have in a public forum. Commented Mar 11, 2019 at 20:30
  • There is a proposal to screen scrape /changemgmt/listOutboundChangeSet.apexp. I'm just waiting to see if @brettmn wants to post it. Commented Mar 11, 2019 at 20:32

3 Answers 3

4

To get a list of outbound change sets using the Salesforce CLI some set up would be required. A file that contains the following:

changeset.apex

PageReference pr=new PageReference('/changemgmt/listOutboundChangeSet.apexp');
Blob output=pr.getContent();
System.debug('### Content = \n' + output.toString());

Then the command sfdx force:apex:execute -f changeset.apex --json -u <username>

This would return the change set page as part of a json response.

It is possible to parse the HTML from the response, I've used cheerio for this, and then loop through the rows of the change set table searching for the ids.

A full working example as a self contained plugin for SFDX is available at https://github.com/BrettMN/sfdx-wipd-plugin

With the plugin it is possible to get the json output with providing only a username but it does not currently return the id of the change set

To see where the response is handle Line 44 of commands/changeset_list.js is where it begins.

0
1

The following apex code should parse the change set setup page and put the change set names into a list:

PageReference pr = new PageReference('/changemgmt/listOutboundChangeSet.apexp');
String output = pr.getContent().toString();
List<String> entitiesToParse = output.split('"Clone - ');
List<String> changeSetNames = new List<String>();

for (String entity : entitiesToParse) {
String changeSetName = entity.SubStringBefore('"');
    changeSetNames.add(changeSetName);
}

System.debug(changeSetNames);

I have a use case for this that uploads the change sets as a metadata zip to a Github repo. It involves using the above code to grab the list of outbound change sets, commit the list as a comma delimitted string to a file on Github (via HTTP callout to Github from Apex). Then I have github actions that notice the commit and uses SFDX commands to retrieve the change sets like so:

sfdx force:mdapi:retrieve -r ./backup-changeset-manifests/Partial-Sandbox/in-flight-backups -p "$CHANGESETS_TO_RETRIEVE" -u $USERNAME
-1

Poll EventLogFile every hour, extract any change sets


    def getChangeSetStatus(self):

        hourlyTimestamp = datetime.fromtimestamp(datetime.utcnow().timestamp() - (3600 * 1))
        dtFormat = '%Y-%m-%dT%H:%M:%S-00:00%z'
        query = f"SELECT EventType, LogDate, LogFileFieldNames, LogFile, Interval FROM EventLogFile WHERE EventType='ChangeSetOperation' AND Interval='Hourly' AND LogDate >= {hourlyTimestamp.strftime(dtFormat)}"
        print(query)
        url = f"{self._auth['instanceUrl']}/services/data/v{apiVersion}/query/?q={quote_plus(query)}"
        eventTypeLogResult = self._client.request('GET', url).json()

        for result in eventTypeLogResult['records']:
          
            url = f"{self._auth['instanceUrl']}{result['LogFile']}"
            logfileResult = self._client.request('GET', url).text
            df = pd.read_csv(StringIO(logfileResult), usecols=['CHANGE_SET_NAME'])

            for i, row in df.iterrows():
                sfdxCmd = f"sfdx force:source:retrieve -n '{row['CHANGE_SET_NAME']}' --targetusername={self.targetusername} --json"
                try:
                    p = subprocess.Popen(sfdxCmd, shell=True,
                                stdout=subprocess.PIPE, encoding='utf-8')
                    result = p.communicate()[0].strip()
                    p.stdout.close()
                except:
                    raise PopenError(f"Error with cmd: {sfdxCmd}. Error Msg: {sys.exc_info()[0]}")

                findCmd = f"find '{row['CHANGE_SET_NAME']}' -name '*' -type f -print | awk -F/ '{{ print $(NF-1) \" - \" $NF }}'"
                try:
                    p = subprocess.Popen(findCmd, shell=True,
                                stdout=subprocess.PIPE, encoding='utf-8')
                    result = p.communicate()[0].strip()
                    p.stdout.close()
                except:
                    raise PopenError(f"Error with cmd: {findCmd}. Error Msg: {sys.exc_info()[0]}")

                print(result)
3
  • Welcome to SFSE! Please add some additional description of your solution in text, as well as sharing code examples. In particular, it would be helpful to (1) describe the solution and (2) call out that this is Python code and it uses specific libraries (pandas and whatever you're using to authenticate).
    – David Reed
    Commented Sep 3, 2020 at 0:29
  • Hello @DavidReed I feel what you're asking is out of scope for the question, this isn't a question about authentication. Provided is a working routine and feel the code is more than explanatory. Commented Sep 3, 2020 at 7:15
  • Respectfully, for most participants in this community, this is not a working routine. It is incomplete Python code with a variety of unspecified dependencies. Now, we don't necessarily look for a complete runnable piece of code as an answer, but we do look for explanation and code that can be consumed by the community (see How to Answer); that's really all I'm asking here.
    – David Reed
    Commented Sep 3, 2020 at 14:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .