Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MonitorBackupCommand
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 3
552
0.00% covered (danger)
0.00%
0 / 1
 getAllowedAppEnvs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 checkWorkflowRun
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2
3namespace Olz\Command;
4
5use Olz\Command\Common\OlzCommand;
6use Symfony\Component\Console\Attribute\AsCommand;
7use Symfony\Component\Console\Command\Command;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10
11#[AsCommand(name: 'olz:monitor-backup')]
12class 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}