การสร้าง Multi-Monorepo สาธารณะ/ส่วนตัวสำหรับโครงการ PHP
เผยแพร่แล้ว: 2022-03-10เพื่อให้ประสบการณ์การพัฒนาของฉันเร็วขึ้น ฉันย้ายแพ็คเกจ PHP ทั้งหมดที่โปรเจ็กต์ของฉันต้องการไปยัง monorepo เมื่อแต่ละแพ็คเกจโฮสต์ในที่เก็บของตัวเอง (แนวทาง multirepo) ฉันจะต้องพัฒนาและทดสอบด้วยตัวเองแล้วจึงเผยแพร่ไปยัง Packagist ก่อนจึงจะสามารถติดตั้งในแพ็คเกจอื่นผ่าน Composer ได้ ด้วย monorepo เนื่องจากแพ็คเกจทั้งหมดถูกโฮสต์ไว้ด้วยกัน จึงสามารถพัฒนา ทดสอบ กำหนดเวอร์ชัน และเผยแพร่ได้ในเวลาเดียวกัน
monorepo ที่โฮสต์แพ็คเกจ PHP ของฉันเป็นแบบสาธารณะ ทุกคนใน GitHub สามารถเข้าถึงได้ ที่เก็บ Git ไม่สามารถให้สิทธิ์การเข้าถึงเนื้อหาที่แตกต่างกันได้ ทั้งหมดเป็นสาธารณะหรือส่วนตัว เนื่องจากฉันวางแผนที่จะเปิดตัวปลั๊กอิน WordPress ระดับมืออาชีพ ฉันจึงต้องการให้แพ็คเกจของมันถูกเก็บไว้เป็นส่วนตัว ซึ่งหมายความว่าไม่สามารถเพิ่มลงใน monorepo สาธารณะได้
วิธีแก้ปัญหาที่ฉันพบคือใช้วิธีการ "multi-monorepo" ซึ่งประกอบด้วย monorepos สองตัว: แบบสาธารณะและแบบส่วนตัวโดยที่ monorepo ส่วนตัวฝังโค้ดสาธารณะเป็นโมดูลย่อย Git ทำให้สามารถเข้าถึงไฟล์ได้ monorepo สาธารณะถือได้ว่าเป็น upstream และ monorepo ส่วนตัวเป็น downstream
ขณะที่ฉันวนซ้ำในโค้ดของฉัน การตั้งค่าพื้นที่เก็บข้อมูลที่ฉันจำเป็นต้องใช้ในแต่ละขั้นตอนของโปรเจ็กต์ของฉันก็จำเป็นต้องได้รับการอัปเกรดด้วย ดังนั้นฉันจึงไม่ได้มาถึงแนวทาง 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
:
เนื่องจากที่เก็บส่วนตัวมีโมดูลย่อย ในการโคลน เราจึงต้องจัดเตรียมตัวเลือก --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 ไม่เหมาะกับโครงการหรือทีมทุกประเภท ฉันเชื่อว่าผู้รับผลประโยชน์รายใหญ่ที่สุดคือผู้สร้างปลั๊กอินที่เผยแพร่ปลั๊กอินสาธารณะซึ่งจะได้รับการอัปเกรดเป็นเวอร์ชันโปร รวมถึงหน่วยงานที่ปรับแต่งปลั๊กอินสำหรับลูกค้าของตน
ในกรณีของฉัน ฉันค่อนข้างพอใจกับแนวทางนี้ การทำให้ถูกต้องต้องใช้เวลาและความพยายามเล็กน้อย แต่เป็นการลงทุนเพียงครั้งเดียว เมื่อการตั้งค่าสิ้นสุดลง ฉันสามารถมุ่งความสนใจไปที่การสร้างปลั๊กอินระดับมือโปร และเวลาที่บันทึกไว้ด้วยการจัดการโครงการก็อาจมีขนาดใหญ่