Last active
April 8, 2026 16:21
-
-
Save gartnera/8fef1ed5707d2ab5901d688b3d6a8dc6 to your computer and use it in GitHub Desktop.
Allow copying github artifacts URLs. Show -profile.gz as a clickable link to perfetto UI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name GitHub Actions Artifact Presigned URL Copier | |
| // @namespace https://agartner.com/ | |
| // @version 1.5 | |
| // @description Copy presigned URLs for GitHub Actions artifacts, open -profile.gz in Perfetto | |
| // @match https://github.com/*/actions/runs/* | |
| // @grant GM_setClipboard | |
| // @grant GM_xmlhttpRequest | |
| // @connect productionresultssa15.blob.core.windows.net | |
| // @connect blob.core.windows.net | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| function getGithubArtifactUrl(repoPath, runId, artifactId) { | |
| return 'https://github.com/' + repoPath + '/actions/runs/' + runId + '/artifacts/' + artifactId; | |
| } | |
| function getPresignedUrl(repoPath, runId, artifactId) { | |
| return new Promise(function (resolve, reject) { | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: 'https://github.com/' + repoPath + '/actions/runs/' + runId + '/artifacts/' + artifactId, | |
| redirect: 'follow', | |
| onload: function (res) { | |
| console.log('[presigned] status:', res.status, 'finalUrl:', res.finalUrl); | |
| if (res.finalUrl && res.finalUrl.indexOf('blob.core.windows.net') !== -1) { | |
| resolve(res.finalUrl); | |
| } else { | |
| reject(new Error('Unexpected finalUrl: ' + res.finalUrl)); | |
| } | |
| }, | |
| onerror: function (err) { | |
| reject(new Error('GM_xmlhttpRequest error: ' + JSON.stringify(err))); | |
| }, | |
| }); | |
| }); | |
| } | |
| function setButtonState(btn, state, label) { | |
| var original = { html: btn.innerHTML, color: btn.style.color }; | |
| btn.textContent = label; | |
| btn.style.color = state === 'success' | |
| ? 'var(--fgColor-success, #3fb950)' | |
| : 'var(--fgColor-danger, #f85149)'; | |
| setTimeout(function () { | |
| btn.innerHTML = original.html; | |
| btn.style.color = original.color; | |
| btn.disabled = false; | |
| }, 2000); | |
| } | |
| function makeBtn(title, svgPath) { | |
| var btn = document.createElement('button'); | |
| btn.type = 'button'; | |
| btn.title = title; | |
| btn.style.cssText = 'background:none;border:none;cursor:pointer;padding:4px;color:var(--fgColor-muted,#848d97);display:inline-flex;align-items:center;vertical-align:middle;'; | |
| var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
| svg.setAttribute('aria-hidden', 'true'); | |
| svg.setAttribute('height', '16'); | |
| svg.setAttribute('width', '16'); | |
| svg.setAttribute('viewBox', '0 0 16 16'); | |
| svg.setAttribute('fill', 'currentColor'); | |
| var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| path.setAttribute('d', svgPath); | |
| svg.appendChild(path); | |
| btn.appendChild(svg); | |
| return btn; | |
| } | |
| var COPY_PATH = 'M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'; | |
| var PERFETTO_PATH = 'M9.504.43a1.516 1.516 0 0 1 2.437 1.713L10.415 5.5h2.123c1.57 0 2.346 1.909 1.22 3.004l-7.34 7.142a1.249 1.249 0 0 1-.871.354h-.302a1.25 1.25 0 0 1-1.157-1.723L5.633 10.5H3.462c-1.57 0-2.346-1.909-1.22-3.004L9.504.43Zm.094 2.497L4.557 8.485a.25.25 0 0 0 .178.427h2.668a.75.75 0 0 1 .697 1.023l-1.2 3.015 5.047-4.91a.25.25 0 0 0-.178-.427h-2.497a.75.75 0 0 1-.626-1.166l1.952-3.06Z'; | |
| function createCopyButton(repoPath, runId, artifactId) { | |
| var btn = makeBtn('Copy presigned URL', COPY_PATH); | |
| btn.classList.add('presigned-copy-btn'); | |
| btn.addEventListener('click', async function () { | |
| btn.disabled = true; | |
| try { | |
| var url = await getPresignedUrl(repoPath, runId, artifactId); | |
| GM_setClipboard(url); | |
| setButtonState(btn, 'success', '✓'); | |
| } catch (err) { | |
| console.error('presigned-url-copier:', err); | |
| setButtonState(btn, 'error', '✗'); | |
| } | |
| }); | |
| return btn; | |
| } | |
| function createPerfettoButton(repoPath, runId, artifactId) { | |
| var btn = makeBtn('Open in Perfetto', PERFETTO_PATH); | |
| btn.classList.add('presigned-perfetto-btn'); | |
| btn.addEventListener('click', async function () { | |
| btn.disabled = true; | |
| try { | |
| var url = await getPresignedUrl(repoPath, runId, artifactId); | |
| window.open('https://perfetto.agartner.com/#!/?url=' + encodeURIComponent(url), '_blank'); | |
| setButtonState(btn, 'success', '✓'); | |
| } catch (err) { | |
| console.error('presigned-url-copier:', err); | |
| setButtonState(btn, 'error', '✗'); | |
| } | |
| }); | |
| return btn; | |
| } | |
| function injectButtons() { | |
| var urlMatch = window.location.pathname.match(/^\/([^/]+\/[^/]+)\/actions\/runs\/(\d+)/); | |
| if (!urlMatch) return; | |
| var repoPath = urlMatch[1]; | |
| var runId = urlMatch[2]; | |
| document.querySelectorAll('tr[data-artifact-id]').forEach(function (row) { | |
| var artifactId = row.getAttribute('data-artifact-id'); | |
| if (!artifactId || row.querySelector('.presigned-copy-btn')) return; | |
| var downloadLink = row.querySelector('a[data-test-selector="download-artifact-button"]'); | |
| if (!downloadLink) return; | |
| var btnContainer = downloadLink.closest('.d-flex'); | |
| if (!btnContainer) return; | |
| var nameAnchor = row.querySelector('a[aria-label^="Download"]'); | |
| var artifactName = nameAnchor ? nameAnchor.getAttribute('aria-label') : ''; | |
| var isProfile = /-profile\.gz/.test(artifactName); | |
| var deleteForm = btnContainer.querySelector('form'); | |
| var copyBtn = createCopyButton(repoPath, runId, artifactId); | |
| if (deleteForm) { | |
| btnContainer.insertBefore(copyBtn, deleteForm); | |
| if (isProfile) { | |
| btnContainer.insertBefore(createPerfettoButton(repoPath, runId, artifactId), deleteForm); | |
| } | |
| } else { | |
| btnContainer.appendChild(copyBtn); | |
| if (isProfile) { | |
| btnContainer.appendChild(createPerfettoButton(repoPath, runId, artifactId)); | |
| } | |
| } | |
| }); | |
| } | |
| injectButtons(); | |
| new MutationObserver(injectButtons).observe(document.body, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment