GitHub Actions - Extra Buttons

Adds a button on GitHub Actions pages to scroll to the top easily

2025-11-13 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         GitHub Actions - Extra Buttons
// @namespace    https://github.com/
// @version      1.0
// @description  Adds a button on GitHub Actions pages to scroll to the top easily
// @author       chaoscreater
// @match        https://github.com/*/actions/runs/*/job/*
// @match        https://github.*.co.nz/*/*/actions/runs/*/job/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Check if current URL matches the @match patterns
    function shouldShowButton() {
        const url = window.location.href;
        // Match: https://github.com/*/actions/runs/*/job/*
        const pattern1 = /^https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/[^/]+\/job\/[^/]+/;
        // Match: https://github.*.co.nz/*/*/actions/runs/*/job/*
        const pattern2 = /^https:\/\/github[^/]*\.co\.nz\/[^/]+\/[^/]+\/actions\/runs\/[^/]+\/job\/[^/]+/;
        return pattern1.test(url) || pattern2.test(url);
    }

    // Helper function to create a button
    function createButton(text, backgroundColor, bottom) {
        const button = document.createElement('button');
        button.innerText = text;
        Object.assign(button.style, {
            position: 'fixed',
            bottom: bottom,
            left: '20px',
            padding: '4px 7px',
            fontSize: '11px',
            borderRadius: '4px',
            border: 'none',
            backgroundColor: backgroundColor,
            color: '#fff',
            cursor: 'pointer',
            boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
            zIndex: '9999',
            opacity: '0.8',
            transition: 'opacity 0.2s, transform 0.2s',
        });

        // Hover effect
        button.addEventListener('mouseenter', () => {
            button.style.opacity = '1';
            button.style.transform = 'scale(1.05)';
        });
        button.addEventListener('mouseleave', () => {
            button.style.opacity = '0.8';
            button.style.transform = 'scale(1.0)';
        });

        return button;
    }

    // Create the Top button
    const topBtn = createButton('⬆️ Top', '#2ea44f', '20px');
    topBtn.addEventListener('click', () => {
        // Find all workflow step details elements and close them
        const stepDetails = document.querySelectorAll('.js-checks-log-details');
        stepDetails.forEach(detail => {
            if (detail.hasAttribute('open')) {
                detail.removeAttribute('open');
            }
        });

        // Scroll to top
        window.scrollTo({ top: 0, behavior: 'smooth' });
    });

    // Create the Collapse All button
    const collapseBtn = createButton('➖ Collapse All', '#0969da', '50px');
    collapseBtn.addEventListener('click', () => {
        const stepDetails = document.querySelectorAll('.js-checks-log-details');
        stepDetails.forEach(detail => {
            if (detail.hasAttribute('open')) {
                detail.removeAttribute('open');
            }
        });
    });

    // Create the Expand All button
    const expandBtn = createButton('➕ Expand All', '#8250df', '80px');
    expandBtn.addEventListener('click', () => {
        const stepDetails = document.querySelectorAll('.js-checks-log-details');
        stepDetails.forEach(detail => {
            if (!detail.hasAttribute('open')) {
                detail.setAttribute('open', '');
            }
        });
    });

    // Create the Summary button
    const summaryBtn = createButton('📋 Summary', '#bf8700', '110px');
    summaryBtn.addEventListener('click', () => {
        // Extract the run ID from current URL and navigate to summary
        const url = window.location.href;
        const runMatch = url.match(/\/actions\/runs\/(\d+)/);
        if (runMatch) {
            const runId = runMatch[1];
            const baseUrl = url.split('/actions/')[0];
            window.location.href = `${baseUrl}/actions/runs/${runId}`;
        }
    });

    // Create the Workflow button
    const workflowBtn = createButton('🔄 Workflow', '#6e7781', '140px');
    workflowBtn.addEventListener('click', () => {
        // Navigate to the actions/workflows page
        const url = window.location.href;
        const baseUrl = url.split('/actions/')[0];
        window.location.href = `${baseUrl}/actions`;
    });

    // Update button visibility based on URL
    function updateButtonVisibility() {
        const buttons = [topBtn, collapseBtn, expandBtn, summaryBtn, workflowBtn];
        if (shouldShowButton()) {
            buttons.forEach(btn => {
                if (!btn.parentElement) {
                    document.body.appendChild(btn);
                }
                btn.style.display = 'block';
            });
        } else {
            buttons.forEach(btn => {
                btn.style.display = 'none';
            });
        }
    }

    // Initial check
    updateButtonVisibility();

    // Monitor URL changes (GitHub uses client-side routing)
    let lastUrl = location.href;
    new MutationObserver(() => {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            updateButtonVisibility();
        }
    }).observe(document, { subtree: true, childList: true });
})();