Skip to content

Instantly share code, notes, and snippets.

@fffonion
Last active November 19, 2025 17:59
Show Gist options
  • Select an option

  • Save fffonion/171720e4b6a23f0587e50dbecbab1ebf to your computer and use it in GitHub Desktop.

Select an option

Save fffonion/171720e4b6a23f0587e50dbecbab1ebf to your computer and use it in GitHub Desktop.
Auto retrigger GHA
// ==UserScript==
// @name Rerun GHA automagically
// @namespace http://yooooo.us
// @updateURL https://gist.github.com/fffonion/171720e4b6a23f0587e50dbecbab1ebf/raw/rerun-gha.user.js
// @downloadURL https://gist.github.com/fffonion/171720e4b6a23f0587e50dbecbab1ebf/raw/rerun-gha.user.js
// @version 0.47
// @description awesome!
// @author You
// @match https://github.com/*/*/pull/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @run-at document-idle
// @homepageURL https://gist.github.com/fffonion/171720e4b6a23f0587e50dbecbab1ebf
// @supportURL https://gist.github.com/fffonion/171720e4b6a23f0587e50dbecbab1ebf
// @grant none
// ==/UserScript==
//const TestsToRetry = /(?:Build & Test|CI)/;
const TestsToRetry = /^(?!.*check-pr-title)(?!.*Changelog)(?!.*Pull Request Template Validation).*$/;
const FormXHRHeaders = new Map([
['Content-type', 'application/x-www-form-urlencoded']
]);
const AutoRerunMaxTimes = 5;
const ghaLogger = console.context("storage", { color: "lemonchiffon" });
(function () {
'use strict';
function request(method, url, body, headers) {
let err = function (e) {
console.error(method, url, "=>", e)
};
return new Promise(function (resolve, reject) {
try {
let xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (headers) {
for (let [key, value] of headers) {
xhr.setRequestHeader(key, value);
}
}
xhr.withCredentials = true;
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.send(body);
} catch (error) {
reject(error);
}
});
}
let needRetry = true;
function grabChecks() {
let checkLinks = new Map();
let sections = document.getElementsByTagName("section");
let section
for(let i=0; i < sections.length; i++) {
if (sections[i].getAttribute("aria-label") === "Checks") {
section = sections[i];
break
}
}
if (section === undefined) {
return checkLinks
}
let checks = section.getElementsByTagName("li");
if (checks.length == 0) {
return checkLinks;
}
let failedElements = [];
let firstCheck = null;
let checkNames = new Map();
for (let i = 0; i < checks.length; i++) {
let check = checks[i];
// skip the other group
// if (check.childNodes.length < 6) continue;
let name = check.childNodes[2].childNodes[0].innerText
let result = check.childNodes[2].childNodes[2].innerText
if (name === "" || result === "") continue;
let failed = result.match(/(?:Failing after|Cancelled)/); // failed or cancelled
let pending = result.match(/Queued/);
let running = result.match(/This check has started/);
if (name.match(TestsToRetry)) {
let as = check.childNodes[2].childNodes[0].getElementsByTagName("a");
if(as.length < 1) continue;
let a = as[0].getAttribute("href");
let url = a && a.match(/.+\/actions\/runs\/\d+/);
if (!url) continue; // not GHA
url = url[0];
let nameWorkflow = name.match(/^[^/]+/)[0].trim();
checkNames.set(url, nameWorkflow);
//console.log(name, failed, url);
if (failed) {
checkLinks.set(url, true);
failedElements.push(check);
} else if (running || pending) {
checkLinks.set(url, false);
//console.log("Skipping because it's still running " + name);
}
}
};
let _needRetry = checkLinks.size > 0; // if = 0 then all passed, no need to retry; otherwise default to retry
let canRerunLinks = new Map();
checkLinks.forEach(function(v, k, m) {
if(v) {
canRerunLinks.set(k, checkNames.get(k));
// at least one rerunnable workflow found
_needRetry = false;
}
})
needRetry = _needRetry;
/* no need anymore
if(!_needRetry) {
console.log(">", checks[0]);
console.log("?>>>", failedElements);
for(let i=0; i < failedElements.length; i++) {
firstCheck.before(failedElements[i]);
}
}
*/
return canRerunLinks;
};
let autoRerunCounter = 0;
function doTheWork() {
let btn, btn2;
// wait dom complete
if(document.getElementsByTagName("section")[0] === undefined) {
return setTimeout(doTheWork, 500);
}
let checkLinks;
function refreshStatus(f) {
needRetry = true;
checkLinks = grabChecks();
// console.log(">> needRetry ", needRetry, checkLinks.size);
if (needRetry) {
setTimeout(f || refreshStatus, 5000);
return false;
}
if (checkLinks.size > 0 && btn2 !== undefined) {
btn2.removeAttribute('disabled');
}
return true;
}
refreshStatus();
function rerun() {
btn2.setAttribute('disabled', '');
for (let [key, value] of checkLinks) {
ghaLogger.log("Rerunning workflow " + value + " at https://github.com" + key);
let url = "https://github.com" + key;
request("GET", url + "/job_rerun_dialogs_partial").then(function (r) {
// console.log(r.responseText)
let token = r.responseText.match(/authenticity_token" value="(.*?)"/)[1]
request("POST",
url + "/rerequest_check_suite",
'_method=put&authenticity_token=' + token + '&only_failed_check_runs=true',
FormXHRHeaders,
);
})
}
setTimeout(refreshStatus, 10000);
}
let mbContainer;
mbContainer = document.getElementsByTagName("section")[0].parentNode;
let div = document.createElement("div");
btn = document.createElement("button");
btn.className = "btn js-quick-submit-alternative js-comment-and-button";
btn.style = "position: absolute;right: 1em;bottom: 3.4em;";
btn.innerText = "Auto Rerun";
//let autoRerunTesting = false;
let lastLog = Date.now();
function autoRerun() {
if(Date.now() - lastLog > 60000) {
ghaLogger.log("I'm still working, if you are curious");
lastLog = Date.now();
}
if(!refreshStatus(autoRerun)) return;
if(autoRerunCounter > AutoRerunMaxTimes) {
ghaLogger.log("Already rerun 5 times, still failing 😭");
btn.innerHTML = "Auto Rerun";
btn.removeAttribute('disabled')
return;
} else if (btn2.getAttribute('disabled') !== null) {
ghaLogger.log("Test running wait");
} else if (checkLinks.size > 0) {
//autoRerunTesting = true;
autoRerunCounter += 1;
ghaLogger.log("Rerunning test, remaming rerun counter " + (AutoRerunMaxTimes - autoRerunCounter));
rerun();
}
setTimeout(autoRerun, 10000);
}
btn.addEventListener("click", function (e) {
e.stopPropagation();
ghaLogger.log("Auto rerun monitor started, max rerun counter " + AutoRerunMaxTimes);
autoRerun();
btn.innerHTML = '<span class="prc-Spinner-Box-qNUI9"><svg style="position: relative;right: 5px;bottom: -2px;" height="16px" width="16px" viewBox="0 0 16 16" fill="none" aria-hidden="true" aria-labelledby=":r1t:" class="prc-Spinner-SpinnerAnimation-e7Gf-"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-opacity="0.25" stroke-width="2" vector-effect="non-scaling-stroke"></circle><path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke"></path></svg><span class="prc-VisuallyHidden-VisuallyHidden-UNWQp" id=":r1t:">Loading</span></span>Thinking';
btn.setAttribute('disabled', '');
});
div.appendChild(btn);
btn2 = document.createElement("button");
btn2.className = "btn js-quick-submit-alternative js-comment-and-button";
btn2.style = "position: absolute;right: 8.3em;bottom: 3.4em;border-radius: 6px 0 0 6px;";
btn2.innerText = "Rerun Failed";
if(checkLinks.size == 0) btn2.setAttribute('disabled', '');
btn2.addEventListener("click", function (e) {
e.stopPropagation();
rerun();
});
div.appendChild(btn2);
mbContainer.appendChild(div);
}
setTimeout(doTheWork, 500);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment