Tworzenie publicznego/prywatnego multi-monorepo dla projektów PHP

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Zobaczmy, jak wykorzystać podejście „multi-monorepo”, aby przyspieszyć programowanie, zachowując jednocześnie prywatność pakietów PHP. To rozwiązanie może być szczególnie korzystne dla twórców wtyczek PRO.

Aby moje doświadczenie programistyczne było szybsze, przeniosłem wszystkie pakiety PHP wymagane przez moje projekty do monorepo. Kiedy każdy pakiet jest hostowany w swoim własnym repozytorium (podejście multirepo), musiałbym go samodzielnie opracować i przetestować, a następnie opublikować w Packagist, zanim będę mógł zainstalować go w innych pakietach za pomocą Composera. Dzięki monorepo, ponieważ wszystkie pakiety są hostowane razem, mogą być rozwijane, testowane, wersjonowane i wydawane w tym samym czasie.

Monorepo, które obsługuje moje pakiety PHP, jest publiczne, dostępne dla każdego na GitHub. Repozytoria Git nie mogą przyznawać różnego dostępu do różnych zasobów; to wszystko jest publiczne lub prywatne. Ponieważ planuję wydać wtyczkę pro WordPress, chcę, aby jej pakiety były prywatne, co oznacza, że ​​nie można ich dodawać do publicznego monorepo.

Rozwiązanie, które znalazłem, to zastosowanie podejścia „multi-monorepo”, składającego się z dwóch monorepo: jednego publicznego i jednego prywatnego, przy czym prywatne monorepo zawiera osadzone publiczne jako podmoduł Git, umożliwiając mu dostęp do swoich plików. Publiczne monorepo można uznać za upstream, a prywatne monorepo za downstream.

Architektura multi-monorepo
Architektura multi-monorepo. (duży podgląd)

Ponieważ ciągle iterowałem swój kod, konfiguracja repozytorium, której potrzebowałem na każdym etapie mojego projektu, również musiała zostać zaktualizowana. Dlatego nie doszedłem do podejścia multi-monorepo pierwszego dnia; był to proces, który trwał kilka lat i wymagał sporego wysiłku, przechodząc od jednego repozytorium do wielu repozytoriów, do monorepo i wreszcie do multi-monorepo.

W tym artykule opiszę, jak skonfigurować multi-monorepo za pomocą Monorepo Builder, który działa w projektach PHP i jest oparty na Composerze.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Ponowne wykorzystanie kodu w Multi-Monorepo

Publiczne monorepo w leoloso/PoP to miejsce, w którym trzymam wszystkie moje projekty PHP.

To monorepo zawiera plik workflow generate_plugins.yml , który generuje wiele wtyczek WordPress do dystrybucji, gdy tworzę wydanie na GitHub:

Generowanie wtyczek podczas tworzenia wydania
Generowanie wtyczek podczas tworzenia wydania. (duży podgląd)

Konfiguracja przepływu pracy nie jest zapisana na sztywno w pliku YAML, ale jest wstrzykiwana za pomocą kodu PHP:

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

A konfiguracja jest dostarczana przez niestandardową klasę PHP:

 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', ], ]; } }

Generowanie wielu wtyczek WordPress razem i konfigurowanie przepływu pracy za pomocą PHP skróciło czas potrzebny na zarządzanie projektem. Przepływ pracy obsługuje obecnie dwie wtyczki (GraphQL API i demo jego rozszerzenia), ale może obsłużyć 200 bez dodatkowego wysiłku z mojej strony.

Właśnie tę konfigurację chcę ponownie wykorzystać w moim prywatnym monorepo w leoloso/GraphQLAPI-PRO , aby wtyczki pro mogły być również generowane bez wysiłku.

Kod do ponownego wykorzystania będzie zawierał:

  • przepływy pracy GitHub Actions do generowania wtyczek WordPress (w tym określanie zakresu, obniżanie wersji PHP 8.0 do 7.1 i przesyłanie na stronę wydań).
  • niestandardowe usługi PHP do konfigurowania przepływów pracy.

Prywatne monorepo może następnie wygenerować wtyczki pro WordPress, po prostu uruchamiając przepływy pracy z publicznego monorepo i nadpisując ich konfigurację w PHP.

Łączenie Monorepos za pomocą podmodułów Git

Aby osadzić repozytorium publiczne w prywatnym, używamy podmodułów Git:

 git submodule add <public repo URL>

