Auto-Tick options in Github / GHES

Auto-tick skip, untick GHES modules, populate search with ${SEARCH_PREFIX}, set default branch to YourUserName.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Auto-Tick options in Github / GHES
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description Auto-tick skip, untick GHES modules, populate search with ${SEARCH_PREFIX}, set default branch to YourUserName.
// @locale en
// @match        https://github.com/*/*/workflows*
// @match        https://github.*.co.nz/*/*/actions/workflows/*
// @match        https://github.*.co.nz/*/*/actions/runs/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const DEBUG = true; // Set to false to disable logging
    const log = (...args) => DEBUG && console.log('[RL-AutoTick]', ...args);
    const logState = () => log('State:', { allowTick, allowDevTick, branchSet, searchSet, userModified, url: window.location.href });

    let allowTick = false; // Start as false - only enable after branch is selected
    let allowDevTick = true;
    let lastBranch = null;
    let branchSet = false;
    let searchSet = false;
    let userModified = false;
    let branchSelectionAttempted = false;

    const SEARCH_PREFIX = "feature/RickyL";

    log('Script initialized');
    logState();

    const targetId = "input_3"; // skipPROD checkbox
    const untickTargetId = "input_1"; // use-modules-on-ghes checkbox

    let skipProdDone = false;
    let ghesModulesDone = false;

    const tickBox = () => {
        const el = document.getElementById(targetId);
        if (el && !el.checked) {
            log('tickBox() - found unchecked, ticking now');
            el.checked = true;
            el.dispatchEvent(new Event("change", { bubbles: true }));
            log('✓ Ticked skipPROD checkbox');
            skipProdDone = true;
            return true;
        } else if (el && el.checked) {
            skipProdDone = true; // Already checked
        }
        return false;
    };

    const untickBox = () => {
        const el = document.getElementById(untickTargetId);
        if (el && el.checked) {
            log('untickBox() - found checked, unticking now');
            el.checked = false;
            el.dispatchEvent(new Event("change", { bubbles: true }));
            log('✓ Unticked GHES modules checkbox');
            ghesModulesDone = true;
            return true;
        } else if (el && !el.checked) {
            ghesModulesDone = true; // Already unchecked
        }
        return false;
    };

    // Helper function to check if approval overlay is visible
    const isApprovalOverlayVisible = () => {
        // Look for the dialog with class js-gates-dialog that has the 'open' attribute
        const dialog = document.querySelector('dialog.js-gates-dialog[open]');
        if (!dialog) return false;

        // Double-check that it contains the approval environment elements
        const approvalEnv = dialog.querySelector('.ActionsApprovalOverlay-environment');
        return !!approvalEnv;
    };

    const tickDevBox = () => {
        // Find DEV checkbox by looking for the label containing "DEV" text
        const labels = document.querySelectorAll('.ActionsApprovalOverlay-environment');
        if (labels.length === 0) return false;

        for (const label of labels) {
            const text = label.textContent.trim();
            if (text.startsWith('DEV') || text.match(/^DEV\s/)) {
                const checkbox = label.querySelector('input[type="checkbox"]');
                if (checkbox && !checkbox.checked) {
                    log('tickDevBox() - found unchecked DEV, ticking now');
                    // Trigger a real click event instead of just setting checked
                    checkbox.click();
                    log('✓ Ticked DEV checkbox');
                    return true;
                }
            }
        }
        return false;
    };


    const setBranch = () => {
        if (branchSet) return;
        const buttons = document.querySelectorAll('button[name="branch"]');
        if (buttons.length === 0) return;

        branchSelectionAttempted = true;
        log('setBranch() - found', buttons.length, 'branch buttons, checking for match');

        let foundMatch = false;
        for (const btn of buttons) {
            if (btn.value.startsWith(SEARCH_PREFIX + '/')) {
                log('✓ Clicking branch button:', btn.value);
                btn.click();
                branchSet = true;
                foundMatch = true;

                // Tick checkboxes after branch selection
                // Wait for form to reset, then tick
                setTimeout(() => {
                    log('Post-setBranch: enabling checkbox ticking');
                    skipProdDone = false;
                    ghesModulesDone = false;
                    allowTick = true;
                    tickBox();
                    untickBox();
                }, 500);

                return;
            }
        }

        if (!foundMatch) {
            log('No matching branch found with prefix:', SEARCH_PREFIX);
            // No matching branch found, but a branch is already selected
            // Enable checkbox ticking for the current branch
            const branchButton = document.querySelector('button[data-hotkey="b"]');
            if (branchButton) {
                const span = branchButton.querySelector('span[data-menu-button]');
                const currentBranch = span ? span.textContent.trim() : '';
                if (currentBranch) {
                    log('Using existing branch:', currentBranch);
                    setTimeout(() => {
                        log('No custom branch found, ticking checkboxes for existing branch');
                        skipProdDone = false;
                        ghesModulesDone = false;
                        allowTick = true;
                        tickBox();
                        untickBox();
                    }, 400);
                }
            }
        }
    };

    // Watch for branch changes by monitoring the branch display button
    let branchObserverSetup = false;
    const watchBranchChanges = () => {
        // Find the branch button that shows the currently selected branch
        const branchButton = document.querySelector('button[data-hotkey="b"]');
        if (!branchButton) {
            return false;
        }

        const getCurrentBranch = () => {
            const span = branchButton.querySelector('span[data-menu-button]');
            return span ? span.textContent.trim() : '';
        };

        const currentBranch = getCurrentBranch();
        if (lastBranch === null && currentBranch) {
            lastBranch = currentBranch;
            log('Initial branch detected:', lastBranch);

            // If branch already selected on load, tick checkboxes
            setTimeout(() => {
                log('Branch present on load, ticking checkboxes');
                skipProdDone = false;
                ghesModulesDone = false;
                allowTick = true;
                tickBox();
                untickBox();
            }, 400);
        }

        if (branchObserverSetup) return true;

        // Set up observer on the branch button
        const branchObs = new MutationObserver(() => {
            const newBranch = getCurrentBranch();
            if (newBranch && newBranch !== lastBranch) {
                log('Branch changed from', lastBranch, 'to', newBranch);
                lastBranch = newBranch;

                // Reset and re-enable checkbox ticking after branch change
                skipProdDone = false;
                ghesModulesDone = false;
                allowTick = true;

                // Wait for form to reset, then tick checkboxes
                setTimeout(() => {
                    log('Post-branch-change: ticking checkboxes');
                    tickBox();
                    untickBox();
                }, 400);
            }
        });

        branchObs.observe(branchButton, { childList: true, subtree: true, characterData: true });
        log('Branch observer set up successfully on branch button');
        branchObserverSetup = true;
        return true;
    };

    // Track overlay state to detect close events
    let overlayWasOpen = false;

    // Observe for approval overlay appearance and disappearance
    const approvalObs = new MutationObserver(() => {
        const isOpen = isApprovalOverlayVisible();

        if (isOpen && !overlayWasOpen) {
            log('🔵 Review pending deployments menu OPENED');
            overlayWasOpen = true;
            // Overlay is visible - ensure flag is set to allow ticking
            if (!allowDevTick) {
                log('Re-enabling allowDevTick');
                allowDevTick = true;
            }
        } else if (!isOpen && overlayWasOpen) {
            // Overlay just closed - reset flag for next time it opens
            log('🔴 Review pending deployments menu CLOSED');
            overlayWasOpen = false;
            allowDevTick = true;
        }
    });
    approvalObs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });

    // Trigger lazy-loading of branch list by simulating hover
    const triggerBranchLoad = (menu) => {
        log('Triggering branch list lazy-load');
        // Find the scrollable list container
        const list = menu.querySelector('.SelectMenu-list');
        if (list) {
            // Dispatch mouse events to trigger lazy loading
            list.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
            list.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));

            // Also try focusing on the container
            const container = menu.querySelector('[role="menu"]') || list;
            if (container) {
                container.focus();
            }
        }
    };

    // Observe for modal appearance or visibility - with smarter branch detection
    let modalOpenTime = 0;
    let branchLoadTriggered = false;
    const modalObs = new MutationObserver(() => {
        const menu = document.querySelector('.SelectMenu-modal');
        if (menu && (menu.offsetHeight > 0 || menu.style.display !== 'none')) {
            const now = Date.now();
            // Only log once per modal open
            if (now - modalOpenTime > 500) {
                log('Branch selection modal opened');
                modalOpenTime = now;
                branchLoadTriggered = false;
            }

            // Trigger lazy-load if not already done
            if (!branchLoadTriggered && !branchSet) {
                triggerBranchLoad(menu);
                branchLoadTriggered = true;
            }

            const el = document.getElementById("context-commitish-filter-field");
            if (el && !searchSet) {
                const populateSearchField = (attempt = 1) => {
                    // Check if modal is still open before retrying
                    const menuStillOpen = document.querySelector('.SelectMenu-modal');
                    if (!menuStillOpen || menuStillOpen.offsetHeight === 0) {
                        log('Modal closed, stopping search field population');
                        return;
                    }

                    // Check if element is ready for input
                    if (el && el.offsetParent !== null && !el.disabled) {
                        if (el.value === "") {
                            log('Populating search field with:', SEARCH_PREFIX);
                            el.focus(); // Ensure field has focus
                            el.value = SEARCH_PREFIX;
                            el.dispatchEvent(new Event("input", { bubbles: true }));
                            el.dispatchEvent(new Event("change", { bubbles: true })); // Additional event
                            searchSet = true;
                            el.addEventListener('input', () => {
                                if (el.value !== SEARCH_PREFIX) {
                                    log('User modified search field');
                                    userModified = true;
                                }
                            });
                        }
                    } else if (attempt < 8) {
                        log('Search field not ready, retry', attempt);
                        // More retries with longer delays for Mac performance
                        setTimeout(() => populateSearchField(attempt + 1), 300 * attempt);
                    }
                };
                setTimeout(() => populateSearchField(), 150);
            }

            // Only try setBranch if we haven't already attempted AND buttons exist
            if (!branchSelectionAttempted) {
                const buttons = document.querySelectorAll('button[name="branch"]');
                if (buttons.length > 0) {
                    setBranch();
                }
            }
        }
    });
    modalObs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });

    // Observe for branch selection modal close to re-tick checkboxes
    let branchModalWasOpen = false;
    const menuWaitObs = new MutationObserver(() => {
        const menu = document.querySelector('.SelectMenu-modal');
        const isModalOpen = menu && menu.offsetHeight > 0;

        if (isModalOpen && !branchModalWasOpen) {
            branchModalWasOpen = true;
        } else if (!isModalOpen && branchModalWasOpen) {
            log('🔄 Branch selection modal CLOSED - re-ticking checkboxes');
            branchModalWasOpen = false;

            // Reset search state if user modified
            if (userModified) {
                searchSet = false;
                userModified = false;
            }

            // Check if popover is still open
            const popover = document.querySelector('.Popover-message');
            const popoverStillOpen = popover &&
                                    popover.offsetParent !== null &&
                                    popover.offsetHeight > 0 &&
                                    window.getComputedStyle(popover).display !== 'none';

            if (popoverStillOpen) {
                // Popover still open, re-tick checkboxes after modal closes
                setTimeout(() => {
                    log('Modal closed, popover still open - re-ticking checkboxes');
                    skipProdDone = false;
                    ghesModulesDone = false;
                    allowTick = true;
                    tickBox();
                    untickBox();
                }, 200);
            }
        }
    });
    menuWaitObs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });

    // Observe for Run Workflow Popover close to reset state
    // The popover exists in the DOM but is hidden, so check for visibility differently
    let popoverWasOpen = false;
    let popoverCheckCount = 0;
    const popoverObs = new MutationObserver(() => {
        const popover = document.querySelector('.Popover-message');

        // Check if popover is actually visible (not just in DOM)
        const isOpen = popover &&
                      popover.offsetParent !== null &&
                      popover.offsetHeight > 0 &&
                      window.getComputedStyle(popover).display !== 'none';

        popoverCheckCount++;
        if (popoverCheckCount % 100 === 0) {
            log('Popover check #' + popoverCheckCount + ' - found:', !!popover, 'visible:', isOpen, 'wasOpen:', popoverWasOpen);
        }

        if (isOpen && !popoverWasOpen) {
            log('🎯 Run workflow popover OPENED');
            popoverWasOpen = true;
        } else if (!isOpen && popoverWasOpen) {
            log('🎯 Run workflow popover CLOSED - resetting state for next open');
            popoverWasOpen = false;
            // Reset state so checkboxes can be ticked again next time
            allowTick = false;
            skipProdDone = false;
            ghesModulesDone = false;
            branchSet = false;
            branchSelectionAttempted = false;
            searchSet = false;
            userModified = false;
            logState();
        }
    });
    popoverObs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
    log('Popover observer set up, watching for .Popover-message');

    // Set up branch watcher first
    log('Setting up branch change watcher');
    watchBranchChanges();

    // Try DEV checkbox immediately
    log('Initial attempt to tick DEV checkbox');
    tickDevBox();

    // Try setting branch immediately (if no branch selected yet)
    log('Initial attempt to set branch');
    setBranch();

    // DOM is dynamic → observe until element appears
    // Throttle to reduce excessive calls but not too aggressive
    let lastObsRun = 0;
    const obs = new MutationObserver(() => {
        const now = Date.now();
        if (now - lastObsRun < 50) return; // Throttle to max 20 calls/sec (less aggressive)
        lastObsRun = now;

        // Set up branch watcher if not already done
        if (!branchObserverSetup) {
            watchBranchChanges();
        }

        if (allowTick) {
            tickBox();
            untickBox();

            // Only disable allowTick when BOTH are done
            if (skipProdDone && ghesModulesDone) {
                allowTick = false;
                log('Both checkboxes handled, disabling allowTick');
                logState();
            }
        }
        if (allowDevTick) {
            const devTicked = tickDevBox();
            if (devTicked) {
                allowDevTick = false; // Disable after successful tick, just like skip PROD
                log('Mutation observer DEV tick successful, disabling allowDevTick');
                logState();
            }
        }
        // Don't call setBranch here - it's handled by modalObs when buttons exist
    });

    obs.observe(document.body, { childList: true, subtree: true });

    // Monitor URL changes for SPA navigation - multiple detection methods
    let lastUrl = window.location.href;
    
    const resetStateForNewPage = () => {
        log('Navigated to workflows/runs page - resetting state');
        allowTick = false; // Start false, enable after branch selection
        allowDevTick = true;
        searchSet = false;
        branchSet = false;
        branchSelectionAttempted = false;
        skipProdDone = false;
        ghesModulesDone = false;
        lastBranch = null;
        branchObserverSetup = false;
        popoverWasOpen = false;
        branchModalWasOpen = false;
        overlayWasOpen = false;
        logState();

        // Set up branch watcher for new page
        setTimeout(() => watchBranchChanges(), 100);
    };

    const handleUrlChange = (newUrl) => {
        if (newUrl !== lastUrl) {
            log('URL changed from', lastUrl, 'to', newUrl);
            lastUrl = newUrl;
            if (newUrl.includes('/workflows') || newUrl.includes('/actions/runs/') || newUrl.includes('/actions/workflows/')) {
                resetStateForNewPage();
            }
        }
    };

    // Method 1: Polling (fallback)
    setInterval(() => {
        handleUrlChange(window.location.href);
    }, 200);

    // Method 2: Listen to popstate (browser back/forward)
    window.addEventListener('popstate', () => {
        log('popstate event detected');
        setTimeout(() => handleUrlChange(window.location.href), 50);
    });

    // Method 3: Monitor pushState/replaceState (GitHub's SPA navigation)
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;

    history.pushState = function(...args) {
        originalPushState.apply(this, args);
        log('pushState detected');
        setTimeout(() => handleUrlChange(window.location.href), 50);
    };

    history.replaceState = function(...args) {
        originalReplaceState.apply(this, args);
        log('replaceState detected');
        setTimeout(() => handleUrlChange(window.location.href), 50);
    };

    // Method 4: Watch for turbo:load event (GitHub uses Turbo)
    document.addEventListener('turbo:load', () => {
        log('turbo:load event detected');
        setTimeout(() => handleUrlChange(window.location.href), 50);
    });

    // Method 5: Watch for pjax events (older GitHub navigation)
    document.addEventListener('pjax:end', () => {
        log('pjax:end event detected');
        setTimeout(() => handleUrlChange(window.location.href), 50);
    });

    log('All observers set up, monitoring for changes');

})();