Création d'un multi-monorepo public/privé pour les projets PHP
Publié: 2022-03-10Pour accélérer mon expérience de développement, j'ai déplacé tous les packages PHP requis par mes projets vers un monorepo. Lorsque chaque package est hébergé dans son propre référentiel (l'approche multirepo), je devrais le développer et le tester par lui-même, puis le publier sur Packagist avant de pouvoir l'installer dans d'autres packages via Composer. Avec le monorepo, comme tous les packages sont hébergés ensemble, ils peuvent être développés, testés, versionnés et publiés en même temps.
Le monorepo qui héberge mes packages PHP est public, accessible à tous sur GitHub. Les référentiels Git ne peuvent pas accorder un accès différent à différents actifs ; tout est public ou privé. Parce que je prévois de publier un plugin WordPress professionnel, je souhaite que ses packages restent privés, ce qui signifie qu'ils ne peuvent pas être ajoutés au monorepo public.
La solution que j'ai trouvée consiste à utiliser une approche "multi-monorepo", comprenant deux monorepos : un public et un privé, le monorepo privé intégrant le public en tant que sous-module Git, lui permettant d'accéder à ses fichiers. Le monorepo public peut être considéré comme celui en amont, et le monorepo privé comme celui en aval.
Comme je continuais à itérer sur mon code, la configuration du référentiel que je devais utiliser à chaque étape de mon projet devait également être mise à jour. Par conséquent, je ne suis pas arrivé à l'approche multi-monorepo dès le premier jour ; c'était un processus qui a duré plusieurs années et a demandé pas mal d'efforts, passant d'un référentiel unique à plusieurs référentiels, au monorepo et enfin au multi-monorepo.
Dans cet article, je vais décrire comment j'ai configuré mon multi-monorepo en utilisant Monorepo Builder, qui fonctionne pour les projets PHP et est basé sur Composer.
Réutiliser le code dans le Multi-Monorepo
Le monorepo public de leoloso/PoP
est l'endroit où je conserve tous mes projets PHP.
Ce monorepo contient le fichier de workflow generate_plugins.yml
, qui génère plusieurs plugins WordPress à distribuer lorsque je crée une release sur GitHub :
La configuration du workflow n'est pas codée en dur dans le fichier YAML, mais plutôt injectée via du code PHP :
- id: output_data run: | echo "::set-output name=plugin_config_entries::$(vendor/bin/monorepo-builder plugin-config-entries-json)"
Et la configuration est fournie via une classe PHP personnalisée :
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 génération de plusieurs plugins WordPress ensemble et la configuration du flux de travail via PHP ont réduit le temps dont j'avais besoin pour gérer le projet. Le flux de travail gère actuellement deux plugins (l'API GraphQL et sa démo d'extension), mais il pourrait en gérer 200 sans effort supplémentaire de ma part.
C'est cette configuration que je souhaite réutiliser pour mon monorepo privé chez leoloso/GraphQLAPI-PRO
, afin que les plugins pro puissent également être générés sans effort.
Le code à réutiliser comprendra :
- les flux de travail GitHub Actions pour générer les plugins WordPress (y compris la portée, la rétrogradation de PHP 8.0 à 7.1 et le téléchargement sur la page des versions).
- les services PHP personnalisés pour configurer les workflows.
Le monorepo privé peut alors générer les plugins WordPress pro simplement en déclenchant les workflows du monorepo public et en remplaçant leur configuration en PHP.
Lier Monorepos via des sous-modules Git
Pour intégrer le référentiel public dans le référentiel privé, nous utilisons les sous-modules Git :
git submodule add <public repo URL>
J'ai intégré le référentiel public dans les sous- submodules
de sous-dossiers du monorepo privé, ce qui me permet d'ajouter plus de monorepos en amont à l'avenir si nécessaire. Dans GitHub, le dossier affiche le commit spécifique du sous-module, et cliquer dessus m'amènera à ce commit sur leoloso/PoP
:
Comme le dépôt privé contient des sous-modules, pour le cloner, nous devons fournir l'option --recursive
:
git clone --recursive <private repo URL>
Réutiliser les flux de travail GitHub Actions
GitHub Actions charge uniquement les workflows sous .github/workflows
. Étant donné que les flux de travail publics dans le monorepo en aval se trouvent sous submodules/PoP/.github/workflows
, ils doivent être dupliqués à l'emplacement prévu.
Afin de conserver les flux de travail en amont comme source unique de vérité, nous pouvons nous limiter à copier les fichiers en aval sous .github/workflows
, mais jamais les éditer ici. Si une modification doit être effectuée, elle doit être effectuée dans le monorepo en amont, puis copiée.
En remarque, notez comment cela signifie que le multi-monorepo fuit : le monorepo en amont n'est pas entièrement autonome et il devra être adapté pour convenir au monorepo en aval.
Lors de ma première itération pour copier les workflows, j'ai créé un simple 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');\"" ] } }
Ensuite, après avoir modifié les flux de travail dans le monorepo en amont, je les copierais en aval en exécutant ce qui suit :
composer copy-workflows
Mais ensuite, j'ai réalisé qu'il ne suffisait pas de copier les flux de travail : ils devaient également être modifiés au cours du processus. En effet, l'extraction du monorepo en aval nécessite l'option --recurse-submodules
afin d'extraire également les sous-modules.
Dans GitHub Actions, le paiement en aval se fait comme ceci :
- uses: actions/checkout@v2 with: submodules: recursive
Ainsi, l'extraction du référentiel en aval nécessite les submodules: recursive
, mais pas celui en amont, et ils utilisent tous les deux le même fichier source.
La solution que j'ai trouvée est de fournir la valeur des sous- submodules
d'entrée via la variable d'environnement CHECKOUT_SUBMODULES
, qui par défaut est vide pour le référentiel en amont :
env: CHECKOUT_SUBMODULES: "" jobs: provide_data: steps: - uses: actions/checkout@v2 with: submodules: ${{ env.CHECKOUT_SUBMODULES }}
Ensuite, lors de la copie des workflows d'amont en aval, la valeur de CHECKOUT_SUBMODULES
est remplacée par recursive
:
env: CHECKOUT_SUBMODULES: "recursive"
Lors de la modification du flux de travail, il est conseillé d'utiliser une expression régulière (regex), afin qu'elle fonctionne pour différents formats dans le fichier source (tels que CHECKOUT_SUBMODULES: ""
ou CHECKOUT_SUBMODULES:''
ou CHECKOUT_SUBMODULES:
). Cela empêchera la création de bogues pour ces types de modifications apparemment inoffensives.
Ainsi, le script Composer copy-workflows
présenté ci-dessus n'est pas assez bon pour gérer cette complexité.
Dans ma prochaine itération, j'ai créé une commande PHP, CopyUpstreamMonorepoFilesCommand
, à exécuter via Monorepo Builder :
vendor/bin/monorepo-builder copy-upstream-monorepo-files
Cette commande utilise un service personnalisé, FileCopierSystem
, pour copier tous les fichiers d'un dossier source vers la destination spécifiée, tout en remplaçant éventuellement leur contenu :
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); } } }
Lors de l'appel de cette méthode pour copier tous les workflows en aval, je remplace également la valeur de 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 );
Le flux de travail dans generate_plugins.yml
besoin d'un remplacement supplémentaire. Lorsque le plugin WordPress est généré, son code est rétrogradé de PHP 8.0 à 7.1 en appelant le 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 }}"
Dans le monorepo en aval, ce fichier sera situé sous submodules/PoP/ci/downgrade/downgrade_code.sh
. Ensuite, nous orientons le flux de travail en aval vers le bon chemin avec ce remplacement :
$regexReplacements = [ // ... '#(ci/downgrade/downgrade_code\.sh)#' => 'submodules/PoP/$1', ];
Configuration des packages dans Monorepo Builder
Le fichier monorepo-builder.php
— placé à la racine du monorepo — contient la configuration de Monorepo Builder. Dans celui-ci, nous devons indiquer où se trouvent les packages (et les plugins, les clients et tout le reste) :
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', ]); };
Le monorepo privé doit avoir accès à tout le code : ses propres packages, plus ceux du monorepo public. Ensuite, il doit définir tous les packages des deux monorepos dans le fichier de configuration. Ceux du monorepo public sont situés sous /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', ]); };
En l'état, les configurations pour l'amont et l'aval sont à peu près les mêmes, les différences étant que l'aval :
- changer le chemin vers les packages publics,
- ajouter les packages privés.
Il est donc logique de réécrire la configuration à l'aide de la programmation orientée objet (POO). Suivons le principe DRY ("ne vous répétez pas") en faisant en sorte qu'une classe PHP dans le référentiel public soit étendue dans le référentiel privé.
Recréer la configuration via OOP
Refactorisons la configuration. Dans le référentiel public, le fichier monorepo-builder.php
fera simplement référence à une nouvelle classe, ContainerConfigurationService
, où toutes les actions se produiront :
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(); };
Le paramètre __DIR__
pointe vers la racine du monorepo. Il sera nécessaire pour obtenir le chemin complet vers les répertoires des packages.
La classe ContainerConfigurationService
est maintenant en charge de produire la configuration :
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 configuration peut être répartie sur plusieurs classes. Dans ce cas, ContainerConfigurationService
récupère la configuration du package via la classe PackageOrganizationDataSource
, dont vous pouvez voir l'implémentation :
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', ]; } }
Remplacement de la configuration dans le Monorepo en aval
Maintenant que la configuration du monorepo public a été configurée à l'aide de la POO, nous pouvons l'étendre pour répondre aux besoins du monorepo privé.
Afin de permettre au monorepo privé de charger automatiquement le code PHP depuis le monorepo public, nous devons d'abord configurer le fichier composer.json
en aval pour référencer le code source en amont, qui se trouve sous le chemin submodules/PoP/src
:
{ "autoload": { "psr-4": { "PoP\\GraphQLAPIPRO\\": "src", "PoP\\PoP\\": "submodules/PoP/src" } } }
Ci-dessous se trouve le fichier monorepo-builder.php
pour le monorepo privé. Notez que la classe référencée ContainerConfigurationService
dans le référentiel en amont appartenait à l'espace de noms PoP\PoP
mais a maintenant été basculée vers l'espace de noms PoP\GraphQLAPIPRO
. Cette classe doit recevoir l'entrée supplémentaire de $upstreamRelativeRootPath
(avec une valeur de submodules/PoP
) afin de recréer le chemin complet vers les packages publics :
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 en aval ContainerConfigurationService
remplace la classe PackageOrganizationDataSource
utilisée dans la configuration :
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 ); } }
Enfin, la classe en aval PackageOrganizationDataSource
contient le chemin complet vers les packages publics et privés :
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', ] ); } }
Injecter la configuration de PHP dans les actions GitHub
Monorepo Builder propose la commande packages-json
, que nous pouvons utiliser pour injecter les chemins de package dans le workflow 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 }}
Cette commande produit un JSON sous forme de chaîne. Dans le workflow, il doit être converti en objet JSON via fromJson
:
jobs: split_monorepo: needs: provide_data strategy: matrix: package: ${{ fromJson(needs.provide_data.outputs.matrix) }}
Malheureusement, la commande packages-json
affiche les noms des packages mais pas leurs chemins. Cela fonctionnerait si tous les packages se trouvaient dans le même dossier (tels que packages/
), mais cela ne fonctionne pas dans notre cas car les packages publics et privés sont situés dans des dossiers différents.
Heureusement, Monorepo Builder peut être étendu avec des services PHP personnalisés. J'ai donc créé une commande personnalisée, package-entries-json
(via la classe PackageEntriesJsonCommand
), qui affiche le chemin d'accès au package.
Le workflow a ensuite été mis à jour avec la nouvelle commande :
run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json)"
Exécuté sur le monorepo public, cela produit les packages suivants (parmi beaucoup d'autres) :
[ { "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" } ]
Exécuté sur le monorepo privé, il produit les entrées suivantes (parmi beaucoup d'autres) :
[ { "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" } ]
Cela fonctionne très bien. La configuration du monorepo en aval contient à la fois des packages publics et privés, et les chemins vers les publics sont précédés de submodules/PoP
.
Ignorer les packages publics dans le Monorepo en aval
Jusqu'à présent, le monorepo en aval inclut à la fois des packages publics et privés dans sa configuration. Cependant, toutes les commandes ne doivent pas être exécutées sur les packages publics.
Prenez l'analyse statique, par exemple. Le monorepo public exécute déjà PHPStan sur tous les packages publics via le fichier de workflow phpstan.yml
, comme indiqué dans cette exécution. Si le monorepo en aval exécutait à nouveau PHPStan sur les packages publics, ce serait une perte de temps de calcul. Le flux de travail phpstan.yml
ne doit s'exécuter que sur les packages privés.
Cela signifie que, selon la commande à exécuter dans le référentiel en aval, nous pourrions vouloir inclure à la fois des packages publics et privés ou uniquement des packages privés.
Pour déterminer s'il faut ajouter des packages publics dans la configuration en aval, nous adaptons la classe en aval PackageOrganizationDataSource
pour vérifier cette condition via l'entrée $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', ] ); } }
Ensuite, nous devons fournir la valeur $includeUpstreamPackages
comme true
ou false
, selon la commande à exécuter.
Nous pouvons le faire en remplaçant le fichier de configuration monorepo-builder.php
par deux autres fichiers de configuration : monorepo-builder-with-upstream-packages.php
(qui passe $includeUpstreamPackages
=> true
) et monorepo-builder-without-upstream-packages.php
(qui passe $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(); };
Nous mettons ensuite à jour ContainerConfigurationService
pour recevoir le paramètre $includeUpstreamPackages
et le transmettons à 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, ); } }
Ensuite, nous devons invoquer le monorepo-builder
avec l'un ou l'autre des fichiers de configuration, en fournissant l'option --config
:
jobs: provide_data: steps: - id: output_data name: Calculate matrix for packages run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json --config=monorepo-builder-without-upstream-packages.php)"
Cependant, comme nous l'avons vu précédemment, nous souhaitons conserver les flux de travail GitHub Actions dans le monorepo en amont comme source unique de vérité, et ils n'ont clairement pas besoin de ces changements.
La solution que j'ai trouvée à ce problème est de toujours fournir une option --config
dans le référentiel en amont, chaque commande obtenant son propre fichier de configuration, comme la commande validate
recevant le fichier de configuration validate.php
:
- name: Run validation run: vendor/bin/monorepo-builder validate --config=config/monorepo-builder/validate.php
Maintenant, il n'y a pas de fichiers de configuration dans le monorepo en amont, car il n'en a pas besoin. Mais il ne se cassera pas, car Monorepo Builder vérifie si le fichier de configuration existe et, si ce n'est pas le cas, il charge le fichier de configuration par défaut à la place. Donc, soit nous annulerons la configuration, soit rien ne se passera.
Le référentiel en aval fournit les fichiers de configuration pour chaque commande, en spécifiant s'il faut ajouter les packages en amont :
En remarque, ceci est un autre exemple de la façon dont le multi-monorepo fuit.
// File config/monorepo-builder/validate.php return require_once __DIR__ . '/monorepo-builder-with-upstream-packages.php';
Remplacement de la configuration
On a presque fini. À présent, le monorepo en aval peut remplacer la configuration du monorepo en amont. Il ne reste donc plus qu'à fournir la nouvelle configuration.
Dans la classe PluginDataSource
, je remplace la configuration dont les plugins WordPress doivent être générés, en fournissant les pros à la place :
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 création d'une nouvelle version sur GitHub déclenchera le workflow generate_plugins.yml
et générera les plugins pro dans mon monorepo privé :
Ta-da !
Conclusion
Comme toujours, il n'y a pas de "meilleure" solution, seulement des solutions qui pourraient mieux fonctionner selon le contexte. L'approche multi-monorepo n'est pas adaptée à tous les types de projets ou d'équipes. Je pense que les plus grands bénéficiaires seraient les créateurs de plugins qui publient des plugins publics qui seront mis à niveau vers leurs versions professionnelles, ainsi que les agences qui personnalisent les plugins pour leurs clients.
Dans mon cas, je suis assez satisfait de cette approche. Bien faire les choses demande un peu de temps et d'efforts, mais c'est un investissement ponctuel. Une fois la configuration terminée, je peux me concentrer sur la construction de mes plugins pro, et le temps gagné avec la gestion de projet pourrait être énorme.