Dieser Post ist Teil 2 meiner Serie „Source Management in SFDX“. Im ersten Teil habe ich die Anforderungen an eine Source Management Strategie analysiert und das 1:1:1-Setup vorgestellt. In diesem Teil gehe ich auf die Ordnerstruktur des 1:1:1-Setup für SFDX ein und zeige, wie Teams jeder Größe damit arbeiten können.
- Teil 1: Source Management und Skalierung in SFDX: Anforderungen und Konzept
- Teil 2: Ordnerstruktur und Prozesse (dieser Post)
- Teil 3: CI/CD Pipelines und DevOps mit dem 1:1:1 Setup
Zusammenfassung
Ich habe auf GitHub ein kleines Repository mit Template für das 1:1:1 Setup vorbereitet, welches jederzeit gecloned und verwendet werden kann. Die Ordnerstruktur orientiert sich zwar am sfdx-isv/falcon-template, ist aber speziell darauf optimiert, für mehrere Packages als Teil einer komplexen Org eingesetzt zu werden. Das 1:1:1 Setup unterstützt weiterhin Package LCM Prozesse (von der Idee und Konzeptionierung eines oder mehrerer Packages, über Development bis zum Deployment auf Production) und Package Version LCM Prozesse (Scoping einer Package Version, Development, Testing und Deployment).
Ordnerstruktur
Jedes Repository (und folglich jedes Package) soll autark entwickelt werden können. Dafür wird in dem Template für nicht kritischen Inhalt bewusst Duplizierung in Kauf genommen. Konkret bedeutet das, dass jedes Package alle Config Files (.forceignore
, .gitignore
, scratch-org-def.json
), Test Daten und Import Plans sowie Scripte für die Automation (Scratch Org Setup, Build & Deployment Scripte) mitbringt. In der Praxis ist der Mehraufwand für die Wartung minimal, aber der Gewinn durch Wegfall von Abhängigkeiten eines „Mutter Repositories“ immens.
Die Struktur des gesamten SFDX Projects ist sehr nah am Salesforce Standard und dem sfdx-isv/falcon-template. Ich folge damit im Grunde dem principle of least surprise und versuche, so wenig wie Möglichkeit von etablierten Standards abzuweichen. Die Ordnerstruktur für ein SFDX Projekt sieht damit im Grunde wie folgt aus:
config data |-- plans force-app scripts |-- apex |-- build |-- lib |-- setup src |-- deploy |-- packaged |-- main |-- default |-- classes |-- objects |-- labels |-- utils |-- feature-one |-- classes |-- lwc |-- pages |-- feature-two |-- feature-n |-- unpackaged
Source Ordner
Das 1:1:1 Setup weicht nur unwesentlich von bekannten Konzepten ab: force-app
wird als „default“-Location für alle Metadaten verwendet, die über force:source:pull
frisch bezogen wurden und dient lediglich als Zwischenstation, bis die Metadaten endgültig sortiert wurden. Der Name des eigentlichen Source Ordners ist nicht sfdx-source
sondern schlicht src
. Aus Auto-Vervollständigungs-Gründen der meisten Konsolen („sfdx“ + Tab führt in der Powershell z.B. zu sfdx-project.json
und nicht zu meinem Ordner sfdx-source
).
Während force-app
nur temporären Inhalt hat, beinhaltet der Ordner src
alle final sortierten Metadaten im Source Format, organisiert in den Unterordnern deploy
, packaged
und unpackaged
.
deploy
– Metadaten, die nicht überforce:source:push|pull
mit der Scratch Org synchronisiert werden sollen, aus anderen Gründen nicht packaged sind (z.B. Translations) und sich abhängig von der Zielumgebung unterscheiden können (Queues, Classic Email Templates, Workflow Rules mit organisationsweiten Email-Adressen, etc).packaged
– Der vollständige Content des Packages. Wird mit der Scratch Org synchronisiert.main
– Core Source der App, also mind. Datenmodell, Utility Classes, uswdefault
– Standard Ordner für alle Kern Metadatenutils
– Wie der Name schon sagt: Utilities, Test Fixtures, usw.
feature-one
: Ein App Feature, das aufmain
aufbaut. Feature-Ordner machen nur Sinn, wenn das Package auch ohne Sie gebaut werden könnte (die Abhängigkeit ist also streng linear). Dadurch können einzelne Features, wenn sie zu komplex werden, schnell in eigene Packages ausgelagert werden.
unpackaged
: Für dieses Projekt lokale Metadaten wie Layouts von Standardobjekten. Der Source ist nicht Teil des Packages, wird jedoch mit der Scratch Org synchronisiert.
Das sfdx-project.json
könnte so oder so ähnlich aussehen:
{
"packageDirectories": [
{
"path": "force-app",
"default": true
},
{
"path": "src/unpackaged",
"default": false
},
{
"path": "src/packaged",
"package": "Package Name",
"definitionFile": "config/default-scratch-def.json",
"versionName": "Package Version Name",
"versionNumber": "0.1.0.NEXT",
"default": false,
"dependencies": [
{
"package": "First Dependency",
"versionNumber": "1.0.0.LATEST"
},
{
"package": "Second Dependency",
"versionNumber": "1.0.0.LATEST"
}
]
}
],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"First Dependency": "0HoXXXXXXXXXXXX",
"Second Dependency": "0HoXXXXXXXXXXXX",
"Package Name": "0HoXXXXXXXXXXXX"
}
}
Data Order
Der data
Ordner beinhaltet schlicht alle Testdaten und die zugehörigen Import Pläne. Wichtig ist nur, dass wirklich alle Testdaten, die für einen vollständigen Setup der Scratch Org notwendig sind, hinzugefügt werden. Benötigt man auch Testdaten aus Abhängigkeiten, müssen diese ebenfalls hier organisiert werden. Da die Abhängigkeiten auf eine Version fixiert sind macht es Sinn, dass die Testdaten ebenfalls fixiert sind. Ändert sich die Struktur der Testdaten, und ist dies für unser Package relevant, bedeutet das so oder so ein Update der Abhängigkeit.
Script Ordner
Dieser Ordner dient der Organisation von Automatisierungs-Scripten. Je nach Ausbau der CI/CD Pipeline können das auch Build und Deployment Scripte sein. In der Regel sind es mindestens Setup Scripte für Scratch Orgs und Apex Code für z.B. den Reset von Testdaten oder das Anlegen komplexer Testdaten, die nicht über sfdx force:data:tree:import
abgebildet werden können (Preisbuch Einträge, Quote/Order/Opportunity Line Items, usw)
Prozesse
Die Entwicklung von 2nd gen Packages hat im Grunde zwei Lifecycles: Den Package Lifecycle und den Package Version Lifecycle. Der Package Lifecycle ist organisationsweit und bezieht sich auf Setup, Management und Retirement von Packages als Ganzes. Der Package Version Lifecycle bezieht sich auf die Definition und Umsetzung einer neuen Package Version.
Die Ordnerstruktur des 1:1:1 Setup ist darauf ausgelegt, beide Lifecycle Management Prozesse optimal zu unterstützen: Die gesamte Struktur ist leicht für neue Packages duplizierbar und Automatisierung kann mit wenig Aufwand auf neue Packages umgestellt werden. Durch diszipliniertes Source Management können zu komplex gewordene Packages leicht dekomponiert werden. So können einzelne, ehemalige „Features“ eines Packages als eigenständiges Package ausgegliedert werden und autark entwickelt werden.
Package Lifecycle Management (Package LCM)
Das Package Lifecycle Management beschreibt alle Aktivitäten rund um den Setup neuer Packages, die Entwicklung dieser, den natürlichen Refinement Prozess und schlussendlich auch die Ablöse eines Packages (z.B. weil die Funktionalität nicht mehr benötigt wird).
Package Setup
Das Aufsetzen bzw. Definieren neuer Packages ist durch die „Ein Project pro Repository“ Regel denkbar einfach: Das Template Repo wird gecloned, mit force:package:create
wird ein neues Package auf dem DevHub erstellt und die Setup Scripte werden geringfügig für die neue Package Id angepasst.
Package Refinement
Ein bisschen komplexer ist der Refinement Prozess eines Packages: Soll ein Feature aufgrund gewachsener Komplexität als eigenständiges Package weiterentwickelt werden, muss der Source erst aus dem originalen Package entfernt werden und anschließend in einem neuen Repository als eigenes Package eingefügt werden. Disziplin im Source Management hilft hier enorm – wenn der Source vollständig und ohne zirkuläre Abhängigkeiten in einem feature
-Ordnerstruktur ist, kann man ihn mit wenig Aufwand in ein komplett neues Repository verschieben.
Zu beginn clonen wir das 1-1-1-setup-template direkt von GitHub. Anschließend können wir das geclonte Repo direkt als Submodule in unser „Master Project“ integrieren (falls wir submodules verwenden):
git submodule add https://github.com/yourusername/submodule-name.git packages/submodule-name
Nachdem wir den Source code aus dem alten Repo entfernt haben, erzeugen wir eine neue Package Version des originalen Packages (jetzt ohne das Feature). Verwendet man Semantic Versioning, ist dies der Zeitpunkt für eine neuer Major Version.
sfdx force:package:version:create -p $oldPackage -v $devHubUsername -w 30 -k $installationKey
Dann erstellen wir für das ausgegliederte Feature das neues Package im neuen Repository. Gegebenenfalls müssen wir in der sfdx-project.json
noch weitere Anpassungen vornehmen (Package Version, Config Files, Abhängigkeiten, usw).
sfdx force:package:create -n "Mein Neues Package" -r src/packaged -v $devHubUsername -t Unlocked
sfdx force:package:version:create -p "Mein Neues Package" -v $devHubUsername -w 30 -k $installationKey
Auf dem Zielsystem installieren wir die neue Package Version des alten Packages mit DeprecateOnly
-Flag. Dadurch wird die aus dem Package entfernte Funktionalität nicht von der Org entfernt.
sfdx force:package:install -p $oldPackage -t DeprecateOnly -u $targetOrg -k $installationKey -w 10
Jetzt müssen wir nur noch die Package Version des neuen Packages installieren, um die Funktionalität, die im vorherigen Schritt „deprecated“ wurde, wieder in Packaging zu nehmen.
sfdx force:package:install -p $newPackage -u $targetOrg -k $installationKey -w 10
Manchmal lässt sich die neue Package Version des alten Packages (d.h. zum Entfernen/Deprecaten des Features) nicht ohne Weiteres installieren. Das kann z.B. vorkommen, wenn man Apex Klassen aus dem Package entfernt hat, aber beim Migrieren des Features auch gleichzeitig einzelne Methoden aus den Klassen entfernt hat. Meistens lässt sich das ohne Probleme lösen, indem man den kompletten Source des neuen Packages einmal mit force:source:deploy
„drüberbügelt“ und dann regulär das Package installiert.
sfdx force:source:deploy -p .\src\packaged\ -u $targetOrg -w 10 -l RunLocalTests
Package Version Lifecycle Management (Package Version LCM)
Der Overhead des Package Version LCM für Entwickler ist minimal: Das Setup unterstützt alle klassischen Branching/Merging Strategien mit version
und feature
Branches.
Setup einer neuen Package Version
Auschecken eines neuen Version Branches …
git checkout -b version/1.2.3
… und anpassen von sfdx-project.json
und package.json
für die neue Version.
Development an einer Package Version
Optional ausschecken eines Feature Branches (vom Version Branch), je nachdem wie groß das Team ist. Durch den JIRA-Key im Namen des Branches wird dieser auch dem entsprechenden Issue zugeordnet.
git checkout -b feature/JIRA-KEY
Im normalen Entwicklungsprozesses sollten Commits ebenfalls mit dem JIRA-Key „getaggt“ werden. Wird mit Feature Branches gearbeitet, kann nach Abschluss des Features der entsprechende Branch in den Version Branch gemerged werden (alle noch offenen Feature Branches sollten dann rebasen). Package Versions können klassisch vom Entwickler per Hand gebaut und auf Staging installiert werden oder in einer CI Pipeline auf den Pull-Request automatisiert gebaut werden.