Criando um Multi-Monorepo público/privado para projetos PHP
Publicados: 2022-03-10Para tornar minha experiência de desenvolvimento mais rápida, movi todos os pacotes PHP exigidos pelos meus projetos para um monorepo. Quando cada pacote é hospedado em seu próprio repositório (a abordagem multirepo), eu precisaria desenvolvê-lo e testá-lo por conta própria e depois publicá-lo no Packagist antes de poder instalá-lo em outros pacotes via Composer. Com o monorepo, como todos os pacotes são hospedados juntos, eles podem ser desenvolvidos, testados, versionados e lançados ao mesmo tempo.
O monorepo que hospeda meus pacotes PHP é público, acessível a qualquer pessoa no GitHub. Os repositórios Git não podem conceder acessos diferentes a ativos diferentes; é tudo público ou privado. Como pretendo lançar um plugin pro WordPress, quero que seus pacotes sejam mantidos privados, o que significa que não podem ser adicionados ao monorepo público.
A solução que encontrei é usar uma abordagem “multi-monorepo”, composta por dois monorepos: um público e um privado, com o monorepo privado incorporando o público como um submódulo Git, permitindo que ele acesse seus arquivos. O monorepo público pode ser considerado o upstream e o monorepo privado o downstream.
Como eu continuei iterando no meu código, a configuração do repositório que eu precisava usar em cada estágio do meu projeto também precisava ser atualizada. Portanto, não cheguei à abordagem multi-monorepo no primeiro dia; foi um processo que durou vários anos e exigiu bastante esforço, indo de um único repositório para vários repositórios, para o monorepo e, finalmente, para o multi-monorepo.
Neste artigo, descreverei como configuro meu multi-monorepo usando o Monorepo Builder, que funciona para projetos PHP e é baseado no Composer.
Reutilizando código no Multi-Monorepo
O monorepo público em leoloso/PoP
é onde guardo todos os meus projetos PHP.
Este monorepo contém o arquivo de fluxo de trabalho generate_plugins.yml
, que gera vários plugins do WordPress para distribuição quando crio uma versão no GitHub:
A configuração do fluxo de trabalho não é codificada no arquivo YAML, mas sim injetada via código PHP:
- id: output_data run: | echo "::set-output name=plugin_config_entries::$(vendor/bin/monorepo-builder plugin-config-entries-json)"
E a configuração é fornecida por meio de uma classe 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', ], ]; } }
Gerar vários plugins do WordPress juntos e configurar o fluxo de trabalho via PHP reduziu a quantidade de tempo que preciso para gerenciar o projeto. O fluxo de trabalho atualmente lida com dois plugins (a API GraphQL e sua demonstração de extensão), mas poderia lidar com 200 sem esforço adicional de minha parte.
É essa configuração que quero reutilizar para meu monorepo privado em leoloso/GraphQLAPI-PRO
, para que os plugins pro também possam ser gerados sem esforço.
O código a ser reutilizado compreenderá:
- os fluxos de trabalho do GitHub Actions para gerar os plugins do WordPress (incluindo escopo, downgrade do PHP 8.0 para 7.1 e upload para a página de lançamentos).
- os serviços PHP personalizados para configurar os fluxos de trabalho.
O monorepo privado pode gerar os plugins pro WordPress simplesmente acionando os fluxos de trabalho do monorepo público e substituindo sua configuração em PHP.
Vinculando monorepos via submódulos Git
Para incorporar o repositório público no privado, usamos os submódulos do Git:
git submodule add <public repo URL>
Eu incorporei o repositório público nos submodules
de subpasta do monorepo privado, permitindo-me adicionar mais monorepos upstream no futuro, se necessário. No GitHub, a pasta exibe o commit específico do submódulo, e clicar nele me levará a esse commit em leoloso/PoP
:
Como o repositório privado contém submódulos, para cloná-lo, devemos fornecer a opção --recursive
:
git clone --recursive <private repo URL>
Reutilizando os fluxos de trabalho de ações do GitHub
O GitHub Actions carrega apenas fluxos de trabalho em .github/workflows
. Como os fluxos de trabalho públicos no monorepo downstream estão em submodules/PoP/.github/workflows
, eles devem ser duplicados no local esperado.
Para manter os fluxos de trabalho upstream como a única fonte de verdade, podemos nos limitar a copiar os arquivos downstream em .github/workflows
, mas nunca editá-los lá. Se alguma mudança deve ser feita, ela deve ser feita no monorepo upstream e depois copiada.
Como observação lateral, observe como isso significa que o multi-monorepo vaza: O monorepo upstream não é totalmente autônomo e precisará ser adaptado para se adequar ao monorepo downstream.
Na minha primeira iteração para copiar os fluxos de trabalho, criei um script simples do 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');\"" ] } }
Então, depois de editar os fluxos de trabalho no monorepo upstream, eu os copiava downstream executando o seguinte:
composer copy-workflows
Mas então percebi que apenas copiar os fluxos de trabalho não é suficiente: eles também devem ser modificados no processo. Isso ocorre porque o check-out do monorepo downstream requer a opção --recurse-submodules
para também fazer o check-out dos submódulos.
No GitHub Actions, o checkout downstream é feito assim:
- uses: actions/checkout@v2 with: submodules: recursive
Portanto, verificar o repositório downstream precisa dos submodules: recursive
, mas o upstream não, e ambos usam o mesmo arquivo de origem.
A solução que encontrei é fornecer o valor para os submodules
de entrada através da variável de ambiente CHECKOUT_SUBMODULES
, que por padrão está vazia para o repositório upstream:
env: CHECKOUT_SUBMODULES: "" jobs: provide_data: steps: - uses: actions/checkout@v2 with: submodules: ${{ env.CHECKOUT_SUBMODULES }}
Então, ao copiar os fluxos de trabalho de upstream para downstream, o valor de CHECKOUT_SUBMODULES
é substituído por recursive
:
env: CHECKOUT_SUBMODULES: "recursive"
Ao modificar o fluxo de trabalho, é uma boa ideia usar uma expressão regular (regex), para que funcione para diferentes formatos no arquivo de origem (como CHECKOUT_SUBMODULES: ""
ou CHECKOUT_SUBMODULES:''
ou CHECKOUT_SUBMODULES:
). Isso evitará a criação de bugs para esses tipos de alterações aparentemente inofensivas.
Assim, o script do Composer copy-workflows
mostrado acima não é bom o suficiente para lidar com essa complexidade.
Na minha próxima iteração, criei um comando PHP, CopyUpstreamMonorepoFilesCommand
, para ser executado via Monorepo Builder:
vendor/bin/monorepo-builder copy-upstream-monorepo-files
Este comando usa um serviço personalizado, FileCopierSystem
, para copiar todos os arquivos de uma pasta de origem para o destino especificado, substituindo opcionalmente seu conteúdo:
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); } } }
Ao invocar este método para copiar todos os fluxos de trabalho downstream, também substituo o 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 );
O fluxo de trabalho em generate_plugins.yml
precisa de uma substituição adicional. Quando o plugin do WordPress é gerado, seu código é rebaixado de PHP 8.0 para 7.1 invocando o 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 }}"
No monorepo downstream, esse arquivo estará localizado em submodules/PoP/ci/downgrade/downgrade_code.sh
. Em seguida, apontamos o fluxo de trabalho downstream para o caminho certo com esta substituição:
$regexReplacements = [ // ... '#(ci/downgrade/downgrade_code\.sh)#' => 'submodules/PoP/$1', ];
Configurando pacotes no Monorepo Builder
O arquivo monorepo-builder.php
— colocado na raiz do monorepo — contém a configuração do Monorepo Builder. Nela, devemos indicar onde estão localizados os pacotes (e plugins, clientes e qualquer outra coisa):
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', ]); };
O monorepo privado deve ter acesso a todo o código: seus próprios pacotes, mais os do monorepo público. Em seguida, deve definir todos os pacotes de ambos os monorepos no arquivo de configuração. Os do monorepo público estão localizados em /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', ]); };
Como estão, as configurações para upstream e downstream são praticamente as mesmas, com as diferenças sendo que a de downstream:
- alterar o caminho para os pacotes públicos,
- adicione os pacotes privados.
Portanto, faz sentido reescrever a configuração usando programação orientada a objetos (OOP). Vamos seguir o princípio DRY (“não se repita”) fazendo com que uma classe PHP no repositório público seja estendida no repositório privado.
Recriando a configuração via OOP
Vamos refatorar a configuração. No repositório público, o arquivo monorepo-builder.php
simplesmente fará referência a uma nova classe, ContainerConfigurationService
, onde toda a ação acontecerá:
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(); };
O parâmetro __DIR__
aponta para a raiz do monorepo. Será necessário obter o caminho completo para os diretórios de pacotes.
A classe ContainerConfigurationService
agora é responsável por produzir a configuração:
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); } }
A configuração pode ser dividida em várias classes. Neste caso, ContainerConfigurationService
recupera a configuração do pacote através da classe PackageOrganizationDataSource
, cuja implementação você pode 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', ]; } }
Substituindo a configuração no monorepo downstream
Agora que a configuração no monorepo público foi definida usando OOP, podemos estendê-la para atender às necessidades do monorepo privado.
Para permitir que o monorepo privado carregue automaticamente o código PHP do monorepo público, devemos primeiro configurar o arquivo composer.json
downstream para fazer referência ao código-fonte do upstream, que está no caminho submodules/PoP/src
:
{ "autoload": { "psr-4": { "PoP\\GraphQLAPIPRO\\": "src", "PoP\\PoP\\": "submodules/PoP/src" } } }
Abaixo está o arquivo monorepo-builder.php
para o monorepo privado. Observe que a classe referenciada ContainerConfigurationService
no repositório upstream pertencia ao namespace PoP\PoP
, mas agora foi alterada para o namespace PoP\GraphQLAPIPRO
. Esta classe deve receber a entrada adicional de $upstreamRelativeRootPath
(com um valor de submodules/PoP
) para recriar o caminho completo para os pacotes 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(); };
A classe downstream ContainerConfigurationService
substitui qual classe PackageOrganizationDataSource
é usada na configuração:
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 ); } }
Por fim, a classe downstream PackageOrganizationDataSource
contém o caminho completo para os pacotes públicos e 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', ] ); } }
Injetando a configuração do PHP nas ações do GitHub
O Monorepo Builder oferece o comando packages-json
, que podemos usar para injetar os caminhos do pacote no fluxo de trabalho do 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 produz um JSON com string. No fluxo de trabalho, ele deve ser convertido em um objeto JSON via fromJson
:
jobs: split_monorepo: needs: provide_data strategy: matrix: package: ${{ fromJson(needs.provide_data.outputs.matrix) }}
Infelizmente, o comando packages-json
gera os nomes dos pacotes, mas não seus caminhos. Isso funcionaria se todos os pacotes estivessem na mesma pasta (como packages/
), mas não funciona no nosso caso porque os pacotes públicos e privados estão localizados em pastas diferentes.
Felizmente, o Monorepo Builder pode ser estendido com serviços PHP personalizados. Então, criei um comando personalizado, package-entries-json
(por meio da classe PackageEntriesJsonCommand
), que gera o caminho para o pacote.
O fluxo de trabalho foi então atualizado com o novo comando:
run: | echo "::set-output name=matrix::$(vendor/bin/monorepo-builder package-entries-json)"
Executado no monorepo público, produz os seguintes pacotes (entre muitos outros):
[ { "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" } ]
Executado no monorepo privado, produz as seguintes entradas (entre muitas outras):
[ { "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 muito bem. A configuração para o monorepo downstream contém pacotes públicos e privados, e os caminhos para os públicos são prefixados com submodules/PoP
.
Ignorando pacotes públicos no monorepo downstream
Até agora, o monorepo downstream inclui pacotes públicos e privados em sua configuração. No entanto, nem todo comando precisa ser executado nos pacotes públicos.
Veja a análise estática, por exemplo. O monorepo público já executa o PHPStan em todos os pacotes públicos por meio do arquivo de fluxo de trabalho phpstan.yml
, conforme mostrado nesta execução. Se o monorepo downstream executasse o PHPStan mais uma vez nos pacotes públicos, seria um desperdício de tempo de computação. O fluxo de trabalho phpstan.yml
só precisa ser executado nos pacotes privados.
Isso significa que, dependendo do comando a ser executado no repositório downstream, podemos querer incluir pacotes públicos e privados ou apenas privados.
Para determinar se devemos adicionar pacotes públicos na configuração downstream, adaptamos a classe downstream PackageOrganizationDataSource
para verificar essa condição por meio da 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', ] ); } }
Em seguida, precisamos fornecer o valor $includeUpstreamPackages
como true
ou false
, dependendo do comando a ser executado.
Podemos fazer isso substituindo o arquivo de configuração monorepo-builder.php
dois outros arquivos de configuração: monorepo-builder-with-upstream-packages.php
(que passa $includeUpstreamPackages
=> true
) e monorepo-builder-without-upstream-packages.php
(que 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(); };
Em seguida, atualizamos ContainerConfigurationService
para receber o parâmetro $includeUpstreamPackages
e o repassamos para 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, ); } }
Em seguida, temos que invocar o monorepo-builder
com qualquer arquivo de configuração, fornecendo a opção --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)"
No entanto, como vimos anteriormente, queremos manter os fluxos de trabalho do GitHub Actions no monorepo upstream como a única fonte de verdade, e eles claramente não precisam dessas alterações.
A solução que encontrei para esse problema é sempre fornecer uma opção --config
no repositório upstream, com cada comando obtendo seu próprio arquivo de configuração, como o comando validate
recebendo o arquivo de configuração validate.php
:
- name: Run validation run: vendor/bin/monorepo-builder validate --config=config/monorepo-builder/validate.php
Agora, não há arquivos de configuração no monorepo upstream, porque ele não precisa deles. Mas não vai quebrar, porque o Monorepo Builder verifica se o arquivo de configuração existe e, se não existir, ele carrega o arquivo de configuração padrão. Então, ou vamos sobrescrever a configuração ou nada vai acontecer.
O repositório downstream fornece os arquivos de configuração para cada comando, especificando se os pacotes upstream devem ser adicionados:
Como nota lateral, este é outro exemplo de como o multi-monorepo vaza.
// File config/monorepo-builder/validate.php return require_once __DIR__ . '/monorepo-builder-with-upstream-packages.php';
Substituindo a configuração
Estamos quase terminando. Até agora, o monorepo downstream pode substituir a configuração do monorepo upstream. Então, tudo o que resta a fazer é fornecer a nova configuração.
Na classe PluginDataSource
, substituo a configuração de quais plugins do WordPress devem ser gerados, fornecendo os 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', ], ]; } }
Criar uma nova versão no GitHub acionará o fluxo de trabalho generate_plugins.yml
e gerará os plugins pro no meu monorepo privado:
Ta-da!
Conclusão
Como sempre, não existe uma “melhor” solução, apenas soluções que podem funcionar melhor dependendo do contexto. A abordagem multi-monorepo não é adequada para todo tipo de projeto ou equipe. Acredito que os maiores beneficiários seriam os criadores de plugins que lançam plugins públicos que serão atualizados para suas versões pro, bem como agências que personalizam plugins para seus clientes.
No meu caso, estou bastante satisfeito com esta abordagem. Acertar leva um pouco de tempo e esforço, mas é um investimento único. Depois que a configuração estiver concluída, posso me concentrar na criação de meus plugins profissionais, e o tempo economizado com o gerenciamento de projetos pode ser enorme.