Publiczne repozytorium osadziłem w podfolderach podfolderów prywatnego submodules , co w razie potrzeby pozwoliło mi w przyszłości dodać więcej monorepo. W GitHub folder wyświetla konkretne zatwierdzenie podmodułu, a kliknięcie go przeniesie mnie do tego zatwierdzenia w leoloso/PoP :

Osadzanie publicznego monorepo w prywatnym monorepo
Osadzanie publicznego monorepo w prywatnym monorepo. (duży podgląd)

Ponieważ repozytorium prywatne zawiera podmoduły, aby je sklonować, musimy podać opcję --recursive :

 git clone --recursive <private repo URL>

Ponowne wykorzystanie przepływów pracy GitHub Actions

GitHub Actions ładuje przepływy pracy tylko z .github/workflows . Ponieważ publiczne przepływy pracy w podrzędnym monorepo znajdują się pod modułami podrzędnymi submodules/PoP/.github/workflows , należy je zduplikować w oczekiwanej lokalizacji.

Aby zachować przepływy pracy nadrzędne jako jedyne źródło prawdy, możemy ograniczyć się do kopiowania plików w dół do .github/workflows , ale nigdy ich tam nie edytować. Jeśli jakakolwiek zmiana ma być dokonana, należy to zrobić w nadrzędnym monorepo, a następnie skopiować.

Na marginesie, zauważ, że oznacza to, że wycieki multi-monorepo: Monorepo znajdujące się w górnym biegu nie jest w pełni autonomiczne i będzie musiało zostać dostosowane do dalszego monorepo.

W mojej pierwszej iteracji kopiowania przepływów pracy stworzyłem prosty skrypt Composera:

 { "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');\"" ] } }

Następnie, po edycji przepływów pracy w upstream monorepo, skopiowałbym je w dół, wykonując następujące czynności:

 composer copy-workflows

Ale potem zdałem sobie sprawę, że samo skopiowanie przepływów pracy nie wystarczy: muszą one również zostać zmodyfikowane w procesie. Dzieje się tak dlatego, że sprawdzanie niższego monorepo wymaga opcji --recurse-submodules w celu sprawdzenia również submodułów.

W GitHub Actions wyewidencjonowywanie odbywa się w następujący sposób:

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

Tak więc, pobieranie niższego repozytorium wymaga wejściowych submodules: recursive , ale wcześniejszego nie, i oba używają tego samego pliku źródłowego.

Rozwiązaniem, które znalazłem, jest podanie wartości dla submodules wejściowych poprzez zmienną środowiskową CHECKOUT_SUBMODULES , która domyślnie jest pusta dla repozytorium nadrzędnego:

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

Następnie podczas kopiowania przepływów pracy z upstream do downstream, wartość CHECKOUT_SUBMODULES jest zastępowana przez recursive :

 env: CHECKOUT_SUBMODULES: "recursive"

Podczas modyfikowania przepływu pracy dobrym pomysłem jest użycie wyrażenia regularnego (regex), aby działało ono dla różnych formatów w pliku źródłowym (takich jak CHECKOUT_SUBMODULES: "" lub CHECKOUT_SUBMODULES:'' lub CHECKOUT_SUBMODULES: ). Zapobiegnie to tworzeniu błędów dla tego rodzaju pozornie nieszkodliwych zmian.

Dlatego przedstawiony powyżej skrypt Composer copy-workflows nie jest wystarczająco dobry, aby poradzić sobie z taką złożonością.

W kolejnej iteracji stworzyłem polecenie PHP, CopyUpstreamMonorepoFilesCommand , które miało zostać wykonane przez Monorepo Builder:

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

To polecenie używa niestandardowej usługi FileCopierSystem , aby skopiować wszystkie pliki z folderu źródłowego do określonego miejsca docelowego, opcjonalnie zastępując ich zawartość:

 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); } } }

Wywołując tę ​​metodę, aby skopiować wszystkie przepływy pracy w dół, zamieniam również wartość 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 );

Przepływ pracy w generate_plugins.yml wymaga dodatkowej wymiany. Po wygenerowaniu wtyczki WordPress jej kod jest obniżany z PHP 8.0 do 7.1 poprzez wywołanie skryptu ci/downgrade/downgrade_code.sh :

 - 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 }}"

W monorepo downstream ten plik będzie zlokalizowany w submodules/PoP/ci/downgrade/downgrade_code.sh . Następnie kierujemy dalszy przepływ pracy na właściwą ścieżkę za pomocą tego zamiennika:

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

Konfiguracja pakietów w Monorepo Builder

Plik monorepo-builder.php — umieszczony w katalogu głównym monorepo — zawiera konfigurację dla Monorepo Builder. W nim musimy wskazać, gdzie znajdują się pakiety (i wtyczki, klienci i cokolwiek innego):

 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', ]); };

