Last active
November 19, 2025 17:59
-
-
Save fffonion/171720e4b6a23f0587e50dbecbab1ebf to your computer and use it in GitHub Desktop.
Auto retrigger GHA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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