Merge Dependabot PRs Automatically on GitHub with UI and Selection Options

Automatically clicks the merge button on Dependabot PRs and "Done" button on the notification bar

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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,
	);
})();