Crearea unui multi-monorepo public/privat pentru proiecte PHP

Publicat: 2022-03-10
Rezumat rapid ↬ Să vedem cum să folosiți o abordare „multi-monorepo” pentru a face experiența de dezvoltare mai rapidă, păstrând totuși pachetele dvs. PHP private. Această soluție poate fi deosebit de benefică pentru creatorii de pluginuri PRO.

Pentru a-mi face experiența de dezvoltare mai rapidă, am mutat toate pachetele PHP necesare proiectelor mele într-un monorepo. Când fiecare pachet este găzduit în propriul său depozit (abordarea multirepo), ar trebui să îl dezvolt și să îl testez pe cont propriu și apoi să îl public pe Packagist înainte de a-l putea instala în alte pachete prin Composer. Cu monorepo, deoarece toate pachetele sunt găzduite împreună, ele pot fi dezvoltate, testate, versionate și lansate în același timp.

Monorepo-ul care găzduiește pachetele mele PHP este public, accesibil oricui pe GitHub. Arhivele Git nu pot acorda acces diferit la diferite active; totul este public sau privat. Pentru că intenționez să lansez un plugin WordPress pro, vreau ca pachetele acestuia să fie păstrate private, ceea ce înseamnă că nu pot fi adăugate la monorepo public.

Soluția pe care am găsit-o este să folosesc o abordare „multi-monorepo”, care să cuprindă două monorepo-uri: unul public și unul privat, monorepo-ul privat încorporându-l pe cel public ca submodul Git, permițându-i să-și acceseze fișierele. Monorepoul public poate fi considerat cel din amonte, iar monorepoul privat cel din aval.

Arhitectura unui multi-monorepo
Arhitectura unui multi-monorepo. (Previzualizare mare)

Pe măsură ce am continuat să iterez codul meu, configurația depozitului pe care trebuia să o folosesc în fiecare etapă a proiectului trebuia, de asemenea, actualizată. Prin urmare, nu am ajuns la abordarea multi-monorepo în prima zi; a fost un proces care a durat câțiva ani și a luat un efort destul de mare, mergând de la un singur depozit, la mai multe depozite, la monorepo, până la, în sfârșit, multi-monorepo.

În acest articol, voi descrie modul în care mi-am configurat multi-monorepo folosind Monorepo Builder, care funcționează pentru proiecte PHP și se bazează pe Composer.

Mai multe după săritură! Continuați să citiți mai jos ↓

Reutilizarea codului în Multi-Monorepo

Monorepo-ul public de la leoloso/PoP este locul în care îmi păstrez toate proiectele PHP.

Acest monorepo conține fișierul flux de lucru generate_plugins.yml , care generează mai multe plugin-uri WordPress pentru distribuție atunci când creez o versiune pe GitHub:

Generarea de pluginuri la crearea unei versiuni
Generarea de pluginuri la crearea unei versiuni. (Previzualizare mare)

Configurația fluxului de lucru nu este codificată în fișierul YAML, ci mai degrabă injectată prin cod PHP:

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

Și configurația este furnizată printr-o clasă PHP personalizată:

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

Generarea mai multor plugin-uri WordPress împreună și configurarea fluxului de lucru prin PHP a redus timpul de care am nevoie pentru a gestiona proiectul. Fluxul de lucru gestionează în prezent două plugin-uri (API-ul GraphQL și demo-ul extensiei sale), dar ar putea gestiona 200 fără efort suplimentar din partea mea.

Este această configurație pe care vreau să o refolosesc pentru monorepo-ul meu privat la leoloso/GraphQLAPI-PRO , astfel încât și pluginurile pro să poată fi generate fără efort.

Codul care va fi reutilizat va cuprinde:

  • fluxurile de lucru GitHub Actions pentru a genera pluginuri WordPress (inclusiv stabilirea domeniului, downgrade-ul de la PHP 8.0 la 7.1 și încărcarea pe pagina de versiuni).
  • serviciile PHP personalizate pentru a configura fluxurile de lucru.

Monorepo-ul privat poate genera apoi pluginuri pro WordPress pur și simplu prin declanșarea fluxurilor de lucru din monorepo-ul public și suprascriind configurația acestora în PHP.

