
Serpent Team

Andrew Hanna

Short answer: a 2GP CI/CD pipeline creates a package version on every merge, installs that version into a fresh scratch org, runs the tests against it, and promotes only what passes. The pipeline itself is five commands. What actually breaks 2GP builds months later is not the pipeline, it is the ancestry and namespace decisions made in week one.
Five stages, in this order:
The distinction that matters: deploying metadata proves it compiles in an org you control. Installing a package version proves it works in an org you do not.
The pipeline shape is the same for both. The consequences of a bad promote are not: an unlocked version you regret can be replaced, a promoted managed version is public API forever.
sf package version create --package MyPackage --code-coverage
--installation-key-bypass --wait 60. Capture the resulting 04t id as a pipeline output.
sf org create scratch --definition-file config/project-scratch-def.json
--duration-days 1 --wait 10.
sf package install --package 04tXXXX --wait 20, then
sf apex run test --test-level RunLocalTests --code-coverage --wait 30.
sf package version promote --package [email protected].
Everything before step 5 should run on every pull request. Step 5 should run on exactly one branch, behind a human approval.
Ancestry applies to managed 2GP only, and it is where automated pipelines quietly go wrong. Salesforce requires that the ancestor you specify is the highest promoted package version number for that package, and only versions promoted to managed-released state can be listed as an ancestor (Salesforce Developers). Three rules to encode in your pipeline:
"ancestorVersion": "HIGHEST" in
sfdx-project.json sets the ancestor to the highest promoted version
automatically, which is exactly what CI needs. The alternatives are an explicit
major.minor.patch or an ancestorId.
--skip-ancestor-check, but packages only upgrade along
the ancestry line, so any subscriber sitting on an abandoned version loses their
upgrade path.
Patch versions have their own rule: a patch cannot be the direct ancestor of a non-patch version.
sfdx-project.json and declare inter-package dependencies
explicitly, with versions.
sf package convert and migrating installed subscribers across.
RunLocalTests before promote.
You can assemble all of this from the Salesforce CLI plus GitHub Actions, Azure Pipelines or CumulusCI. Serpent gives you the same pipeline without the YAML: native 1GP, 2GP and managed-package workflows, cross-package dependency resolution and version management across subscriber orgs on every plan, with pre-warmed scratch org pooling on Scale. More build and release guides live in SF Guides.
Can you automate 2GP package creation entirely?
Yes. Version creation, installation, testing and promotion are all CLI commands, so the full cycle runs unattended. Keep promotion behind a human approval anyway.
What is package ancestry in 2GP?
The declared lineage between managed package versions. It defines the upgrade path for subscribers, and packages only upgrade along that line.
Do unlocked packages need a namespace?
No. Unlocked packages can be created without one, which is why they suit internal org estates. Managed 2GP requires a registered namespace linked to your Dev Hub.
Why does my package version create fail on ancestry?
Usually because the ancestor named is not a promoted, managed-released version, or a patch version was set as the ancestor of a non-patch version.
Should CI promote every green build?
No. Promotion is irreversible in practice and sets the next ancestor, so it belongs on a release branch behind an approval, not on every merge.
Commitment free!