Prywatne monorepo musi mieć dostęp do całego kodu: własnych pakietów oraz tych z publicznego monorepo. Następnie musi zdefiniować wszystkie pakiety z obu monorepozytorium w pliku konfiguracyjnym. Te z publicznego monorepo znajdują się w /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', ]); };

Ponieważ są, konfiguracje dla upstream i downstream są prawie takie same, różnice polegają na tym, że konfiguracja downstream:

  • zmienić ścieżkę do paczek publicznych,
  • dodaj pakiety prywatne.

Dlatego sensowne jest przepisanie konfiguracji za pomocą programowania obiektowego (OOP). Kierujmy się zasadą DRY („nie powtarzaj się”), rozszerzając klasę PHP w repozytorium publicznym w repozytorium prywatnym.

Odtworzenie konfiguracji przez OOP

Zrefaktoryzujmy konfigurację. W publicznym repozytorium plik monorepo-builder.php będzie po prostu odwoływał się do nowej klasy ContainerConfigurationService , w której nastąpi cała akcja:

 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(); };

Parametr __DIR__ wskazuje na katalog główny monorepo. Będzie potrzebne do uzyskania pełnej ścieżki do katalogów pakietów.

Klasa ContainerConfigurationService jest teraz odpowiedzialna za tworzenie konfiguracji:

 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); } }

Konfigurację można podzielić na kilka klas. W tym przypadku ContainerConfigurationService pobiera konfigurację pakietu poprzez klasę PackageOrganizationDataSource , której implementację widać:

 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', ]; } }

Zastępowanie konfiguracji w podrzędnym Monorepo

Teraz, gdy konfiguracja w publicznym monorepo została skonfigurowana przy użyciu OOP, możemy ją rozszerzyć, aby odpowiadała potrzebom prywatnego monorepo.

Aby pozwolić prywatnemu monorepo na automatyczne ładowanie kodu PHP z publicznego monorepo, musimy najpierw skonfigurować dalszy plik composer.json tak, aby odwoływał się do kodu źródłowego z zewnętrznego repozytorium, który znajduje się w ścieżce submodules/PoP/src :

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

Poniżej znajduje się plik monorepo-builder.php dla prywatnego monorepo. Zauważ, że przywoływana klasa ContainerConfigurationService w repozytorium nadrzędnym należała do przestrzeni nazw PoP\PoP , ale została teraz przełączona do przestrzeni nazw PoP\GraphQLAPIPRO . Ta klasa musi otrzymać dodatkowe dane wejściowe $upstreamRelativeRootPath (z wartością submodules/PoP ), aby odtworzyć pełną ścieżkę do pakietów publicznych:

 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(); };

Klasa downstream ContainerConfigurationService zastępuje klasę PackageOrganizationDataSource używaną w konfiguracji:

 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 ); } }

Wreszcie klasa downstream PackageOrganizationDataSource zawiera pełną ścieżkę do pakietów publicznych i prywatnych:

 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', ] ); } }

Wstrzykiwanie konfiguracji z PHP do działań GitHub

Monorepo Builder oferuje polecenie packages-json , którego możemy użyć do wstrzyknięcia ścieżek pakietów do przepływu pracy GitHub Actions:

 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 }}

To polecenie tworzy ciąg JSON. W przepływie pracy musi zostać przekonwertowany na obiekt JSON za pośrednictwem fromJson :

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

Niestety, polecenie packages-json wyświetla nazwy pakietów, ale nie ich ścieżki. To działałoby, gdyby wszystkie pakiety znajdowały się w tym samym folderze (takim jak packages/ ), ale w naszym przypadku nie działa, ponieważ pakiety publiczne i prywatne znajdują się w różnych folderach.

Na szczęście Monorepo Builder można rozszerzyć o niestandardowe usługi PHP. Stworzyłem więc niestandardowe polecenie package-entries-json (poprzez klasę PackageEntriesJsonCommand ), które wyświetla ścieżkę do pakietu.

Przepływ pracy został następnie zaktualizowany za pomocą nowego polecenia:

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

Wykonywany na publicznym monorepo, daje następujące pakiety (między innymi):

 [ { "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" } ]

Wykonywany na prywatnym monorepo, daje następujące wpisy (między innymi):

 [ { "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" } ]

Działa całkiem dobrze. Konfiguracja dla downstream monorepo zawiera zarówno publiczne, jak i prywatne pakiety, a ścieżki do publicznych są poprzedzone submodules/PoP .

Pomijanie publicznych pakietów w Downstream Monorepo

