Creazione di un multi-Monorepo pubblico/privato per progetti PHP

Pubblicato: 2022-03-10
Riassunto veloce ↬ Vediamo come utilizzare un approccio "multi-monorepo" per rendere più veloce l'esperienza di sviluppo, mantenendo tuttavia privati ​​i pacchetti PHP. Questa soluzione può essere particolarmente vantaggiosa per i creatori di plugin PRO.

Per rendere più veloce la mia esperienza di sviluppo, ho spostato tutti i pacchetti PHP richiesti dai miei progetti in un monorepo. Quando ogni pacchetto è ospitato nel proprio repository (l'approccio multirepo), avrei bisogno di svilupparlo e testarlo da solo e quindi pubblicarlo su Packagist prima di poterlo installare in altri pacchetti tramite Composer. Con il monorepo, poiché tutti i pacchetti sono ospitati insieme, possono essere sviluppati, testati, versionati e rilasciati contemporaneamente.

Il monorepo che ospita i miei pacchetti PHP è pubblico, accessibile a chiunque su GitHub. I repository Git non possono concedere accessi diversi a risorse diverse; è tutto pubblico o privato. Poiché ho intenzione di rilasciare un plugin per WordPress pro, voglio che i suoi pacchetti siano mantenuti privati, il che significa che non possono essere aggiunti al monorepo pubblico.

La soluzione che ho trovato è utilizzare un approccio "multi-monorepo", comprendente due monorepo: uno pubblico e uno privato, con il monorepo privato che incorpora quello pubblico come sottomodulo Git, consentendogli di accedere ai suoi file. Il monorepo pubblico può essere considerato quello a monte, il monorepo privato quello a valle.

Architettura di un multi-monorepo
Architettura di un multi-monorepo. (Grande anteprima)

Mentre continuavo a ripetere il mio codice, anche la configurazione del repository che dovevo utilizzare in ogni fase del mio progetto doveva essere aggiornata. Quindi, non sono arrivato all'approccio multi-monorepo il primo giorno; è stato un processo che è durato diversi anni e ha richiesto un discreto sforzo, passando da un singolo repository, a più repository, al monorepo, fino, infine, al multi-monorepo.

In questo articolo, descriverò come ho impostato il mio multi-monorepo utilizzando Monorepo Builder, che funziona per progetti PHP ed è basato su Composer.

Altro dopo il salto! Continua a leggere sotto ↓

Riutilizzo del codice nel Multi-Monorepo

Il monorepo pubblico di leoloso/PoP è il luogo in cui conservo tutti i miei progetti PHP.

Questo monorepo contiene il file del flusso di lavoro generate_plugins.yml , che genera più plugin di WordPress per la distribuzione quando creo una versione su GitHub:

Generazione di plug-in durante la creazione di una versione
Generazione di plug-in durante la creazione di una versione. (Grande anteprima)

La configurazione del flusso di lavoro non è codificata nel file YAML, ma piuttosto iniettata tramite codice PHP:

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

E la configurazione viene fornita tramite una classe PHP personalizzata:

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

La generazione di più plug-in WordPress tutti insieme e la configurazione del flusso di lavoro tramite PHP ha ridotto il tempo necessario per gestire il progetto. Il flusso di lavoro attualmente gestisce due plug-in (l'API GraphQL e la sua demo di estensione), ma potrebbe gestirne 200 senza ulteriori sforzi da parte mia.

È questa configurazione che voglio riutilizzare per il mio monorepo privato su leoloso/GraphQLAPI-PRO , in modo che anche i plug-in pro possano essere generati senza sforzo.

Il codice da riutilizzare sarà composto da:

  • i flussi di lavoro di GitHub Actions per generare i plugin di WordPress (inclusi l'ambito, il downgrade da PHP 8.0 a 7.1 e il caricamento nella pagina dei rilasci).
  • i servizi PHP personalizzati per configurare i flussi di lavoro.

Il monorepo privato può quindi generare i plugin pro WordPress semplicemente attivando i flussi di lavoro dal monorepo pubblico e sovrascrivendo la loro configurazione in PHP.

Collegamento di Monorepos tramite Git Submodules

Per incorporare il repository pubblico in quello privato, utilizziamo i sottomoduli Git:

 git submodule add <public repo URL>

Ho incorporato il repository pubblico nei submodules delle sottocartelle del monorepo privato, consentendomi di aggiungere più monorepo a monte in futuro, se necessario. In GitHub, la cartella mostra il commit specifico del sottomodulo e facendo clic su di esso mi porterà a quel commit su leoloso/PoP :

Incorporare il monorepo pubblico nel monorepo privato
Incorporare il monorepo pubblico nel monorepo privato. (Grande anteprima)

Poiché il repository privato contiene sottomoduli, per clonarlo, dobbiamo fornire l'opzione --recursive :

 git clone --recursive <private repo URL>

Riutilizzo dei flussi di lavoro delle azioni GitHub

GitHub Actions carica solo i flussi di lavoro da .github/workflows . Poiché i flussi di lavoro pubblici nel monorepo downstream si trovano in submodules/PoP/.github/workflows , questi devono essere duplicati nella posizione prevista.

Per mantenere i flussi di lavoro a monte come unica fonte di verità, possiamo limitarci a copiare i file a valle in .github/workflows , ma non modificarli mai lì. Se è necessario apportare modifiche, è necessario eseguirle nel monorepo a monte e quindi copiarle.

Come nota a margine, si noti come ciò significhi che il multi-monorepo perde: il monorepo a monte non è completamente autonomo e dovrà essere adattato per adattarsi al monorepo a valle.

Nella mia prima iterazione per copiare i flussi di lavoro, ho creato un semplice script Composer:

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

Quindi, dopo aver modificato i flussi di lavoro nel monorepo upstream, li copierei a valle eseguendo quanto segue:

 composer copy-workflows

Ma poi ho capito che non basta copiare i flussi di lavoro: devono anche essere modificati nel processo. Questo perché il controllo del monorepo downstream richiede l'opzione --recurse-submodules per controllare anche i sottomoduli.

In GitHub Actions, il checkout a valle viene eseguito in questo modo:

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

Quindi, il controllo del repository a valle richiede i submodules: recursive , ma quello a monte no, ed entrambi usano lo stesso file sorgente.

La soluzione che ho trovato è fornire il valore per i submodules di input tramite la variabile di ambiente CHECKOUT_SUBMODULES , che per impostazione predefinita è vuota per il repository upstream:

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

Quindi, quando si copiano i flussi di lavoro da upstream a downstream, il valore di CHECKOUT_SUBMODULES viene sostituito con recursive :

 env: CHECKOUT_SUBMODULES: "recursive"

Quando si modifica il flusso di lavoro, è consigliabile utilizzare un'espressione regolare (regex), in modo che funzioni per diversi formati nel file di origine (come CHECKOUT_SUBMODULES: "" o CHECKOUT_SUBMODULES:'' o CHECKOUT_SUBMODULES: ). Ciò impedirà la creazione di bug per questo tipo di modifiche apparentemente innocue.

Pertanto, lo script copy-workflows Composer mostrato sopra non è abbastanza buono per gestire questa complessità.

Nella mia successiva iterazione, ho creato un comando PHP, CopyUpstreamMonorepoFilesCommand , da eseguire tramite Monorepo Builder:

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

Questo comando utilizza un servizio personalizzato, FileCopierSystem , per copiare tutti i file da una cartella di origine alla destinazione specificata, sostituendo facoltativamente il loro contenuto:

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

Quando invoco questo metodo per copiare tutti i flussi di lavoro a valle, sostituisco anche il valore di 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 );

Il flusso di lavoro in generate_plugins.yml necessita di una sostituzione aggiuntiva. Quando il plugin di WordPress viene generato, il suo codice viene declassato da PHP 8.0 a 7.1 invocando lo script 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 }}"

