การสร้าง Multi-Monorepo สาธารณะ/ส่วนตัวสำหรับโครงการ PHP

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างย่อ ↬ มาดูวิธีการใช้แนวทาง “multi-monorepo” เพื่อทำให้การพัฒนาประสบการณ์เร็วขึ้น แต่ยังคงรักษาแพ็คเกจ PHP ของคุณไว้เป็นส่วนตัว โซลูชันนี้สามารถเป็นประโยชน์อย่างยิ่งสำหรับผู้สร้างปลั๊กอิน PRO

เพื่อให้ประสบการณ์การพัฒนาของฉันเร็วขึ้น ฉันย้ายแพ็คเกจ PHP ทั้งหมดที่โปรเจ็กต์ของฉันต้องการไปยัง monorepo เมื่อแต่ละแพ็คเกจโฮสต์ในที่เก็บของตัวเอง (แนวทาง multirepo) ฉันจะต้องพัฒนาและทดสอบด้วยตัวเองแล้วจึงเผยแพร่ไปยัง Packagist ก่อนจึงจะสามารถติดตั้งในแพ็คเกจอื่นผ่าน Composer ได้ ด้วย monorepo เนื่องจากแพ็คเกจทั้งหมดถูกโฮสต์ไว้ด้วยกัน จึงสามารถพัฒนา ทดสอบ กำหนดเวอร์ชัน และเผยแพร่ได้ในเวลาเดียวกัน

monorepo ที่โฮสต์แพ็คเกจ PHP ของฉันเป็นแบบสาธารณะ ทุกคนใน GitHub สามารถเข้าถึงได้ ที่เก็บ Git ไม่สามารถให้สิทธิ์การเข้าถึงเนื้อหาที่แตกต่างกันได้ ทั้งหมดเป็นสาธารณะหรือส่วนตัว เนื่องจากฉันวางแผนที่จะเปิดตัวปลั๊กอิน WordPress ระดับมืออาชีพ ฉันจึงต้องการให้แพ็คเกจของมันถูกเก็บไว้เป็นส่วนตัว ซึ่งหมายความว่าไม่สามารถเพิ่มลงใน monorepo สาธารณะได้

วิธีแก้ปัญหาที่ฉันพบคือใช้วิธีการ "multi-monorepo" ซึ่งประกอบด้วย monorepos สองตัว: แบบสาธารณะและแบบส่วนตัวโดยที่ monorepo ส่วนตัวฝังโค้ดสาธารณะเป็นโมดูลย่อย Git ทำให้สามารถเข้าถึงไฟล์ได้ monorepo สาธารณะถือได้ว่าเป็น upstream และ monorepo ส่วนตัวเป็น downstream

สถาปัตยกรรมของ multi-monorepo
สถาปัตยกรรมของ multi-monorepo (ตัวอย่างขนาดใหญ่)

ขณะที่ฉันวนซ้ำในโค้ดของฉัน การตั้งค่าพื้นที่เก็บข้อมูลที่ฉันจำเป็นต้องใช้ในแต่ละขั้นตอนของโปรเจ็กต์ของฉันก็จำเป็นต้องได้รับการอัปเกรดด้วย ดังนั้นฉันจึงไม่ได้มาถึงแนวทาง multi-monorepo ในวันแรก มันเป็นกระบวนการที่กินเวลาหลายปีและใช้ความพยายามพอสมควร ตั้งแต่ที่เก็บข้อมูลเดียว ไปจนถึงหลายที่เก็บ ไปจนถึง monorepo ไปจนถึง multi-monorepo ในที่สุด

ในบทความนี้ ฉันจะอธิบายวิธีที่ฉันตั้งค่า multi-monorepo โดยใช้ Monorepo Builder ซึ่งใช้ได้กับโปรเจ็กต์ PHP และใช้ Composer

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

การใช้รหัสซ้ำใน Multi-Monorepo

monorepo สาธารณะที่ leoloso/PoP คือที่ที่ฉันเก็บโครงการ PHP ทั้งหมดของฉันไว้

monorepo นี้มีไฟล์เวิร์กโฟลว์ 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 โดยไม่ต้องใช้ความพยายามเพิ่มเติมจากฉัน

เป็นการตั้งค่านี้ที่ฉันต้องการใช้ซ้ำสำหรับ monorepo ส่วนตัวของฉันที่ leoloso/GraphQLAPI-PRO เพื่อให้สามารถสร้างปลั๊กอินสำหรับมืออาชีพได้โดยไม่ต้องใช้ความพยายาม

รหัสที่จะใช้ซ้ำจะประกอบด้วย:

  • เวิร์กโฟลว์ GitHub Actions เพื่อสร้างปลั๊กอิน WordPress (รวมถึงการกำหนดขอบเขต ดาวน์เกรดจาก PHP 8.0 เป็น 7.1 และการอัปโหลดไปยังหน้าเผยแพร่)
  • บริการ PHP แบบกำหนดเองเพื่อกำหนดค่าเวิร์กโฟลว์

จากนั้น monorepo ส่วนตัวก็สามารถสร้างปลั๊กอิน WordPress ระดับโปรได้ง่ายๆ โดยเรียกใช้เวิร์กโฟลว์จาก monorepo สาธารณะและแทนที่การกำหนดค่าใน PHP

การเชื่อมโยง Monorepos ผ่าน Git Submodules

ในการฝังที่เก็บสาธารณะในที่เก็บส่วนตัว เราใช้ Git submodules:

 git submodule add <public repo URL>

ฉันฝังที่เก็บข้อมูลสาธารณะใน submodules ย่อยของโฟลเดอร์ย่อยของ monorepo ส่วนตัว ทำให้ฉันเพิ่ม monorepos อัปสตรีมเพิ่มเติมได้ในอนาคตหากจำเป็น ใน GitHub โฟลเดอร์จะแสดงการคอมมิตเฉพาะของโมดูลย่อย และการคลิกที่มันจะนำฉันไปที่การคอมมิตที่ leoloso/PoP :

การฝัง monorepo สาธารณะใน monorepo ส่วนตัว
การฝัง monorepo สาธารณะใน monorepo ส่วนตัว (ตัวอย่างขนาดใหญ่)

เนื่องจากที่เก็บส่วนตัวมีโมดูลย่อย ในการโคลน เราจึงต้องจัดเตรียมตัวเลือก --recursive :

 git clone --recursive <private repo URL>

การนำเวิร์กโฟลว์การดำเนินการ GitHub กลับมาใช้ใหม่

GitHub Actions โหลดเวิร์กโฟลว์จากภายใต้ .github/workflows เท่านั้น เนื่องจากเวิร์กโฟลว์สาธารณะใน monorepo ดาวน์สตรีมอยู่ภายใต้ submodules/PoP/.github/workflows จึงจำเป็นต้องทำซ้ำสิ่งเหล่านี้ในตำแหน่งที่คาดไว้

เพื่อให้เวิร์กโฟลว์อัปสตรีมเป็นแหล่งความจริงเพียงแหล่งเดียว เราสามารถจำกัดตัวเองให้คัดลอกไฟล์ดาวน์สตรีมภายใต้ .github/workflows แต่ไม่เคยแก้ไขที่นั่น หากต้องการเปลี่ยนแปลงใดๆ จะต้องทำใน upstream monorepo แล้วคัดลอกทับ

โปรดสังเกตว่านี่หมายความว่า multi-monorepo รั่วไหลอย่างไร: monorepo ต้นน้ำไม่ใช่แบบอิสระอย่างสมบูรณ์ และจะต้องปรับให้เหมาะกับ 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');\"" ] } }

จากนั้น หลังจากแก้ไขเวิร์กโฟลว์ใน upstream monorepo ฉันจะคัดลอกดาวน์สตรีมโดยดำเนินการดังต่อไปนี้:

 composer copy-workflows

แต่แล้วฉันก็ตระหนักว่าการคัดลอกเวิร์กโฟลว์ไม่เพียงพอ: ต้องแก้ไขในกระบวนการด้วย เนื่องจากการตรวจสอบ monorepo ดาวน์สตรีมต้องใช้ตัวเลือก --recurse-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"

เมื่อแก้ไขเวิร์กโฟลว์ ควรใช้นิพจน์ทั่วไป (regex) เพื่อให้ใช้ได้กับรูปแบบต่างๆ ในไฟล์ต้นฉบับ (เช่น 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 }}"

ใน 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) ทำตามหลักการ DRY (“อย่าพูดซ้ำ”) โดยมีคลาส PHP ในที่เก็บสาธารณะถูกขยายในที่เก็บส่วนตัว

การสร้างการกำหนดค่าใหม่ผ่าน OOP

มาทำการ refactor คอนฟิกูเรชันกันใหม่ ในที่เก็บสาธารณะ ไฟล์ 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', ]; } }

การแทนที่การกำหนดค่าใน Downstream Monorepo

ตอนนี้การกำหนดค่าใน monorepo สาธารณะได้รับการตั้งค่าโดยใช้ OOP แล้ว เราสามารถขยายการกำหนดค่าเพื่อให้เหมาะกับความต้องการของ monorepo ส่วนตัวได้

เพื่อให้ monorepo ส่วนตัวโหลดโค้ด PHP อัตโนมัติจาก monorepo สาธารณะ ก่อนอื่นเราต้องกำหนดค่าไฟล์ downstream composer.json เพื่ออ้างอิงซอร์สโค้ดจากอัปสตรีม ซึ่งอยู่ภายใต้พาธ submodules/PoP/src :

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

ด้านล่างนี้คือไฟล์ monorepo-builder.php สำหรับ monorepo ส่วนตัว โปรดสังเกตว่าคลาสที่อ้างอิง 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 แบบสตริง ในเวิร์กโฟลว์ จะต้องแปลงเป็นวัตถุ 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)"

ดำเนินการบน 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" } ]

มันใช้งานได้ดีทีเดียว คอนฟิกูเรชันสำหรับ downstream monorepo มีทั้งแพ็คเกจสาธารณะและส่วนตัว และพาธไปยังแพ็คเกจสาธารณะถูกต่อท้ายด้วย submodules/PoP

การข้ามแพ็คเกจสาธารณะใน Monorepo ปลายน้ำ

จนถึงตอนนี้ monorepo ดาวน์สตรีมมีทั้งแพ็คเกจสาธารณะและส่วนตัวในการกำหนดค่า อย่างไรก็ตาม ไม่จำเป็นต้องดำเนินการทุกคำสั่งในแพ็คเกจสาธารณะ

ยกตัวอย่างการวิเคราะห์แบบสถิต monorepo สาธารณะรัน PHPStan บนแพ็คเกจสาธารณะทั้งหมดแล้วผ่านไฟล์เวิร์กโฟลว์ phpstan.yml ดังที่แสดงในการรันนี้ หาก monorepo ดาวน์สตรีมรัน 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 อยู่ใน monorepo ต้นน้ำเป็นแหล่งความจริงเพียงแหล่งเดียว และเห็นได้ชัดว่าไม่ต้องการการเปลี่ยนแปลงเหล่านี้

วิธีแก้ปัญหาที่ฉันพบคือให้ --config ตัวเลือกในที่เก็บอัปสตรีมเสมอ โดยแต่ละคำสั่งจะได้รับไฟล์การกำหนดค่าของตัวเอง เช่น คำสั่ง validate ที่ได้รับไฟล์การกำหนดค่า validate.php :

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

ตอนนี้ ไม่มีไฟล์การกำหนดค่าใน upstream monorepo เนื่องจากไม่ต้องการไฟล์เหล่านั้น แต่มันจะไม่พังเพราะ Monorepo Builder ตรวจสอบว่ามีไฟล์การกำหนดค่าหรือไม่ และถ้าไม่มี มันจะโหลดไฟล์การกำหนดค่าเริ่มต้นแทน ดังนั้น ไม่ว่าเราจะลบล้างการกำหนดค่าหรือจะไม่มีอะไรเกิดขึ้น

ที่เก็บดาวน์สตรีมจัดเตรียมไฟล์คอนฟิกูเรชันสำหรับแต่ละคำสั่ง โดยระบุว่าจะเพิ่มแพ็กเกจอัปสตรีมหรือไม่:

นี่เป็นอีกตัวอย่างหนึ่งของการรั่วไหลของ multi-monorepo

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

เอาชนะการกำหนดค่า

เราเกือบจะเสร็จแล้ว ถึงตอนนี้ monorepo ดาวน์สตรีมสามารถแทนที่การกำหนดค่าจาก monorepo ต้นน้ำได้ ดังนั้น สิ่งเดียวที่ต้องทำคือจัดเตรียมการกำหนดค่าใหม่

ในคลาส 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 และจะสร้างปลั๊กอินโปรใน monorepo ส่วนตัวของฉัน:

กำลังสร้างปลั๊กอินโปร
กำลังสร้างปลั๊กอินโปร (ตัวอย่างขนาดใหญ่)

ทาดา!

บทสรุป

และเช่นเคย ไม่มีวิธีแก้ปัญหาที่ "ดีที่สุด" มีแต่วิธีแก้ปัญหาที่อาจทำงานได้ดีขึ้นโดยขึ้นอยู่กับบริบท แนวทาง multi-monorepo ไม่เหมาะกับโครงการหรือทีมทุกประเภท ฉันเชื่อว่าผู้รับผลประโยชน์รายใหญ่ที่สุดคือผู้สร้างปลั๊กอินที่เผยแพร่ปลั๊กอินสาธารณะซึ่งจะได้รับการอัปเกรดเป็นเวอร์ชันโปร รวมถึงหน่วยงานที่ปรับแต่งปลั๊กอินสำหรับลูกค้าของตน

ในกรณีของฉัน ฉันค่อนข้างพอใจกับแนวทางนี้ การทำให้ถูกต้องต้องใช้เวลาและความพยายามเล็กน้อย แต่เป็นการลงทุนเพียงครั้งเดียว เมื่อการตั้งค่าสิ้นสุดลง ฉันสามารถมุ่งความสนใจไปที่การสร้างปลั๊กอินระดับมือโปร และเวลาที่บันทึกไว้ด้วยการจัดการโครงการก็อาจมีขนาดใหญ่