feat: add AssignAgent, ManagePullRequest, ReportToIssue actions
AssignAgent activates a plan and starts an agent session. ManagePullRequest evaluates PR state/CI checks and merges when ready. ReportToIssue posts progress comments on Forgejo issues. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
08d397fbf6
commit
6ac515d80e
5 changed files with 252 additions and 0 deletions
40
Actions/Forge/AssignAgent.php
Normal file
40
Actions/Forge/AssignAgent.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Actions\Session\StartSession;
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Core\Mod\Agentic\Models\AgentSession;
|
||||
|
||||
/**
|
||||
* Assign an agent to a plan and start a session.
|
||||
*
|
||||
* Activates the plan if it is still in draft status, then
|
||||
* delegates to StartSession to create the working session.
|
||||
*
|
||||
* Usage:
|
||||
* $session = AssignAgent::run($plan, 'opus', $workspaceId);
|
||||
*/
|
||||
class AssignAgent
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(AgentPlan $plan, string $agentType, int $workspaceId): AgentSession
|
||||
{
|
||||
if ($plan->status !== AgentPlan::STATUS_ACTIVE) {
|
||||
$plan->activate();
|
||||
}
|
||||
|
||||
return StartSession::run($agentType, $plan->slug, $workspaceId);
|
||||
}
|
||||
}
|
||||
59
Actions/Forge/ManagePullRequest.php
Normal file
59
Actions/Forge/ManagePullRequest.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Services\ForgejoService;
|
||||
|
||||
/**
|
||||
* Evaluate and merge a Forgejo pull request when ready.
|
||||
*
|
||||
* Checks the PR state, mergeability, and CI status before
|
||||
* attempting the merge. Returns a result array describing
|
||||
* the outcome.
|
||||
*
|
||||
* Usage:
|
||||
* $result = ManagePullRequest::run('core', 'app', 10);
|
||||
*/
|
||||
class ManagePullRequest
|
||||
{
|
||||
use Action;
|
||||
|
||||
/**
|
||||
* @return array{merged: bool, pr_number?: int, reason?: string}
|
||||
*/
|
||||
public function handle(string $owner, string $repo, int $prNumber): array
|
||||
{
|
||||
$forge = app(ForgejoService::class);
|
||||
|
||||
$pr = $forge->getPullRequest($owner, $repo, $prNumber);
|
||||
|
||||
if (($pr['state'] ?? '') !== 'open') {
|
||||
return ['merged' => false, 'reason' => 'not_open'];
|
||||
}
|
||||
|
||||
if (empty($pr['mergeable'])) {
|
||||
return ['merged' => false, 'reason' => 'conflicts'];
|
||||
}
|
||||
|
||||
$headSha = $pr['head']['sha'] ?? '';
|
||||
$status = $forge->getCombinedStatus($owner, $repo, $headSha);
|
||||
|
||||
if (($status['state'] ?? '') !== 'success') {
|
||||
return ['merged' => false, 'reason' => 'checks_pending'];
|
||||
}
|
||||
|
||||
$forge->mergePullRequest($owner, $repo, $prNumber);
|
||||
|
||||
return ['merged' => true, 'pr_number' => $prNumber];
|
||||
}
|
||||
}
|
||||
34
Actions/Forge/ReportToIssue.php
Normal file
34
Actions/Forge/ReportToIssue.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Services\ForgejoService;
|
||||
|
||||
/**
|
||||
* Post a progress comment on a Forgejo issue.
|
||||
*
|
||||
* Wraps ForgejoService::createComment() for use as a
|
||||
* standalone action within the orchestration pipeline.
|
||||
*
|
||||
* Usage:
|
||||
* ReportToIssue::run('core', 'app', 5, 'Phase 1 complete.');
|
||||
*/
|
||||
class ReportToIssue
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(string $owner, string $repo, int $issueNumber, string $message): void
|
||||
{
|
||||
app(ForgejoService::class)->createComment($owner, $repo, $issueNumber, $message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Services;
|
||||
|
|
|
|||
112
tests/Feature/ForgeActionsTest.php
Normal file
112
tests/Feature/ForgeActionsTest.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Actions\Forge\AssignAgent;
|
||||
use Core\Mod\Agentic\Actions\Forge\ManagePullRequest;
|
||||
use Core\Mod\Agentic\Actions\Forge\ReportToIssue;
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Core\Mod\Agentic\Models\AgentSession;
|
||||
use Core\Mod\Agentic\Services\ForgejoService;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->workspace = Workspace::factory()->create();
|
||||
|
||||
$this->service = new ForgejoService(
|
||||
baseUrl: 'https://forge.example.com',
|
||||
token: 'test-token-abc',
|
||||
);
|
||||
|
||||
$this->app->instance(ForgejoService::class, $this->service);
|
||||
});
|
||||
|
||||
it('assigns an agent to a plan and starts a session', function () {
|
||||
$plan = AgentPlan::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'status' => AgentPlan::STATUS_DRAFT,
|
||||
]);
|
||||
|
||||
$session = AssignAgent::run($plan, 'opus', $this->workspace->id);
|
||||
|
||||
expect($session)->toBeInstanceOf(AgentSession::class);
|
||||
expect($session->agent_type)->toBe('opus');
|
||||
expect($session->agent_plan_id)->toBe($plan->id);
|
||||
|
||||
// Plan should be activated
|
||||
$plan->refresh();
|
||||
expect($plan->status)->toBe(AgentPlan::STATUS_ACTIVE);
|
||||
});
|
||||
|
||||
it('reports progress to a Forgejo issue', function () {
|
||||
Http::fake([
|
||||
'forge.example.com/api/v1/repos/core/app/issues/5/comments' => Http::response([
|
||||
'id' => 1,
|
||||
'body' => 'Progress update: phase 1 complete.',
|
||||
]),
|
||||
]);
|
||||
|
||||
ReportToIssue::run('core', 'app', 5, 'Progress update: phase 1 complete.');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/repos/core/app/issues/5/comments')
|
||||
&& $request['body'] === 'Progress update: phase 1 complete.';
|
||||
});
|
||||
});
|
||||
|
||||
it('merges a PR when checks pass', function () {
|
||||
Http::fake([
|
||||
// Get PR — open and mergeable
|
||||
'forge.example.com/api/v1/repos/core/app/pulls/10' => Http::response([
|
||||
'number' => 10,
|
||||
'state' => 'open',
|
||||
'mergeable' => true,
|
||||
'head' => ['sha' => 'abc123'],
|
||||
]),
|
||||
|
||||
// Combined status — success
|
||||
'forge.example.com/api/v1/repos/core/app/commits/abc123/status' => Http::response([
|
||||
'state' => 'success',
|
||||
]),
|
||||
|
||||
// Merge
|
||||
'forge.example.com/api/v1/repos/core/app/pulls/10/merge' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
$result = ManagePullRequest::run('core', 'app', 10);
|
||||
|
||||
expect($result)->toMatchArray([
|
||||
'merged' => true,
|
||||
'pr_number' => 10,
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not merge PR when checks are pending', function () {
|
||||
Http::fake([
|
||||
'forge.example.com/api/v1/repos/core/app/pulls/10' => Http::response([
|
||||
'number' => 10,
|
||||
'state' => 'open',
|
||||
'mergeable' => true,
|
||||
'head' => ['sha' => 'abc123'],
|
||||
]),
|
||||
|
||||
'forge.example.com/api/v1/repos/core/app/commits/abc123/status' => Http::response([
|
||||
'state' => 'pending',
|
||||
]),
|
||||
]);
|
||||
|
||||
$result = ManagePullRequest::run('core', 'app', 10);
|
||||
|
||||
expect($result)->toMatchArray([
|
||||
'merged' => false,
|
||||
'reason' => 'checks_pending',
|
||||
]);
|
||||
});
|
||||
Reference in a new issue