Conectarea Monorepos prin submodule Git

Pentru a încorpora depozitul public în cel privat, folosim submodule Git:

 git submodule add <public repo URL>

Am încorporat depozitul public în submodules subfolderului monorepo-ului privat, permițându-mi să adaug mai multe monorepo-uri în amonte în viitor, dacă este necesar. În GitHub, folderul afișează commit-ul specific al submodulului, iar făcând clic pe el mă va duce la acel commit la leoloso/PoP :

Încorporarea monorepo-ului public în monorepo-ul privat
Încorporarea monorepo-ului public în monorepo-ul privat. (Previzualizare mare)

Deoarece depozitul privat conține submodule, pentru a-l clona, ​​trebuie să oferim opțiunea --recursive :

 git clone --recursive <private repo URL>

Reutilizarea fluxurilor de lucru GitHub Actions

GitHub Actions încarcă numai fluxuri de lucru de sub .github/workflows . Deoarece fluxurile de lucru publice din monorepo din aval sunt submodules/PoP/.github/workflows , acestea trebuie duplicate în locația așteptată.

Pentru a păstra fluxurile de lucru din amonte ca sursă unică de adevăr, ne putem limita la a copia fișierele din aval sub .github/workflows , dar niciodată să le edităm acolo. Dacă urmează să fie făcută vreo modificare, aceasta trebuie făcută în monorepo din amonte și apoi copiată.

Ca o notă secundară, observați cum acest lucru înseamnă că multi-monorepo-ul curge: monorepo-ul din amonte nu este complet autonom și va trebui adaptat pentru a se potrivi cu monorepo-ul din aval.

În prima mea iterație pentru a copia fluxurile de lucru, am creat un script Composer simplu:

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

Apoi, după editarea fluxurilor de lucru în monorepo amonte, le-aș copia în aval executând următoarele:

 composer copy-workflows

Dar apoi mi-am dat seama că doar copiarea fluxurilor de lucru nu este suficientă: trebuie să fie și modificate în proces. Acest lucru se datorează faptului că verificarea monorepo-ului din aval necesită opțiunea --recurse-submodules pentru a verifica și submodulele.

În GitHub Actions, checkout-ul în aval se face astfel:

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

Deci, verificarea depozitului din aval are nevoie de submodules: recursive , dar cel din amonte nu și ambele folosesc același fișier sursă.

Soluția pe care am găsit-o este să furnizez valoarea pentru submodules de intrare prin variabila de mediu CHECKOUT_SUBMODULES , care în mod implicit este goală pentru depozitul din amonte:

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

Apoi, atunci când se copiază fluxurile de lucru din amonte în aval, valoarea lui CHECKOUT_SUBMODULES este înlocuită cu recursive :

 env: CHECKOUT_SUBMODULES: "recursive"

Când modificați fluxul de lucru, este o idee bună să utilizați o expresie regulată (regex), astfel încât să funcționeze pentru diferite formate din fișierul sursă (cum ar fi CHECKOUT_SUBMODULES: "" sau CHECKOUT_SUBMODULES:'' sau CHECKOUT_SUBMODULES: ). Acest lucru va împiedica crearea de erori pentru aceste tipuri de modificări aparent inofensive.

Astfel, scriptul Composer copy-workflows prezentat mai sus nu este suficient de bun pentru a gestiona această complexitate.

În următoarea mea iterație, am creat o comandă PHP, CopyUpstreamMonorepoFilesCommand , care să fie executată prin Monorepo Builder:

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

Această comandă folosește un serviciu personalizat, FileCopierSystem , pentru a copia toate fișierele dintr-un folder sursă la destinația specificată, înlocuind opțional conținutul acestora:

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

Când invoc această metodă pentru a copia toate fluxurile de lucru în aval, înlocuiesc și valoarea 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 );

Fluxul de lucru din generate_plugins.yml are nevoie de o înlocuire suplimentară. Când pluginul WordPress este generat, codul său este retrogradat de la PHP 8.0 la 7.1 prin invocarea scriptului 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 }}"

