Lines
97.82%
45 / 46
Functions and Methods
80.00%
4 / 5
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ReleasePoller | 97.82% | 45 / 46 | 80.00% | 4 / 5 | 15 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| startPolling | 100.00% | 16 / 16 | 100.00% | 1 / 1 | 4 | |||||
| stopPolling | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 1 | |||||
| pollForRelease | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 5 | |||||
| deployLatestRelease | 92.85% | 13 / 14 | 0.00% | 0 / 1 | 4.01 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Runtime\Broker\Deploy; | |
| 6 | ||
| 7 | use Amp\Cancellation; | |
| 8 | use Amp\CancelledException; | |
| 9 | use Amp\DeferredCancellation; | |
| 10 | use Psr\Log\LoggerInterface; | |
| 11 | use Stewart\Contracts\Time\Clock; | |
| 12 | use Stewart\Contracts\Time\Timers; | |
| 13 | use Stewart\Runtime\Broker\BrokerRun; | |
| 14 | use Stewart\Runtime\Config\GitDeployConfig; | |
| 15 | use Stewart\Runtime\Exception\DeployException; | |
| 16 | use Stewart\Runtime\Time\RepeatingTimer; | |
| 17 | use Throwable; | |
| 18 | ||
| 19 | use function Amp\async; | |
| 20 | ||
| 21 | final class ReleasePoller | |
| 22 | { | |
| 23 | private ?RepeatingTimer $timer = null; | |
| 24 | ||
| 25 | private ?DeferredCancellation $stop = null; | |
| 26 | ||
| 27 | private bool $polling = false; | |
| 28 | ||
| 29 | public function __construct( | |
| 30 | private readonly GitDeployConfig $gitDeploy, | |
| 31 | private readonly RunningReleaseDetector $runningRelease, | |
| 32 | private readonly ReleaseScript $script, | |
| 33 | private readonly DeployState $state, | |
| 34 | private readonly BrokerRun $run, | |
| 35 | private readonly Timers $timers, | |
| 36 | private readonly Clock $clock, | |
| 37 | private readonly LoggerInterface $logger, | |
| 38 | ) {} | |
| 39 | ||
| 40 | public function startPolling(): void | |
| 41 | { | |
| 42 | $interval = $this->gitDeploy->poll->findDuration(); | |
| 43 | ||
| 44 | if ($interval === null || $this->timer !== null) { | |
| 45 | return; | |
| 46 | } | |
| 47 | ||
| 48 | $commit = $this->runningRelease->findRunningCommit(); | |
| 49 | ||
| 50 | if ($commit === null) { | |
| 51 | $this->logger->warning('deploy.git.poll is set, but Stewart is not running from a release directory, so new commits are not deployed.'); | |
| 52 | ||
| 53 | return; | |
| 54 | } | |
| 55 | ||
| 56 | $this->state->recordRunningCommit($commit); | |
| 57 | $this->stop = new DeferredCancellation(); | |
| 58 | $this->timer = new RepeatingTimer( | |
| 59 | $this->timers, | |
| 60 | $interval, | |
| 61 | $this->pollForRelease(...), | |
| 62 | fn(Throwable $e) => $this->logger->warning('Polling for a new commit failed', ['exception' => $e]), | |
| 63 | ); | |
| 64 | $this->timer->start(); | |
| 65 | } | |
| 66 | ||
| 67 | public function stopPolling(): void | |
| 68 | { | |
| 69 | $this->timer?->stop(); | |
| 70 | $this->timer = null; | |
| 71 | $this->stop?->cancel(); | |
| 72 | $this->stop = null; | |
| 73 | } | |
| 74 | ||
| 75 | public function pollForRelease(): void | |
| 76 | { | |
| 77 | if ($this->polling || $this->stop === null) { | |
| 78 | return; | |
| 79 | } | |
| 80 | ||
| 81 | $this->polling = true; | |
| 82 | $cancellation = $this->stop->getCancellation(); | |
| 83 | ||
| 84 | async(function () use ($cancellation): void { | |
| 85 | try { | |
| 86 | $this->deployLatestRelease($cancellation); | |
| 87 | } catch (CancelledException) { | |
| 88 | } catch (Throwable $e) { | |
| 89 | $this->logger->warning('Could not prepare the latest commit; trying again at the next poll', ['exception' => $e]); | |
| 90 | } finally { | |
| 91 | $this->polling = false; | |
| 92 | } | |
| 93 | })->ignore(); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * @throws DeployException | |
| 98 | * @throws CancelledException | |
| 99 | */ | |
| 100 | private function deployLatestRelease(Cancellation $cancellation): void | |
| 101 | { | |
| 102 | $release = $this->script->prepareLatestRelease($cancellation); | |
| 103 | $this->state->recordPoll($this->clock->getNow()); | |
| 104 | ||
| 105 | if ($release->state !== ReleaseState::Prepared) { | |
| 106 | return; | |
| 107 | } | |
| 108 | ||
| 109 | $failure = $this->script->findCheckFailure($release->commit, $cancellation); | |
| 110 | ||
| 111 | if ($failure !== null) { | |
| 112 | $this->state->recordFailure(new ReleaseFailure($release->commit, $failure, $this->clock->getNow())); | |
| 113 | $this->logger->error('A new commit failed stewart check; the running release stays', ['commit' => $release->commit->value, 'reason' => $failure]); | |
| 114 | ||
| 115 | return; | |
| 116 | } | |
| 117 | ||
| 118 | if (!$this->run->isRunning()) { | |
| 119 | return; | |
| 120 | } | |
| 121 | ||
| 122 | $this->script->stageRelease($release->commit, $cancellation); | |
| 123 | $this->logger->info('Deploying a new commit', ['commit' => $release->commit->value]); | |
| 124 | $this->run->stopToRestart('deploying commit ' . $release->commit->value); | |
| 125 | } | |
| 126 | } |