Add GitHub Releases Tab

Add Releases tab after Code tab on GitHub repos

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Add GitHub Releases Tab
// @namespace    https://github.com/nvbangg/nvbangg-projects
// @version      1.7
// @description  Add Releases tab after Code tab on GitHub repos
// @author       nvbangg (https://github.com/nvbangg)
// @copyright    Copyright (c) 2026 nvbangg (github.com/nvbangg)
// @match        https://github.com/*/*
// @icon         https://github.com/favicon.ico
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(() => {
  "use strict";

  const TAB_ID = "releases-tab-link";

  // Returns true when the current page is the releases section.
  function isReleasesPage() {
    return /\/releases(\/|$)/.test(location.pathname);
  }

  function getCodeAnchor() {
    return document.querySelector(
      'nav[aria-label="Repository"] a[data-tab-item="code"], nav[aria-label="Repository"] #code-tab'
    );
  }

  // Sets or removes aria-current="page" on our tab to match GitHub's
  // own active-tab convention (the only visual difference between tabs).
  // When Releases is active, also strip aria-current from the Code tab so
  // only one tab appears highlighted at a time.
  function updateActiveState() {
    const anchor     = document.getElementById(TAB_ID);
    const codeAnchor = getCodeAnchor();
    if (!anchor) return;

    if (isReleasesPage()) {
      anchor.setAttribute("aria-current", "page");
      // Code tab must not appear active while we're on the releases page.
      if (codeAnchor) codeAnchor.removeAttribute("aria-current");
    } else {
      anchor.removeAttribute("aria-current");
      // Let GitHub's own rendering decide whether Code is active again;
      // we only restore it if it was previously cleared by us and GitHub
      // hasn't already re-set it via Turbo.
      if (codeAnchor && !codeAnchor.hasAttribute("aria-current")) {
        const onCodePage = /^\/[^/]+\/[^/]+(\/tree\/|\/blob\/|\/)?$/.test(location.pathname);
        if (onCodePage) codeAnchor.setAttribute("aria-current", "page");
      }
    }
  }

  function insertReleasesTab() {
    const nav = document.querySelector('nav[aria-label="Repository"]');
    if (!nav || nav.querySelector(`#${TAB_ID}`)) {
      // Tab already exists — just sync the active state.
      updateActiveState();
      return;
    }

    // Bail if releases tab already exists natively.
    if (nav.querySelector('a[href$="/releases"]')) return;

    const codeAnchor = nav.querySelector('a[data-tab-item="code"], #code-tab');
    if (!codeAnchor) return;

    const urlParts = location.pathname.split("/");
    if (urlParts.length < 3) return;
    const releasesHref = `/${urlParts[1]}/${urlParts[2]}/releases`;

    // Read class names live from the Code tab so we match whatever
    // GitHub is currently using — no hardcoded strings that can go stale.
    const liClass = codeAnchor.closest("li")?.className ?? "";
    const aClass  = codeAnchor.className ?? "";

    // Build the SVG using the same attributes as every other tab icon.
    const existingSvg = codeAnchor.querySelector("svg");
    const svgAttrs = existingSvg
      ? `data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-tag" viewBox="0 0 16 16" width="${existingSvg.getAttribute("width") || 16}" height="${existingSvg.getAttribute("height") || 16}" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"`
      : `aria-hidden="true" focusable="false" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"`;

    // aria-current="page" is set here immediately if we are already on the
    // releases page (e.g. hard reload directly onto a releases URL).
    const ariaCurrent = isReleasesPage() ? 'aria-current="page"' : "";

    const html = `
      <li class="${liClass}">
        <a id="${TAB_ID}"
           href="${releasesHref}"
           class="${aClass}"
           ${ariaCurrent}
           data-tab-item="releases"
           data-turbo-frame="repo-content-turbo-frame"
           data-discover="true"
           aria-label="Releases">
          <span data-component="icon">
            <svg ${svgAttrs}>
              <path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
            </svg>
          </span>
          <span data-component="text" data-content="Releases">Releases</span>
        </a>
      </li>`;

    const codeLi = codeAnchor.closest("li") ?? codeAnchor;
    codeLi.insertAdjacentHTML("afterend", html);

    // Guard against GitHub's overflow system hiding our tab.
    const injectedLi     = document.getElementById(TAB_ID)?.closest("li");
    const injectedAnchor = document.getElementById(TAB_ID);
    if (!injectedLi || !injectedAnchor) return;

    new MutationObserver(() => {
      if (injectedLi.getAttribute("aria-hidden") === "true")
        injectedLi.removeAttribute("aria-hidden");
      if (injectedAnchor.getAttribute("tabindex") === "-1")
        injectedAnchor.removeAttribute("tabindex");
    }).observe(injectedLi, { attributes: true });
  }

  // ── SPA navigation ────────────────────────────────────────────────────────
  // insertReleasesTab re-inserts the tab after Turbo swaps the nav out,
  // and also calls updateActiveState() when the tab already exists.
  // popstate covers back/forward without a Turbo event.
  document.addEventListener("turbo:load",      insertReleasesTab);
  document.addEventListener("turbolinks:load", insertReleasesTab);
  window.addEventListener("popstate",          insertReleasesTab);

  insertReleasesTab();
  new MutationObserver(insertReleasesTab).observe(document.documentElement, {
    childList: true,
    subtree: true,
  });
})();