Nel monorepo downstream, questo file si troverà in submodules/PoP/ci/downgrade/downgrade_code.sh . Quindi, indichiamo il flusso di lavoro a valle sulla strada giusta con questa sostituzione:

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

Configurazione dei pacchetti in Monorepo Builder

Il file monorepo-builder.php — posto alla radice del monorepo — contiene la configurazione per Monorepo Builder. In esso, dobbiamo indicare dove si trovano i pacchetti (e plugin, client e quant'altro):

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

Il monorepo privato deve avere accesso a tutto il codice: i propri pacchetti, più quelli del monorepo pubblico. Quindi, deve definire tutti i pacchetti da entrambi i monorepos nel file di configurazione. Quelli del monorepo pubblico si trovano in /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', ]); };

Così com'è, le configurazioni per upstream e downstream sono praticamente le stesse, la differenza è che quella a valle:

  • cambia il percorso dei pacchetti pubblici,
  • aggiungi i pacchetti privati.

Quindi, ha senso riscrivere la configurazione utilizzando la programmazione orientata agli oggetti (OOP). Seguiamo il principio DRY ("non ripetere te stesso") facendo estendere una classe PHP nel repository pubblico nel repository privato.

Ricreare la configurazione tramite OOP

Ridimensioniamo la configurazione. Nel repository pubblico, il file monorepo-builder.php farà semplicemente riferimento a una nuova classe, ContainerConfigurationService , in cui avverrà tutta l'azione:

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

Il parametro __DIR__ punta alla radice del monorepo. Sarà necessario per ottenere il percorso completo delle directory dei pacchetti.

La classe ContainerConfigurationService è ora incaricata di produrre la configurazione:

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

La configurazione può essere suddivisa in più classi. In questo caso, ContainerConfigurationService recupera la configurazione del pacchetto tramite la classe PackageOrganizationDataSource , di cui puoi vedere l'implementazione:

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

Sovrascrivere la configurazione nel Monorepo a valle

Ora che la configurazione nel monorepo pubblico è stata impostata tramite OOP, possiamo estenderla per soddisfare le esigenze del monorepo privato.

Per consentire al monorepo privato di caricare automaticamente il codice PHP dal monorepo pubblico, dobbiamo prima configurare il file composer.json a valle per fare riferimento al codice sorgente a monte, che si trova nel percorso submodules/PoP/src :

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

Di seguito è riportato il file monorepo-builder.php per il monorepo privato. Si noti che la classe di riferimento ContainerConfigurationService nel repository upstream apparteneva allo spazio dei nomi PoP\PoP ma ora è stata commutata nello spazio dei nomi PoP\GraphQLAPIPRO . Questa classe deve ricevere l'input aggiuntivo di $upstreamRelativeRootPath (con un valore di submodules/PoP ) per ricreare il percorso completo dei pacchetti pubblici:

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

La classe a valle ContainerConfigurationService esegue l'override della classe PackageOrganizationDataSource utilizzata nella configurazione:

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

Infine, la classe a valle PackageOrganizationDataSource contiene il percorso completo dei pacchetti pubblici e privati:

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

Iniezione della configurazione da PHP nelle azioni di GitHub

Monorepo Builder offre il comando packages-json , che possiamo utilizzare per inserire i percorsi del pacchetto nel flusso di lavoro di 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 }}