În monorepo din aval, acest fișier va fi localizat în submodules/PoP/ci/downgrade/downgrade_code.sh . Apoi, îndreptăm fluxul de lucru din aval către calea corectă cu această înlocuire:

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

Configurarea pachetelor în Monorepo Builder

Fișierul monorepo-builder.php — plasat la rădăcina monorepo — conține configurația pentru Monorepo Builder. În el, trebuie să indicăm unde se află pachetele (și pluginurile, clienții și orice altceva):

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

Monorepo-ul privat trebuie să aibă acces la tot codul: pachetele proprii, plus cele din monorepo-ul public. Apoi, trebuie să definească toate pachetele din ambele monorepos în fișierul de configurare. Cele din monorepo public sunt situate sub /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', ]); };

Așa cum sunt, configurațiile pentru amonte și aval sunt aproape aceleași, diferențele fiind că cea din aval va:

  • schimba calea către pachetele publice,
  • adăugați pachetele private.

Deci, are sens să rescrieți configurația folosind programarea orientată pe obiecte (OOP). Să urmăm principiul DRY („nu te repeta”) prin extinderea unei clase PHP în depozitul public în depozitul privat.

Recrearea configurației prin OOP

Să refactorăm configurația. În depozitul public, fișierul monorepo-builder.php va face referire pur și simplu la o nouă clasă, ContainerConfigurationService , unde se va întâmpla toată acțiunea:

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

Parametrul __DIR__ indică rădăcina monorepo-ului. Va fi necesar pentru a obține calea completă către directoarele pachetelor.

Clasa ContainerConfigurationService este acum responsabilă de producerea configurației:

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

Configurația poate fi împărțită în mai multe clase. În acest caz, ContainerConfigurationService preia configurația pachetului prin clasa PackageOrganizationDataSource , a cărei implementare o puteți vedea:

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

Ignorarea configurației în aval Monorepo

Acum că configurația din monorepo publică a fost configurată folosind OOP, o putem extinde pentru a se potrivi nevoilor monorepo-ului privat.

Pentru a permite monorepo-ului privat să încarce automat codul PHP din monorepo-ul public, trebuie mai întâi să configuram fișierul composer.json din aval să facă referire la codul sursă din amonte, care se află sub calea submodules/PoP/src :

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

Mai jos este fișierul monorepo-builder.php pentru monorepo privat. Observați că clasa la care se face referire ContainerConfigurationService din depozitul din amonte a aparținut spațiului de nume PoP\PoP , dar acum a fost schimbată la spațiul de nume PoP\GraphQLAPIPRO . Această clasă trebuie să primească intrarea suplimentară a lui $upstreamRelativeRootPath (cu o valoare de submodules/PoP ) pentru a recrea calea completă către pachetele publice:

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

Clasa din aval ContainerConfigurationService suprascrie clasa PackageOrganizationDataSource utilizată în configurație:

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

În cele din urmă, clasa din aval PackageOrganizationDataSource conține calea completă către pachetele publice și private:

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

Injectarea configurației din PHP în acțiunile GitHub

Monorepo Builder oferă comanda packages-json , pe care o putem folosi pentru a injecta căile pachetelor în fluxul de lucru 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 }}

Această comandă produce un JSON stringificat. În fluxul de lucru, acesta trebuie convertit într-un obiect JSON prin fromJson :

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

Din păcate, comanda packages-json afișează numele pachetelor, dar nu și căile acestora. Acest lucru ar funcționa dacă toate pachetele ar fi în același folder (cum ar fi packages/ ), dar nu funcționează în cazul nostru, deoarece pachetele publice și private sunt situate în foldere diferite.

Din fericire, Monorepo Builder poate fi extins cu servicii PHP personalizate. Deci, am creat o comandă personalizată, package-entries-json (prin clasa PackageEntriesJsonCommand ), care scoate calea către pachet.

Fluxul de lucru a fost apoi actualizat cu noua comandă:

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

Executat pe monorepo public, acesta produce următoarele pachete (printre multe altele):

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

Execut pe monorepo privat, produce următoarele înregistrări (printre multe altele):

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

Funcționează destul de bine. Configurația pentru monorepo din aval conține atât pachete publice, cât și pachete private, iar căile către cele publice sunt predate cu submodules/PoP .

