Creazione di un multi-Monorepo pubblico/privato per progetti PHP
Pubblicato: 2022-03-10Per 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.
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.
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:
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
:
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:
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.