Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
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 / 62
0.00% covered (danger)
0.00%
0 / 3
420
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 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 checkWorkflowRun
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
156
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        $url = "https://api.github.com/"
23            ."repos/olzimmerberg/olz-website/actions/workflows/ci-scheduled.yml/runs"
24            ."?page=1&per_page=3&status=completed";
25
26        $ch = $this->httpUtils()->curlInit($url, [
27            'headers' => ['Accept: application/vnd.github.v3+json'],
28            'userAgent' => self::$user_agent_string,
29        ]);
30
31        $completed_runs_raw = $this->httpUtils()->curlExec($ch);
32        $completed_runs = json_decode($completed_runs_raw, true);
33        if (!$completed_runs) {
34            throw new \Exception("No completed runs JSON");
35        }
36        $workflow_runs = $completed_runs['workflow_runs'] ?? null;
37        if (!$workflow_runs) {
38            throw new \Exception("No workflow_runs");
39        }
40        if (count($workflow_runs) !== 3) {
41            throw new \Exception("Expected exactly 3 workflow runs");
42        }
43        $has_successful = false;
44        $errors = '';
45        foreach ($workflow_runs as $workflow_run) {
46            try {
47                $this->checkWorkflowRun($workflow_run);
48                $has_successful = true;
49            } catch (\Throwable $th) {
50                $errors .= "  ".$th->getMessage()."\n";
51            }
52        }
53        if ($has_successful) {
54            $this->logAndOutput("OK:");
55            return Command::SUCCESS;
56        }
57        $this->logAndOutput("All 3 backup runs have problems:\n {$errors}");
58        return Command::FAILURE;
59    }
60
61    /** @param array<string, mixed> $workflow_run */
62    protected function checkWorkflowRun(array $workflow_run): void {
63        if ($workflow_run['name'] !== 'CI:scheduled') {
64            throw new \Exception("Expected workflow_run name to be CI:scheduled");
65        }
66        if ($workflow_run['head_branch'] !== 'main') {
67            throw new \Exception("Expected workflow_run head_branch to be main");
68        }
69        if ($workflow_run['status'] !== 'completed') {
70            throw new \Exception("Expected workflow_run status to be completed");
71        }
72        $now = new \DateTime();
73        $minus_two_days = \DateInterval::createFromDateString("-2 days");
74        $two_days_ago = $now->add($minus_two_days);
75        $created_at = new \DateTime($workflow_run['created_at']);
76        if ($created_at->getTimestamp() < $two_days_ago->getTimestamp()) {
77            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')})");
78        }
79        if ($workflow_run['conclusion'] !== 'success') {
80            throw new \Exception("Expected workflow_run conclusion to be success");
81        }
82        if (!$workflow_run['artifacts_url']) {
83            throw new \Exception("Expected workflow_run artifacts_url");
84        }
85
86        $ch = $this->httpUtils()->curlInit($workflow_run['artifacts_url'], [
87            'userAgent' => self::$user_agent_string,
88        ]);
89        $artifacts_raw = $this->httpUtils()->curlExec($ch);
90        $artifacts_dict = json_decode($artifacts_raw, true);
91        if (!$artifacts_dict) {
92            throw new \Exception("No artifacts JSON");
93        }
94        $artifacts = $artifacts_dict['artifacts'] ?? null;
95        if (!$artifacts) {
96            throw new \Exception("No artifacts");
97        }
98        if (count($artifacts) !== 1) {
99            throw new \Exception("Expected exactly 1 artifact");
100        }
101        $artifact = $artifacts[0];
102        if ($artifact['name'] !== 'backup') {
103            throw new \Exception("Expected artifact name to be backup");
104        }
105        if ($artifact['expired'] !== false) {
106            throw new \Exception("Expected artifact expired to be false");
107        }
108    }
109}