API de merge asíncrono de GitHub: cómo fusionar pull requests apilados desde tu bot

La API de merge asíncrono de GitHub (async merge API) es GA desde el 1 de octubre de 2026 y es la única forma programática de hacer merge de pull requests apilados (stacked pull requests). Tu automatización envía un PUT a merge-async, recibe un UUID y consulta el resultado: el merge corre en segundo plano, no dentro de una sola petición. Si mantienes un bot de merge, un script de releases o un agente que integra PRs, esto cambia qué significa “terminado”.

GitHub presenta la API asíncrona como la vía recomendada para los merges programáticos, en lugar del endpoint REST síncrono y de las mutaciones de GraphQL. Recomendado no es lo mismo que obsoleto: al momento de publicar esta nota, el endpoint síncrono PUT …/merge seguía documentado y funcionando. Pero el caso de uso de los stacked PRs solo existe en el nuevo. En palabras del propio GitHub: “It’s also the only merge API that supports stacked pull requests.”

¿Qué son los pull requests apilados (stacked pull requests)?

Los pull requests apilados son una cadena ordenada de PRs pequeños en el mismo repositorio, donde cada uno apunta a la rama del PR que tiene debajo y toda la cadena termina en una única rama base, normalmente main. En lugar de un PR de 2.000 líneas que nadie quiere revisar, abres tres o cuatro capas enfocadas que distintos revisores pueden trabajar en paralelo.

GitHub lanzó los stacks nativos en public preview el 30 de julio de 2026. Puedes crearlos en github.com, desde GitHub Mobile o con la extensión de CLI github/gh-stack (el comando gh stack), que también se distribuye como skill para agentes, de modo que los coding agents pueden armar stacks por su cuenta. Cuando haces merge de una capa, todas las capas sin integrar que están debajo entran en la misma operación; los PRs de arriba siguen abiertos y se re-apuntan solos.

Dos advertencias con fecha:

  • La async merge API es GA; los stacked PRs en sí seguían en public preview al momento de publicar esta nota.
  • En su anuncio de julio, GitHub indicó que el soporte de merge queue para stacks se iría habilitando de forma progresiva en las semanas siguientes. Verifica que tu repositorio lo tenga antes de depender de merge_queue para un stack.

¿Qué cambia la API de merge asíncrono de GitHub?

Divide el merge en dos llamadas: una para pedirlo y otra para conocer el resultado.

Síncrono (PUT …/pulls/{n}/merge) Asíncrono (PUT …/pulls/{n}/merge-async)
Cuándo se ejecuta el merge Dentro de la petición En segundo plano
Qué te devuelve merged: true/false y un SHA Un status y un UUID para consultar
Stacked PRs No soportados Soportados (también integra las capas inferiores)
Merge queue — merge_action: merge_queue
Merges complejos Pueden agotar el tiempo de espera Diseñado para evitar timeouts; algunos errores se pueden reintentar

El segundo endpoint es GET /repos/{owner}/{repo}/pulls/{pull_number}/merge-async/{uuid}, que devuelve el resultado actual de una petición que ya hiciste.

¿Cómo hacer merge de un pull request con la API de GitHub?

Envías un PUT con los mismos campos que ya conoces del endpoint síncrono, más uno nuevo:

  • commit_title y commit_message
  • sha: el SHA del head con el que debe coincidir el PR
  • merge_method: merge, squash o rebase
  • merge_action (el nuevo): default, direct_merge o merge_queue. Con default, GitHub elige la vía más apropiada.
curl -L -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  https://api.github.com/repos/OWNER/REPO/pulls/PULL_NUMBER/merge-async \
  -d '{"sha":"HEAD_SHA","merge_method":"squash","merge_action":"default"}'

El código HTTP de esa primera llamada ya te dice mucho:

  • 202: la petición fue aceptada y el merge correrá en segundo plano. Guarda details.uuid.
  • 200: el PR ya estaba integrado o ya está en una merge queue.
  • 409: ya existe una petición de merge asíncrono para este PR. GitHub te devuelve el UUID de esa petición, y sus opciones pueden ser distintas de las que acabas de enviar.
  • 400: el PR no se puede integrar ahora, por ejemplo porque está cerrado o sigue en borrador.

Los permisos exactos del token están en la referencia oficial de la API; revísalos ahí antes de dar acceso a tu bot.

¿Qué significan pending, merged, enqueued y failed?

Cada respuesta trae un status con uno de cuatro valores, y un bot correcto trata cada uno como un resultado distinto:

status Qué significa Qué debe hacer tu bot
pending Aceptado, sin terminar Seguir consultando con backoff
merged Listo; la respuesta incluye el SHA del commit de merge Registrar el SHA y seguir
enqueued Entregado a la merge queue, todavía no integrado Dejar de consultar el registro asíncrono y verificar el estado final del PR por separado
failed El merge no ocurrió; details.message explica por qué Mostrar el mensaje a una persona o a tu política de reintentos

