Создание общедоступного/частного мультимонорепозитория для проектов PHP

Опубликовано: 2022-03-10
Краткий обзор ↬ Давайте посмотрим, как использовать подход «мульти-монорепозиториев», чтобы ускорить процесс разработки, сохраняя при этом конфиденциальность ваших пакетов PHP. Это решение может быть особенно полезным для создателей плагинов PRO.

Чтобы ускорить процесс разработки, я переместил все пакеты PHP, необходимые для моих проектов, в монорепозиторий. Когда каждый пакет размещается в своем собственном репозитории (подход с несколькими репозиториями), мне нужно будет разработать и протестировать его самостоятельно, а затем опубликовать в Packagist, прежде чем я смогу установить его в другие пакеты через Composer. Поскольку в монорепозитории все пакеты размещаются вместе, их можно разрабатывать, тестировать, создавать версии и выпускать одновременно.

Монорепозиторий, в котором размещены мои пакеты PHP, является общедоступным и доступным для всех на GitHub. Репозитории Git не могут предоставлять разный доступ к разным ресурсам; это все либо публичное, либо приватное. Поскольку я планирую выпустить профессиональный плагин WordPress, я хочу, чтобы его пакеты оставались закрытыми, то есть их нельзя было добавить в общедоступный монорепозиторий.

Решение, которое я нашел, заключается в использовании подхода с несколькими монорепозиториями, состоящего из двух монорепозиториев: одного общедоступного и одного частного, при этом частное монорепозиторий встраивает общедоступное в качестве подмодуля Git, позволяя ему получать доступ к своим файлам. Общедоступный монорепозиторий можно считать восходящим, а частный монорепозиторий — нижестоящим.

Архитектура мультимонорепо
Архитектура мультимонорепозитория. (Большой превью)

Поскольку я продолжал повторять свой код, настройка репозитория, которую мне нужно было использовать на каждом этапе моего проекта, также нуждалась в обновлении. Следовательно, я не пришел к подходу с несколькими монорепо в первый день; это был процесс, который длился несколько лет и требовал немало усилий, переходя от одного репозитория к нескольким репозиториям, к монорепозиторию и, наконец, к мультимонорепозиторию.

В этой статье я опишу, как я настроил свой мультирепозиторий с помощью Monorepo Builder, который работает для проектов PHP и основан на Composer.

Еще после прыжка! Продолжить чтение ниже ↓

Повторное использование кода в Multi-Monorepo

В общедоступном монорепозитории на leoloso/PoP я храню все свои PHP-проекты.

Этот монорепозиторий содержит файл рабочего процесса generate_plugins.yml , который генерирует несколько плагинов WordPress для распространения, когда я создаю выпуск на GitHub:

Генерация плагинов при создании релиза
Генерация плагинов при создании релиза. (Большой превью)

Конфигурация рабочего процесса не жестко запрограммирована в файле YAML, а вводится через код PHP:

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

И конфигурация предоставляется через собственный класс PHP:

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

Совместное создание нескольких плагинов WordPress и настройка рабочего процесса с помощью PHP сократили время, необходимое мне для управления проектом. В настоящее время рабочий процесс обрабатывает два плагина (GraphQL API и его демо-расширение), но он может обрабатывать 200 без дополнительных усилий с моей стороны.

Именно эту настройку я хочу повторно использовать для моего частного монорепозитория на leoloso/GraphQLAPI-PRO , чтобы профессиональные плагины также можно было создавать без усилий.

Код для повторного использования будет включать:

  • рабочие процессы GitHub Actions для создания плагинов WordPress (включая определение области действия, переход с PHP 8.0 на 7.1 и загрузку на страницу выпусков).
  • пользовательские службы PHP для настройки рабочих процессов.

Затем частный монорепозиторий может генерировать профессиональные плагины WordPress, просто запуская рабочие процессы из общедоступного монорепозитория и переопределяя их конфигурацию в PHP.

Связывание монорепозиториев через подмодули Git

Чтобы встроить публичный репозиторий в приватный, мы используем подмодули Git:

 git submodule add <public repo URL>

