From bcb7e0fe891a47c40e51d259ca5308bb437309f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A0nh=20Nh=C3=A2n?= Date: Sat, 14 Nov 2026 16:21:40 +0700 Subject: [PATCH 1/2] fix: skip Composer virion preparation for DevTools self-build --- docs/dependencies.md | 2 +- src/Build/BuildConfigResolver.php | 9 +--- src/Build/ComposerVirionPlan.php | 3 ++ src/Build/DevToolsSelfPackaging.php | 49 ++++++++++++++++++++++ tests/Build/ComposerVirionPreparerTest.php | 27 ++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/Build/DevToolsSelfPackaging.php diff --git a/docs/dependencies.md b/docs/dependencies.md index a5a4302..649fdf4 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -54,7 +54,7 @@ DevTools explicitly rejects `shared-namespace-root`, Composer `autoload.files`, Root mappings must also resolve each namespace to exactly the directory used by Axolotl-PM, including case. For example, with `src-namespace-prefix: MyPlugin`, `MyPlugin\\Extra\\` must map to `src/Extra/`. Mapping it to `src/extra/` or a namespace outside that plugin prefix is rejected, even though the directory lies under `src/`. -The existing DevTools self-build explicitly packages its PHP parser source and license. DevTools rewrites that bundled source into its private `AxolotlBucketLabs\DevTools\Vendor\PhpParser` namespace at load time, isolating the builder from other plugins that use another PHP-Parser release. That narrow self-packaging path is retained. Arbitrary `include-paths` on a user plugin do not turn off Composer validation or imply that a copied vendor directory will autoload. +The existing DevTools self-build explicitly packages its PHP parser source and license. Composer preparation recognizes that self-build configuration and returns no imported virions; the workflow still installs the locked packages so `include-paths` can package PHP Parser. DevTools rewrites that bundled source into its private `AxolotlBucketLabs\DevTools\Vendor\PhpParser` namespace at load time, isolating the builder from other plugins that use another PHP-Parser release. That narrow self-packaging path is retained. Arbitrary `include-paths` on a user plugin do not turn off Composer validation or imply that a copied vendor directory will autoload. ## Provenance and output safety diff --git a/src/Build/BuildConfigResolver.php b/src/Build/BuildConfigResolver.php index c460a1d..b9f9b7b 100644 --- a/src/Build/BuildConfigResolver.php +++ b/src/Build/BuildConfigResolver.php @@ -118,13 +118,8 @@ public function resolve(string $projectRoot, string $pluginName) : BuildConfig } } - $selfPackaging = strtolower($pluginName) === 'devtools' - && in_array('vendor/nikic/php-parser/lib', $includePaths, true) - && in_array('vendor/nikic/php-parser/LICENSE', $includePaths, true); - if (!$selfPackaging) { - foreach ((new ComposerVirionPlan())->requirements($projectRoot) as $requirement) { - $this->addRequirement($requirement, $virions); - } + foreach ((new ComposerVirionPlan())->requirements($projectRoot) as $requirement) { + $this->addRequirement($requirement, $virions); } return new BuildConfig(array_values($virions), array_values($includePaths)); diff --git a/src/Build/ComposerVirionPlan.php b/src/Build/ComposerVirionPlan.php index 8d55073..4028035 100644 --- a/src/Build/ComposerVirionPlan.php +++ b/src/Build/ComposerVirionPlan.php @@ -56,6 +56,9 @@ public function packages(string $projectRoot) : array if (!is_file($path)) { return []; } + if (DevToolsSelfPackaging::matchesProject($projectRoot)) { + return []; + } $composer = $this->readJson($path); $rootAutoload = $composer['autoload'] ?? []; if (!is_array($rootAutoload) || array_diff(array_keys($rootAutoload), ['psr-4', 'psr-0']) !== []) { diff --git a/src/Build/DevToolsSelfPackaging.php b/src/Build/DevToolsSelfPackaging.php new file mode 100644 index 0000000..f375438 --- /dev/null +++ b/src/Build/DevToolsSelfPackaging.php @@ -0,0 +1,49 @@ +read($configPath); + $includePaths = $config['include-paths'] ?? null; + if (!self::matches(self::PLUGIN_NAME, $includePaths)) { + return false; + } + + $manifest = (new PluginManifestReader($yamlReader))->read($manifestPath); + + return self::matches($manifest->description->getName(), $includePaths); + } +} diff --git a/tests/Build/ComposerVirionPreparerTest.php b/tests/Build/ComposerVirionPreparerTest.php index 3edc885..5b9265e 100644 --- a/tests/Build/ComposerVirionPreparerTest.php +++ b/tests/Build/ComposerVirionPreparerTest.php @@ -648,6 +648,33 @@ public function test_simple_project_prepare_is_noop() : void self::assertDirectoryDoesNotExist($destination); } + public function test_devtools_self_packaging_prepare_keeps_its_locked_php_parser_include_paths() : void + { + $project = $this->copyFixture('BuildPlugin'); + $manifest = file_get_contents($project . '/plugin.yml'); + self::assertIsString($manifest); + file_put_contents($project . '/plugin.yml', str_replace('name: BuildFixture', 'name: DevTools', $manifest)); + $this->json($project . '/composer.json', [ + 'name' => 'axolotlbucketlabs/devtools', + 'require' => ['nikic/php-parser' => '^5.6'], + 'autoload' => ['files' => ['src/Build/php-parser-bootstrap.php']], + ]); + $this->json($project . '/composer.lock', ['packages' => []]); + file_put_contents($project . '/devtools.yml', "include-paths:\n - vendor/nikic/php-parser/lib\n - vendor/nikic/php-parser/LICENSE\n"); + $this->filesystem->ensureDirectory($project . '/vendor/nikic/php-parser/lib/PhpParser'); + file_put_contents($project . '/vendor/nikic/php-parser/lib/PhpParser/Parser.php', 'temporaryDirectory . '/prepared'; + self::assertSame([], (new ComposerVirionPreparer($this->filesystem))->prepare($project, $destination)); + self::assertDirectoryDoesNotExist($destination); + + $result = $this->builder()->build($project, $destination, $project . '/build'); + $phar = new Phar($result->outputPath); + self::assertTrue(isset($phar['vendor/nikic/php-parser/lib/PhpParser/Parser.php'])); + self::assertTrue(isset($phar['vendor/nikic/php-parser/LICENSE'])); + } + public function test_root_mapping_under_src_must_match_axolotl_namespace_layout() : void { $project = $this->copyFixture('BuildPlugin'); From 9a001166aa6e3d03cd6c6e4c9f93d6aa34cad79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A0nh=20Nh=C3=A2n?= Date: Sat, 14 Nov 2026 16:21:40 +0700 Subject: [PATCH 2/2] test: allow SHA-pinned GitHub Actions refs --- tests/Validation/RepositoryMetadataTest.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Validation/RepositoryMetadataTest.php b/tests/Validation/RepositoryMetadataTest.php index cb5caa4..140d6d5 100644 --- a/tests/Validation/RepositoryMetadataTest.php +++ b/tests/Validation/RepositoryMetadataTest.php @@ -317,7 +317,7 @@ public function test_cli_documentation_matches_relative_extract_paths() : void self::assertStringNotContainsString('selects `plugins/` or `virions/` when the destination is relative to a project', $contents); } - public function test_workflow_actions_use_stable_release_tags_and_checkouts_do_not_persist_tokens() : void + public function test_workflow_actions_use_pinned_refs_and_checkouts_do_not_persist_tokens() : void { $root = dirname(__DIR__, 2); $paths = array_merge( @@ -329,9 +329,12 @@ public function test_workflow_actions_use_stable_release_tags_and_checkouts_do_n foreach ($paths as $path) { $contents = file_get_contents($path); self::assertIsString($contents); - $checkoutCount += preg_match_all('/uses:\s*actions\\/checkout@v[0-9]+\.[0-9]+\.[0-9]+/', $contents); + $checkoutCount += preg_match_all('/uses:\s*actions\\/checkout@(?:v[0-9]+\.[0-9]+\.[0-9]+|[a-f0-9]{40})(?:\s|$)/', $contents); $credentialOptOutCount += preg_match_all('/persist-credentials:\s*false/', $contents); - self::assertDoesNotMatchRegularExpression('/uses:\s*actions\\/(?:checkout|cache|upload-artifact|download-artifact)@[a-f0-9]{40}/', $contents); + preg_match_all('/uses:\s*actions\\/(?:checkout|cache|upload-artifact|download-artifact)@([^\s#]+)/', $contents, $matches); + foreach ($matches[1] as $reference) { + self::assertMatchesRegularExpression('/^(?:v[0-9]+\.[0-9]+\.[0-9]+|[a-f0-9]{40})$/', $reference); + } } self::assertGreaterThan(0, $checkoutCount);