Omiterea pachetelor publice în aval Monorepo

Până acum, monorepo-ul din aval include atât pachete publice, cât și private în configurația sa. Cu toate acestea, nu orice comandă trebuie să fie executată pe pachetele publice.

Luați analiza statică, de exemplu. Monorepo-ul public execută deja PHPStan pe toate pachetele publice prin fișierul flux de lucru phpstan.yml , așa cum se arată în această rulare. Dacă monorepo-ul din aval a rulat PHPStan încă o dată pe pachetele publice, ar fi o pierdere de timp de calcul. Fluxul de lucru phpstan.yml trebuie să ruleze doar pe pachetele private.

Aceasta înseamnă că, în funcție de comanda care urmează să fie executată în depozitul din aval, ar putea dori să includem fie pachete publice și private, fie numai pachete private.

Pentru a determina dacă să adăugați pachete publice în configurația din aval, adaptăm clasa din aval PackageOrganizationDataSource pentru a verifica această condiție prin intrarea $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', ] ); } }

În continuare, trebuie să furnizăm valoarea $includeUpstreamPackages ca true sau false , în funcție de comanda care urmează să fie executată.

Putem face acest lucru prin înlocuirea fișierului de configurare monorepo-builder.php cu alte două fișiere de configurare: monorepo-builder-with-upstream-packages.php (care trece $includeUpstreamPackages => true ) și monorepo-builder-without-upstream-packages.php (care trece $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(); };

Apoi actualizăm ContainerConfigurationService pentru a primi parametrul $includeUpstreamPackages și îl transmitem la 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, ); } }

În continuare, trebuie să invocăm monorepo-builder cu oricare fișier de configurare, furnizând opțiunea --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)"

Cu toate acestea, așa cum am văzut mai devreme, dorim să păstrăm fluxurile de lucru GitHub Actions în monorepo din amonte ca sursă unică de adevăr și, în mod clar, nu au nevoie de aceste modificări.

Soluția pe care am găsit-o la această problemă este să furnizez întotdeauna o opțiune --config în depozitul din amonte, fiecare comandă primind propriul fișier de configurare, cum ar fi comanda validate primind fișierul de configurare validate.php :

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

Acum, nu există fișiere de configurare în monorepo din amonte, pentru că nu are nevoie de ele. Dar nu se va rupe, deoarece Monorepo Builder verifică dacă fișierul de configurare există și, dacă nu există, încarcă fișierul de configurare implicit. Deci, fie vom trece peste configurația, fie nu se va întâmpla nimic.

Depozitul din aval oferă fișierele de configurare pentru fiecare comandă, specificând dacă să adăugați pachetele din amonte:

Ca o notă secundară, acesta este un alt exemplu despre cum se scurge multi-monorepo.

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

Suprascrierea configurației

Aproape am terminat. Până acum, monorepo-ul din aval poate suprascrie configurația din monorepo-ul din amonte. Deci, tot ce mai rămâne de făcut este să furnizați noua configurație.

În clasa PluginDataSource , suprascriu configurația a căror pluginuri WordPress trebuie să fie generate, furnizând în schimb pe cele 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', ], ]; } }

Crearea unei noi versiuni pe GitHub va declanșa fluxul de lucru generate_plugins.yml și va genera plugin-urile pro în monorepo-ul meu privat:

Generarea de pluginuri pro
Generarea de pluginuri pro. (Previzualizare mare)

Ta-da!

Concluzie

Ca întotdeauna, nu există o soluție „cea mai bună”, ci doar soluții care ar putea funcționa mai bine în funcție de context. Abordarea multi-monorepo nu este potrivită pentru orice tip de proiect sau echipă. Cred că cei mai mari beneficiari ar fi creatorii de pluginuri care lansează pluginuri publice care vor fi actualizate la versiunile lor pro, precum și agențiile care personalizează pluginuri pentru clienții lor.

În cazul meu, sunt destul de mulțumit de această abordare. Obținerea corectă necesită puțin timp și efort, dar este o investiție unică. Odată ce se termină configurarea, mă pot concentra pe construirea pluginurilor mele pro, iar timpul economisit cu managementul de proiect ar putea fi uriaș.