Automatically clicks the merge button on Dependabot PRs and "Done" button on the notification bar
// ==UserScript==
// @name Merge Dependabot PRs Automatically on GitHub with UI and Selection Options
// @namespace typpi.online
// @version 2.3
// @description Automatically clicks the merge button on Dependabot PRs and "Done" button on the notification bar
// @author Nick2bad4u
// @match https://github.com/*/*/pull/*
// @grant none
// @license UnLicense
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @homepageURL https://github.com/Nick2bad4u/UserStyles
// @supportURL https://github.com/Nick2bad4u/UserStyles/issues
// ==/UserScript==
(function () {
'use strict';
const CHECK_INTERVAL = 1000; // Interval between checks in milliseconds
let lastCheck = 0;
let observer;
let token = getTokenFromCookies() || promptForToken();
if (!token) {
while (!token) {
token = prompt('Please enter your GitHub token:');
if (token) {
document.cookie = `github_token=${token}; path=/; max-age=${60 * 60 * 24 * 365}`;
} else {
alert('GitHub token is required.');
}
}
}
if (!token) {
alert('GitHub token is required for this script to work.');
return;
}
function getTokenFromCookies() {
const match = document.cookie.match(/(^| )\s*github_token\s*=\s*([^;]+)/);
return match ? match[2] : null;
}
function promptForToken() {
let token = prompt('Please enter your GitHub token. You can generate a token at https://github.com/settings/tokens');
if (token) {
document.cookie = `github_token=${token}; path=/; max-age=${60 * 60 * 24 * 365}`;
}
return token;
}
const debounce = (func, delay) => {
let debounceTimer;
return function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, arguments), delay);
};
};
const debouncedCheckAndMerge = debounce(checkAndMerge, 300);
async function checkAndMerge() {
const now = Date.now();
if (now - lastCheck < CHECK_INTERVAL) return;
lastCheck = now;
console.log('checkAndMerge function called');
try {
const authorElement = document.querySelector('.author');
if (authorElement && /^(dependabot\[bot\]|Nick2bad4u)$/.test(authorElement.textContent.trim())) {
const prNumber = window.location.pathname.split('/').pop();
const repoPath = window.location.pathname.split('/').slice(1, 3).join('/');
console.log('PR is created by dependabot or specified user, attempting to merge via API');
const mergeSuccess = await mergePR(repoPath, prNumber);
if (mergeSuccess) {
console.log('PR merged successfully');
markAsDone();
} else {
console.log('Failed to merge PR');
}
} else {
console.log('PR is not created by dependabot or specified user');
}
} catch (error) {
console.error('Error in checkAndMerge:', error);
}
}
async function mergePR(repoPath, prNumber) {
try {
const response = await fetch(`https://api.github.com/repos/${repoPath}/pulls/${prNumber}/merge`, {
method: 'PUT',
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
commit_title: `Merge PR #${prNumber}`,
merge_method: 'merge',
}),
});
if (response.ok) {
return true;
} else {
const errorData = await response.json();
console.error('Failed to merge PR:', errorData);
return false;
}
} catch (error) {
console.error('Error in mergePR:', error);
return false;
}
}
async function markAsDone() {
try {
const notificationBar = document.querySelector('.js-flash-container');
if (!notificationBar) {
console.log('Notification bar not found');
return;
}
let doneButton = document.querySelector('button[aria-label="Done"].btn.btn-sm');
if (!doneButton) {
doneButton = Array.from(document.querySelectorAll('button.btn.btn-sm')).find(button => button.textContent.trim() === 'Done');
}
if (doneButton) {
console.log('Done button found, clicking it');
doneButton.click();
} else {
console.log('Done button not found, attempting to mark as done via API');
const notificationId = getNotificationId();
if (notificationId) {
const response = await fetch(`https://api.github.com/notifications/threads/${notificationId}`, {
method: 'PATCH',
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
state: 'done',
}),
});
if (response.ok) {
console.log('Notification marked as done via API');
} else {
const errorData = await response.json();
console.error('Failed to mark notification as done via API:', errorData);
}
} else {
console.log('Notification ID not found');
}
}
} catch (error) {
console.error('Error in markAsDone:', error);
}
}
/**
* Retrieves the notification ID from the URL parameters.
*/
function getNotificationId() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('notification_id');
}
globalThis.addEventListener(
'load',
function () {
console.log('Page loaded');
const targetNode = document.querySelector('.gh-header-meta');
if (!targetNode) {
console.log('Target node for observation not found');
return;
}
observer = new MutationObserver(() => {
console.log('Relevant DOM mutation detected');
debouncedCheckAndMerge();
});
observer.observe(targetNode, {
childList: true,
subtree: true,
});
checkAndMerge();
},
false,
);
})();