La trampa es enqueued. Parece un éxito, y un bot que anuncia “merged” en ese punto se equivoca cada vez que la cola rechaza el PR más tarde. Para confirmar el merge final, usa el endpoint existente GET /repos/{owner}/{repo}/pulls/{pull_number}/merge, que responde 204 cuando el PR está integrado y 404 mientras no lo esté.

¿Cómo migrar tu bot de merge a la API asíncrona?

Reemplaza “una llamada y revisar merged” por una pequeña máquina de estados. Este esquema está construido solo a partir del contrato de petición y respuesta documentado y no se ejecutó contra un repositorio real: pruébalo primero en un repositorio desechable.

#!/usr/bin/env bash
set -euo pipefail
API="https://api.github.com/repos/$OWNER/$REPO/pulls/$PR"
H=(-H "Accept: application/vnd.github+json"
   -H "Authorization: Bearer $GITHUB_TOKEN"
   -H "X-GitHub-Api-Version: 2026-03-10")

# 1. Pedir el merge, fijado al SHA del head que revisaste
out=$(curl -sS -L -X PUT "${H[@]}" -w '\n%{http_code}' "$API/merge-async" \
  -d "{\"sha\":\"$HEAD_SHA\",\"merge_method\":\"squash\",\"merge_action\":\"default\"}")
code=$(tail -n1 <<<"$out"); body=$(sed '$d' <<<"$out")

case "$code" in
  400) echo "No se puede integrar: $(jq -r '.details.message' <<<"$body")"; exit 1 ;;
  200) echo "Ya resuelto: $(jq -r '.status' <<<"$body")"; exit 0 ;;
  409) echo "Ya existe una petición; revisa sus opciones:"; jq '.details' <<<"$body" ;;
esac
uuid=$(jq -r '.details.uuid' <<<"$body")

# 2. Consultar hasta llegar a un estado terminal
delay=5
while true; do
  out=$(curl -sS -L "${H[@]}" -w '\n%{http_code}' "$API/merge-async/$uuid")
  code=$(tail -n1 <<<"$out"); body=$(sed '$d' <<<"$out")
  [ "$code" = "404" ] && { echo "Resultado expirado o no encontrado"; exit 1; }
  case "$(jq -r '.status' <<<"$body")" in
    merged)   echo "Integrado: $(jq -r '.details.sha' <<<"$body")"; exit 0 ;;
    enqueued) echo "En la merge queue; verifica el estado final con GET $API/merge"; exit 0 ;;
    failed)   echo "Falló: $(jq -r '.details.message' <<<"$body")"; exit 1 ;;
    pending)  sleep "$delay"; delay=$(( delay < 60 ? delay * 2 : 60 )) ;;
  esac
done

GitHub no documenta un intervalo de consulta; el backoff exponencial con tope de 60 segundos es un valor razonable, no un requisito.

Si ya integras PRs desde la terminal, este flujo encaja con lo que vimos en Copilot CLI estrena tabs para issues, PRs y gists sin salir de la terminal.

¿Qué errores hay que evitar con la async merge API?

  • Un 202 no es una aprobación. Al momento de la petición, GitHub solo hace verificaciones básicas del estado del PR. Las reglas de protección de ramas y las reglas del repositorio se evalúan cuando el merge realmente se ejecuta, así que una petición aceptada puede terminar en failed.
  • Envía siempre sha. Si lo omites, GitHub usa el head al momento de la petición, y el merge se cancela si alguien hace push entre medio. Enviar el SHA que revisaste deja esa regla explícita, que es lo que quieres en una automatización.
  • Los resultados expiran. Según la referencia de la API al momento de publicar esta nota, un resultado asíncrono se conserva 24 horas desde su última actualización; después, su UUID devuelve 404. No uses el UUID como registro de auditoría permanente: guarda el resultado tú mismo.
  • Los stacks se integran hacia abajo. Pedir el merge de la capa 3 integra las capas 1, 2 y 3. Asegúrate de que la idea de tu bot sobre “qué PR estoy integrando” coincida con eso.
  • Saltarse reglas no es el comportamiento por defecto. El anuncio menciona que puedes omitir reglas de forma opcional si tienes permiso. Mantén tu bot en la ruta protegida y reserva esa excepción para una decisión deliberada a cargo de una persona.

¿Conviene migrar ya?

Sí, si tu automatización integra PRs grandes o en repositorios con mucha actividad, o si tu equipo empieza a usar stacks, sobre todo generados por agentes. La migración son dos endpoints y cuatro estados. Los equipos que nunca hacen merge programático no pierden nada esperando; los que sí lo hacen van a querer el manejo explícito de estados terminales de todos modos, porque es lo que hace que el “merged” de un bot signifique realmente integrado.