Skip to content

Instantly share code, notes, and snippets.

@freehuntx
Created July 11, 2025 13:23
Show Gist options
  • Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.
Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.
Github Tag Creator
// ==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">&times;</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