Я внедрил общедоступный репозиторий в submodules подпапок частного монорепозитория, что позволило мне добавить в будущем больше монорепозиториев основной ветки, если это необходимо. В GitHub папка отображает конкретную фиксацию подмодуля, и нажатие на нее приведет меня к этой фиксации в leoloso/PoP :

Встраивание публичного монорепозитория в частное монорепозиторий
Встраивание публичного монорепозитория в частное монорепозиторий. (Большой превью)

Поскольку частный репозиторий содержит подмодули, для его клонирования мы должны указать параметр --recursive :

 git clone --recursive <private repo URL>

Повторное использование рабочих процессов GitHub Actions

GitHub Actions загружает рабочие процессы только из-под .github/workflows . Поскольку общедоступные рабочие процессы в нижестоящем монорепозитории находятся в разделе submodules submodules/PoP/.github/workflows , их необходимо дублировать в ожидаемом месте.

Чтобы сохранить исходные рабочие процессы в качестве единственного источника правды, мы можем ограничиться копированием нижестоящих файлов в .github/workflows , но никогда не редактировать их там. Если необходимо внести какое-либо изменение, оно должно быть выполнено в вышестоящем монорепозитории, а затем скопировано.

В качестве примечания обратите внимание, как это означает утечку мультирепозитория: вышестоящий монорепозиторий не является полностью автономным, и его необходимо будет адаптировать для соответствия нижестоящему монорепозиторию.

В моей первой итерации для копирования рабочих процессов я создал простой скрипт 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');\"" ] } }

Затем, после редактирования рабочих процессов в вышестоящем монорепозитории, я скопировал их ниже по течению, выполнив следующее:

 composer copy-workflows

Но потом я понял, что просто скопировать рабочие процессы недостаточно: их нужно еще и модифицировать в процессе. Это связано с тем, что для извлечения подчиненного монорепозитория требуется параметр --recurse-submodules submodules, чтобы также извлекать подмодули.

В GitHub Actions проверка вниз по течению выполняется следующим образом:

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

Итак, для проверки нижестоящего репозитория нужны входные submodules: recursive , а вышестоящему нет, и оба они используют один и тот же исходный файл.

Решение, которое я нашел, состоит в том, чтобы указать значение для входных submodules через переменную среды CHECKOUT_SUBMODULES , которая по умолчанию пуста для вышестоящего репозитория:

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

Затем при копировании рабочих процессов из восходящего потока в нисходящий значение CHECKOUT_SUBMODULES заменяется на recursive :

 env: CHECKOUT_SUBMODULES: "recursive"

При изменении рабочего процесса рекомендуется использовать регулярное выражение (регулярное выражение), чтобы оно работало для разных форматов в исходном файле (например, CHECKOUT_SUBMODULES: "" или CHECKOUT_SUBMODULES:'' или CHECKOUT_SUBMODULES: ). Это предотвратит создание ошибок для таких якобы безвредных изменений.

Таким образом, показанный выше сценарий Composer copy-workflows недостаточно хорош для решения этой сложности.

В моей следующей итерации я создал PHP-команду CopyUpstreamMonorepoFilesCommand для выполнения через Monorepo Builder:

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

Эта команда использует специальную службу FileCopierSystem для копирования всех файлов из исходной папки в указанное место назначения с возможной заменой их содержимого:

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

При вызове этого метода для копирования всех рабочих процессов вниз по течению я также заменяю значение 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 );

Рабочий процесс в generate_plugins.yml нуждается в дополнительной замене. Когда создается плагин WordPress, его код понижается с PHP 8.0 до 7.1, вызывая скрипт 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 }}"

В нижестоящем монорепозитории этот файл будет находиться в папке submodules submodules/PoP/ci/downgrade/downgrade_code.sh . Затем мы указываем нижестоящему рабочему процессу правильный путь с помощью этой замены:

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

Настройка пакетов в Monorepo Builder

