Auto-Tick options in Github / GHES

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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');

})();