Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 61 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
| MonitorBackupCommand | |
0.00% |
0 / 61 |
|
0.00% |
0 / 3 |
552 | |
0.00% |
0 / 1 |
| getAllowedAppEnvs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| handle | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
90 | |||
| checkWorkflowRun | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
182 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Olz\Command; |
| 4 | |
| 5 | use Olz\Command\Common\OlzCommand; |
| 6 | use Symfony\Component\Console\Attribute\AsCommand; |
| 7 | use Symfony\Component\Console\Command\Command; |
| 8 | use Symfony\Component\Console\Input\InputInterface; |
| 9 | use Symfony\Component\Console\Output\OutputInterface; |
| 10 | |
| 11 | #[AsCommand(name: 'olz:monitor-backup')] |
| 12 | class MonitorBackupCommand extends OlzCommand { |
| 13 | /** @var non-empty-string */ |
| 14 | protected static string $user_agent_string = "Mozilla/5.0 (compatible; backup_monitoring/2.1; +https://github.com/olzimmerberg/olz-website/blob/main/src/Command/MonitorBackupCommand.php)"; |
| 15 | |
| 16 | /** @return array<string> */ |
| 17 | protected function getAllowedAppEnvs(): array { |
| 18 | return ['dev', 'test', 'staging', 'prod']; |
| 19 | } |
| 20 | |
| 21 | protected function handle(InputInterface $input, OutputInterface $output): int { |
| 22 | $ch = curl_init(); |
| 23 | |
| 24 | curl_setopt($ch, CURLOPT_URL, "https://api.github.com/repos/olzimmerberg/olz-website/actions/workflows/ci-scheduled.yml/runs?page=1&per_page=3&status=completed"); |
| 25 | curl_setopt($ch, CURLOPT_USERAGENT, self::$user_agent_string); |
| 26 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
| 27 | $completed_runs_raw = curl_exec($ch) ?: ''; |
| 28 | |
| 29 | $completed_runs = json_decode(!is_bool($completed_runs_raw) ? $completed_runs_raw : '', true); |
| 30 | if (!$completed_runs) { |
| 31 | throw new \Exception("No completed runs JSON"); |
| 32 | } |
| 33 | $workflow_runs = $completed_runs['workflow_runs'] ?? null; |
| 34 | if (!$workflow_runs) { |
| 35 | throw new \Exception("No workflow_runs"); |
| 36 | } |
| 37 | if (count($workflow_runs) !== 3) { |
| 38 | throw new \Exception("Expected exactly 3 workflow runs"); |
| 39 | } |
| 40 | $has_successful = false; |
| 41 | $errors = ''; |
| 42 | foreach ($workflow_runs as $workflow_run) { |
| 43 | try { |
| 44 | $this->checkWorkflowRun($workflow_run); |
| 45 | $has_successful = true; |
| 46 | } catch (\Throwable $th) { |
| 47 | $errors .= " ".$th->getMessage()."\n"; |
| 48 | } |
| 49 | } |
| 50 | if ($has_successful) { |
| 51 | $this->logAndOutput("OK:"); |
| 52 | return Command::SUCCESS; |
| 53 | } |
| 54 | $this->logAndOutput("All 3 backup runs have problems:\n {$errors}"); |
| 55 | return Command::FAILURE; |
| 56 | } |
| 57 | |
| 58 | /** @param array<string, mixed> $workflow_run */ |
| 59 | protected function checkWorkflowRun(array $workflow_run): void { |
| 60 | if ($workflow_run['name'] !== 'CI:scheduled') { |
| 61 | throw new \Exception("Expected workflow_run name to be CI:scheduled"); |
| 62 | } |
| 63 | if ($workflow_run['head_branch'] !== 'main') { |
| 64 | throw new \Exception("Expected workflow_run head_branch to be main"); |
| 65 | } |
| 66 | if ($workflow_run['status'] !== 'completed') { |
| 67 | throw new \Exception("Expected workflow_run status to be completed"); |
| 68 | } |
| 69 | $now = new \DateTime(); |
| 70 | $minus_two_days = \DateInterval::createFromDateString("-2 days"); |
| 71 | $two_days_ago = $now->add($minus_two_days); |
| 72 | $created_at = new \DateTime($workflow_run['created_at']); |
| 73 | if ($created_at->getTimestamp() < $two_days_ago->getTimestamp()) { |
| 74 | throw new \Exception("Expected workflow_run created_at ({$created_at->format('Y-m-d H:i:s')}) to be in the last 2 days ({$two_days_ago->format('Y-m-d H:i:s')})"); |
| 75 | } |
| 76 | if ($workflow_run['conclusion'] !== 'success') { |
| 77 | throw new \Exception("Expected workflow_run conclusion to be success"); |
| 78 | } |
| 79 | if (!$workflow_run['artifacts_url']) { |
| 80 | throw new \Exception("Expected workflow_run artifacts_url"); |
| 81 | } |
| 82 | |
| 83 | $ch = curl_init(); |
| 84 | |
| 85 | curl_setopt($ch, CURLOPT_URL, $workflow_run['artifacts_url']); |
| 86 | curl_setopt($ch, CURLOPT_USERAGENT, self::$user_agent_string); |
| 87 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
| 88 | $artifacts_raw = curl_exec($ch); |
| 89 | |
| 90 | curl_close($ch); |
| 91 | |
| 92 | $artifacts_dict = json_decode(!is_bool($artifacts_raw) ? $artifacts_raw : '', true); |
| 93 | if (!$artifacts_dict) { |
| 94 | throw new \Exception("No artifacts JSON"); |
| 95 | } |
| 96 | $artifacts = $artifacts_dict['artifacts'] ?? null; |
| 97 | if (!$artifacts) { |
| 98 | throw new \Exception("No artifacts"); |
| 99 | } |
| 100 | if (count($artifacts) !== 1) { |
| 101 | throw new \Exception("Expected exactly 1 artifact"); |
| 102 | } |
| 103 | $artifact = $artifacts[0]; |
| 104 | if ($artifact['name'] !== 'backup') { |
| 105 | throw new \Exception("Expected artifact name to be backup"); |
| 106 | } |
| 107 | if ($artifact['expired'] !== false) { |
| 108 | throw new \Exception("Expected artifact expired to be false"); |
| 109 | } |
| 110 | } |
| 111 | } |