為 PHP 項目創建一個公共/私有的多單體存儲庫

已發表: 2022-03-10
快速總結 ↬讓我們看看如何使用“multi-monorepo”方法來加快開發體驗,同時保持 PHP 包的私密性。 此解決方案對 PRO 插件創建者特別有益。

為了讓我的開發體驗更快,我將項目所需的所有 PHP 包都移到了 monorepo。 當每個包都託管在自己的存儲庫中時(多存儲庫方法),我需要自己開發和測試它,然後將它發佈到 Packagist,然後我才能通過 Composer 將它安裝到其他包中。 使用 monorepo,因為所有包都託管在一起,所以它們可以同時開發、測試、版本控制和發布。

託管我的 PHP 包的 monorepo 是公開的,GitHub 上的任何人都可以訪問。 Git 存儲庫不能授予對不同資產的不同訪問權限; 這都是公共的或私人的。 因為我計劃發布一個專業的 WordPress 插件,我希望它的包是私有的,這意味著它們不能被添加到公共 monorepo。

我找到的解決方案是使用“multi-monorepo”方法,包括兩個monorepo:一個公共的和一個私有的,私有的monorepo 將公共的monorepo 嵌入為Git 子模塊,允許它訪問其文件。 公共的monorepo可以被認為是上游的,私有的monorepo可以被認為是下游的。

多單體的架構
多單體倉庫的架構。 (大預覽)

當我不斷迭代我的代碼時,我需要在項目的每個階段使用的存儲庫設置也需要升級。 因此,我並沒有在第一天就採用多單體方案。 這是一個跨越數年的過程,花費了相當多的努力,從單個存儲庫到多個存儲庫,再到 monorepo,最後到 multi-monorepo。

在本文中,我將描述如何使用 Monorepo Builder 設置我的 multi-monorepo,它適用於 PHP 項目並且基於 Composer。

跳躍後更多! 繼續往下看↓

在 Multi-Monorepo 中重用代碼

leoloso/PoP的公共 monorepo 是我保存所有 PHP 項目的地方。

這個 monorepo 包含工作流文件generate_plugins.yml ,當我在 GitHub 上創建發佈時,它會生成多個 WordPress 插件以供分發:

創建發佈時生成插件
創建發佈時生成插件。 (大預覽)

工作流配置不是硬編碼在 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為我的私人 monorepo 重用這個設置,這樣也可以毫不費力地生成專業插件。

要重用的代碼將包括:

  • 用於生成 WordPress 插件的 GitHub Actions 工作流程(包括範圍界定、從 PHP 8.0 降級到 7.1 以及上傳到發布頁面)。
  • 自定義 PHP 服務來配置工作流。

然後,私有 monorepo 可以簡單地通過從公共 monorepo 觸發工作流並覆蓋它們在 PHP 中的配置來生成 pro WordPress 插件。

通過 Git 子模塊鏈接 Monorepos

要將公共存儲庫嵌入私有存儲庫,我們使用 Git 子模塊:

 git submodule add <public repo URL>

我將公共存儲庫嵌入到私有 monorepo 的子文件夾子submodules中,如果需要,我可以在將來添加更多上游 monorepos。 在 GitHub 中,該文件夾顯示子模塊的特定提交,單擊它將帶我到leoloso/PoP的提交:

將公共 monorepo 嵌入私有 monorepo
將公共 monorepo 嵌入到私有 monorepo 中。 (大預覽)

因為私有存儲庫包含子模塊,所以要克隆它,我們必須提供--recursive選項:

 git clone --recursive <private repo URL>

重用 GitHub Actions 工作流

GitHub Actions 僅從.github/workflows下加載工作流。 由於下游 monorepo 中的公共工作流位於submodules/PoP/.github/workflows下,因此必須在預期位置複製這些工作流。

為了保持上游工作流作為唯一的真實來源,我們可以限制自己在下游複製.github/workflows下的文件,但永遠不要在那裡編輯它們。 如果要進行任何更改,必須在上游 monorepo 中完成,然後復製過來。

作為旁注,請注意這意味著多單體存儲庫如何洩漏:上游單體存儲庫不是完全自主的,需要對其進行調整以適應下游單體存儲庫。

在我複制工作流的第一次迭代中,我創建了一個簡單的 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');\"" ] } }

然後,在上游 monorepo 中編輯工作流後,我將通過執行以下命令將它們複製到下游:

 composer copy-workflows

但後來我意識到僅僅複製工作流是不夠的:它們還必須在過程中進行修改。 這是因為檢查下游 monorepo 需要選項--recurse-submodules才能檢查子模塊。

在 GitHub Actions 中,下游的結帳是這樣完成的:

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

因此,檢查下游存儲庫需要輸入submodules: recursive ,但上游不需要,它們都使用相同的源文件。

我找到的解決方案是通過環境變量CHECKOUT_SUBMODULES為輸入submodules提供值,上游存儲庫默認為空:

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

然後,當將工作流從上游複製到下游時, CHECKOUT_SUBMODULES的值被替換為recursive

 env: CHECKOUT_SUBMODULES: "recursive"

修改工作流時,最好使用正則表達式 (regex),以便它適用於源文件中的不同格式(例如CHECKOUT_SUBMODULES: ""CHECKOUT_SUBMODULES:''CHECKOUT_SUBMODULES: )。 這將防止為這些表面上無害的更改創建錯誤。

因此,上面顯示的copy-workflows Composer 腳本不足以處理這種複雜性。

在我的下一次迭代中,我創建了一個 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 插件時,通過調用腳本ci/downgrade/downgrade_code.sh其代碼從 PHP 8.0 降級到 7.1:

 - 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 }}"

在下游 monorepo 中,此文件將位於submodules/PoP/ci/downgrade/downgrade_code.sh下。 然後,我們用這個替換將下游工作流指向正確的路徑:

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

在 Monorepo Builder 中配置包

文件monorepo-builder.php - 位於 monorepo 的根目錄 - 包含 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', ]); };

私有 monorepo 必須可以訪問所有代碼:它自己的包,以及來自公共 monorepo 的包。 然後,它必須在配置文件中定義來自兩個 monorepos 的所有包。 來自公共 monorepo 的位於/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', ]); };

實際上,上游和下游的配置幾乎相同,不同之處在於下游將:

  • 更改公共包的路徑,
  • 添加私有包。

因此,使用面向對象編程 (OOP) 重寫配置是有意義的。 讓我們通過在私有存儲庫中擴展公共存儲庫中的 PHP 類來遵循 DRY 原則(“不要重複自己”)。

通過 OOP 重新創建配置

讓我們重構配置。 在公共存儲庫中,文件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__參數指向 monorepo 的根。 需要獲取包目錄的完整路徑。

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

覆蓋下游 Monorepo 中的配置

既然已經使用 OOP 設置了公共 monorepo 中的配置,我們可以對其進行擴展以滿足私有 monorepo 的需求。

為了讓私有 monorepo 自動加載公共 monorepo 中的 PHP 代碼,我們必須首先配置下游的composer.json文件以引用來自上游的源代碼,該文件位於路徑submodules/PoP/src下:

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

下面是私有 monorepo 的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 Actions

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。 在工作流程中,必須通過fromJson將其轉換為 JSON 對象:

 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)"

在公共 monorepo 上執行,這會產生以下包(以及許多其他包):

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

在私有 monorepo 上執行,它會生成以下條目(以及許多其他條目):

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

它工作得很好。 下游 monorepo 的配置包含公共包和私有包,公共包的路徑以submodules/PoP開頭。

在下游 Monorepo 中跳過公共包

到目前為止,下游 monorepo 在其配置中包括公共和私有包。 但是,並不是每個命令都需要在公共包上執行。

以靜態分析為例。 公共 monorepo 已經通過工作流文件phpstan.yml在所有公共包上執行 PHPStan,如本次運行所示。 如果下游的 monorepo 再次在公共包上運行 PHPStan,那將是浪費計算時間。 phpstan.yml工作流只需要在私有包上運行。

這意味著,根據要在下游存儲庫中執行的命令,我們可能希望同時包含公共包和私有包,或者只包含私有包。

為了確定是否在下游配置中添加公共包,我們通過輸入$includeUpstreamPackages調整下游類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, 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提供為truefalse ,具體取決於要執行的命令。

我們可以通過將配置文件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, ); } }

接下來,我們必須通過提供--config選項,使用任一配置文件調用monorepo-builder

 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)"

但是,正如我們之前看到的,我們希望將上游 monorepo 中的 GitHub Actions 工作流作為唯一的事實來源,它們顯然不需要這些更改。

我發現這個問題的解決方案是始終在上游存儲庫中提供一個--config選項,每個命令都有自己的配置文件,例如validate命令接收validate.php配置文件:

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

現在,上游 monorepo 中沒有配置文件,因為它不需要它們。 但它不會中斷,因為 Monorepo Builder 會檢查配置文件是否存在,如果不存在,它會加載默認配置文件。 所以,要么我們將覆蓋配置,要么什麼都不會發生。

下游倉庫提供每個命令的配置文件,指定是否添加上游包:

作為旁注,這是多單體如何洩漏的另一個例子。

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

覆蓋配置

我們快完成了。 現在,下游的 monorepo 可以覆蓋上游 monorepo 的配置。 因此,剩下要做的就是提供新配置。

PluginDataSource類中,我覆蓋了必須生成哪些 WordPress 插件的配置,而是提供了 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', ], ]; } }

在 GitHub 上創建新版本將觸發generate_plugins.yml工作流程,並將在我的私有 monorepo 中生成專業插件:

生成專業插件
生成專業插件。 (大預覽)

達達!

結論

與往常一樣,沒有“最佳”解決方案,只有根據具體情況可能效果更好的解決方案。 multi-monorepo 方法並不適合所有類型的項目或團隊。 我相信最大的受益者將是發佈公共插件並將升級到專業版的插件創建者,以及為其客戶定制插件的代理機構。

就我而言,我對這種方法非常滿意。 把它做好需要一點時間和精力,但這是一次性的投資。 設置完成後,我可以專注於構建我的專業插件,項目管理節省的時間可能是巨大的。