Skip to content

Instantly share code, notes, and snippets.

@erseco
Last active July 12, 2026 08:46
Show Gist options
  • Select an option

  • Save erseco/85dbc72191e74450da4671277ad34f65 to your computer and use it in GitHub Desktop.

Select an option

Save erseco/85dbc72191e74450da4671277ad34f65 to your computer and use it in GitHub Desktop.
github-actions-bulk-delete.user.js
// ==UserScript==
// @name GitHub Actions Bulk Delete
// @namespace https://github.com/erseco
// @version 0.2.0
// @description Select and delete GitHub Actions workflow runs without confirmation.
// @author Ernesto Serrano
// @match https://github.com/*/*/actions*
// @grant none
// @run-at document-idle
// ==/UserScript==
(() => {
'use strict';
const CONFIG = {
delayBetweenDeletesMs: 350,
};
const SELECTORS = {
row: '.Box-row[id^="check_suite_"]',
runLink: 'a[href*="/actions/runs/"]',
statusIcon: 'a[href*="/actions/runs/"] svg',
optionsDetails: 'details.details-overlay',
deleteForm: 'form[action*="/actions/runs/"] input[name="_method"][value="delete"]',
};
const ICONS = {
trash: `
<svg
aria-hidden="true"
viewBox="0 0 16 16"
width="16"
height="16"
fill="currentColor"
>
<path d="M6.5 1.75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1
.75.75V2h3.75a.75.75 0 0 1 0 1.5h-.541l-.853
10.237A1.75 1.75 0 0 1 10.112 15H5.888a1.75 1.75
0 0 1-1.744-1.605L3.291 3.5H2.75a.75.75 0 0 1
0-1.5H6.5v-.25ZM4.796 3.5l.812 9.77a.25.25 0 0
0 .249.23h4.286a.25.25 0 0 0 .249-.23l.812-9.77H4.796Z">
</path>
</svg>
`,
};
let deleting = false;
let enhancementScheduled = false;
/**
* Pause execution for a specified duration.
*
* @param {number} milliseconds Duration in milliseconds.
*
* @return {Promise<void>}
*/
function sleep(milliseconds) {
return new Promise((resolve) => {
window.setTimeout(resolve, milliseconds);
});
}
/**
* Extract the workflow run ID from a row.
*
* @param {HTMLElement} row Workflow run row.
*
* @return {string|null}
*/
function getRunId(row) {
const link = row.querySelector(SELECTORS.runLink);
if (!link) {
return null;
}
const match = link.href.match(/\/actions\/runs\/(\d+)/);
return match ? match[1] : null;
}
/**
* Return the delete form contained in a workflow run row.
*
* @param {HTMLElement} row Workflow run row.
*
* @return {HTMLFormElement|null}
*/
function getDeleteForm(row) {
const methodInput = row.querySelector(SELECTORS.deleteForm);
if (!methodInput) {
return null;
}
return methodInput.closest('form');
}
/**
* Delete one workflow run using GitHub's existing form.
*
* @param {HTMLElement} row Workflow run row.
*
* @return {Promise<void>}
*/
async function deleteRun(row) {
const form = getDeleteForm(row);
if (!form) {
throw new Error('Delete form not found for this workflow run.');
}
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method || 'POST',
body: formData,
credentials: 'same-origin',
redirect: 'follow',
headers: {
Accept: 'text/html, application/xhtml+xml',
'X-Requested-With': 'XMLHttpRequest',
},
});
if (!response.ok) {
throw new Error(
`GitHub returned HTTP ${response.status} while deleting the run.`
);
}
row.remove();
}
/**
* Return all selected workflow run rows.
*
* @return {HTMLElement[]}
*/
function getSelectedRows() {
return [...document.querySelectorAll(
`${SELECTORS.row} .ghabd-run-checkbox:checked`
)]
.map((checkbox) => checkbox.closest(SELECTORS.row))
.filter(Boolean);
}
/**
* Update the bulk-delete toolbar.
*
* @return {void}
*/
function updateToolbar() {
const selectedRows = getSelectedRows();
const deleteButton = document.querySelector(
'#ghabd-delete-selected'
);
const selectAll = document.querySelector('#ghabd-select-all');
if (deleteButton) {
deleteButton.disabled = selectedRows.length === 0 || deleting;
deleteButton.textContent = selectedRows.length > 0
? `Delete selected (${selectedRows.length})`
: 'Delete selected';
}
if (selectAll) {
const checkboxes = [
...document.querySelectorAll('.ghabd-run-checkbox'),
];
const checked = checkboxes.filter(
(checkbox) => checkbox.checked
).length;
selectAll.checked = (
checkboxes.length > 0 &&
checked === checkboxes.length
);
selectAll.indeterminate = (
checked > 0 &&
checked < checkboxes.length
);
}
}
/**
* Set toolbar status text.
*
* @param {string} text Status message.
*
* @return {void}
*/
function setStatus(text) {
const status = document.querySelector('#ghabd-status');
if (status) {
status.textContent = text;
}
}
/**
* Delete all selected workflow runs sequentially.
*
* @return {Promise<void>}
*/
async function deleteSelectedRuns() {
if (deleting) {
return;
}
const rows = getSelectedRows();
if (rows.length === 0) {
return;
}
deleting = true;
updateToolbar();
let deleted = 0;
let failed = 0;
for (const row of rows) {
const runId = getRunId(row);
setStatus(
`Deleting ${deleted + failed + 1} of ${rows.length}…`
);
try {
await deleteRun(row);
deleted += 1;
} catch (error) {
failed += 1;
console.error(
`[GitHub Actions Bulk Delete] Run ${runId}:`,
error
);
row.classList.add('ghabd-delete-failed');
}
await sleep(CONFIG.delayBetweenDeletesMs);
}
deleting = false;
updateToolbar();
if (failed > 0) {
setStatus(`Deleted ${deleted}; failed ${failed}.`);
return;
}
setStatus(
`Deleted ${deleted} workflow run${deleted === 1 ? '' : 's'}.`
);
}
/**
* Create the checkbox shown immediately before the status icon.
*
* @param {HTMLElement} row Workflow run row.
* @param {string} runId Workflow run ID.
*
* @return {void}
*/
function addCheckbox(row, runId) {
if (row.querySelector('.ghabd-run-checkbox')) {
return;
}
const statusIcon = row.querySelector(SELECTORS.statusIcon);
if (!statusIcon) {
return;
}
const statusWrapper = statusIcon.parentElement;
if (!statusWrapper) {
return;
}
const checkboxWrapper = document.createElement('span');
checkboxWrapper.className = 'ghabd-checkbox-wrapper';
checkboxWrapper.innerHTML = `
<input
type="checkbox"
class="ghabd-run-checkbox"
aria-label="Select workflow run ${runId}"
title="Select workflow run ${runId}"
>
`;
statusWrapper.parentElement.insertBefore(
checkboxWrapper,
statusWrapper
);
checkboxWrapper
.querySelector('.ghabd-run-checkbox')
.addEventListener('click', (event) => {
event.stopPropagation();
});
checkboxWrapper
.querySelector('.ghabd-run-checkbox')
.addEventListener('change', updateToolbar);
}
/**
* Create the direct-delete icon immediately before the options menu.
*
* @param {HTMLElement} row Workflow run row.
* @param {string} runId Workflow run ID.
*
* @return {void}
*/
function addDeleteButton(row, runId) {
if (row.querySelector('.ghabd-delete-direct')) {
return;
}
const optionsDetails = row.querySelector(
SELECTORS.optionsDetails
);
if (!optionsDetails || !optionsDetails.parentElement) {
return;
}
const button = document.createElement('button');
button.type = 'button';
button.className = [
'ghabd-delete-direct',
'btn-link',
'color-fg-danger',
].join(' ');
button.title = `Delete workflow run ${runId}`;
button.setAttribute(
'aria-label',
`Delete workflow run ${runId}`
);
button.innerHTML = ICONS.trash;
optionsDetails.parentElement.insertBefore(
button,
optionsDetails
);
button.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
if (button.disabled) {
return;
}
button.disabled = true;
button.classList.add('ghabd-is-deleting');
try {
await deleteRun(row);
updateToolbar();
} catch (error) {
console.error(
`[GitHub Actions Bulk Delete] Run ${runId}:`,
error
);
button.disabled = false;
button.classList.remove('ghabd-is-deleting');
row.classList.add('ghabd-delete-failed');
window.alert(
`Could not delete workflow run ${runId}. ` +
'Check the browser console for details.'
);
}
});
}
/**
* Enhance one GitHub Actions workflow run row.
*
* @param {HTMLElement} row Workflow run row.
*
* @return {void}
*/
function enhanceRow(row) {
const runId = getRunId(row);
if (!runId) {
return;
}
row.dataset.ghabdRunId = runId;
addCheckbox(row, runId);
addDeleteButton(row, runId);
}
/**
* Add the bulk-selection toolbar.
*
* @return {void}
*/
function addToolbar() {
if (document.querySelector('#ghabd-toolbar')) {
return;
}
const firstRow = document.querySelector(SELECTORS.row);
if (!firstRow || !firstRow.parentElement) {
return;
}
const toolbar = document.createElement('div');
toolbar.id = 'ghabd-toolbar';
toolbar.className = [
'Box',
'd-flex',
'flex-items-center',
'flex-wrap',
'gap-2',
'p-2',
'mb-3',
].join(' ');
toolbar.innerHTML = `
<label class="d-flex flex-items-center">
<input
id="ghabd-select-all"
type="checkbox"
class="mr-2"
>
Select all visible
</label>
<button
id="ghabd-delete-selected"
type="button"
class="btn btn-danger btn-sm"
disabled
>
Delete selected
</button>
<span
id="ghabd-status"
class="color-fg-muted text-small"
></span>
`;
firstRow.parentElement.insertBefore(toolbar, firstRow);
toolbar
.querySelector('#ghabd-select-all')
.addEventListener('change', (event) => {
document
.querySelectorAll('.ghabd-run-checkbox')
.forEach((checkbox) => {
checkbox.checked = event.currentTarget.checked;
});
updateToolbar();
});
toolbar
.querySelector('#ghabd-delete-selected')
.addEventListener('click', deleteSelectedRuns);
}
/**
* Add userscript styles.
*
* @return {void}
*/
function addStyles() {
if (document.querySelector('#ghabd-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'ghabd-styles';
style.textContent = `
.ghabd-checkbox-wrapper {
display: inline-flex;
align-items: center;
align-self: center;
margin-right: 8px;
flex: 0 0 auto;
}
.ghabd-run-checkbox {
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
}
.ghabd-delete-direct {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-right: 8px;
padding: 0;
border: 0;
border-radius: 6px;
cursor: pointer;
}
.ghabd-delete-direct:hover {
background: var(--bgColor-danger-muted, #ffebe9);
}
.ghabd-delete-direct:disabled {
cursor: wait;
opacity: 0.5;
}
.ghabd-delete-direct svg {
pointer-events: none;
}
.ghabd-is-deleting svg {
animation: ghabd-pulse 0.8s ease-in-out infinite alternate;
}
.ghabd-delete-failed {
outline: 2px solid var(--fgColor-danger, #d1242f);
outline-offset: -2px;
}
@keyframes ghabd-pulse {
from {
opacity: 0.25;
}
to {
opacity: 1;
}
}
`;
document.head.appendChild(style);
}
/**
* Enhance the current GitHub Actions page.
*
* @return {void}
*/
function enhancePage() {
addStyles();
document
.querySelectorAll(SELECTORS.row)
.forEach(enhanceRow);
addToolbar();
updateToolbar();
}
/**
* Schedule page enhancement after dynamic GitHub updates.
*
* @return {void}
*/
function scheduleEnhancement() {
if (enhancementScheduled) {
return;
}
enhancementScheduled = true;
window.requestAnimationFrame(() => {
enhancementScheduled = false;
enhancePage();
});
}
const observer = new MutationObserver(scheduleEnhancement);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
document.addEventListener('turbo:load', scheduleEnhancement);
document.addEventListener('turbo:render', scheduleEnhancement);
document.addEventListener('pjax:end', scheduleEnhancement);
enhancePage();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment