Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ This package also has a root `post-autoload-dump` script:
"post-autoload-dump": "@php bin/install-captainhook.php"
```

That helper keeps hooks installed for this repository. Consuming projects get automatic hook installation from the PHPForge Composer plugin with project `captainhook.json` when present, otherwise with the bundled PHPForge `captainhook.json`.
That helper keeps hooks installed for this repository. Consuming projects get automatic hook installation from the PHPForge Composer plugin: it uses project `captainhook.json` when present, otherwise it copies the bundled `captainhook.json` into project root and installs hooks from there.

## GitHub Actions

Expand Down
51 changes: 41 additions & 10 deletions src/Composer/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,61 @@ public function getCapabilities(): array

public function installHooks(Event $event): void
{
$configPath = Paths::config('captainhook.json');
try {
$configPath = $this->ensureProjectCaptainHookConfig();

if (!is_file($configPath)) {
return;
}
if (!is_string($configPath)) {
return;
}

$process = new Process(CaptainHook::installCommand($configPath), getcwd() ?: null);
$process->setTimeout(null);
$process->run();

$process = new Process(CaptainHook::installCommand($configPath), getcwd() ?: null);
$process->setTimeout(null);
$process->run();
if ($process->isSuccessful()) {
return;
}

if (!$process->isSuccessful()) {
$message = (trim($process->getErrorOutput()) ?: trim($process->getOutput())) ?: 'CaptainHook install failed.';

throw new \RuntimeException($message);
} catch (\RuntimeException $exception) {
if (getenv('IC_HOOKS_STRICT') !== '0') {
throw new \RuntimeException($message);
throw $exception;
}

$event->getIO()->writeError('<warning>PHPForge could not install CaptainHook hooks; continuing because IC_HOOKS_STRICT=0.</warning>');
$event->getIO()->writeError($message);
$event->getIO()->writeError($exception->getMessage());
}
}

public function uninstall(Composer $composer, IOInterface $io): void {}

private function ensureProjectCaptainHookConfig(): ?string
{
$projectConfig = Paths::projectRootPath() . DIRECTORY_SEPARATOR . 'captainhook.json';

if (is_file($projectConfig)) {
return $projectConfig;
}

$bundledConfig = Paths::bundledConfigFileOrNull('captainhook.json');

if (!is_string($bundledConfig) || !is_file($bundledConfig)) {
return null;
}

if (!copy($bundledConfig, $projectConfig)) {
throw new \RuntimeException(sprintf(
'Failed to copy bundled CaptainHook config from "%s" to "%s".',
$bundledConfig,
$projectConfig,
));
}

return $projectConfig;
}

private function reportMissingAllowPlugins(): void
{
$composerJson = (getcwd() ?: '') . DIRECTORY_SEPARATOR . 'composer.json';
Expand Down
89 changes: 89 additions & 0 deletions tests/Composer/PluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

use Composer\Composer;
use Composer\IO\NullIO;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Infocyph\PHPForge\Composer\Plugin;

function removePluginTestTree(string $path): void
{
if (!is_dir($path)) {
return;
}

$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);

foreach ($iterator as $item) {
if ($item->isDir()) {
rmdir($item->getPathname());

continue;
}

unlink($item->getPathname());
}

rmdir($path);
}

it('copies bundled captainhook config into project root when missing', function (): void {
$originalCwd = getcwd();
$projectRoot = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpforge-plugin-'.uniqid('', true);
$vendorResources = $projectRoot.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'infocyph'.DIRECTORY_SEPARATOR.'phpforge'.DIRECTORY_SEPARATOR.'resources';
$vendorBin = $projectRoot.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'bin';
$bundledConfig = dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'captainhook.json';

mkdir($vendorResources, 0755, true);
mkdir($vendorBin, 0755, true);
file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'composer.json', '{"name":"example/project"}');
copy($bundledConfig, $vendorResources.DIRECTORY_SEPARATOR.'captainhook.json');
file_put_contents($vendorBin.DIRECTORY_SEPARATOR.'captainhook', "<?php\nexit(0);\n");

chdir($projectRoot);

try {
$event = new Event(ScriptEvents::POST_AUTOLOAD_DUMP, new Composer(), new NullIO());
$plugin = new Plugin();
$projectConfig = $projectRoot.DIRECTORY_SEPARATOR.'captainhook.json';

expect(fn () => $plugin->installHooks($event))->not->toThrow(RuntimeException::class);
expect(is_file($projectConfig))->toBeTrue();
expect(file_get_contents($projectConfig))->toBe(file_get_contents($bundledConfig));
} finally {
if (is_string($originalCwd)) {
chdir($originalCwd);
}

removePluginTestTree($projectRoot);
}
});

it('keeps strict hook installation when project captainhook config exists', function (): void {
$originalCwd = getcwd();
$projectRoot = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpforge-plugin-'.uniqid('', true);

mkdir($projectRoot, 0755, true);
file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'composer.json', '{"name":"example/project"}');
file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'captainhook.json', '{}');

chdir($projectRoot);

try {
$event = new Event(ScriptEvents::POST_AUTOLOAD_DUMP, new Composer(), new NullIO());
$plugin = new Plugin();

expect(fn () => $plugin->installHooks($event))->toThrow(RuntimeException::class);
} finally {
if (is_string($originalCwd)) {
chdir($originalCwd);
}

removePluginTestTree($projectRoot);
}
});