12 min de lecturaIntegraciones

Acortador de URL para GitHub Actions: enlaces cortos desde tu CI

Crea y actualiza enlaces cortos desde GitHub Actions: la clave de API como secreto cifrado, un flujo de trabajo funcional, upserts idempotentes y claves con los privilegios mínimos.

Marius Voß
DevRel · edge infra
Un paso de acortador de URL para GitHub Actions representado como una canalización: una ejecución del flujo de trabajo lee un secreto cifrado, busca el slug y después actualiza el enlace corto existente o crea uno nuevo

Un paso de acortador de URL para GitHub Actions son unas pocas líneas de shell: lee una clave de API de un secreto cifrado, comprueba si el slug ya existe y después actualiza su destino o lo crea. Ejecútalo en cada push y el mismo enlace corto siempre apuntará a la vista previa, compilación de documentación o artefacto más reciente. No hace falta ninguna acción del Marketplace. curl y jq vienen incluidos en todos los ejecutores Ubuntu alojados por GitHub.

Esa es toda la respuesta, y el resto de esta entrada es la parte que hace que funcione bien a lo largo de varios cientos de ejecuciones del flujo de trabajo. Quienes buscan cómo crear un enlace corto en GitHub Actions normalmente llegan hasta una única solicitud POST, que funciona hasta el segundo push a la misma solicitud de incorporación de cambios, cuando la llamada de creación devuelve un conflicto y el trabajo falla. La solución es tratar el paso como un upsert, no como una creación. El otro problema es la propia clave, que suele ser el token personal de alguien con mucho más alcance del que necesita un trabajo de CI.

Si ya gestionas los enlaces como código, enlaces cortos como Terraform es la versión declarativa de la misma idea y encaja mejor con enlaces que cambian según el calendario de una persona. Un paso del flujo de trabajo gana cuando el destino solo existe una vez terminada una compilación.

Cómo funciona un paso de acortador de URL para GitHub Actions

Cada ejecución hace las mismas tres cosas contra la API REST en https://api.elido.app/v1. Enumera los enlaces del espacio de trabajo filtrados por el slug. Envía un PATCH al enlace que encuentra, o un POST si no encuentra ninguno. Escribe la URL corta en $GITHUB_OUTPUT para que pueda usarla el paso siguiente.

¿Por qué no dejar que el acortador genere un slug aleatorio? Porque después no podrás volver a encontrar el enlace. El slug debe proceder de algo que el flujo de trabajo ya conozca en cada ejecución: el número de la solicitud de incorporación de cambios, el nombre de la rama o una palabra fija como latest. Un slug estable significa una URL corta estable, y ese es todo su valor para quienes revisan y la guardan en sus marcadores o para responsables de producto que la pegan en un ticket.

Cómo se ejecuta un paso de acortador de URL para GitHub Actions: el flujo de trabajo lee la clave de API de un secreto cifrado, enumera los enlaces por slug, envía PATCH cuando el slug existe o POST cuando no existe y después escribe la URL corta en la salida de un paso

Guardar la clave de API como secreto cifrado

Crea la clave en el panel, cópiala una vez (se muestra exactamente una vez y empieza por elido_) y guárdala en Settings, después Secrets and variables y después Actions, con el nombre ELIDO_API_KEY. La guía de GitHub sobre usar secretos en GitHub Actions cubre los niveles de repositorio, entorno y organización. Para cualquier cosa que despliegue, yo la pondría en un entorno con revisores obligatorios para que una rama accidental no pueda usarla.

Hay tres valores que no son secretos y deben estar en variables de configuración, donde puedas consultarlos: ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID y ELIDO_HOST. El ID del dominio importa porque la llamada de creación lo requiere. Puedes consultarlo una vez con GET /v1/workspaces/{workspace_id}/domains, que devuelve el id y el hostname de cada dominio.

Asigna el secreto al único paso que llama a la API, no al trabajo completo. Un env a nivel de paso evita que llegue a cualquier otro proceso que inicie el trabajo, incluidas las acciones de terceros que no has escrito tú.

Un flujo de trabajo funcional para acortar una URL en cada solicitud de incorporación de cambios

Este es el archivo completo para el motivo más habitual para acortar una URL en un flujo de trabajo de GitHub: un enlace de vista previa por solicitud de incorporación de cambios. Déjalo en .github/workflows/preview-link.yml y cambia la línea DEST por el lugar donde se publiquen tus vistas previas.

