Erstellen eines öffentlichen/privaten Multi-Monorepo für PHP-Projekte

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Lassen Sie uns sehen, wie Sie einen „Multi-Monorepo“-Ansatz verwenden, um die Entwicklungserfahrung zu beschleunigen und dennoch Ihre PHP-Pakete privat zu halten. Diese Lösung kann besonders für Ersteller von PRO-Plugins von Vorteil sein.

Um meine Entwicklungserfahrung zu beschleunigen, habe ich alle für meine Projekte erforderlichen PHP-Pakete in ein Monorepo verschoben. Wenn jedes Paket in seinem eigenen Repository gehostet wird (der Multirepo-Ansatz), müsste ich es selbst entwickeln und testen und es dann in Packagist veröffentlichen, bevor ich es über Composer in anderen Paketen installieren kann. Da beim Monorepo alle Pakete zusammen gehostet werden, können sie gleichzeitig entwickelt, getestet, versioniert und veröffentlicht werden.

Das Monorepo, das meine PHP-Pakete hostet, ist öffentlich und für jeden auf GitHub zugänglich. Git-Repositories können keinen unterschiedlichen Zugriff auf unterschiedliche Assets gewähren; es ist alles entweder öffentlich oder privat. Da ich plane, ein professionelles WordPress-Plugin zu veröffentlichen, möchte ich, dass seine Pakete privat bleiben, was bedeutet, dass sie nicht zum öffentlichen Monorepo hinzugefügt werden können.

Die Lösung, die ich gefunden habe, besteht darin, einen „Multi-Monorepo“-Ansatz zu verwenden, der zwei Monorepos umfasst: ein öffentliches und ein privates, wobei das private Monorepo das öffentliche als Git-Submodul einbettet und ihm den Zugriff auf seine Dateien ermöglicht. Das öffentliche Monorepo kann als Upstream- und das private Monorepo als Downstream-Repo betrachtet werden.

Architektur eines Multi-Monorepos
Architektur eines Multi-Monorepos. (Große Vorschau)

Während ich meinen Code weiter iterierte, musste auch das Repository-Setup, das ich in jeder Phase meines Projekts verwenden musste, aktualisiert werden. Daher kam ich nicht am ersten Tag zum Multi-Monorepo-Ansatz; Es war ein Prozess, der sich über mehrere Jahre erstreckte und ziemlich viel Aufwand erforderte, von einem einzelnen Repository über mehrere Repositorys bis hin zum Monorepo und schließlich zum Multi-Monorepo.

In diesem Artikel beschreibe ich, wie ich mein Multi-Monorepo mit Monorepo Builder einrichte, das für PHP-Projekte funktioniert und auf Composer basiert.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Wiederverwendung von Code im Multi-Monorepo

Im öffentlichen Monorepo bei leoloso/PoP bewahre ich alle meine PHP-Projekte auf.

Dieses Monorepo enthält die Workflow-Datei generate_plugins.yml , die mehrere WordPress-Plugins zur Verteilung generiert, wenn ich ein Release auf GitHub erstelle:

Generieren von Plugins beim Erstellen eines Releases
Generieren von Plugins beim Erstellen eines Releases. (Große Vorschau)

Die Workflow-Konfiguration ist nicht in der YAML-Datei fest codiert, sondern wird über PHP-Code eingefügt:

 - id: output_data run: | echo "::set-output name=plugin_config_entries::$(vendor/bin/monorepo-builder plugin-config-entries-json)"

Und die Konfiguration wird über eine benutzerdefinierte PHP-Klasse bereitgestellt:

 class PluginDataSource { public function getPluginConfigEntries(): array { return [ // GraphQL API for WordPress [ 'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-for-wp', 'zip_file' => 'graphql-api.zip', 'main_file' => 'graphql-api.php', 'dist_repo_organization' => 'GraphQLAPI', 'dist_repo_name' => 'graphql-api-for-wp-dist', ], // GraphQL API - Extension Demo [ 'path' => 'layers/GraphQLAPIForWP/plugins/extension-demo', 'zip_file' => 'graphql-api-extension-demo.zip', 'main_file' => 'graphql-api-extension-demo.php', 'dist_repo_organization' => 'GraphQLAPI', 'dist_repo_name' => 'extension-demo-dist', ], ]; } }

Das Generieren mehrerer WordPress-Plugins zusammen und das Konfigurieren des Workflows über PHP hat die Zeit reduziert, die ich für die Verwaltung des Projekts benötige. Der Workflow verarbeitet derzeit zwei Plugins (die GraphQL-API und ihre Erweiterungsdemo), aber er könnte ohne zusätzlichen Aufwand von mir 200 verarbeiten.

Genau dieses Setup möchte ich für mein privates Monorepo bei leoloso/GraphQLAPI-PRO , damit auch die Pro-Plugins ohne Aufwand generiert werden können.

Der wiederzuverwendende Code umfasst:

  • die GitHub Actions-Workflows zum Generieren der WordPress-Plugins (einschließlich Scoping, Downgrade von PHP 8.0 auf 7.1 und Hochladen auf die Releases-Seite).
  • die benutzerdefinierten PHP-Dienste zum Konfigurieren der Workflows.

Das private Monorepo kann dann die Pro-WordPress-Plugins generieren, indem es einfach die Workflows aus dem öffentlichen Monorepo auslöst und ihre Konfiguration in PHP überschreibt.

Verknüpfen von Monorepos über Git-Submodule

Um das öffentliche Repository in das private einzubetten, verwenden wir Git-Submodule:

 git submodule add <public repo URL>

Ich habe das öffentliche Repository in die submodules des Unterordners des privaten Monorepos eingebettet, sodass ich bei Bedarf in Zukunft weitere Upstream-Monorepos hinzufügen kann. In GitHub zeigt der Ordner das spezifische Commit des Submoduls an, und wenn ich darauf klicke, gelange ich zu diesem Commit unter leoloso/PoP :

Einbettung des öffentlichen Monorepos in das private Monorepo
Einbettung des öffentlichen Monorepos in das private Monorepo. (Große Vorschau)

Da das private Repository Submodule enthält, müssen wir zum Klonen die Option --recursive :

 git clone --recursive <private repo URL>

Wiederverwendung der GitHub Actions-Workflows

GitHub Actions lädt nur Workflows von unter .github/workflows . Da sich die öffentlichen Workflows im nachgelagerten Monorepo unter submodules/PoP/.github/workflows , müssen diese am erwarteten Speicherort dupliziert werden.

Um die vorgelagerten Workflows als Single Source of Truth zu behalten, können wir uns darauf beschränken, die Dateien nachgelagert unter .github/workflows zu kopieren, dort aber niemals zu bearbeiten. Wenn eine Änderung vorgenommen werden soll, muss sie im Upstream-Monorepo vorgenommen und dann herüberkopiert werden.

Beachten Sie als Randbemerkung, dass dies bedeutet, dass das Multi-Monorepo leckt: Das vorgelagerte Monorepo ist nicht vollständig autonom und muss an das nachgelagerte Monorepo angepasst werden.

In meiner ersten Iteration zum Kopieren der Workflows habe ich ein einfaches Composer-Skript erstellt:

 { "scripts": { "copy-workflows": [ "php -r \"copy('submodules/PoP/.github/workflows/generate_plugins.yml', '.github/workflows/generate_plugins.yml');\"", "php -r \"copy('submodules/PoP/.github/workflows/split_monorepo.yaml', '.github/workflows/split_monorepo.yaml');\"" ] } }

Nachdem ich die Workflows im vorgelagerten Monorepo bearbeitet habe, würde ich sie nachgelagert kopieren, indem ich Folgendes ausführe:

 composer copy-workflows

Aber dann habe ich gemerkt, dass es nicht reicht, nur die Workflows zu kopieren: Sie müssen dabei auch modifiziert werden. Dies liegt daran, dass das Auschecken des nachgelagerten Monorepos die Option --recurse-submodules erfordert, um auch die Submodule auszuchecken.

In GitHub-Aktionen erfolgt der nachgelagerte Checkout wie folgt:

 - uses: actions/checkout@v2 with: submodules: recursive

Das Auschecken des Downstream-Repositorys benötigt also die Eingabe- submodules: recursive , das Upstream-Repository jedoch nicht, und beide verwenden dieselbe Quelldatei.

Die Lösung, die ich gefunden habe, besteht darin, den Wert für die Eingabe- submodules über die Umgebungsvariable CHECKOUT_SUBMODULES , die für das Upstream-Repository standardmäßig leer ist:

 env: CHECKOUT_SUBMODULES: "" jobs: provide_data: steps: - uses: actions/checkout@v2 with: submodules: ${{ env.CHECKOUT_SUBMODULES }}

Dann wird beim Kopieren der Workflows von Upstream nach Downstream der Wert von CHECKOUT_SUBMODULES durch recursive ersetzt:

 env: CHECKOUT_SUBMODULES: "recursive"

Wenn Sie den Workflow ändern, ist es eine gute Idee, einen regulären Ausdruck (Regex) zu verwenden, damit er für verschiedene Formate in der Quelldatei funktioniert (z. B. CHECKOUT_SUBMODULES: "" oder CHECKOUT_SUBMODULES:'' oder CHECKOUT_SUBMODULES: ). Dadurch wird verhindert, dass Fehler für diese Art von scheinbar harmlosen Änderungen erstellt werden.

Daher ist das oben gezeigte copy-workflows Composer-Skript nicht gut genug, um diese Komplexität zu bewältigen.

In meiner nächsten Iteration habe ich einen PHP-Befehl, CopyUpstreamMonorepoFilesCommand , erstellt, der über Monorepo Builder ausgeführt werden soll:

 vendor/bin/monorepo-builder copy-upstream-monorepo-files

Dieser Befehl verwendet einen benutzerdefinierten Dienst, FileCopierSystem , um alle Dateien aus einem Quellordner in das angegebene Ziel zu kopieren und optional ihren Inhalt zu ersetzen:

 namespace PoP\GraphQLAPIPRO\Extensions\Symplify\MonorepoBuilder\SmartFile; use Nette\Utils\Strings; use Symplify\SmartFileSystem\Finder\SmartFinder; use Symplify\SmartFileSystem\SmartFileSystem; final class FileCopierSystem { public function __construct( private SmartFileSystem $smartFileSystem, private SmartFinder $smartFinder, ) { } /** * @param array $patternReplacements a regex pattern to search, and its replacement */ public function copyFilesFromFolder( string $fromFolder, string $toFolder, array $patternReplacements = [] ): void { $smartFileInfos = $this->smartFinder->find([$fromFolder], '*'); foreach ($smartFileInfos as $smartFileInfo) { $fromFile = $smartFileInfo->getRealPath(); $fileContent = $this->smartFileSystem->readFile($fromFile); foreach ($patternReplacements as $pattern => $replacement) { $fileContent = Strings::replace($fileContent, $pattern, $replacement); } $toFile = $toFolder . substr($fromFile, strlen($fromFolder)); $this->smartFileSystem->dumpFile($toFile, $fileContent); } } } namespace PoP\GraphQLAPIPRO\Extensions\Symplify\MonorepoBuilder\SmartFile; use Nette\Utils\Strings; use Symplify\SmartFileSystem\Finder\SmartFinder; use Symplify\SmartFileSystem\SmartFileSystem; final class FileCopierSystem { public function __construct( private SmartFileSystem $smartFileSystem, private SmartFinder $smartFinder, ) { } /** * @param array $patternReplacements a regex pattern to search, and its replacement */ public function copyFilesFromFolder( string $fromFolder, string $toFolder, array $patternReplacements = [] ): void { $smartFileInfos = $this->smartFinder->find([$fromFolder], '*'); foreach ($smartFileInfos as $smartFileInfo) { $fromFile = $smartFileInfo->getRealPath(); $fileContent = $this->smartFileSystem->readFile($fromFile); foreach ($patternReplacements as $pattern => $replacement) { $fileContent = Strings::replace($fileContent, $pattern, $replacement); } $toFile = $toFolder . substr($fromFile, strlen($fromFolder)); $this->smartFileSystem->dumpFile($toFile, $fileContent); } } }

Wenn ich diese Methode aufrufe, um alle nachgelagerten Workflows zu kopieren, ersetze ich auch den Wert von CHECKOUT_SUBMODULES :

 /** * Copy all workflows to `.github/`, and convert: * `CHECKOUT_SUBMODULES: ""` * into: * `CHECKOUT_SUBMODULES: "recursive"` */ $regexReplacements = [ '#CHECKOUT_SUBMODULES:(\s+".*")?#' => 'CHECKOUT_SUBMODULES: "recursive"', ]; (new FileCopierSystem())->copyFilesFromFolder( 'submodules/PoP/.github/workflows', '.github/workflows', $regexReplacements );

Der Workflow in generate_plugins.yml muss zusätzlich ersetzt werden. Wenn das WordPress-Plugin generiert wird, wird sein Code von PHP 8.0 auf 7.1 heruntergestuft, indem das Skript ci/downgrade/downgrade_code.sh wird:

 - name: Downgrade code for production (to PHP 7.1) run: ci/downgrade/downgrade_code.sh "${{ matrix.pluginConfig.rector_downgrade_config }}" "" "${{ matrix.pluginConfig.path }}" "${{ matrix.pluginConfig.additional_rector_configs }}"

Im nachgelagerten Monorepo befindet sich diese Datei unter submodules/PoP/ci/downgrade/downgrade_code.sh . Dann lenken wir den nachgelagerten Workflow mit dieser Ersetzung auf den richtigen Weg:

 $regexReplacements = [ // ... '#(ci/downgrade/downgrade_code\.sh)#' => 'submodules/PoP/$1', ];

Konfigurieren von Paketen im Monorepo Builder

Die Datei monorepo-builder.php – platziert im Stammverzeichnis des Monorepo – enthält die Konfiguration für Monorepo Builder. Darin müssen wir angeben, wo sich die Pakete (und Plugins, Clients und alles andere) befinden:

 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\MonorepoBuilder\ValueObject\Option; return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PACKAGE_DIRECTORIES, [ __DIR__ . '/packages', __DIR__ . '/plugins', ]); };

Das private Monorepo muss Zugriff auf den gesamten Code haben: seine eigenen Pakete plus die aus dem öffentlichen Monorepo. Dann muss es alle Pakete aus beiden Monorepos in der Konfigurationsdatei definieren. Die aus dem öffentlichen Monorepo befinden sich unter /submodules/PoP :

 return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PACKAGE_DIRECTORIES, [ // public code __DIR__ . '/submodules/PoP/packages', __DIR__ . '/submodules/PoP/plugins', // private code __DIR__ . '/packages', __DIR__ . '/plugins', __DIR__ . '/clients', ]); };

So wie sie sind, sind die Konfigurationen für Upstream und Downstream ziemlich gleich, mit den Unterschieden, dass die Downstream-Konfiguration:

  • den Pfad zu den öffentlichen Paketen ändern,
  • Fügen Sie die privaten Pakete hinzu.

Daher ist es sinnvoll, die Konfiguration mithilfe der objektorientierten Programmierung (OOP) neu zu schreiben. Folgen wir dem DRY-Prinzip („don’t repeat yourself“), indem wir eine PHP-Klasse im öffentlichen Repository im privaten Repository erweitern lassen.

Wiederherstellen der Konfiguration über OOP

Lassen Sie uns die Konfiguration umgestalten. Im öffentlichen Repository verweist die Datei monorepo-builder.php einfach auf eine neue Klasse, ContainerConfigurationService , in der die gesamte Aktion ausgeführt wird:

 use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__ ); $containerConfigurationService->configureContainer(); };

Der Parameter __DIR__ zeigt auf die Wurzel des Monorepos. Es wird benötigt, um den vollständigen Pfad zu den Paketverzeichnissen zu erhalten.

Für die Erstellung der Konfiguration ist nun die Klasse ContainerConfigurationService zuständig:

 namespace PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\MonorepoBuilder\ValueObject\Option; class ContainerConfigurationService { public function __construct( protected ContainerConfigurator $containerConfigurator, protected string $rootDirectory, ) { } public function configureContainer(): void { $parameters = $this->containerConfigurator->parameters(); if ($packageOrganizationConfig = $this->getPackageOrganizationDataSource($this->rootDirectory)) { $parameters->set( Option::PACKAGE_DIRECTORIES, $packageOrganizationConfig->getPackageDirectories() ); } } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource($this->rootDirectory); } }

Die Konfiguration kann auf mehrere Klassen aufgeteilt werden. In diesem Fall ruft ContainerConfigurationService die Paketkonfiguration über die Klasse PackageOrganizationDataSource ab, deren Implementierung Sie sehen können:

 namespace PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources; class PackageOrganizationDataSource { public function __construct(protected string $rootDir) { } public function getPackageDirectories(): array { return array_map( fn (string $packagePath) => $this->rootDir . '/' . $packagePath, $this->getRelativePackagePaths() ); } public function getRelativePackagePaths(): array { return [ 'packages', 'plugins', ]; } }

Überschreiben der Konfiguration im nachgeschalteten Monorepo

Nachdem die Konfiguration im öffentlichen Monorepo nun mit OOP eingerichtet wurde, können wir sie für die Bedürfnisse des privaten Monorepos erweitern.

Damit das private Monorepo den PHP-Code aus dem öffentlichen Monorepo automatisch laden kann, müssen wir zuerst die Downstream-Datei composer.json so konfigurieren, dass sie auf den Quellcode von Upstream verweist, der sich unter dem Pfad submodules/PoP/src befindet:

 { "autoload": { "psr-4": { "PoP\\GraphQLAPIPRO\\": "src", "PoP\\PoP\\": "submodules/PoP/src" } } }

Unten ist die Datei monorepo-builder.php für das private Monorepo. Beachten Sie, dass die referenzierte Klasse ContainerConfigurationService im Upstream-Repository zum PoP\PoP -Namespace gehörte, jetzt aber zum PoP\GraphQLAPIPRO Namespace gewechselt wurde. Diese Klasse muss die zusätzliche Eingabe von $upstreamRelativeRootPath (mit einem Wert von submodules/PoP ) erhalten, um den vollständigen Pfad zu den öffentlichen Paketen neu zu erstellen:

 use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__, 'submodules/PoP' ); $containerConfigurationService->configureContainer(); };

Die Downstream-Klasse ContainerConfigurationService überschreibt, welche PackageOrganizationDataSource -Klasse in der Konfiguration verwendet wird:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService as UpstreamContainerConfigurationService; use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; class ContainerConfigurationService extends UpstreamContainerConfigurationService { public function __construct( ContainerConfigurator $containerConfigurator, string $rootDirectory, protected string $upstreamRelativeRootPath ) { parent::__construct( $containerConfigurator, $rootDirectory ); } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource( $this->rootDirectory, $this->upstreamRelativeRootPath ); } }

Schließlich enthält die Downstream-Klasse PackageOrganizationDataSource den vollständigen Pfad zu öffentlichen und privaten Paketen:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource as UpstreamPackageOrganizationDataSource; class PackageOrganizationDataSource extends UpstreamPackageOrganizationDataSource { public function __construct( string $rootDir, protected string $upstreamRelativeRootPath ) { parent::__construct($rootDir); } public function getRelativePackagePaths(): array { return array_merge( // Public packages - Prepend them with "submodules/PoP/" array_map( fn ($upstreamPackagePath) => $this->upstreamRelativeRootPath . '/' . $upstreamPackagePath, parent::getRelativePackagePaths() ), // Private packages [ 'packages', 'plugins', 'clients', ] ); } }

Einfügen der Konfiguration von PHP in GitHub-Aktionen

Monorepo Builder bietet den Befehl packages-json , mit dem wir die Paketpfade in den GitHub Actions-Workflow einfügen können:

 jobs: provide_data: steps: - id: output_data name: Calculate matrix for packages run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder packages-json)" outputs: matrix: ${{ steps.output_data.outputs.matrix }}

Dieser Befehl erzeugt ein stringifiziertes JSON. Im Workflow muss es über fromJson in ein JSON-Objekt umgewandelt werden:

 jobs: split_monorepo: needs: provide_data strategy: matrix: package: ${{ fromJson(needs.provide_data.outputs.matrix) }}

Leider gibt der Befehl packages-json die Paketnamen, aber nicht deren Pfade aus. Dies würde funktionieren, wenn sich alle Pakete im selben Ordner befinden (z. B. packages/ ), aber in unserem Fall funktioniert es nicht, da sich die öffentlichen und privaten Pakete in unterschiedlichen Ordnern befinden.

Glücklicherweise kann Monorepo Builder mit benutzerdefinierten PHP-Diensten erweitert werden. Also habe ich einen benutzerdefinierten Befehl, package-entries-json (über die Klasse PackageEntriesJsonCommand ), erstellt, der den Pfad zum Paket ausgibt.

Der Workflow wurde dann mit dem neuen Befehl aktualisiert:

 run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json)"

Auf dem öffentlichen Monorepo ausgeführt, erzeugt dies die folgenden Pakete (neben vielen anderen):

 [ { "name": "graphql-api-for-wp", "path": "layers/GraphQLAPIForWP/plugins/graphql-api-for-wp" }, { "name": "extension-demo", "path": "layers/GraphQLAPIForWP/plugins/extension-demo" }, { "name": "access-control", "path": "layers/Engine/packages/access-control" }, { "name": "api", "path": "layers/API/packages/api" }, { "name": "api-clients", "path": "layers/API/packages/api-clients" } ]

Auf dem privaten Monorepo ausgeführt, erzeugt es die folgenden Einträge (neben vielen anderen):

 [ { "name": "graphql-api-for-wp", "path": "submodules/PoP/layers/GraphQLAPIForWP/plugins/graphql-api-for-wp" }, { "name": "extension-demo", "path": "submodules/PoP/layers/GraphQLAPIForWP/plugins/extension-demo" }, { "name": "access-control", "path": "submodules/PoP/layers/Engine/packages/access-control" }, { "name": "api", "path": "submodules/PoP/layers/API/packages/api" }, { "name": "api-clients", "path": "submodules/PoP/layers/API/packages/api-clients" }, { "name": "graphql-api-pro", "path": "layers/GraphQLAPIForWP/plugins/graphql-api-pro" }, { "name": "convert-case-directives", "path": "layers/Schema/packages/convert-case-directives" }, { "name": "export-directive", "path": "layers/GraphQLByPoP/packages/export-directive" } ]

Es funktioniert ganz gut. Die Konfiguration für das Downstream-Monorepo enthält sowohl öffentliche als auch private Pakete, und den Pfaden zu den öffentlichen sind submodules/PoP vorangestellt.

Überspringen öffentlicher Pakete im nachgeschalteten Monorepo

Bisher umfasst das nachgelagerte Monorepo sowohl öffentliche als auch private Pakete in seiner Konfiguration. Allerdings muss nicht jeder Befehl auf den öffentlichen Paketen ausgeführt werden.

Nehmen Sie zum Beispiel die statische Analyse. Das öffentliche Monorepo führt PHPStan bereits auf allen öffentlichen Paketen über die Workflow-Datei phpstan.yml , wie in diesem Durchlauf gezeigt. Wenn das nachgelagerte Monorepo PHPStan noch einmal auf den öffentlichen Paketen ausführen würde, wäre dies eine Verschwendung von Rechenzeit. Der phpstan.yml Workflow muss nur auf den privaten Paketen ausgeführt werden.

Das bedeutet, dass wir abhängig vom auszuführenden Befehl im Downstream-Repository möglicherweise sowohl öffentliche als auch private Pakete oder nur private Pakete einschließen möchten.

Um zu bestimmen, ob öffentliche Pakete in der Downstream-Konfiguration hinzugefügt werden sollen, passen wir die Downstream-Klasse PackageOrganizationDataSource an, um diese Bedingung über die Eingabe $includeUpstreamPackages zu überprüfen:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource as UpstreamPackageOrganizationDataSource; class PackageOrganizationDataSource extends UpstreamPackageOrganizationDataSource { public function __construct( string $rootDir, protected string $upstreamRelativeRootPath, protected bool $includeUpstreamPackages ) { parent::__construct($rootDir); } public function getRelativePackagePaths(): array { return array_merge( // Add the public packages? $this->includeUpstreamPackages ? // Public packages - Prepend them with "submodules/PoP/" array_map( fn ($upstreamPackagePath) => $this->upstreamRelativeRootPath . '/' . $upstreamPackagePath, parent::getRelativePackagePaths() ) : [], // Private packages [ 'packages', 'plugins', 'clients', ] ); } }

Als nächstes müssen wir den Wert $includeUpstreamPackages entweder als true oder false angeben, je nach auszuführendem Befehl.

Wir können dies tun, indem wir die Konfigurationsdatei monorepo-builder.php durch zwei andere Konfigurationsdateien ersetzen: monorepo-builder-with-upstream-packages.php (die $includeUpstreamPackages => true übergibt) und monorepo-builder-without-upstream-packages.php (die $includeUpstreamPackages => false übergibt):

 // File monorepo-builder-without-upstream-packages.php use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurationService = new ContainerConfigurationService( $containerConfigurator, __DIR__, 'submodules/PoP', false, // This is $includeUpstreamPackages ); $containerConfigurationService->configureContainer(); };

Wir aktualisieren dann ContainerConfigurationService , um den Parameter $includeUpstreamPackages zu erhalten, und übergeben ihn an PackageOrganizationDataSource :

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\Configurators; use PoP\PoP\Config\Symplify\MonorepoBuilder\Configurators\ContainerConfigurationService as UpstreamContainerConfigurationService; use PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources\PackageOrganizationDataSource; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; class ContainerConfigurationService extends UpstreamContainerConfigurationService { public function __construct( ContainerConfigurator $containerConfigurator, string $rootDirectory, protected string $upstreamRelativeRootPath, protected bool $includeUpstreamPackages, ) { parent::__construct( $containerConfigurator, $rootDirectory, ); } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource( $this->rootDirectory, $this->upstreamRelativeRootPath, $this->includeUpstreamPackages, ); } }

Als nächstes müssen wir den monorepo-builder mit einer der beiden Konfigurationsdateien aufrufen, indem wir die Option --config :

 jobs: provide_data: steps: - id: output_data name: Calculate matrix for packages run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json --config=monorepo-builder-without-upstream-packages.php)"

Wie wir jedoch bereits gesehen haben, möchten wir die GitHub Actions-Workflows im Upstream-Monorepo als Single Source of Truth beibehalten, und sie benötigen diese Änderungen eindeutig nicht.

Die Lösung, die ich für dieses Problem gefunden habe, besteht darin, immer eine Option --config im Upstream-Repository bereitzustellen, wobei jeder Befehl seine eigene Konfigurationsdatei erhält, z. B. der Befehl validate , der die Konfigurationsdatei validate.php erhält:

 - name: Run validation run: vendor/bin/monorepo-builder validate --config=config/monorepo-builder/validate.php

Jetzt gibt es keine Konfigurationsdateien im Upstream-Monorepo, weil es sie nicht benötigt. Aber es wird nicht brechen, weil Monorepo Builder prüft, ob die Konfigurationsdatei existiert, und wenn dies nicht der Fall ist, lädt es stattdessen die Standard-Konfigurationsdatei. Entweder überschreiben wir die Konfiguration oder es passiert nichts.

Das Downstream-Repository stellt die Konfigurationsdateien für jeden Befehl bereit und gibt an, ob die Upstream-Pakete hinzugefügt werden sollen:

Als Randbemerkung ist dies ein weiteres Beispiel dafür, wie das Multi-Monorepo leckt.

 // File config/monorepo-builder/validate.php return require_once __DIR__ . '/monorepo-builder-with-upstream-packages.php';

Überschreiben der Konfiguration

Wir sind fast fertig. Inzwischen kann das nachgelagerte Monorepo die Konfiguration des vorgelagerten Monorepos überschreiben. Sie müssen also nur noch die neue Konfiguration bereitstellen.

In der PluginDataSource -Klasse überschreibe ich die Konfiguration, welche WordPress-Plugins generiert werden müssen, und stelle stattdessen die Pro-Plugins bereit:

 namespace PoP\GraphQLAPIPRO\Config\Symplify\MonorepoBuilder\DataSources; use PoP\PoP\Config\Symplify\MonorepoBuilder\DataSources\PluginDataSource as UpstreamPluginDataSource; class PluginDataSource extends UpstreamPluginDataSource { public function getPluginConfigEntries(): array { return [ // GraphQL API PRO [ 'path' => 'layers/GraphQLAPIForWP/plugins/graphql-api-pro', 'zip_file' => 'graphql-api-pro.zip', 'main_file' => 'graphql-api-pro.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-pro-dist', ], // GraphQL API Extensions // Google Translate [ 'path' => 'layers/GraphQLAPIForWP/plugins/google-translate', 'zip_file' => 'graphql-api-google-translate.zip', 'main_file' => 'graphql-api-google-translate.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-google-translate-dist', ], // Events Manager [ 'path' => 'layers/GraphQLAPIForWP/plugins/events-manager', 'zip_file' => 'graphql-api-events-manager.zip', 'main_file' => 'graphql-api-events-manager.php', 'dist_repo_organization' => 'GraphQLAPI-PRO', 'dist_repo_name' => 'graphql-api-events-manager-dist', ], ]; } }

Das Erstellen einer neuen Version auf GitHub löst den Workflow generate_plugins.yml und generiert die Pro-Plugins in meinem privaten Monorepo:

Erstellen von Pro-Plugins
Erstellen von Pro-Plugins. (Große Vorschau)

Ta-da!

Fazit

Wie immer gibt es keine „beste“ Lösung, sondern nur Lösungen, die je nach Kontext besser funktionieren. Der Multi-Monorepo-Ansatz ist nicht für jede Art von Projekt oder Team geeignet. Ich glaube, die größten Nutznießer wären Plugin-Ersteller, die öffentliche Plugins veröffentlichen, die auf ihre Pro-Versionen aktualisiert werden, sowie Agenturen, die Plugins für ihre Kunden anpassen.

In meinem Fall bin ich sehr zufrieden mit diesem Ansatz. Es richtig zu machen, erfordert ein wenig Zeit und Mühe, aber es ist eine einmalige Investition. Sobald die Einrichtung abgeschlossen ist, kann ich mich auf die Erstellung meiner Pro-Plugins konzentrieren, und die Zeitersparnis beim Projektmanagement könnte enorm sein.