Skip to content

Instantly share code, notes, and snippets.

@freehuntx
Created July 11, 2025 13:23
Show Gist options
  • Select an option

  • Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.

Select an option

Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.

Revisions

  1. freehuntx created this gist Jul 11, 2025.
    641 changes: 641 additions & 0 deletions github-tag-creator.users.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,641 @@
    // ==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: 'freehuntx@users.noreply.github.com' });
    }
    }
    });
    });

    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();
    }
    })();