name: Preview short link

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: preview-link-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  short-link:
    # Forks get no secrets; skip them instead of failing.
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    env:
      API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
      DOMAIN_ID: ${{ vars.ELIDO_DOMAIN_ID }}
      HOST: ${{ vars.ELIDO_HOST }}
      SLUG: pr-${{ github.event.pull_request.number }}-myapp
      DEST: https://pr-${{ github.event.pull_request.number }}.preview.example.com
    steps:
      - name: Create or update the short link
        id: link
        env:
          ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
        run: |
          set -euo pipefail
          auth=(-H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json")

          # 1. Find an existing link with exactly this slug on this domain.
          link_id=$(curl -sS --fail-with-body "${auth[@]}" "$API/links?q=$SLUG&limit=100" \
            | jq -r --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1)

          if [ -n "$link_id" ]; then
            # 2a. Found: point it at the new destination.
            curl -sS --fail-with-body -X PATCH "${auth[@]}" "$API/links/$link_id" \
              -d "$(jq -n --arg u "$DEST" '{destination_url: $u, status: "active"}')" > /dev/null
          else
            # 2b. Not found: create it. The key makes curl's retries safe.
            curl -sS --fail-with-body --retry 3 -X POST "${auth[@]}" "$API/links" \
              -H "Idempotency-Key: $GITHUB_REPOSITORY-$SLUG-$GITHUB_RUN_ID" \
              -d "$(jq -n --arg u "$DEST" --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                  '{domain_id: $d, slug: $s, destination_url: $u, tags: ["ci", "preview"]}')" > /dev/null
          fi

          echo "url=https://$HOST/$SLUG" >> "$GITHUB_OUTPUT"

      - name: Comment once, when the pull request opens
        if: github.event.action == 'opened'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh pr comment "${{ github.event.pull_request.number }}" \
            --repo "${{ github.repository }}" \
            --body "Preview: ${{ steps.link.outputs.url }}"

Algunas líneas merecen un comentario. --fail-with-body convierte un 4xx o 5xx en un paso fallido y, al mismo tiempo, imprime el cuerpo del error, algo que curl -s no hace; sale con 0 ante un 401 y el trabajo continúa alegremente. Los cuerpos de las solicitudes se construyen con jq -n en lugar de interpolación de cadenas, así que un destino que contenga comillas o un ampersand no puede romper el JSON. Además, el paso del comentario solo se ejecuta en opened. Como la URL corta nunca cambia, un comentario sigue siendo correcto durante toda la vida de la solicitud de incorporación de cambios y nadie recibe una notificación en cada push.

El bloque concurrency no es decorativo. De lo contrario, dos pushes rápidos iniciarían dos ejecuciones que verían «todavía no hay enlace» y ambas intentarían crear uno. La documentación de GitHub sobre controlar la simultaneidad de los flujos de trabajo explica la agrupación; aquí se cancela la ejecución más antigua y la carrera nunca ocurre.

Idempotencia: actualiza el enlace corto, no lo dupliques

La palabra idempotente oculta dos fallos distintos, y el flujo de trabajo los gestiona por separado. El primero es la repetición: un segundo push, un «Re-run jobs» manual o una solicitud de incorporación de cambios reabierta. Para eso sirve la rama de búsqueda seguida de PATCH. El segundo es la solicitud reintentada, en la que curl envía el POST, la red se cae antes de que llegue la respuesta y curl lo envía de nuevo. El encabezado Idempotency-Key cubre ese caso. Elido guarda durante 24 horas la primera respuesta correcta asociada a la clave y la reproduce para un retry coincidente, por lo que la creación se ejecuta una sola vez; la mecánica completa está en límites de frecuencia, reintentos e idempotencia.

Flujo de decisión para crear un enlace corto en GitHub Actions sin duplicados: una coincidencia exacta del slug en tu espacio de trabajo lleva a PATCH, ninguna coincidencia lleva a POST y un 409 significa que otro espacio de trabajo de un dominio compartido ya posee el slug

Las colisiones de slugs son la parte que la gente pasa por alto. Los slugs son únicos por dominio de redirección, no por espacio de trabajo. En un dominio compartido, todos los demás clientes de Elido están en el mismo espacio de nombres, y es probable que alguien ya haya reclamado un slug tan sencillo como pr-12. La búsqueda no verá su enlace (solo enumera el de tu espacio de trabajo), así que el POST se envía y vuelve con 409 slug already exists for this domain. Hay dos soluciones: añade una palabra de proyecto al slug o coloca los enlaces de CI en tu propio dominio personalizado, donde el espacio de nombres es solo tuyo. Yo haría ambas cosas.

Hay una segunda razón, más sutil, por la que el nombre del repositorio aparece al final del slug y no al principio. El parámetro q busca coincidencias parciales en el slug, el destino y el título. Con myapp-pr-1, la búsqueda también devuelve myapp-pr-10 a myapp-pr-199, que son más de los 100 resultados que devuelve una sola página, y el enlace que querías, al ser el más antiguo, desaparece del final. pr-1-myapp no coincide con nada salvo consigo mismo. Un detalle pequeño, y me llevó una tarde vergonzosamente larga de «por qué PR #1 sigue recibiendo un 409» descubrirlo.

Tres trabajos de enlaces cortos de CI que merece la pena automatizar

El flujo de trabajo de vistas previas es un patrón. Cambia el activador, el slug y el destino, y el mismo paso cubre la mayor parte de lo que los equipos automatizan de verdad. (Las notas de versión son un tema propio, cubierto en acortar enlaces en las notas de versión.)

Caso de usoActivadorSlugQué hace el paso
Despliegue de vista previa por PRpull_requestpr-42-myappUpsert en cada push, eliminar al cerrar
Despliegue de documentaciónpush a maindocs-myappPATCH a la URL de la documentación recién desplegada
Última compilaciónpush a main o una etiquetalatest-myappPATCH mediante un ID de enlace guardado, sin búsqueda
Artefacto nocturnoschedulenightly-myappPATCH a la URL del artefacto más reciente

El caso de la última compilación es el más sencillo. Crea el enlace manualmente una vez, guarda su ID numérico como variable y el trabajo se reduce a una sola llamada:

- name: Point the latest link at this build
  env:
    ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
    API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
    DEST: https://builds.example.com/${{ github.sha }}/
  run: |
    curl -sS --fail-with-body -X PATCH \
      -H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json" \
      "$API/links/${{ vars.ELIDO_LATEST_LINK_ID }}" \
      -d "$(jq -n --arg u "$DEST" '{destination_url: $u}')"

Mantén estos enlaces cambiantes en una 302, que es la opción predeterminada cuando no estableces redirect_status. Una 301 indica a los navegadores que pueden almacenar la respuesta en caché, y quienes hicieron clic ayer seguirán llegando a la compilación de ayer; nuestro artículo sobre redirecciones 301 frente a 302 ofrece la versión larga.

Para los enlaces de vista previa, limpia al cerrar la solicitud de incorporación de cambios. Añade closed a los tipos de activador, reutiliza la búsqueda y envía DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Un slug eliminado queda libre para reutilizarlo. Si prefieres conservar el historial de clics, usa PATCH con {"status": "disabled"}; el upsert anterior establece status: "active" en cada ejecución, así que una solicitud de incorporación de cambios reabierta recupera su enlace.

¿Listo para probarlo en un repositorio? Crea un espacio de trabajo gratuito, crea una clave y el flujo de trabajo anterior funcionará tal cual una vez configuradas las tres variables.

Claves de API con privilegios mínimos para CI

La clave de un secreto de CI debe poder hacer exactamente lo que hace el flujo de trabajo, y nada más. Es más difícil de lo que parece por la forma en que funcionan las claves personales.

Una clave de API personal se autentica como la persona que la creó. La clave puede hacer todo lo que esa persona puede hacer y, cuando esa persona deja la empresa, la clave se va con su cuenta. Para CI yo usaría en su lugar un usuario de máquina: una cuenta de servicio que pertenece a un único espacio de trabajo, tiene su propio rol y dispone de tokens que solo puede crear o revocar un administrador humano que haya iniciado sesión. Créalo en Machine users, dentro del panel, con el rol de editor, que es el rol integrado más bajo que puede crear, editar y eliminar enlaces; después genera un token con fecha de caducidad. Desactivar el usuario de máquina invalida de golpe todos los tokens que posee, justo el botón que quieres tener el día que se filtra un secreto.

Hay cuatro hábitos más que no cuestan nada:

  • Un token por repositorio, con su nombre, para que el registro de auditoría indique qué repositorio creó cada enlace.
  • Secretos de entorno con revisores obligatorios para cualquier flujo de trabajo que cambie un enlace del que dependa la gente.
  • permissions: establecido explícitamente en la parte superior del flujo de trabajo, como en el ejemplo, para que GITHUB_TOKEN obtenga solo lo que necesita el trabajo.
  • Nunca uses pull_request_target para acceder al secreto desde solicitudes de incorporación de cambios de forks. El artículo de GitHub Security Lab sobre prevenir las pwn requests muestra por qué ejecutar código no confiable junto a un token de escritura acaba mal.

Los espacios de trabajo también pueden restringir el acceso a la API mediante una lista de IP permitidas. Es un control sólido para ejecutores autohospedados con salida fija y casi inútil para los ejecutores alojados por GitHub, cuyas direcciones proceden de un conjunto muy grande y cambiante. La propia referencia de uso seguro de GitHub merece una hora si tus flujos de trabajo tocan producción.

Qué falla en la práctica

La mayoría de los fallos proceden de cuatro lugares, y cada uno aparece como un error legible si --fail-with-body está activado. Un 404 en todas las llamadas suele significar que la variable del ID del espacio de trabajo es incorrecta o que la clave pertenece a otro espacio de trabajo. Un 400 que dice domain_id is required significa que la variable está vacía, normalmente porque se estableció en un entorno distinto del que usa el trabajo. Un 409 es la colisión del espacio de nombres compartido de la sección sobre idempotencia. Y un 429 significa que has superado el límite de frecuencia por clave, algo que un solo upsert por ejecución no provocará, pero sí una matriz de cincuenta trabajos.

Una cosa no es un error en absoluto. Después de un PATCH, un visitante puede seguir llegando al destino antiguo durante un breve periodo, porque las redirecciones se almacenan en caché cerca del visitante para mantenerlas rápidas. Una prueba de humo que compruebe el nuevo destino inmediatamente después de la actualización fallará de forma intermitente. Haz sondeos con una espera corta y progresiva o comprueba la respuesta de la API.

Si quieres que el paso informe hacia fuera, combínalo con webhooks para eventos de enlaces, que se activan cuando cambia un enlace, o con los patrones de curl y jq de la guía de CLI para hacer pruebas locales antes de confirmar el flujo de trabajo. La referencia de la API y los SDK enumera todos los campos que aceptan los endpoints de enlaces.

Lee el artículo principal → Gestiona tus enlaces cortos como Terraform

Relacionado en el blog

Preguntas frecuentes

¿Puede GitHub Actions crear enlaces cortos?

Sí. Un paso del flujo de trabajo puede llamar a la API REST de cualquier acortador con curl, que viene preinstalado en los ejecutores alojados por GitHub junto con jq. El paso lee la clave de API de un secreto cifrado, envía la URL de destino y escribe la URL corta resultante en la salida de un paso para que los pasos posteriores puedan publicarla en un comentario de una solicitud de incorporación de cambios o en el resumen de un trabajo.

¿Cómo guardo una clave de API de un acortador de URL en GitHub Actions?

Guárdala como secreto cifrado del repositorio o del entorno y después asígnala al único paso que la necesita con una entrada env como ELIDO_API_KEY: secrets.ELIDO_API_KEY dentro de la sintaxis de expresiones. GitHub oculta el valor en los registros. Mantén en cambio los valores que no son secretos, como el ID del espacio de trabajo y el ID del dominio, en variables de configuración para que sigan siendo legibles.

¿Cómo evito crear enlaces cortos duplicados en cada ejecución del flujo de trabajo?

Convierte el paso en un upsert. Deriva el slug de algo estable, como el número de la solicitud de incorporación de cambios, búscalo primero y envía un PATCH para cambiar el destino cuando ya exista. Crea el enlace solo si la búsqueda no devuelve nada. Un encabezado Idempotency-Key en la llamada de creación cubre el caso aparte de una solicitud reintentada tras un tiempo de espera de red.

¿Por qué mi flujo de trabajo recibe un 409 al crear un enlace corto?

El slug ya está ocupado en ese dominio. En Elido, los slugs son únicos por dominio de redirección, y un dominio compartido se comparte con todos los demás espacios de trabajo, así que es probable que ya exista un slug genérico como pr-12. Añade un prefijo o sufijo de proyecto al slug, o usa tu propio dominio personalizado, donde todo el espacio de nombres te pertenece.

¿Funcionan los pasos de enlaces cortos en solicitudes de incorporación de cambios de forks?

No con el activador pull_request normal, porque GitHub no pasa los secretos del repositorio a los flujos de trabajo iniciados por un fork. Omite el trabajo para los forks con una condición if sobre el repositorio de origen. Cambiar a pull_request_target para obtener el secreto es arriesgado, ya que se ejecuta con acceso de escritura junto a código que no has revisado.

¿Debe un enlace que apunta a la última compilación usar una redirección 301 o 302?

Usa una 302 o 307. Los navegadores pueden almacenar una 301 en caché indefinidamente, así que quienes vuelvan seguirán llegando a una compilación antigua después de que tu flujo de trabajo haya movido el enlace. Los enlaces de Elido usan 302 de forma predeterminada cuando no estableces redirect_status, que es la opción adecuada para cualquier enlace cuyo destino cambie una canalización.

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Gratis, sin registro · 2 por día

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
github actions url shortener
create short link in github actions
shorten url github workflow
preview deployments
ci/cd
api keys

Seguir leyendo