Файл monorepo-builder.php расположенный в корне монорепозитория, содержит конфигурацию для Monorepo Builder. В нем мы должны указать, где находятся пакеты (и плагины, клиенты и все остальное):

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

Частный монорепозиторий должен иметь доступ ко всему коду: своим собственным пакетам, а также пакетам из общедоступного монорепозитория. Затем он должен определить все пакеты из обоих монорепозиториев в файле конфигурации. Те из общедоступного монорепозитория находятся в /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', ]); };

Как бы то ни было, конфигурации для восходящего и нисходящего потоков практически одинаковы, разница заключается в том, что нисходящий поток будет:

  • изменить путь к общедоступным пакетам,
  • добавить частные пакеты.

Итак, имеет смысл переписать конфигурацию с помощью объектно-ориентированного программирования (ООП). Давайте будем следовать принципу DRY («не повторяйся»), расширив класс PHP в общедоступном репозитории в частном репозитории.

Воссоздание конфигурации через ООП

Проведем рефакторинг конфигурации. В общедоступном репозитории файл monorepo-builder.php будет просто ссылаться на новый класс ContainerConfigurationService , в котором будут происходить все действия:

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

Параметр __DIR__ указывает на корень монорепозитория. Он понадобится для получения полного пути к директориям пакетов.

Теперь за создание конфигурации отвечает класс ContainerConfigurationService :

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

Конфигурация может быть разделена на несколько классов. В этом случае ContainerConfigurationService извлекает конфигурацию пакета через класс PackageOrganizationDataSource , реализацию которого вы можете увидеть:

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

Переопределение конфигурации в нисходящем монорепозитории

Теперь, когда конфигурация общедоступного монорепозитория настроена с использованием ООП, мы можем расширить ее, чтобы удовлетворить потребности частного монорепозитория.

Чтобы частный монорепозиторий мог автоматически загружать код PHP из общедоступного монорепозитория, мы должны сначала настроить нижестоящий файл composer.json для ссылки на исходный код из вышестоящего, который находится по пути submodules/PoP/src :

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

Ниже приведен файл monorepo-builder.php для частного монорепозитория. Обратите внимание, что упомянутый класс ContainerConfigurationService в вышестоящем репозитории принадлежал к пространству имен PoP\PoP , но теперь был переключен на пространство имен PoP\GraphQLAPIPRO . Этот класс должен получить дополнительные входные данные $upstreamRelativeRootPath (со значением submodules/PoP ), чтобы воссоздать полный путь к публичным пакетам:

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

Нижестоящий класс ContainerConfigurationService переопределяет класс 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 ) { parent::__construct( $containerConfigurator, $rootDirectory ); } protected function getPackageOrganizationDataSource(): ?PackageOrganizationDataSource { return new PackageOrganizationDataSource( $this->rootDirectory, $this->upstreamRelativeRootPath ); } }

Наконец, нижестоящий класс PackageOrganizationDataSource содержит полный путь как к публичным, так и к приватным пакетам:

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

Внедрение конфигурации из PHP в действия GitHub

Monorepo Builder предлагает команду packages-json , которую мы можем использовать для внедрения путей пакетов в рабочий процесс 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 }}

Эта команда создает строковый JSON. В рабочем процессе его необходимо преобразовать в объект JSON через fromJson :

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

К сожалению, команда packages-json выводит имена пакетов, но не пути к ним. Это сработало бы, если бы все пакеты находились в одной папке (например, packages/ ), но в нашем случае это не сработает, потому что общедоступные и частные пакеты расположены в разных папках.

К счастью, Monorepo Builder можно расширить с помощью пользовательских служб PHP. Итак, я создал пользовательскую команду package-entries-json (через класс PackageEntriesJsonCommand ), которая выводит путь к пакету.

Затем рабочий процесс был обновлен новой командой:

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

При выполнении в общедоступном монорепозитории создаются следующие пакеты (среди многих других):

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

Выполненный в частном монорепозитории, он создает следующие записи (среди многих других):

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

Это работает очень хорошо. Конфигурация нижестоящего монорепозитория содержит как общедоступные, так и частные пакеты, а пути к общедоступным предваряются submodules submodules/PoP .

