Can 2GP packages upgrade 1GP packages?

September 10, 2020 by Trieste LaPorte
Read Time : 20 minutes

Can 2GP upgrade 1GP packages?

That’s the idea of this article. I’m using a slightly different format this time. I’ll be documenting each step along the way so I’ll be discovering the outcomes as I go.

My goal here is to document the process of packaging in both 1GP and 2GP, and to have a bit of fun along the way. I hope to make a discovery or two along the way as well. Enjoy!

What’s the plan?

Here’s a rough outline of the plan.

  1. Create up a Developer Edition
  2. Set up a namespace
  3. Create a 1GP package
  4. Start documenting (We are here)
  5. Add some simple code and a simple unit test
  6. Release 1GP package
  7. Create DevHub
  8. Register Namespace (Can we do this?)
  9. Build SFDX/2GP Project, specifying 1GP package id as ancestor.
  10. Add same code, try to release 2GP package

Skip the easy stuff (DE, Namespace, Package)

You’re probably already familiar with creating a Developer Edition, assigning a Namespace and defining a package. But if not here are some articles on how to do this stuff.

I’ve probably missed some stuff here, but there’s also a trailhead if you prefer a guided approach: https://trailhead.salesforce.com/content/learn/modules/isv_app_development/isv_app_development_packaging

If you google around you’ll probably see a bunch of SFDX or Second Generation Packaging articles. I am intentionally doing things the old way here. Those who cannot remember the past are condemned to repeat it. Right? Actually… you’d probably be fine just learning SFDX today and skipping 1GP, but that’s not my goal here.

Sample Code

I’m using an old Developer Edition (DE) org here since creating new namespaces and packages for 1GP packages feels like a waste at this time. You’ll see ‘ContactNewOverride’ - this is an artefact of re-using this DE.

For the components I’m just going to add a trigger, a class, and a simple unit test.

trigger ContactTrigger on Contact (before insert, before update) {
    ContactTriggers.makeFirstNameBob(trigger.new);
}
public without sharing class ContactTriggers {

    @TestVisible
    private static final String theNameWeWant = 'Bob';

    public static void makeFirstNameBob (List<Contact> contactRecords) {
        for (Contact theContactRecord : contactRecords) {
            theContactRecord.FirstName = theNameWeWant;
        }
    }
}
@IsTest (SeeAllData=false)
private class ContactTriggersTest {
    @IsTest
    static void testTheNameSetter () {
        Contact testContact = new Contact(
                FirstName = 'Notbob',
                LastName = 'The Contact'
        );

        Test.startTest();
        insert testContact;
        Test.stopTest();

        List<Contact> contactsAfterTest = [
                SELECT Id,
                        FirstName
                FROM Contact
                WHERE Id = :testContact.Id
        ];
        System.assertEquals(1, contactsAfterTest.size(), 'Should be a single contact returned.');
        System.assertEquals(ContactTriggers.theNameWeWant, contactsAfterTest[0].FirstName, 'The contact first name should be what\'s defined in the ContactTriggers constant.');
    }
}

Just in case you’re not reading the code as we go - please do not install this in any customer orgs. This will break data. You’ve been warned.

Here’s what my managed package looks like after I add the components.

Package Definition

Go ahead and upload it as a “Managed Released” package. When done you should be given an install URL, it looks something like this (don’t worry, by the time you read this, I’ll have killed this package so that you can’t install it): https://login.salesforce.com/packaging/installPackage.apexp?p0=my1GPPackageVersionId

We want just the package id, here’s mine, if I used the above sanitized URL: my1GPPackageVersionId

Write this down somewhere safe, and let’s get on to creating our SFDX/2GP environment.

2GP Phase

In order to begin development on a 2GP app we have a few things to get out of the way. This topic has been covered ad nauseam in the wild, so I’ll make it quick.

Step 1 - create a new developer org, log in, configure it as a Dev Hub. Make sure you also enable Second Generation Managed Packages, and enable My Domain and deploy it to users. I always forget to do this part - especially the deployment piece.

Step 2 - try to register the namespace the from the DE where we created the 1GP packge. To do this I used the standard Link Namespace, and on the OAuth popup I logged into the DE I created the 1GP package in. I’m honestly shocked this worked, but I’ll take it. Now I don’t have to end the article here.

Namespace Prefix Registered

Step 3 - Let’s make the new package, write some code and see if we can release it.

Here are the commands for the web auth flow to save some googling:

sfdx force:auth:web:login --setalias yourDevHubAlias
sfdx force:project:create -n yourProjectName
sfdx force:package:create -n "Upgrade Tester" -t Managed -r /force-app -v yourDevHubAlias

I then updated the sfdx-project.json to look like this, notice the ancestorId is the package version id we grabbed earlier from the 1GP DE:

{
  "packageDirectories": [
    {
      "path": "force-app",
      "versionNumber": "2.0",
      "package": "Upgrade Tester",
      "default": true,
      "ancestorId": "my1GPPackageVersionId"
    }
  ],
  "namespace": "ThisWillBeYourNamespaceNotMine",
  "sfdcLoginUrl": "https://login.salesforce.com",
  "sourceApiVersion": "49.0",
  "packageAliases": {
        "Upgrade Tester": "my2GPPackageId"
  }
}

Now let’s add some code, and see if we can release it. We’ll be adding the same code as we added to the DE. In my IDE it looks like this:

IDE 2GP Example

The commands to build the package, again so you don’t have to google it:

sfdx force:package:version:create --wait 20 --path force-app/ --definitionfile config/project-scratch-def.json --tag "r2.0.0" --versionnumber 2.0.0.NEXT --versionname "ver 2.0" --codecoverage --json -v yourDevHubAlias --installationkeybypass

Here’s where it gets sad folks. SFDX/2GP isn’t seeing the ancestor as a valid package. Here’s the error I get:

{
  "status": 1,
  "name": "Error",
  "message": "The Subscriber Package Version Id my1GPPackageVersionId is invalid, as a corresponding Package Version Id was not found",
  "exitCode": 1,
  "actions": [
    "It`s possible that this package was created on a different Dev Hub. Authenticate to the Dev Hub org that owns the package, and reference that Dev Hub when running the command."
  ],
  "commandName": "PackageVersionCreateCommand",
  "stack": "<snip/>",
  "warnings": []
}

I think the key sentence here is this one: It's possible that this package was created on a different Dev Hub.

And I can’t combat the statement, it was built using an entirely different packaging org.

“But, but!” and Conclusion

At this point you might be asking, “What if we nominate the original DE as the DevHub?” I thought about this too. Unfortunately it won’t work, since we would need to register the namespace in a separate DE, and it would have been registered in the DE/Packaging Org/DevHub. Put differently, a DevHub cannot use itself as the namespace registry.

So I think we’re blocked here. If you plan to convert an existing 1GP managed package into a 2GP managed package you’ll have to replace it entirely. I wish I had better news.

On the other hand, we learned that we can recycle the namespace from the 1GP DE packaging org. So we don’t have to lose that at least.

If I’ve missed something along the way, please reach out and let me know. I hope you enjoyed this short journey.

ABOUT THE AUTHOR

Trieste LaPorte | Technical Architect

Trieste is a Technical Architect with Foglight Solutions and has been breaking the platform for a decade.