Questo comando produce un JSON stringato. Nel flusso di lavoro, deve essere convertito in un oggetto JSON tramite fromJson :

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

Sfortunatamente, il comando packages-json restituisce i nomi dei pacchetti ma non i loro percorsi. Funzionerebbe se tutti i pacchetti si trovassero nella stessa cartella (come packages/ ), ma nel nostro caso non funziona perché i pacchetti pubblici e privati ​​si trovano in cartelle diverse.

Fortunatamente, Monorepo Builder può essere esteso con servizi PHP personalizzati. Quindi, ho creato un comando personalizzato, package-entries-json (tramite la classe PackageEntriesJsonCommand ), che restituisce il percorso del pacchetto.

Il flusso di lavoro è stato quindi aggiornato con il nuovo comando:

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

Eseguito sul monorepo pubblico, questo produce i seguenti pacchetti (tra molti altri):

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

Eseguito sul monorepo privato, produce le seguenti voci (tra molte altre):

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

Funziona abbastanza bene. La configurazione per il monorepo a valle contiene pacchetti sia pubblici che privati, e i percorsi per quelli pubblici sono anteposti con submodules/PoP .

Saltare i pacchetti pubblici nel Monorepo a valle

Finora, il monorepo a valle include nella sua configurazione pacchetti sia pubblici che privati. Tuttavia, non tutti i comandi devono essere eseguiti sui pacchetti pubblici.

