agent/php/Actions/Forge/ManagePullRequest.php
Snider 83df8ad71a fix(agent): address CodeRabbit + SonarCloud findings on PR #6
20+ CHANGES_REQUESTED dispositions across PHP MCP services, Go pkg/agentic,
hermes_runner_mcp Python server, plugin shell scripts.

Highlights:
- DatabaseSchema.php: identifier quoting
- AwardCredits.php: task row locking order
- CreditTransaction.php: fail-fast row decoding
- OpenApiGenerator.php: YAML parse handling + uri query params
- CaptureDispatchResultJob.php: AgentProfile namespace fix
- CreditsController.php: missing workspace_id fail-closed
- QueryAuditService.php: prose query false positives + unbounded aggregation
- McpHealthService.php: proc_close after timeout + env var resolution
- CreditLedger.php + FleetOverview.php: workspace agent + dispatch target validation
- McpAgentServerCommand.php: quota burn on failed tool calls
- McpMetricsService.php: N-day window consistency
- hermes_runner_mcp: API key off command line + invalid method+id + run_id encoding
- CircuitBreaker.php: extracted CircuitOpenException class with autoload-correct placement
- pkg/agentic + brain + flow: SonarCloud sendMessage/fetchLoopRepoRefs/commitWorkspace/Connect annotations
- shell scripts: removed [[ usage for portability

43 files modified, 1 new (CircuitOpenException.php).

Verification: gofmt -w + php -l + python3 -m py_compile + bash -n all clean.
Touched-package go test passes (pkg/lib/flow, pkg/lib).
Full go test ./... blocked by pre-existing dappco.re module graph drift, out of scope.

Parked for separate work:
- Mantis #1062: go.mod local replace removal (cross-repo architectural)
- Mantis #1063: Sonar residual line-length / duplication quality-gate cluster

Closes findings on https://github.com/dAppCore/agent/pull/6

Co-authored-by: Codex <noreply@openai.com>
2026-04-27 13:39:24 +01:00

116 lines
3 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Mod\Agentic\Actions\Forge;
use Core\Actions\Action;
use Core\Mod\Agentic\Pipeline\ForgejoMetaReader;
use Core\Mod\Agentic\Pipeline\MetaReader;
use Core\Mod\Agentic\Services\ForgejoService;
/**
* Evaluate and merge a Forgejo pull request when ready.
*
* Checks the PR state, mergeability, and CI status from MetaReader
* before attempting the merge. Returns a result array describing the
* outcome.
*
* Usage:
* $result = ManagePullRequest::run('core', 'app', 10);
*/
class ManagePullRequest
{
use Action;
public function __construct(
private ?MetaReader $metaReader = null,
) {}
/**
* @return array{merged: bool, pr_number?: int, reason?: string}
*/
public function handle(string $owner, string $repo, int $prNumber): array
{
$forge = app(ForgejoService::class);
$metaReader = $this->resolveMetaReader($owner, $repo);
try {
$prMeta = $metaReader->getPRMeta($prNumber);
} catch (\Throwable $exception) {
return [
'merged' => false,
'reason' => 'meta_unavailable',
];
}
if ($prMeta->state !== 'open') {
return ['merged' => false, 'reason' => 'not_open'];
}
if ($prMeta->mergeability !== 'mergeable') {
return ['merged' => false, 'reason' => 'conflicts'];
}
if (! $this->checksHavePassed($prMeta->checkStatuses)) {
return ['merged' => false, 'reason' => 'checks_pending'];
}
try {
$forge->mergePullRequest($owner, $repo, $prNumber);
} catch (\Throwable $exception) {
return [
'merged' => false,
'reason' => 'merge_failed',
];
}
return ['merged' => true, 'pr_number' => $prNumber];
}
/**
* @param array<int, array{name: string, conclusion: string|null, status: string|null}> $checkStatuses
*/
private function checksHavePassed(array $checkStatuses): bool
{
if ($checkStatuses === []) {
return false;
}
foreach ($checkStatuses as $checkStatus) {
if (($checkStatus['status'] ?? null) !== 'completed') {
return false;
}
if (($checkStatus['conclusion'] ?? null) !== 'success') {
return false;
}
}
return true;
}
private function resolveMetaReader(string $owner, string $repo): MetaReader
{
if ($this->metaReader instanceof MetaReader) {
return $this->metaReader;
}
/** @var MetaReader $metaReader */
$metaReader = app()->makeWith(ForgejoMetaReader::class, [
'owner' => $owner,
'repo' => $repo,
]);
return $metaReader;
}
}