Jak dotąd monorepo downstream zawiera w swojej konfiguracji zarówno pakiety publiczne, jak i prywatne. Jednak nie każde polecenie musi być wykonywane na pakietach publicznych.

Weźmy na przykład analizę statyczną. Publiczne monorepo już wykonuje PHPStan na wszystkich publicznych pakietach za pośrednictwem pliku przepływu pracy phpstan.yml , jak pokazano w tym uruchomieniu. Gdyby monorepo downstream uruchomiło PHPStan ponownie na publicznych pakietach, byłaby to strata czasu obliczeniowego. Przepływ pracy phpstan.yml musi działać tylko na pakietach prywatnych.

Oznacza to, że w zależności od polecenia, które ma zostać wykonane w dalszym repozytorium, możemy chcieć uwzględnić zarówno pakiety publiczne, jak i prywatne lub tylko prywatne.

Aby określić, czy dodać publiczne pakiety w konfiguracji niższej, dostosowujemy podrzędną klasę PackageOrganizationDataSource , aby sprawdzić ten warunek za pomocą wejścia $includeUpstreamPackages :

 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', ] ); } }

Następnie musimy podać wartość $includeUpstreamPackages jako true lub false , w zależności od polecenia, które ma zostać wykonane.

Możemy to zrobić, zastępując plik konfiguracyjny monorepo-builder.php dwoma innymi plikami konfiguracyjnymi: monorepo-builder-with-upstream-packages.php (który przekazuje $includeUpstreamPackages => true ) i monorepo-builder-without-upstream-packages.php (który przekazuje $includeUpstreamPackages => false ):

 // 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(); };

Następnie aktualizujemy ContainerConfigurationService , aby otrzymać parametr $includeUpstreamPackages i przekazać go dalej do 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, ); } }

Następnie musimy wywołać monorepo-builder z dowolnym plikiem konfiguracyjnym, podając opcję --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)"

Jednak, jak widzieliśmy wcześniej, chcemy zachować przepływy pracy GitHub Actions w monorepo nadrzędnym jako jedyne źródło prawdy i wyraźnie nie potrzebują tych zmian.

Rozwiązanie, które znalazłem na ten problem, polega na tym, że zawsze udostępniam opcję --config w głównym repozytorium, przy czym każde polecenie otrzymuje swój własny plik konfiguracyjny, na przykład polecenie validate odbiera plik konfiguracyjny validate.php :

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

Teraz nie ma plików konfiguracyjnych w zewnętrznym monorepo, ponieważ nie są one potrzebne. Ale to się nie zepsuje, ponieważ Monorepo Builder sprawdza, czy plik konfiguracyjny istnieje, a jeśli nie, ładuje zamiast tego domyślny plik konfiguracyjny. Więc albo zmienimy konfigurację, albo nic się nie stanie.

Repozytorium podrzędne udostępnia pliki konfiguracyjne dla każdego polecenia, określając, czy dodać pakiety nadrzędne:

Na marginesie, jest to kolejny przykład wycieku multi-monorepo.

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

Zastępowanie konfiguracji

Prawie skończyliśmy. Do tej pory monorepo downstream może zastąpić konfigurację z monorepo upstream. Więc wszystko, co pozostało do zrobienia, to dostarczenie nowej konfiguracji.

W klasie PluginDataSource nadpisuję konfigurację, które wtyczki do WordPressa mają zostać wygenerowane, podając w zamian te pro:

 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', ], ]; } }

Utworzenie nowej wersji na GitHub uruchomi przepływ pracy generate_plugins.yml i wygeneruje wtyczki pro w moim prywatnym monorepo:

Generowanie wtyczek pro
Generowanie wtyczek pro. (duży podgląd)

Ta-da!

Wniosek

Jak zawsze, nie ma „najlepszego” rozwiązania, są tylko takie, które mogą działać lepiej w zależności od kontekstu. Podejście multi-monorepo nie jest odpowiednie dla każdego rodzaju projektu lub zespołu. Uważam, że największymi beneficjentami byliby twórcy wtyczek, którzy wypuszczają publiczne wtyczki, które zostaną uaktualnione do wersji pro, a także agencje, które dostosowują wtyczki dla swoich klientów.

W moim przypadku jestem całkiem zadowolony z takiego podejścia. Poprawne wykonanie wymaga trochę czasu i wysiłku, ale jest to jednorazowa inwestycja. Po zakończeniu konfiguracji mogę skupić się na budowaniu moich profesjonalnych wtyczek, a czas zaoszczędzony na zarządzaniu projektami może być ogromny.