PHP 프로젝트를 위한 퍼블릭/프라이빗 멀티 모노레포 생성

게시 됨: 2022-03-10
빠른 요약 ↬ PHP 패키지를 비공개로 유지하면서 개발 경험을 더 빠르게 만들기 위해 "다중 모노레포" 접근 방식을 사용하는 방법을 살펴보겠습니다. 이 솔루션은 PRO 플러그인 제작자에게 특히 유용할 수 있습니다.

개발 경험을 더 빠르게 하기 위해 프로젝트에 필요한 모든 PHP 패키지를 모노레포로 옮겼습니다. 각 패키지가 자체 리포지토리에서 호스팅되는 경우(다중 리포지토리 접근 방식), 자체적으로 개발 및 테스트한 다음 Composer를 통해 다른 패키지에 설치하기 전에 이를 Packagist에 게시해야 합니다. monorepo를 사용하면 모든 패키지가 함께 호스팅되기 때문에 동시에 개발, 테스트, 버전 관리 및 릴리스할 수 있습니다.

내 PHP 패키지를 호스팅하는 monorepo는 공개되어 있으며 GitHub의 모든 사람이 액세스할 수 있습니다. Git 리포지토리는 서로 다른 자산에 대해 서로 다른 액세스 권한을 부여할 수 없습니다. 모두 공개 또는 비공개입니다. Pro WordPress 플러그인을 출시할 계획이기 때문에 패키지를 비공개로 유지하고 싶습니다. 즉, 공개 모노레포에 추가할 수 없습니다.

내가 찾은 해결책은 "다중 모노레포" 접근 방식을 사용하는 것입니다. 하나는 공개 및 다른 하나는 비공개 모노리포지토리로 구성되며 비공개 모노레포에는 공개 하나를 Git 하위 모듈로 포함하여 파일에 액세스할 수 있습니다. 공개 모노레포는 업스트림으로, 사설 모노레포는 다운스트림으로 간주할 수 있습니다.

다중 모노레포의 아키텍처
다중 모노레포의 아키텍처입니다. (큰 미리보기)

코드를 계속 반복하면서 프로젝트의 각 단계에서 사용해야 하는 저장소 설정도 업그레이드해야 했습니다. 따라서 나는 첫날에 다중 모노레포 접근 방식에 도달하지 않았습니다. 단일 리포지토리에서 여러 리포지토리, 모노 리포지토리, 그리고 마침내 다중 모노 리포지토리에 이르기까지 수년에 걸쳐 상당한 노력이 필요한 프로세스였습니다.

이 기사에서는 PHP 프로젝트에서 작동하고 Composer를 기반으로 하는 Monorepo Builder를 사용하여 다중 모노레포를 설정하는 방법을 설명합니다.

점프 후 더! 아래에서 계속 읽기 ↓

다중 모노레포에서 코드 재사용

leoloso/PoP 의 공개 모노레포는 모든 PHP 프로젝트를 보관하는 곳입니다.

이 모노레포에는 워크플로 파일 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 에서 내 개인 모노레포를 위해 재사용하고 싶은 것은 이 설정이므로 pro 플러그인도 노력 없이 생성될 수 있습니다.

재사용할 코드는 다음으로 구성됩니다.

  • WordPress 플러그인을 생성하기 위한 GitHub 작업 워크플로(범위 지정, PHP 8.0에서 7.1로 다운그레이드, 릴리스 페이지에 업로드 포함).
  • 워크플로를 구성하기 위한 사용자 정의 PHP 서비스.

그런 다음 비공개 모노리포는 공개 모노리포에서 워크플로를 트리거하고 PHP에서 해당 구성을 재정의하여 프로 WordPress 플러그인을 생성할 수 있습니다.

Git 하위 모듈을 통한 Monorepos 연결

공개 리포지토리를 비공개 리포지토리에 포함하려면 Git 하위 모듈을 사용합니다.

 git submodule add <public repo URL>

개인 모노리포지토리의 하위 폴더 하위 submodules 에 공개 리포지토리를 포함하여 향후 필요할 경우 더 많은 업스트림 모노리포지토리를 추가할 수 있습니다. GitHub에서 폴더는 하위 모듈의 특정 커밋을 표시하고 클릭하면 leoloso/PoP 의 커밋으로 이동합니다.

비공개 모노레포에 공개 모노레포 포함
공개 모노레포를 사설 모노레포에 포함시키기. (큰 미리보기)

개인 저장소에는 하위 모듈이 포함되어 있으므로 복제하려면 --recursive 옵션을 제공해야 합니다.

 git clone --recursive <private repo URL>

GitHub 작업 워크플로 재사용

GitHub Actions는 .github/workflows 아래에서만 워크플로를 로드합니다. 다운스트림 모노레포의 공개 워크플로는 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 옵션이 필요하기 때문입니다.

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 스크립트는 이러한 복잡성을 처리하기에 충분하지 않습니다.

다음 반복에서는 Monorepo Builder를 통해 실행할 PHP 명령 CopyUpstreamMonorepoFilesCommand 를 만들었습니다.

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

다운스트림 모노레포에서 이 파일은 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', ]); };

private monorepo는 모든 코드에 접근할 수 있어야 합니다: 자신의 패키지와 public 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__ 매개변수는 모노레포의 루트를 가리킵니다. 패키지 디렉토리에 대한 전체 경로를 얻으려면 필요합니다.

이제 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를 사용하여 설정되었으므로 이제 사설 모노레포의 요구 사항에 맞게 확장할 수 있습니다.

private monorepo가 ​​public monorepo에서 PHP 코드를 자동 로드할 수 있도록 하려면 먼저 다운스트림 composer.json 파일을 구성하여 submodules/PoP/src 경로에 있는 업스트림의 소스 코드를 참조해야 합니다.

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

아래는 private 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 작업으로 구성 주입

Monorepo Builder는 패키지 경로를 GitHub 작업 워크플로에 삽입하는 데 사용할 수 있는 packages-json 명령을 제공합니다.

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

공개 모노레포에서 실행하면 다음과 같은 패키지가 생성됩니다(많은 패키지 중에서).

 [ { "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/PoP 가 추가됩니다.

다운스트림 Monorepo에서 공개 패키지 건너뛰기

지금까지 다운스트림 모노레포는 구성에 공개 및 비공개 패키지를 모두 포함합니다. 그러나 모든 명령을 공개 패키지에서 실행할 필요는 없습니다.

예를 들어 정적 분석을 수행하십시오. 공개 monorepo는 이 실행에서 볼 수 있듯이 워크플로 파일 phpstan.yml 을 통해 모든 공개 패키지에서 이미 PHPStan을 실행합니다. 다운스트림 모노레포가 공개 패키지에서 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 로 교체하여 이를 수행할 수 있습니다. 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(); };

그런 다음 $includeUpstreamPackages 매개변수를 수신하고 PackageOrganizationDataSource 에 전달하도록 ContainerConfigurationService 를 업데이트합니다.

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

그러나 앞에서 보았듯이 우리는 GitHub Actions 워크플로를 업스트림 모노레포에서 단일 소스로 유지하기를 원하며 이러한 변경이 필요하지 않습니다.

이 문제에 대한 해결책은 항상 업스트림 리포지토리에 --config 옵션을 제공하는 것입니다. 각 명령은 validate.php 구성 파일을 수신하는 validate 명령과 같이 고유한 구성 파일을 가져옵니다.

 - 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 워크플로가 트리거되고 내 개인 모노레포에 프로 플러그인이 생성됩니다.

프로 플러그인 생성
프로 플러그인 생성. (큰 미리보기)

타다!

결론

항상 그렇듯이 "최상의" 솔루션은 없으며 상황에 따라 더 잘 작동할 수 있는 솔루션만 있을 뿐입니다. 다중 모노레포 접근 방식은 모든 종류의 프로젝트 또는 팀에 적합하지 않습니다. 가장 큰 수혜자는 프로 버전으로 업그레이드될 공개 플러그인을 출시하는 플러그인 제작자와 클라이언트에 맞게 플러그인을 사용자 정의하는 대행사일 것입니다.

제 경우에는 이 접근 방식에 매우 만족합니다. 제대로 하려면 약간의 시간과 노력이 필요하지만 일회성 투자입니다. 설정이 끝나면 프로 플러그인 구축에 집중할 수 있고 프로젝트 관리로 절약되는 시간은 엄청날 수 있습니다.