Skip to content

Commit 70ae407

Browse files
committed
Update badge loading code to be more robust
I tried quite a lot to keep the previous approach to badge sorting (i.e. hashing from the image content), but was not able to overcome CORS failures in all cases when attempting to load and parse the badge image data asynchronously via JavaScript. So now we use api.github.com instead to access the last build status that way. It now requires a GitHub personal access token, unfortunately, but this way we stay within GitHub's badge access rate limit.
1 parent 0ede784 commit 70ae407

File tree

1 file changed

+152
-36
lines changed

1 file changed

+152
-36
lines changed

sortable-badges.js

Lines changed: 152 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,160 @@
11
// This is free and unencumbered software released into the public domain.
22
// See the UNLICENSE file for details.
33

4-
function imageData(img) {
5-
var canvas = document.createElement("canvas");
6-
canvas.width = img.width;
7-
canvas.height = img.height;
8-
9-
// HACK: Without this, canvas.toDataURL() triggers the error
10-
// 'Tainted canvases may not be exported'.
11-
// See: http://stackoverflow.com/a/27260385/1207769
12-
var anonImg = new Image();
13-
anonImg.setAttribute('crossOrigin', 'anonymous');
14-
anonImg.src = img.src;
15-
16-
canvas.getContext("2d").drawImage(anonImg, 0, 0);
17-
return canvas.toDataURL();
4+
// Thanks to Claude AI for the batch-based loading
5+
// approach to stay within GitHub's request rate limit.
6+
7+
function addTokenUI() {
8+
const controlDiv = document.createElement('div');
9+
controlDiv.style.position = 'fixed';
10+
controlDiv.style.top = '10px';
11+
controlDiv.style.left = '10px';
12+
controlDiv.style.background = '#f0f0f0';
13+
controlDiv.style.padding = '10px';
14+
controlDiv.style.borderRadius = '5px';
15+
controlDiv.style.border = '1px solid #ccc';
16+
17+
const tokenInput = document.createElement('input');
18+
tokenInput.type = 'password';
19+
tokenInput.placeholder = 'GitHub token (workflow read only)';
20+
tokenInput.style.marginRight = '10px';
21+
22+
const loadButton = document.createElement('button');
23+
loadButton.textContent = 'Load Build Status';
24+
25+
const progress = document.createElement('div');
26+
progress.style.marginTop = '5px';
27+
progress.style.fontSize = '12px';
28+
29+
controlDiv.appendChild(tokenInput);
30+
controlDiv.appendChild(loadButton);
31+
controlDiv.appendChild(progress);
32+
document.body.appendChild(controlDiv);
33+
34+
return { tokenInput, loadButton, progress };
1835
}
1936

20-
function hashCode(s) {
21-
// NB: Using the first 128 characters is good enough, and much faster.
22-
return s.substring(0, 128);
23-
/*
24-
var hash = 0;
25-
if (s.length === 0) return hash;
26-
for (var i = 0; i < s.length; i++) {
27-
var c = s.charCodeAt(i);
28-
hash = ((hash << 5) - hash) + c;
29-
hash |= 0; // Convert to 32-bit integer
30-
}
31-
return hash.toString(16);
32-
*/
37+
async function getWorkflowStatus(badgeUrl, token) {
38+
const match = badgeUrl.match(/github\.com\/([^/]+)\/([^/]+)\/actions\/workflows\/([^/]+)\/badge\.svg/);
39+
if (!match) return null;
40+
41+
const [, owner, repo, workflow] = match;
42+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?per_page=1`;
43+
44+
try {
45+
console.log('Fetching:', apiUrl);
46+
const headers = token ? { 'Authorization': `token ${token}` } : {};
47+
const response = await fetch(apiUrl, { headers });
48+
if (!response.ok) {
49+
console.error('API request failed:', response.status, await response.text());
50+
return null;
51+
}
52+
53+
const data = await response.json();
54+
console.log('API response:', data);
55+
56+
if (data.workflow_runs && data.workflow_runs.length > 0) {
57+
const latestRun = data.workflow_runs[0];
58+
return {
59+
conclusion: latestRun.conclusion || 'unknown',
60+
status: latestRun.status,
61+
timestamp: latestRun.updated_at
62+
};
63+
}
64+
65+
return null;
66+
} catch (error) {
67+
console.error('Error fetching workflow status:', error);
68+
return null;
69+
}
3370
}
3471

35-
// Thanks to Andreas: http://stackoverflow.com/users/402037/andreas
36-
// See: http://stackoverflow.com/q/43686686/1207769
37-
function makeBadgesSortable() {
38-
var tds = document.body.getElementsByClassName("badge");
39-
for (var i=0; i<tds.length; i++) {
40-
var imgs = tds[i].getElementsByTagName("img");
41-
if (imgs.length < 1) continue;
42-
tds[i].setAttribute("sorttable_customkey", hashCode(imageData(imgs[0])));
43-
}
72+
function getSortKey(status) {
73+
if (!status) return 'z_unknown';
74+
75+
// Sort successful builds first, then in-progress, then failed
76+
if (status.conclusion === 'success') return 'a_success_' + status.timestamp;
77+
if (status.status === 'in_progress') return 'b_running_' + status.timestamp;
78+
if (status.conclusion === 'failure') return 'c_failed_' + status.timestamp;
79+
80+
// Other states like skipped, cancelled, etc.
81+
return 'd_other_' + status.conclusion + '_' + status.timestamp;
4482
}
83+
84+
async function loadBadgesGradually(token, progressCallback) {
85+
// Initialize sort keys
86+
const badges = document.getElementsByClassName('badge');
87+
Array.from(badges).forEach(badge => {
88+
badge.setAttribute('sorttable_customkey', 'loading');
89+
});
90+
91+
const badgeArray = Array.from(badges);
92+
const batchSize = 1; // number of badges per batch
93+
const delayMs = 200; // time between batches
94+
let processedCount = 0;
95+
96+
async function loadBatch(startIndex) {
97+
const batch = badgeArray.slice(startIndex, startIndex + batchSize);
98+
console.log(`Loading batch starting at ${startIndex}`);
99+
100+
// Process each badge in the batch
101+
for (const badge of batch) {
102+
const img = badge.querySelector('img');
103+
if (img && img.dataset.src) {
104+
console.log('Processing badge:', img.dataset.src);
105+
106+
// Get workflow status first
107+
const status = await getWorkflowStatus(img.dataset.src, token);
108+
const sortKey = getSortKey(status);
109+
badge.setAttribute('sorttable_customkey', sortKey);
110+
console.log('Set sort key:', sortKey);
111+
112+
// Then load the badge image
113+
img.src = img.dataset.src;
114+
115+
// Update progress
116+
processedCount++;
117+
progressCallback(processedCount, badgeArray.length);
118+
}
119+
}
120+
121+
// Schedule next batch if there are more badges
122+
if (startIndex + batchSize < badgeArray.length) {
123+
console.log(`Scheduling next batch in ${delayMs}ms`);
124+
return new Promise(resolve => {
125+
setTimeout(() => {
126+
loadBatch(startIndex + batchSize).then(resolve);
127+
}, delayMs);
128+
});
129+
}
130+
}
131+
132+
// Start loading first batch.
133+
await loadBatch(0);
134+
}
135+
136+
// Initialize UI and handle load button click.
137+
const { tokenInput, loadButton, progress } = addTokenUI();
138+
139+
loadButton.addEventListener('click', async () => {
140+
const token = tokenInput.value.trim();
141+
if (!token) {
142+
alert('Please enter a GitHub token. You can create one at https://github.com/settings/tokens with only "workflow" read permission.');
143+
return;
144+
}
145+
146+
loadButton.disabled = true;
147+
loadButton.textContent = 'Loading...';
148+
149+
try {
150+
await loadBadgesGradually(token, (current, total) => {
151+
progress.textContent = `Progress: ${current}/${total} (${Math.round(current/total*100)}%)`;
152+
});
153+
} catch (error) {
154+
console.error('Error loading badges:', error);
155+
alert('Error loading badges: ' + error.message);
156+
} finally {
157+
loadButton.disabled = false;
158+
loadButton.textContent = 'Load Build Status';
159+
}
160+
});

0 commit comments

Comments
 (0)