2 puntos por GN⁺ 2026-01-15 | 1 comentarios | Compartir por WhatsApp
  • Mientras usaban la API de GitHub, surgió un problema en la función para generar enlaces de comentarios de PR: los enlaces no funcionaban por una discrepancia de ID
  • Tras investigar, descubrieron que GitHub usa en paralelo dos sistemas de ID: el node ID de GraphQL y el database ID de la API REST
  • Al decodificar en base64 el node ID, confirmaron que los 32 bits inferiores incluyen el database ID, por lo que se puede convertir con una simple operación de bitmask
  • Un análisis adicional reveló que GitHub mezcla un nuevo formato de ID basado en MessagePack con un formato legado basado en cadenas
  • Esta estructura muestra la dualidad del sistema interno de identificación de objetos de GitHub, y que los desarrolladores deben tener cuidado al integrar APIs

Descubrimiento del sistema dual de ID de GitHub

  • Durante el desarrollo de una función del tool de revisión de código con IA de Greptile, apareció un problema en el que los enlaces a comentarios de PR de GitHub no funcionaban
    • Guardaban el ID del comentario y lo conectaban a la URL, pero al hacer clic no se abría la página de GitHub
  • Al revisar la documentación de GitHub, encontraron que el node ID de la API GraphQL y el database ID de la API REST existen como sistemas distintos
    • Ejemplo de node ID: PRRC_kwDOL4aMSs6Tkzl8
    • Ejemplo de database ID: 2475899260
  • El node ID es una cadena codificada en base64 para identificar objetos globalmente dentro de GitHub, mientras que el database ID se usa como identificador entero en URL

Análisis de la relación entre node ID y database ID

  • Al comparar los node ID y database ID de varios comentarios de PR, confirmaron que ambos valores aumentan con un patrón constante
  • Al decodificar la parte base64 del node ID, obtuvieron un entero de 96 bits, y los 32 bits inferiores coincidían con el database ID
    • Ejemplo: PRRC_kwDOL4aMSs6Tkzl8 → 32 bits inferiores = 2475899260
  • Es posible extraer el database ID con una operación simple de bitmask
    • La conversión se hace con una operación como decoded & ((1 << 32) - 1)

El formato legado de ID de GitHub

  • Al decodificar el node ID de un repositorio antiguo (torvalds/linux), apareció una cadena con un formato distinto
    • Ejemplo: MDEwOlJlcG9zaXRvcnkyMzI1Mjk4010:Repository2325298
  • Este formato sigue la estructura [número de tipo de objeto]:[nombre del objeto][Database ID], y es un identificador explícito basado en cadenas
  • En el caso de objetos árbol, aparece como 04:Tree2325298:7201bfb9..., incluyendo el ID del repositorio y el valor SHA
  • GitHub usa en paralelo el formato legado y el nuevo formato, y el formato cambia según el tipo de objeto y el momento en que fue creado

Estructura del nuevo formato de node ID

  • La guía de migración de GraphQL de GitHub indica que el node ID debe tratarse como una cadena opaca, pero sí existe una estructura interna
  • Tras decodificar en base64 y desempaquetar con MessagePack, aparece un arreglo de datos
    • Ejemplo: [0, 47954445, 2475899260]
  • Composición del arreglo
    • Primer elemento (0): se estima que es un identificador de versión
    • Segundo elemento (47954445): el database ID del repositorio
    • Tercer elemento (2475899260): el database ID del objeto
  • La longitud del arreglo cambia según el tipo de objeto; los commits incluyen el SHA y los repositorios solo incluyen dos elementos

Uso práctico y conclusión

  • Ejemplo de código Python para extraer el database ID de un node ID nuevo
    import base64, msgpack
    def node_id_to_database_id(node_id):
        prefix, encoded = node_id.split('_')
        packed = base64.b64decode(encoded)
        array = msgpack.unpackb(packed)
        return array[-1]
    
  • Con este método, se puede extraer directamente el database ID de un comentario de PR y resolver el problema de enlaces URL
  • Actualmente GitHub mantiene al mismo tiempo el nuevo sistema de ID basado en MessagePack y el sistema legado basado en cadenas
  • Esta estructura muestra el proceso de transición interna de GitHub y sus esfuerzos por mantener compatibilidad, por lo que quienes usan la API deben prestar atención a las diferencias entre formatos de ID

