Repository navigation
Expand file tree
/
Copy pathgithub.php
More file actions
135 lines (127 loc) · 5.12 KB
/
Copy pathgithub.php
File metadata and controls
135 lines (127 loc) · 5.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
/**
* Live repository status from the GitHub API: the latest CI run on main and the last push, per repo.
*
* Fetched server-side (so the page keeps a strict same-origin CSP), in parallel with curl_multi, and
* cached for 30 minutes in a file above the web root. If GitHub is slow or unreachable, the last good
* cache is used, and with no cache at all the page simply shows no live badges. It never fails a page.
*/
declare(strict_types=1);
const GITHUB_OWNER = 'codeROCK-IZZY';
const GITHUB_TTL = 1800;
/** @return array{checked: int, repos: array<string, array{ci: string, pushed: string}>}|null */
function github_status(array $repos): ?array
{
$file = github_cache_file();
$cached = is_file($file) ? json_decode((string) @file_get_contents($file), true) : null;
if (is_array($cached) && ($cached['checked'] ?? 0) > time() - GITHUB_TTL) {
return ($cached['repos'] ?? []) !== [] ? $cached : null; // empty repos: a back-off marker
}
if (!function_exists('curl_multi_init') || getenv('PORTFOLIO_OFFLINE')) {
return is_array($cached) ? $cached : null;
}
$fresh = github_fetch($repos);
if ($fresh === null) {
// Back off for 5 minutes either way, so a rate-limited server is not asked again on every page view.
$marker = is_array($cached) ? $cached : ['repos' => []];
$marker['checked'] = time() - GITHUB_TTL + 300;
@file_put_contents($file, json_encode($marker), LOCK_EX);
return is_array($cached) && ($cached['repos'] ?? []) !== [] ? $cached : null;
}
// A partial answer (some requests rate-limited) keeps each missing repo's last known value.
foreach ($fresh['repos'] as $repo => $r) {
$old = $cached['repos'][$repo] ?? null;
if (is_array($old)) {
if ($r['ci'] === 'unknown') {
$fresh['repos'][$repo]['ci'] = $old['ci'];
}
if ($r['pushed'] === '') {
$fresh['repos'][$repo]['pushed'] = $old['pushed'];
}
}
}
@file_put_contents($file, json_encode($fresh), LOCK_EX);
return $fresh;
}
function github_cache_file(): string
{
$dir = dirname(__DIR__, 3) . '/.portfolio-cache';
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
return (is_dir($dir) && is_writable($dir) ? $dir : sys_get_temp_dir()) . '/github-status.json';
}
/**
* One request per repository for the latest CI run, plus one for every repository's last push: 12 requests
* for 11 projects, well inside the 60 an hour GitHub allows an unauthenticated server.
*
* @return array{checked: int, repos: array<string, array{ci: string, pushed: string}>}|null
*/
function github_fetch(array $repos): ?array
{
$base = 'https://api.github.com/';
$urls = ['__list' => $base . 'users/' . GITHUB_OWNER . '/repos?per_page=100&sort=pushed'];
foreach ($repos as $repo) {
$urls[$repo] = $base . 'repos/' . GITHUB_OWNER . '/' . rawurlencode($repo) . '/actions/runs?branch=main&per_page=1&exclude_pull_requests=true';
}
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $key => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 4,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_HTTPHEADER => ['Accept: application/vnd.github+json', 'User-Agent: israelrocklyn.info', 'X-GitHub-Api-Version: 2022-11-28'],
]);
curl_multi_add_handle($mh, $ch);
$handles[$key] = $ch;
}
do {
$status = curl_multi_exec($mh, $running);
if ($running) {
curl_multi_select($mh, 0.5);
}
} while ($running && $status === CURLM_OK);
$bodies = [];
foreach ($handles as $key => $ch) {
$bodies[$key] = json_decode((string) curl_multi_getcontent($ch), true);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
$pushed = [];
foreach (is_array($bodies['__list']) && array_is_list($bodies['__list']) ? $bodies['__list'] : [] as $r) {
if (is_array($r) && isset($r['name'])) {
$pushed[(string) $r['name']] = (string) ($r['pushed_at'] ?? '');
}
}
$out = ['checked' => time(), 'repos' => []];
$ok = 0;
foreach ($repos as $repo) {
$run = $bodies[$repo]['workflow_runs'][0] ?? null;
$ci = 'unknown';
if (is_array($run)) {
$ci = $run['status'] !== 'completed' ? 'running' : ($run['conclusion'] === 'success' ? 'passing' : 'failing');
$ok++;
}
$out['repos'][$repo] = ['ci' => $ci, 'pushed' => $pushed[$repo] ?? ''];
}
return $ok > 0 ? $out : null; // nothing usable (rate limit, network): keep the old cache
}
/** "3 hours ago" style. */
function ago(string $iso): string
{
$t = strtotime($iso);
if ($t === false) {
return '';
}
$d = max(0, time() - $t);
foreach ([[86400 * 30, 'month'], [86400 * 7, 'week'], [86400, 'day'], [3600, 'hour'], [60, 'minute']] as [$s, $unit]) {
if ($d >= $s) {
$n = intdiv($d, $s);
return $n . ' ' . $unit . ($n > 1 ? 's' : '') . ' ago';
}
}
return 'just now';
}