Пропуск общедоступных пакетов в нисходящем монорепозитории

Пока монорепозиторий нижнего уровня включает в свою конфигурацию как публичные, так и приватные пакеты. Однако не каждую команду нужно выполнять в публичных пакетах.

Возьмем, к примеру, статический анализ. Общедоступный монорепозиторий уже выполняет PHPStan для всех общедоступных пакетов через файл рабочего процесса phpstan.yml , как показано в этом прогоне. Если нижестоящий монорепозиторий снова запустит PHPStan для общедоступных пакетов, это будет пустой тратой вычислительного времени. Рабочий процесс phpstan.yml нужно запускать только на приватных пакетах.

Это означает, что, в зависимости от команды, которая будет выполняться в нижестоящем репозитории, мы можем захотеть включить как публичные, так и приватные пакеты, или только приватные.

Чтобы определить, следует ли добавлять публичные пакеты в нижестоящую конфигурацию, мы адаптируем нижестоящий класс PackageOrganizationDataSource для проверки этого условия с помощью входных данных $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', ] ); } }

Затем нам нужно указать значение $includeUpstreamPackages как true или false , в зависимости от выполняемой команды.

Мы можем сделать это, заменив файл конфигурации monorepo-builder.php двумя другими файлами конфигурации: monorepo-builder-with-upstream-packages.php (который передает $includeUpstreamPackages => true ) и monorepo-builder-without-upstream-packages.php (который передает $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(); };

Затем мы обновляем ContainerConfigurationService , чтобы получить параметр $includeUpstreamPackages и передать его 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, ); } }

Затем мы должны вызвать monorepo-builder с любым файлом конфигурации, указав параметр --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)"

Однако, как мы видели ранее, мы хотим сохранить рабочие процессы GitHub Actions в вышестоящем монорепозитории как единственный источник правды, и им явно не нужны эти изменения.

Решение, которое я нашел для этой проблемы, заключается в том, чтобы всегда предоставлять параметр --config в вышестоящем репозитории, при этом каждая команда получает свой собственный файл конфигурации, например, команда validate получает файл конфигурации validate.php :

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

Теперь в вышестоящем монорепозитории нет файлов конфигурации, потому что они ему не нужны. Но он не сломается, потому что Monorepo Builder проверяет, существует ли файл конфигурации, и, если нет, вместо него загружает файл конфигурации по умолчанию. Итак, либо мы переопределим конфигурацию, либо ничего не произойдет.

Нижестоящий репозиторий предоставляет файлы конфигурации для каждой команды, указывая, следует ли добавлять вышестоящие пакеты:

В качестве примечания, это еще один пример того, как происходит утечка мульти-монорепо.

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

Переопределение конфигурации

Мы почти закончили. К настоящему времени нижестоящий монорепозиторий может переопределить конфигурацию вышестоящего монорепозитория. Итак, все, что осталось сделать, это предоставить новую конфигурацию.

В классе PluginDataSource я переопределяю конфигурацию того, какие плагины WordPress должны быть сгенерированы, вместо этого предоставляя профессиональные:

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

Создание новой версии на GitHub вызовет рабочий процесс generate_plugins.yml и сгенерирует профессиональные плагины в моем частном монорепозитории:

Создание профессиональных плагинов
Создание профессиональных плагинов. (Большой превью)

Та-да!

Заключение

Как всегда, нет «лучшего» решения, есть решения, которые могут работать лучше в зависимости от контекста. Подход с несколькими монорепозиториями подходит не для каждого проекта или команды. Я считаю, что больше всего выиграют создатели плагинов, которые выпускают общедоступные плагины, которые будут обновлены до их профессиональных версий, а также агентства, которые настраивают плагины для своих клиентов.

В моем случае я вполне доволен таким подходом. Чтобы сделать это правильно, требуется немного времени и усилий, но это единовременная инвестиция. После завершения настройки я могу сосредоточиться на создании своих профессиональных плагинов, и время, сэкономленное на управлении проектом, может быть огромным.