1 comentarios

 
GN⁺ 2026-01-15
Comentarios en Hacker News
  • El GitHub global node ID más reciente se puede forzar mediante el header 'X-Github-Next-Global-ID'
    El ID está compuesto por el prefijo de tipo del objeto y un payload msgpack codificado en base64
    Por ejemplo, mi ID de usuario "U_kgDOAAhEkg" se decodifica como [0, 541842], lo que coincide con el databaseId de la API REST
    Pero es mejor no depender de este tipo de implementación interna y consultar directamente el campo databaseId en la API GraphQL
    Documentación relacionada: guía de migración de GraphQL global node IDs, mi información de usuario de GitHub, ejemplo de decodificación en CyberChef, implementación de GitHub ETag

  • Decodificarlo de esta forma me parece frágil
    Los global node IDs de GraphQL, en principio, deberían ser opacos (opaque)
    Varios tipos de GitHub (como PullRequest) exponen el campo databaseId, así que lo correcto es usar eso
    La mayoría de las APIs GraphQL codifican en base64 el nombre del tipo y el ID de la BD, pero no hay garantía de que esa convención se mantenga para siempre
    Referencia: documentación del objeto PullRequest, especificación de GraphQL global IDs

    • Los tipos GraphQL de GitHub tienen campos como permalink, url y la interfaz UniformResourceLocatable, así que no hace falta construir URLs manualmente
    • Este tipo de estructura interna muy probablemente se rompa con el tiempo
      Para eso existe que la API entregue permalink. Los IDs o patrones de links pueden cambiar en cualquier momento
    • Si quieres meter metadatos dentro de un identificador, lo ideal es cifrarlos para que el usuario no dependa de la estructura interna
      Este enfoque también se usa mucho en tokens de paginación
  • IDs como 010:Repository2325298 tienen una estructura clara
    010 es el enum del tipo, Repository es el nombre y 2325298 es el ID de la BD
    O sea, es una forma de prefijo de longitud (length prefix). Repository tiene 10 caracteres, Tree tiene 4

    • Me recuerda al protocolo BitTorrent
    • Casi parece un URN
  • Opus 4.5 conoce este truco para decodificar IDs de GitHub y escribe automáticamente el código para hacerlo

  • Lo que descubrió el autor es técnicamente correcto, pero no está documentado ni soportado
    GitHub ya ha cambiado silenciosamente la estructura interna de los node IDs en el pasado
    Si agregan campos al arreglo MessagePack, cambian la codificación, la cifran o la cambian por algo basado en UUID,
    cualquier sistema que dependa de esa estructura interna se rompe de inmediato

  • Los identificadores de GitHub que guardo explícitamente son más bien claves URL inmutables (número de issue/PR o hash de commit)
    El ID de comentario simplemente lo meto dentro de un blob JSON
    No hace falta intentar normalizarlo todo. JSON es suficientemente rápido
    A menos que hagas consultas cruzadas por comentario, casi nunca se va a notar como un problema de rendimiento

    • Pero las URLs de issue/PR no son inmutables
      Si el repositorio cambia de nombre o se mueve a otra organización, la URL puede cambiar
  • En la vieja API v3 no había IDs, así que si alguien cambiaba su nombre de usuario o de repositorio era difícil seguirle el rastro
    Por eso implementé yo mismo un sistema de gestión de ownership por equipos
    Como el provider de Terraform no era muy bueno, al hacer offboarding pasaban seguido cosas como “se fue la única persona que era admin”
    Todos los repositorios pertenecen a un equipo y los permisos de acceso también se asignan solo a nivel de equipo

    • Pensar en términos de “no le das acceso a un usuario, le das permisos a un equipo y el usuario forma parte de ese equipo” es mucho más eficiente
      Este tipo de control de acceso basado en equipos sirve no solo para GitHub, sino también para otros sistemas
  • Es un caso típico de Hyrum’s Law — cuando la gente empieza a depender de comportamientos no documentados, tarde o temprano algo se rompe

  • En diseño de bases de datos, normalmente se entrega hacia afuera una clave natural opaca y por dentro se usa un ID entero incremental

    • Hay dos razones para esto
      1. para no exponer cuántos objetos existen
      2. para evitar que alguien recorra todos los objetos simplemente incrementando IDs
        Pero si usas IDs compuestos, estos problemas se reducen.
        Por ejemplo, si dentro del ID de un repositorio va incluido el ID del objeto, al incrementar el ID solo se exploran objetos dentro de ese mismo repositorio
        Si además le mezclas entropía o timestamps, se vuelve casi imposible de explotar
    • Pero las claves naturales pueden cambiar
      Por eso es más seguro exponer una surrogate key sin significado
      Por ejemplo, YouTube puede usar internamente un número de índice, pero hacia afuera entrega IDs en forma de código sin significado
  • Ahora se entiende por qué el equipo de GitHub ha ampliado tanto el soporte de Rails para sharded/multi-database en los últimos años