feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Discovery\DiscoveryRegistry;
|
||||
use App\Framework\Discovery\Results\AttributeRegistry;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Scheduler\Services\SchedulerService;
|
||||
use App\Framework\Worker\Every;
|
||||
use App\Framework\Worker\Schedule;
|
||||
use App\Framework\Worker\ScheduleDiscoveryService;
|
||||
|
||||
// Test job classes with Schedule attribute
|
||||
#[Schedule(at: new Every(minutes: 5))]
|
||||
final class TestFiveMinuteJob
|
||||
{
|
||||
public static int $executionCount = 0;
|
||||
|
||||
public function handle(): array
|
||||
{
|
||||
self::$executionCount++;
|
||||
return ['status' => 'success', 'count' => self::$executionCount];
|
||||
}
|
||||
}
|
||||
|
||||
#[Schedule(at: new Every(hours: 1))]
|
||||
final class TestHourlyJob
|
||||
{
|
||||
public static int $executionCount = 0;
|
||||
|
||||
public function __invoke(): string
|
||||
{
|
||||
self::$executionCount++;
|
||||
return 'hourly job executed';
|
||||
}
|
||||
}
|
||||
|
||||
describe('ScheduleDiscoveryService Integration', function () {
|
||||
beforeEach(function () {
|
||||
// Reset execution counters
|
||||
TestFiveMinuteJob::$executionCount = 0;
|
||||
TestHourlyJob::$executionCount = 0;
|
||||
|
||||
// Create minimal logger mock
|
||||
$this->logger = Mockery::mock(Logger::class);
|
||||
$this->logger->shouldReceive('debug')->andReturn(null);
|
||||
$this->logger->shouldReceive('info')->andReturn(null);
|
||||
$this->logger->shouldReceive('warning')->andReturn(null);
|
||||
$this->logger->shouldReceive('error')->andReturn(null);
|
||||
|
||||
$this->schedulerService = new SchedulerService(
|
||||
$this->logger
|
||||
);
|
||||
|
||||
// Create minimal DiscoveryRegistry mock
|
||||
$this->discoveryRegistry = Mockery::mock(DiscoveryRegistry::class);
|
||||
|
||||
$this->scheduleDiscovery = new ScheduleDiscoveryService(
|
||||
$this->discoveryRegistry,
|
||||
$this->schedulerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('discovers and registers scheduled jobs from attribute registry', function () {
|
||||
// Mock discovery to return our test jobs
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(2);
|
||||
|
||||
// Verify tasks were registered with scheduler
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('generates correct task IDs from class names', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
$taskIds = array_map(fn($task) => $task->taskId, $scheduledTasks);
|
||||
|
||||
expect($taskIds)->toContain('test-five-minute-job');
|
||||
expect($taskIds)->toContain('test-hourly-job');
|
||||
});
|
||||
|
||||
it('executes scheduled jobs correctly', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestFiveMinuteJob::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
// Get the scheduled task
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Execute the task
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
expect($result->result)->toBeArray();
|
||||
expect($result->result['status'])->toBe('success');
|
||||
expect($result->result['count'])->toBe(1);
|
||||
expect(TestFiveMinuteJob::$executionCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('executes callable jobs correctly', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestHourlyJob::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Execute the task
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
expect($result->result)->toBe('hourly job executed');
|
||||
expect(TestHourlyJob::$executionCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('uses correct intervals from Every value object', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class, // 5 minutes = 300 seconds
|
||||
TestHourlyJob::class // 1 hour = 3600 seconds
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
// Find the 5-minute job
|
||||
$fiveMinuteTask = array_values(array_filter(
|
||||
$scheduledTasks,
|
||||
fn($task) => $task->taskId === 'test-five-minute-job'
|
||||
))[0] ?? null;
|
||||
|
||||
expect($fiveMinuteTask)->not->toBeNull();
|
||||
|
||||
// Execute task
|
||||
$result = $this->schedulerService->executeTask($fiveMinuteTask);
|
||||
|
||||
expect($result->success)->toBeTrue();
|
||||
|
||||
// Get updated task
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
$updatedTask = array_values(array_filter(
|
||||
$scheduledTasks,
|
||||
fn($task) => $task->taskId === 'test-five-minute-job'
|
||||
))[0] ?? null;
|
||||
|
||||
// Next execution should be set (schedule updated)
|
||||
expect($updatedTask->nextExecution)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('handles jobs without handle() or __invoke() gracefully', function () {
|
||||
// Create a job class without handle() or __invoke()
|
||||
$invalidJobClass = new class {
|
||||
// No handle() or __invoke()
|
||||
};
|
||||
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([$invalidJobClass::class]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(1);
|
||||
|
||||
$task = $scheduledTasks[0];
|
||||
|
||||
// Executing should throw RuntimeException
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
|
||||
expect($result->success)->toBeFalse();
|
||||
expect($result->error)->toContain('must have handle() method or be callable');
|
||||
});
|
||||
|
||||
it('returns 0 when no scheduled jobs found', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([]);
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(0);
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
expect($scheduledTasks)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('can retrieve scheduled tasks via getScheduledTasks()', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$tasks = $this->scheduleDiscovery->getScheduledTasks();
|
||||
|
||||
expect($tasks)->toHaveCount(2);
|
||||
expect($tasks[0])->toHaveProperty('taskId');
|
||||
expect($tasks[0])->toHaveProperty('nextExecution');
|
||||
});
|
||||
|
||||
it('executes multiple jobs independently', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestFiveMinuteJob::class,
|
||||
TestHourlyJob::class
|
||||
]);
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
$scheduledTasks = $this->schedulerService->getScheduledTasks();
|
||||
|
||||
// Execute both jobs
|
||||
foreach ($scheduledTasks as $task) {
|
||||
$result = $this->schedulerService->executeTask($task);
|
||||
expect($result->success)->toBeTrue();
|
||||
}
|
||||
|
||||
// Both counters should have incremented
|
||||
expect(TestFiveMinuteJob::$executionCount)->toBe(1);
|
||||
expect(TestHourlyJob::$executionCount)->toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user