Creando un Multi-Monorepo Público/Privado para Proyectos PHP
Publicado: 2022-03-10Para hacer que mi experiencia de desarrollo sea más rápida, moví todos los paquetes de PHP requeridos por mis proyectos a un monorepo. Cuando cada paquete está alojado en su propio repositorio (el enfoque multirepo), necesitaría desarrollarlo y probarlo por sí mismo y luego publicarlo en Packagist antes de poder instalarlo en otros paquetes a través de Composer. Con el monorepo, debido a que todos los paquetes se alojan juntos, se pueden desarrollar, probar, versionar y lanzar al mismo tiempo.
El monorepo que aloja mis paquetes PHP es público, accesible para cualquier persona en GitHub. Los repositorios de Git no pueden otorgar diferentes accesos a diferentes activos; todo es público o privado. Debido a que planeo lanzar un complemento profesional de WordPress, quiero que sus paquetes se mantengan privados, lo que significa que no se pueden agregar al monorepo público.
La solución que encontré es utilizar un enfoque "multi-monorepo", que comprende dos monorepos: uno público y otro privado, con el monorepo privado incrustando el público como un submódulo de Git, lo que le permite acceder a sus archivos. El monorepo público se puede considerar el upstream y el privado el downstream.
Mientras seguí iterando en mi código, la configuración del repositorio que necesitaba usar en cada etapa de mi proyecto también necesitaba ser actualizada. Por lo tanto, no llegué al enfoque multi-monorepo el primer día; fue un proceso que abarcó varios años y requirió una gran cantidad de esfuerzo, pasando de un solo repositorio a múltiples repositorios, al monorepo y, finalmente, al multi-monorepo.
En este artículo, describiré cómo configuro mi multi-monorepo usando Monorepo Builder, que funciona para proyectos PHP y está basado en Composer.
Reutilización de código en el Multi-Monorepo
El monorepo público en leoloso/PoP
es donde guardo todos mis proyectos PHP.
Este monorepo contiene el archivo de flujo de trabajo generate_plugins.yml
, que genera múltiples complementos de WordPress para su distribución cuando creo un lanzamiento en GitHub:
La configuración del flujo de trabajo no está codificada en el archivo YAML, sino que se inyecta a través del código PHP:
- id: output_data run: | echo "::set-output name=plugin_config_entries::$(vendor/bin/monorepo-builder plugin-config-entries-json)"
Y la configuración se proporciona a través de una clase de PHP personalizada:
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', ], ]; } }
Generar múltiples complementos de WordPress todos juntos y configurar el flujo de trabajo a través de PHP ha reducido la cantidad de tiempo que necesito para administrar el proyecto. El flujo de trabajo actualmente maneja dos complementos (la API de GraphQL y su demostración de extensión), pero podría manejar 200 sin un esfuerzo adicional de mi parte.
Es esta configuración la que quiero reutilizar para mi monorepo privado en leoloso/GraphQLAPI-PRO
, para que los complementos profesionales también se puedan generar sin esfuerzo.
El código a reutilizar comprenderá:
- los flujos de trabajo de GitHub Actions para generar los complementos de WordPress (incluido el alcance, la degradación de PHP 8.0 a 7.1 y la carga en la página de lanzamientos).
- los servicios PHP personalizados para configurar los flujos de trabajo.
El monorepo privado puede generar los complementos profesionales de WordPress simplemente activando los flujos de trabajo desde el monorepo público y anulando su configuración en PHP.
Vinculación de Monorepos a través de submódulos de Git
Para incrustar el repositorio público en el privado, usamos submódulos de Git:
git submodule add <public repo URL>
Incrusté el repositorio público en los submodules
de la subcarpeta del monorepositorio privado, lo que me permite agregar más monorepos ascendentes en el futuro si es necesario. En GitHub, la carpeta muestra la confirmación específica del submódulo, y hacer clic en ella me llevará a esa confirmación en leoloso/PoP
:
Debido a que el repositorio privado contiene submódulos, para clonarlo, debemos proporcionar la opción --recursive
:
git clone --recursive <private repo URL>
Reutilización de los flujos de trabajo de acciones de GitHub
GitHub Actions solo carga flujos de trabajo desde .github/workflows
. Debido a que los flujos de trabajo públicos en el monorepositorio descendente se encuentran en submodules/PoP/.github/workflows
, estos deben duplicarse en la ubicación esperada.
Para mantener los flujos de trabajo ascendentes como la única fuente de verdad, podemos limitarnos a copiar los archivos descendentes en .github/workflows
, pero nunca editarlos allí. Si se va a realizar algún cambio, debe hacerse en el monorepo anterior y luego copiarse.
Como nota al margen, observe cómo esto significa que el multi-monorepo se filtra: el monorepo ascendente no es completamente autónomo y deberá adaptarse para adaptarse al monorepo descendente.
En mi primera iteración para copiar los flujos de trabajo, creé un script de Composer simple:
{ "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');\"" ] } }
Luego, después de editar los flujos de trabajo en el monorepo ascendente, los copiaría en sentido descendente ejecutando lo siguiente:
composer copy-workflows
Pero luego me di cuenta de que solo copiar los flujos de trabajo no es suficiente: también deben modificarse en el proceso. Esto se debe a que verificar el monorepo descendente requiere la opción --recurse-submodules
para verificar también los submódulos.
En GitHub Actions, el pago en sentido descendente se realiza de la siguiente manera:
- uses: actions/checkout@v2 with: submodules: recursive
Por lo tanto, verificar el repositorio descendente necesita los submodules: recursive
, pero el ascendente no, y ambos usan el mismo archivo fuente.
La solución que encontré es proporcionar el valor para los submodules
de entrada a través de la variable de entorno CHECKOUT_SUBMODULES
, que por defecto está vacía para el repositorio ascendente:
env: CHECKOUT_SUBMODULES: "" jobs: provide_data: steps: - uses: actions/checkout@v2 with: submodules: ${{ env.CHECKOUT_SUBMODULES }}
Luego, al copiar los flujos de trabajo de arriba a abajo, el valor de CHECKOUT_SUBMODULES
se reemplaza con recursive
:
env: CHECKOUT_SUBMODULES: "recursive"
Al modificar el flujo de trabajo, es una buena idea usar una expresión regular (regex), de modo que funcione para diferentes formatos en el archivo de origen (como CHECKOUT_SUBMODULES: ""
o CHECKOUT_SUBMODULES:''
o CHECKOUT_SUBMODULES:
). Esto evitará que se creen errores para este tipo de cambios aparentemente inofensivos.
Por lo tanto, el script Composer copy-workflows
que se muestra arriba no es lo suficientemente bueno para manejar esta complejidad.
En mi próxima iteración, creé un comando PHP, CopyUpstreamMonorepoFilesCommand
, para ejecutarlo a través de Monorepo Builder:
vendor/bin/monorepo-builder copy-upstream-monorepo-files
Este comando usa un servicio personalizado, FileCopierSystem
, para copiar todos los archivos de una carpeta de origen al destino especificado, mientras reemplaza opcionalmente su contenido:
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); } } }
Al invocar este método para copiar todos los flujos de trabajo posteriores, también reemplazo el valor 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 );
El flujo de trabajo en generate_plugins.yml
necesita un reemplazo adicional. Cuando se genera el complemento de WordPress, su código se degrada de PHP 8.0 a 7.1 invocando el 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 }}"
En el monorepo descendente, este archivo se ubicará en submodules/PoP/ci/downgrade/downgrade_code.sh
. Luego, dirigimos el flujo de trabajo descendente hacia la ruta correcta con este reemplazo:
$regexReplacements = [ // ... '#(ci/downgrade/downgrade_code\.sh)#' => 'submodules/PoP/$1', ];
Configuración de paquetes en Monorepo Builder
El archivo monorepo-builder.php
, ubicado en la raíz de monorepo, contiene la configuración de Monorepo Builder. En él, debemos indicar dónde se encuentran los paquetes (y los complementos, los clientes y todo lo demás):
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', ]); };
El monorepo privado debe tener acceso a todo el código: sus propios paquetes, más los del monorepo público. Luego, debe definir todos los paquetes de ambos monorepos en el archivo de configuración. Los del monorepo público se encuentran en /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', ]); };
Tal como están, las configuraciones para aguas arriba y aguas abajo son prácticamente las mismas, con la diferencia de que la de aguas abajo:
- cambiar la ruta a los paquetes públicos,
- agregar los paquetes privados.
Por lo tanto, tiene sentido reescribir la configuración mediante programación orientada a objetos (POO). Sigamos el principio DRY (“no te repitas”) haciendo que una clase de PHP en el repositorio público se extienda al repositorio privado.
Recreando la configuración a través de OOP
Vamos a refactorizar la configuración. En el repositorio público, el archivo monorepo-builder.php
simplemente hará referencia a una nueva clase, ContainerConfigurationService
, donde ocurrirá toda la acción:
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(); };
El parámetro __DIR__
apunta a la raíz del monorepo. Será necesario obtener la ruta completa a los directorios del paquete.
La clase ContainerConfigurationService
ahora está a cargo de producir la configuración:
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 configuración se puede dividir en varias clases. En este caso, ContainerConfigurationService
recupera la configuración del paquete a través de la clase PackageOrganizationDataSource
, cuya implementación puedes ver:
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', ]; } }
Anulación de la configuración en el Monorepo descendente
Ahora que la configuración en el monorepo público se ha establecido mediante OOP, podemos ampliarla para adaptarla a las necesidades del monorepo privado.
Para permitir que el monorepo privado cargue automáticamente el código PHP desde el monorepo público, primero debemos configurar el archivo composer.json
descendente para que haga referencia al código fuente ascendente, que se encuentra en la ruta submodules/PoP/src
:
{ "autoload": { "psr-4": { "PoP\\GraphQLAPIPRO\\": "src", "PoP\\PoP\\": "submodules/PoP/src" } } }
A continuación se muestra el archivo monorepo-builder.php
para el monorepo privado. Observe que la clase a la que se hace referencia ContainerConfigurationService
en el repositorio ascendente pertenecía al espacio de nombres PoP\PoP
pero ahora se ha cambiado al espacio de nombres PoP\GraphQLAPIPRO
. Esta clase debe recibir la entrada adicional de $upstreamRelativeRootPath
(con un valor de submodules/PoP
) para recrear la ruta completa a los paquetes públicos:
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 clase descendente ContainerConfigurationService
anula qué clase PackageOrganizationDataSource
se usa en la configuración:
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 ); } }
Finalmente, la clase descendente PackageOrganizationDataSource
contiene la ruta completa a los paquetes públicos y privados:
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', ] ); } }
Inyectar la configuración de PHP en las acciones de GitHub
Monorepo Builder ofrece el comando packages-json
, que podemos usar para inyectar las rutas de los paquetes en el flujo de trabajo de 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 }}
Este comando produce un JSON en cadena. En el flujo de trabajo, debe convertirse en un objeto JSON a través fromJson
:
jobs: split_monorepo: needs: provide_data strategy: matrix: package: ${{ fromJson(needs.provide_data.outputs.matrix) }}
Desafortunadamente, el comando packages-json
genera los nombres de los paquetes pero no sus rutas. Esto funcionaría si todos los paquetes estuvieran en la misma carpeta (como packages/
), pero no funciona en nuestro caso porque los paquetes públicos y privados están ubicados en carpetas diferentes.
Afortunadamente, Monorepo Builder se puede ampliar con servicios PHP personalizados. Entonces, creé un comando personalizado, package-entries-json
(a través de la clase PackageEntriesJsonCommand
), que genera la ruta al paquete.
Luego, el flujo de trabajo se actualizó con el nuevo comando:
run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json)"
Ejecutado en el monorepo público, produce los siguientes paquetes (entre muchos otros):
[ { "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" } ]
Ejecutado en el monorepo privado, produce las siguientes entradas (entre muchas otras):
[ { "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" } ]
Funciona bastante bien. La configuración para el monorepo descendente contiene paquetes públicos y privados, y las rutas a los públicos se anteponen con submodules/PoP
.
Omitir paquetes públicos en el Monorepo descendente
Hasta ahora, el monorepo descendente incluye paquetes públicos y privados en su configuración. Sin embargo, no es necesario ejecutar todos los comandos en los paquetes públicos.
Tome el análisis estático, por ejemplo. El monorepositorio público ya ejecuta PHPStan en todos los paquetes públicos a través del archivo de flujo de trabajo phpstan.yml
, como se muestra en esta ejecución. Si el monorepo descendente ejecutara PHPStan una vez más en los paquetes públicos, sería una pérdida de tiempo informático. El flujo de trabajo de phpstan.yml
solo necesita ejecutarse en los paquetes privados.
Esto significa que, según el comando que se ejecutará en el repositorio descendente, es posible que deseemos incluir paquetes públicos y privados o solo privados.
Para determinar si agregar paquetes públicos en la configuración descendente, adaptamos la clase descendente PackageOrganizationDataSource
para verificar esta condición a través de la entrada $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', ] ); } }
A continuación, debemos proporcionar el valor $includeUpstreamPackages
como true
o false
, según el comando que se ejecute.
Podemos hacer esto reemplazando el archivo de configuración monorepo-builder.php
con otros dos archivos de configuración: monorepo-builder-with-upstream-packages.php
(que pasa $includeUpstreamPackages
=> true
) y monorepo-builder-without-upstream-packages.php
(que pasa $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(); };
Luego actualizamos ContainerConfigurationService
para recibir el parámetro $includeUpstreamPackages
y lo pasamos 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, ); } }
A continuación, tenemos que invocar el monorepo-builder
con cualquiera de los archivos de configuración, proporcionando la opción --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)"
Sin embargo, como vimos anteriormente, queremos mantener los flujos de trabajo de GitHub Actions en el monorepo ascendente como la única fuente de verdad, y claramente no necesitan estos cambios.
La solución que encontré para este problema es proporcionar siempre una opción --config
en el repositorio ascendente, con cada comando obteniendo su propio archivo de configuración, como el comando de validate
que recibe el archivo de configuración de validate.php
:
- name: Run validation run: vendor/bin/monorepo-builder validate --config=config/monorepo-builder/validate.php
Ahora, no hay archivos de configuración en el monorepo ascendente, porque no los necesita. Pero no se romperá, porque Monorepo Builder verifica si el archivo de configuración existe y, si no es así, carga el archivo de configuración predeterminado en su lugar. Entonces, anularemos la configuración o no pasará nada.
El repositorio de aguas abajo proporciona los archivos de configuración para cada comando, especificando si agregar los paquetes de aguas arriba:
Como nota al margen, este es otro ejemplo de cómo se filtra el multi-monorepo.
// File config/monorepo-builder/validate.php return require_once __DIR__ . '/monorepo-builder-with-upstream-packages.php';
Anular la configuración
Casi terminamos. Por ahora, el monorepo descendente puede anular la configuración del monorepo ascendente. Entonces, todo lo que queda por hacer es proporcionar la nueva configuración.
En la clase PluginDataSource
, anulo la configuración de los complementos de WordPress que deben generarse, proporcionando los profesionales en su lugar:
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 creación de una nueva versión en GitHub activará el flujo de trabajo generate_plugins.yml
y generará los complementos profesionales en mi monorepo privado:
Ta-da!
Conclusión
Como siempre, no existe la "mejor" solución, solo soluciones que podrían funcionar mejor según el contexto. El enfoque multi-monorepo no es adecuado para todo tipo de proyecto o equipo. Creo que los mayores beneficiarios serían los creadores de complementos que lanzan complementos públicos que se actualizarán a sus versiones pro, así como las agencias que personalizan los complementos para sus clientes.
En mi caso, estoy bastante contento con este enfoque. Hacerlo bien requiere un poco de tiempo y esfuerzo, pero es una inversión única. Una vez que finaliza la configuración, puedo concentrarme en crear mis complementos profesionales, y el tiempo ahorrado con la gestión de proyectos podría ser enorme.