Prendi l'analisi statica, per esempio. Il monorepo pubblico esegue già PHPStan su tutti i pacchetti pubblici tramite il file di flusso di lavoro phpstan.yml , come mostrato in questa esecuzione. Se il monorepo a valle eseguisse nuovamente PHPStan sui pacchetti pubblici, sarebbe una perdita di tempo di elaborazione. Il flusso di lavoro phpstan.yml deve essere eseguito solo sui pacchetti privati.

Ciò significa che, a seconda del comando da eseguire nel repository a valle, potremmo voler includere sia i pacchetti pubblici che quelli privati ​​o solo quelli privati.

Per determinare se aggiungere pacchetti pubblici nella configurazione downstream, adattiamo la classe downstream PackageOrganizationDataSource per verificare questa condizione tramite l'input $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', ] ); } }

Successivamente, dobbiamo fornire il valore $includeUpstreamPackages come true o false , a seconda del comando da eseguire.

Possiamo farlo sostituendo il file di configurazione monorepo-builder.php con altri due file di configurazione: monorepo-builder-with-upstream-packages.php (che passa $includeUpstreamPackages => true ) e monorepo-builder-without-upstream-packages.php (che passa $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(); };

Quindi aggiorniamo ContainerConfigurationService per ricevere il parametro $includeUpstreamPackages e passarlo a 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, ); } }

Successivamente, dobbiamo invocare il monorepo-builder con uno dei file di configurazione, fornendo l'opzione --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)"

Tuttavia, come abbiamo visto in precedenza, vogliamo mantenere i flussi di lavoro di GitHub Actions nel monorepo upstream come unica fonte di verità e chiaramente non hanno bisogno di queste modifiche.

La soluzione che ho trovato a questo problema è fornire sempre un'opzione --config nel repository upstream, con ogni comando che ottiene il proprio file di configurazione, come il comando validate che riceve il file di configurazione validate.php :

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

Ora, non ci sono file di configurazione nel monorepo upstream, perché non ne ha bisogno. Ma non si romperà, perché Monorepo Builder controlla se il file di configurazione esiste e, in caso contrario, carica invece il file di configurazione predefinito. Quindi, o sovrascriveremo la configurazione o non accadrà nulla.

Il repository a valle fornisce i file di configurazione per ogni comando, specificando se aggiungere i pacchetti a monte:

Come nota a margine, questo è un altro esempio di come trapela il multi-monorepo.

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

Sovrascrivere la configurazione

Abbiamo quasi finito. A questo punto, il monorepo a valle può sovrascrivere la configurazione del monorepo a monte. Quindi, tutto ciò che resta da fare è fornire la nuova configurazione.

Nella classe PluginDataSource , sovrascrivo la configurazione di quali plugin di WordPress devono essere generati, fornendo invece quelli 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', ], ]; } }

La creazione di una nuova versione su GitHub attiverà il flusso di lavoro generate_plugins.yml e genererà i plug-in pro nel mio monorepo privato:

Generazione di plugin professionali
Generazione di plugin professionali. (Grande anteprima)

Ta-da!

Conclusione

Come sempre, non esiste una soluzione “migliore”, solo soluzioni che potrebbero funzionare meglio a seconda del contesto. L'approccio multi-monorepo non è adatto a ogni tipo di progetto o team. Credo che i maggiori beneficiari sarebbero i creatori di plug-in che rilasciano plug-in pubblici che verranno aggiornati alle loro versioni pro, nonché le agenzie che personalizzano i plug-in per i loro clienti.

Nel mio caso, sono abbastanza soddisfatto di questo approccio. Farlo bene richiede un po' di tempo e fatica, ma è un investimento una tantum. Una volta terminata la configurazione, posso concentrarmi sulla creazione dei miei plug-in professionali e il tempo risparmiato con la gestione del progetto potrebbe essere enorme.