Created
July 11, 2025 13:23
-
-
Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.
Github Tag Creator
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 Tag Creator | |
// @namespace http://tampermonkey.net/ | |
// @version 2.2 | |
// @description Add ability to create tags on GitHub via API | |
// @author freehuntx | |
// @match https://github.com/*/*/tags* | |
// @grant GM_xmlhttpRequest | |
// @grant GM_addStyle | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
console.log('[GitHub Tag Creator] Script loaded on:', window.location.href); | |
// Add CSS for the tag creator UI | |
GM_addStyle(` | |
.tag-creator-button { | |
margin-left: 16px; | |
} | |
.tag-creator-nav-button { | |
display: inline-block; | |
padding: 5px 16px; | |
font-size: 14px; | |
font-weight: 500; | |
line-height: 20px; | |
white-space: nowrap; | |
vertical-align: middle; | |
cursor: pointer; | |
user-select: none; | |
border: 1px solid; | |
border-radius: 6px; | |
appearance: none; | |
color: var(--color-btn-text); | |
background-color: var(--color-btn-bg); | |
border-color: var(--color-btn-border); | |
box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); | |
transition: 80ms cubic-bezier(0.33, 1, 0.68, 1); | |
transition-property: color, background-color, box-shadow, border-color; | |
} | |
.tag-creator-nav-button:hover { | |
background-color: var(--color-btn-hover-bg); | |
border-color: var(--color-btn-hover-border); | |
} | |
.tag-creator-inline-button { | |
margin-left: 8px; | |
padding: 3px 12px; | |
font-size: 12px; | |
line-height: 20px; | |
color: var(--color-fg-default); | |
background-color: var(--color-btn-bg); | |
border: 1px solid var(--color-btn-border); | |
border-radius: 6px; | |
cursor: pointer; | |
} | |
.tag-creator-inline-button:hover { | |
background-color: var(--color-btn-hover-bg); | |
} | |
.tag-creator-modal { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: var(--color-canvas-default, #ffffff); | |
border: 1px solid var(--color-border-default, #d1d5da); | |
border-radius: 6px; | |
padding: 16px; | |
z-index: 1000; | |
box-shadow: 0 8px 24px rgba(140,149,159,0.2); | |
min-width: 400px; | |
} | |
.tag-creator-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: rgba(0, 0, 0, 0.5); | |
z-index: 999; | |
} | |
.tag-creator-form { | |
display: flex; | |
flex-direction: column; | |
gap: 12px; | |
} | |
.tag-creator-input { | |
padding: 5px 12px; | |
font-size: 14px; | |
line-height: 20px; | |
color: var(--color-fg-default, #24292e); | |
vertical-align: middle; | |
background-color: var(--color-canvas-default, #ffffff); | |
background-repeat: no-repeat; | |
background-position: right 8px center; | |
border: 1px solid var(--color-border-default, #d1d5da); | |
border-radius: 6px; | |
outline: none; | |
box-shadow: inset 0 1px 0 rgba(225,228,232,0.2); | |
} | |
.tag-creator-textarea { | |
min-height: 100px; | |
resize: vertical; | |
} | |
.tag-creator-actions { | |
display: flex; | |
gap: 8px; | |
justify-content: flex-end; | |
margin-top: 8px; | |
} | |
.tag-creator-error { | |
color: var(--color-danger-fg, #cb2431); | |
font-size: 12px; | |
margin-top: 4px; | |
} | |
.tag-creator-success { | |
color: var(--color-success-fg, #28a745); | |
font-size: 12px; | |
margin-top: 4px; | |
} | |
.tag-creator-close { | |
position: absolute; | |
top: 8px; | |
right: 8px; | |
background: none; | |
border: none; | |
font-size: 20px; | |
cursor: pointer; | |
color: var(--color-fg-muted, #586069); | |
padding: 4px 8px; | |
} | |
.tag-creator-close:hover { | |
color: var(--color-fg-default, #24292e); | |
} | |
`); | |
// Function to get repository info from the current page | |
function getRepoInfo() { | |
const pathParts = window.location.pathname.split('/').filter(Boolean); | |
if (pathParts.length >= 2) { | |
return { | |
owner: pathParts[0], | |
repo: pathParts[1] | |
}; | |
} | |
return null; | |
} | |
// Try to extract token from various sources | |
async function getGitHubToken() { | |
// 1. Check if we have a saved token | |
const savedToken = GM_getValue('github_token', ''); | |
if (savedToken) { | |
return savedToken; | |
} | |
// 2. Try to get from GitHub's session (this might work if user has certain browser extensions) | |
try { | |
// Check if we can access GitHub's API with current session | |
const response = await fetch('https://api.github.com/user', { | |
headers: { | |
'Accept': 'application/vnd.github.v3+json' | |
}, | |
credentials: 'include' | |
}); | |
if (response.ok) { | |
// Session auth might work, but we still need a PAT for API calls | |
console.log('GitHub session detected, but Personal Access Token still required for API operations'); | |
} | |
} catch (e) { | |
console.log('No automatic token detection available'); | |
} | |
// 3. Check for GitHub CLI token (gh) in localStorage | |
try { | |
const ghToken = localStorage.getItem('gh-token'); | |
if (ghToken) { | |
return ghToken; | |
} | |
} catch (e) {} | |
return ''; | |
} | |
// Function to get the default branch | |
async function getDefaultBranch(owner, repo, token) { | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: `https://api.github.com/repos/${owner}/${repo}`, | |
headers: { | |
'Authorization': token ? `token ${token}` : '', | |
'Accept': 'application/vnd.github.v3+json' | |
}, | |
onload: function(response) { | |
if (response.status >= 200 && response.status < 300) { | |
const data = JSON.parse(response.responseText); | |
resolve(data.default_branch); | |
} else { | |
reject(new Error(`Failed to fetch repository info: ${response.statusText}`)); | |
} | |
}, | |
onerror: function(error) { | |
reject(new Error(`Network error: ${error}`)); | |
} | |
}); | |
}); | |
} | |
// Function to get the latest commit SHA from default branch | |
async function getLatestCommitSHA(owner, repo, token) { | |
const defaultBranch = await getDefaultBranch(owner, repo, token); | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`, | |
headers: { | |
'Authorization': token ? `token ${token}` : '', | |
'Accept': 'application/vnd.github.v3+json' | |
}, | |
onload: function(response) { | |
if (response.status >= 200 && response.status < 300) { | |
const data = JSON.parse(response.responseText); | |
resolve(data.object.sha); | |
} else { | |
reject(new Error(`Failed to fetch branch info: ${response.statusText}`)); | |
} | |
}, | |
onerror: function(error) { | |
reject(new Error(`Network error: ${error}`)); | |
} | |
}); | |
}); | |
} | |
// Function to create the tag via GitHub API | |
async function createTag(owner, repo, tagName, commitSHA, message, token) { | |
const refUrl = `https://api.github.com/repos/${owner}/${repo}/git/refs`; | |
const tagData = { | |
ref: `refs/tags/${tagName}`, | |
sha: commitSHA | |
}; | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: refUrl, | |
headers: { | |
'Authorization': `token ${token}`, | |
'Accept': 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json' | |
}, | |
data: JSON.stringify(tagData), | |
onload: function(response) { | |
if (response.status >= 200 && response.status < 300) { | |
resolve(JSON.parse(response.responseText)); | |
} else { | |
let errorMsg = `Failed to create tag: ${response.statusText}`; | |
try { | |
const errorData = JSON.parse(response.responseText); | |
if (errorData.message) { | |
errorMsg = errorData.message; | |
} | |
} catch (e) {} | |
reject(new Error(errorMsg)); | |
} | |
}, | |
onerror: function(error) { | |
reject(new Error(`Network error: ${error}`)); | |
} | |
}); | |
}); | |
} | |
// Function to create annotated tag | |
async function createAnnotatedTag(owner, repo, tagName, commitSHA, message, token) { | |
// First, get user info for tagger details | |
const userInfo = await new Promise((resolve) => { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: 'https://api.github.com/user', | |
headers: { | |
'Authorization': `token ${token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
}, | |
onload: function(response) { | |
if (response.status >= 200 && response.status < 300) { | |
resolve(JSON.parse(response.responseText)); | |
} else { | |
resolve({ login: 'freehuntx', email: '[email protected]' }); | |
} | |
} | |
}); | |
}); | |
const tagUrl = `https://api.github.com/repos/${owner}/${repo}/git/tags`; | |
const tagData = { | |
tag: tagName, | |
message: message, | |
object: commitSHA, | |
type: 'commit', | |
tagger: { | |
name: userInfo.name || userInfo.login || 'freehuntx', | |
email: userInfo.email || `${userInfo.login || 'freehuntx'}@users.noreply.github.com`, | |
date: new Date().toISOString() | |
} | |
}; | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: tagUrl, | |
headers: { | |
'Authorization': `token ${token}`, | |
'Accept': 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json' | |
}, | |
data: JSON.stringify(tagData), | |
onload: async function(response) { | |
if (response.status >= 200 && response.status < 300) { | |
const tagObject = JSON.parse(response.responseText); | |
// Now create the reference to this tag object | |
try { | |
await createTag(owner, repo, tagName, tagObject.sha, message, token); | |
resolve(tagObject); | |
} catch (error) { | |
reject(error); | |
} | |
} else { | |
let errorMsg = `Failed to create annotated tag: ${response.statusText}`; | |
try { | |
const errorData = JSON.parse(response.responseText); | |
if (errorData.message) { | |
errorMsg = errorData.message; | |
} | |
} catch (e) {} | |
reject(new Error(errorMsg)); | |
} | |
}, | |
onerror: function(error) { | |
reject(new Error(`Network error: ${error}`)); | |
} | |
}); | |
}); | |
} | |
// Function to show the tag creation modal | |
async function showTagCreatorModal(repoInfo) { | |
// Remove any existing modal | |
const existingModal = document.querySelector('.tag-creator-overlay'); | |
if (existingModal) { | |
existingModal.remove(); | |
} | |
// Try to get saved token | |
const savedToken = await getGitHubToken(); | |
// Create modal HTML with close button | |
const modalHTML = ` | |
<div class="tag-creator-overlay"> | |
<div class="tag-creator-modal"> | |
<button class="tag-creator-close" title="Close">×</button> | |
<h3 style="margin-top: 0;">Create New Tag</h3> | |
<form class="tag-creator-form"> | |
<div> | |
<label for="tag-name">Tag Name:</label> | |
<input type="text" id="tag-name" class="tag-creator-input" placeholder="v1.0.0" required> | |
</div> | |
<div> | |
<label for="commit-sha">Commit SHA:</label> | |
<input type="text" id="commit-sha" class="tag-creator-input" placeholder="Leave empty for latest commit"> | |
</div> | |
<div> | |
<label for="tag-message">Message (optional for annotated tag):</label> | |
<textarea id="tag-message" class="tag-creator-input tag-creator-textarea" placeholder="Tag message..."></textarea> | |
</div> | |
<div> | |
<label for="github-token">GitHub Personal Access Token:</label> | |
<input type="password" id="github-token" class="tag-creator-input" placeholder="ghp_..." required value="${savedToken}"> | |
<small style="color: var(--color-fg-muted);"> | |
Need a token? <a href="https://github.com/settings/tokens/new?scopes=repo" target="_blank">Create one here</a> with 'repo' scope. | |
</small> | |
</div> | |
<div> | |
<label> | |
<input type="checkbox" id="save-token" ${savedToken ? 'checked' : ''}> Save token for future use | |
</label> | |
</div> | |
<div class="tag-creator-error" style="display: none;"></div> | |
<div class="tag-creator-success" style="display: none;"></div> | |
<div class="tag-creator-actions"> | |
<button type="button" class="btn">Cancel</button> | |
<button type="submit" class="btn btn-primary">Create Tag</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
`; | |
// Add modal to page | |
document.body.insertAdjacentHTML('beforeend', modalHTML); | |
// Handle close actions | |
const closeModal = () => { | |
document.querySelector('.tag-creator-overlay').remove(); | |
}; | |
// Close on overlay click | |
document.querySelector('.tag-creator-overlay').addEventListener('click', (e) => { | |
if (e.target.classList.contains('tag-creator-overlay')) { | |
closeModal(); | |
} | |
}); | |
// Close on close button click | |
document.querySelector('.tag-creator-close').addEventListener('click', closeModal); | |
// Close on cancel button click | |
document.querySelector('.tag-creator-form button[type="button"]').addEventListener('click', closeModal); | |
// Handle form submission | |
const form = document.querySelector('.tag-creator-form'); | |
const errorDiv = document.querySelector('.tag-creator-error'); | |
const successDiv = document.querySelector('.tag-creator-success'); | |
form.addEventListener('submit', async (e) => { | |
e.preventDefault(); | |
const tagName = document.getElementById('tag-name').value.trim(); | |
const commitSHA = document.getElementById('commit-sha').value.trim(); | |
const message = document.getElementById('tag-message').value.trim(); | |
const token = document.getElementById('github-token').value.trim(); | |
const saveToken = document.getElementById('save-token').checked; | |
if (!tagName || !token) { | |
errorDiv.textContent = 'Tag name and token are required'; | |
errorDiv.style.display = 'block'; | |
successDiv.style.display = 'none'; | |
return; | |
} | |
// Save token if requested | |
if (saveToken) { | |
GM_setValue('github_token', token); | |
} else { | |
// Clear saved token if unchecked | |
GM_setValue('github_token', ''); | |
} | |
// Disable submit button | |
const submitBtn = form.querySelector('button[type="submit"]'); | |
submitBtn.disabled = true; | |
submitBtn.textContent = 'Creating...'; | |
try { | |
let sha = commitSHA; | |
// If no SHA provided, get the latest commit | |
if (!sha) { | |
sha = await getLatestCommitSHA(repoInfo.owner, repoInfo.repo, token); | |
} | |
// Create the tag | |
if (message) { | |
// Create annotated tag | |
await createAnnotatedTag( | |
repoInfo.owner, | |
repoInfo.repo, | |
tagName, | |
sha, | |
message, | |
token | |
); | |
} else { | |
// Create lightweight tag | |
await createTag(repoInfo.owner, repoInfo.repo, tagName, sha, '', token); | |
} | |
// Success | |
successDiv.textContent = `Tag "${tagName}" created successfully!`; | |
successDiv.style.display = 'block'; | |
errorDiv.style.display = 'none'; | |
// Reload page after 2 seconds | |
setTimeout(() => { | |
window.location.reload(); | |
}, 2000); | |
} catch (error) { | |
errorDiv.textContent = error.message; | |
errorDiv.style.display = 'block'; | |
successDiv.style.display = 'none'; | |
submitBtn.disabled = false; | |
submitBtn.textContent = 'Create Tag'; | |
} | |
}); | |
// Focus on tag name input | |
document.getElementById('tag-name').focus(); | |
} | |
// Function to add the "Create Tag" button to the tags page | |
function addCreateTagButton() { | |
const repoInfo = getRepoInfo(); | |
if (!repoInfo) { | |
console.log('[GitHub Tag Creator] No repo info found'); | |
return; | |
} | |
// Don't add multiple buttons | |
if (document.querySelector('.tag-creator-button')) { | |
console.log('[GitHub Tag Creator] Button already exists'); | |
return; | |
} | |
console.log('[GitHub Tag Creator] Looking for places to add button...'); | |
// Try multiple selectors to find the right place | |
const selectors = [ | |
'nav[aria-label="Releases and Tags"]', | |
'.subnav', | |
'.repository-content nav', | |
'.BorderGrid-row nav', | |
// Look for the nav containing the Tags link | |
'a[href*="/tags"]' | |
]; | |
let targetElement = null; | |
let placement = null; | |
for (const selector of selectors) { | |
const element = document.querySelector(selector); | |
if (element) { | |
console.log(`[GitHub Tag Creator] Found element with selector: ${selector}`, element); | |
// If we found the Tags link, get its parent nav | |
if (selector.includes('href*="/tags"')) { | |
targetElement = element.closest('nav'); | |
placement = 'after-nav'; | |
} else { | |
targetElement = element; | |
placement = 'after-nav'; | |
} | |
break; | |
} | |
} | |
if (!targetElement) { | |
console.log('[GitHub Tag Creator] No suitable location found for button'); | |
return; | |
} | |
console.log('[GitHub Tag Creator] Adding button to:', targetElement); | |
const button = document.createElement('button'); | |
button.className = 'tag-creator-inline-button tag-creator-button'; | |
button.textContent = 'Create Tag'; | |
button.onclick = () => showTagCreatorModal(repoInfo); | |
// Insert the button | |
if (placement === 'after-nav' && targetElement.tagName === 'NAV') { | |
// Create a wrapper div to hold the button | |
const wrapper = document.createElement('div'); | |
wrapper.style.display = 'inline-block'; | |
wrapper.style.verticalAlign = 'middle'; | |
wrapper.appendChild(button); | |
// Insert after the nav | |
targetElement.parentNode.insertBefore(wrapper, targetElement.nextSibling); | |
} else { | |
// Fallback: just append to the element | |
targetElement.appendChild(button); | |
} | |
console.log('[GitHub Tag Creator] Button added successfully'); | |
} | |
// Wait for the page to be ready | |
function waitForPageReady() { | |
// Check if we're on a tags page | |
if (!window.location.pathname.includes('/tags')) { | |
console.log('[GitHub Tag Creator] Not on tags page, skipping'); | |
return; | |
} | |
// Try to add button immediately | |
addCreateTagButton(); | |
// Also try after a delay | |
setTimeout(() => { | |
addCreateTagButton(); | |
}, 1000); | |
// And watch for dynamic changes | |
const observer = new MutationObserver((mutations) => { | |
// Only react to significant changes | |
for (const mutation of mutations) { | |
if (mutation.addedNodes.length > 0) { | |
// Check if nav elements were added | |
for (const node of mutation.addedNodes) { | |
if (node.nodeType === 1 && (node.tagName === 'NAV' || node.querySelector?.('nav'))) { | |
console.log('[GitHub Tag Creator] Nav element added, trying to add button'); | |
setTimeout(addCreateTagButton, 100); | |
break; | |
} | |
} | |
} | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
} | |
// Start when DOM is ready | |
if (document.readyState === 'loading') { | |
document.addEventListener('DOMContentLoaded', waitForPageReady); | |
} else { | |
waitForPageReady(); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment