11 min de lecturaIntegraciones

Acortador de URL de GitLab CI: enlaces cortos desde cada pipeline

Usa GitLab CI como acortador de URL: crea enlaces cortos de review app por solicitud de merge y redirige un enlace estable de latest en los tags, con una clave enmascarada y protegida.

Marius Voß
DevRel · edge infra
Pipeline de acortador de URL de GitLab CI dibujado como etapas pixeladas, donde compilación, pruebas y despliegue terminan en un trabajo de enlaces que escribe un enlace corto para cada solicitud de merge y redirige un enlace de latest en los tags

Sí, GitLab CI puede actuar como tu acortador de URL. Un trabajo con curl y una clave de API puede crear un enlace corto en cada solicitud de merge, apuntarlo a la review app y mover un enlace estable de latest a la nueva versión cuando envías un tag. Son unas cuarenta líneas de YAML. Funciona hoy.

Lo que suele hacerse mal no es la llamada HTTP, sino la clave: dónde vive, qué pipelines pueden leerla y cuánto daño podría causar si un trabajo de una rama la filtrara. Por eso esta guía dedica tanto espacio a las variables y al alcance como al propio .gitlab-ci.yml. Si prefieres gestionar declarativamente enlaces de larga duración, el enfoque de Terraform para enlaces cortos encaja mejor; los pipelines son adecuados para enlaces que nacen y se retiran con el código.

Una nota de estado antes de empezar. La integración nativa de GitLab de Elido está en camino, no activa, y nada de lo que sigue depende de ella. ¿Quieres la versión gestionada? La página de integración de GitLab tiene la lista de espera.

Qué hace un trabajo de acortador de URL de GitLab CI

Un acortador de pipeline hace tres cosas, y solo tres. Crea un enlace cuando no existe un slug, actualiza el destino cuando sí existe y desactiva el enlace cuando desaparece aquello a lo que apuntaba. Las analíticas y los códigos QR se quedan en el lado de Elido.

La superficie de la API es pequeña. Los enlaces viven en /v1/workspaces/{workspace_id}/links: POST crea uno y necesita un domain_id más un destination_url, PATCH /links/{link_id} cambia campos de uno existente y GET /links?q= busca por slug, destino o título. La autenticación es una sola cabecera: Authorization: Bearer elido_.... La clave se obtiene en la página de claves de API del panel.

Ese es todo el contrato. La visión general de la API y los SDK enumera el resto de endpoints, pero un pipeline rara vez necesita más que estos tres.

Guardar la clave como variable enmascarada y protegida

GitLab te da dos opciones importantes aquí, y cumplen funciones distintas. El enmascaramiento oculta un valor en los registros de los trabajos. La protección controla qué pipelines reciben el valor.

En Settings, CI/CD, Variables, elige Masked and hidden al crear la variable. Hidden (disponible de forma general desde GitLab 17.6) significa que después nadie puede revelar el valor en la página de configuración, justo lo que quieres para una credencial. La documentación de variables de GitLab CI/CD enumera los requisitos de un valor enmascarado: una sola línea, sin espacios y con al menos 8 caracteres. Las claves de Elido son elido_ seguido de base32, así que cumplen los requisitos.

La misma página es clara sobre el límite: el enmascaramiento "is not a guaranteed way to prevent malicious users from accessing variable values." Un trabajo que codifique la variable en base64 y la muestre pasa directamente por delante de la máscara. Trata el enmascaramiento como higiene de registros, no como control de acceso.

La protección es el control de acceso. Una variable protegida solo llega a los pipelines de ramas o tags protegidos, lo que crea el problema que todos los equipos encuentran durante su primera semana: tu pipeline de solicitud de merge se ejecuta en una rama de funcionalidad, así que la clave protegida llega como una cadena vacía y el trabajo falla con un 401 que parece un error tipográfico.

Yo lo resolvería con dos claves en lugar de debilitar la única clave. Esta sería la configuración que usaría:

VariableVisibilidadProtegidaLeída por
ELIDO_PREVIEW_KEYMasked and hiddenNoPipelines de solicitudes de merge
ELIDO_RELEASE_KEYMasked and hiddenPipelines de tags protegidos
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSVisibleNoCualquier trabajo (los ID no son secretos)
ELIDO_DOMAIN_ID, SHORT_HOSTVisibleNoCualquier trabajo

La clave de preview pertenece a un espacio de trabajo separado que solo contiene enlaces de revisión. Cualquiera que pueda subir una rama puede, en principio, exfiltrar una variable no protegida, así que asegúrate de que lo peor a lo que pueda acceder sea un montón de enlaces desechables como mr-142, mientras la clave de release vive en tu espacio de trabajo real y solo se ejecuta en tags que hayas protegido.

Asigna a ambas claves el rol Editor y una fecha de caducidad; 90 días es adecuado para la de preview. Editor es el preset más bajo que puede escribir enlaces, y también puede eliminarlos; las claves de API usan uno de los roles predefinidos, y me gustaría que existiera un preset que solo permitiera crear y actualizar para este caso exacto; todavía no existe. La separación de espacios de trabajo es lo que realmente limita el radio de impacto.

Un trabajo funcional de .gitlab-ci.yml para crear un enlace corto

Esta es la pieza compartida: un upsert que busca el slug, crea el enlace si falta y lo modifica si ya existe. Ponlo en un trabajo oculto y extiéndelo.

.elido_upsert:
  image: alpine:3.20
  before_script:
    - apk add --no-cache curl jq
  script:
    - API="https://api.elido.app/v1/workspaces/${ELIDO_WS}"
    - AUTH="Authorization: Bearer ${ELIDO_KEY}"
    - |
      find_id() {
        curl -sS --fail-with-body -H "$AUTH" "$API/links?q=${SLUG}&limit=50" |
          jq -r --arg s "$SLUG" --argjson d "$ELIDO_DOMAIN_ID" \
            '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1
      }
      ID="$(find_id)"
      if [ -z "$ID" ]; then
        CODE=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/links" \
          -H "$AUTH" -H "Content-Type: application/json" \
          -H "Idempotency-Key: ${CI_PIPELINE_ID}-${SLUG}" \
          -d "$(jq -n --arg s "$SLUG" --arg u "$TARGET" --argjson d "$ELIDO_DOMAIN_ID" \
                '{domain_id: $d, slug: $s, destination_url: $u, tags: ["ci"]}')")
        case "$CODE" in
          201) ;;
          409) ID="$(find_id)" ;;   # another pipeline created it first
          *) cat resp.json; exit 1 ;;
        esac
      fi
      if [ -n "$ID" ]; then
        curl -sS --fail-with-body -X PATCH "$API/links/$ID" \
          -H "$AUTH" -H "Content-Type: application/json" \
          -d "$(jq -n --arg u "$TARGET" '{destination_url: $u, status: "active"}')"
      fi
    - echo "SHORT_URL=https://${SHORT_HOST}/${SLUG}" >> link.env
  artifacts:
    reports:
      dotenv: link.env

La búsqueda q es una coincidencia de subcadena, así que el filtro jq la reduce al slug exacto en el dominio exacto. Sin él, una búsqueda de web-mr-14 devolvería sin problemas web-mr-142. Obtén una vez tu domain_id con GET /v1/workspaces/{id}/domains y guárdalo como variable normal; un host de marca configurado mediante dominios personalizados queda mejor en una solicitud de merge que uno genérico.

Ciclo de vida de un enlace corto de review app de GitLab: un pipeline de solicitud de merge hace upsert del slug, escribe SHORT_URL en un informe dotenv usado como URL del entorno y un trabajo de parada desactiva el enlace cuando se cierra la solicitud de merge

Enlaces cortos de review app por solicitud de merge

Review apps es el nombre que da GitLab a un entorno temporal por rama o solicitud de merge, y la documentación de review apps los construye sobre entornos dinámicos. Sus URL suelen ser feas: un hash, un espacio de nombres, el nombre de host de un proveedor en la nube. Un enlace corto como go.example.com/web-mr-142 es algo que puedes decir en voz alta durante una reunión diaria.

review_link:
  extends: .elido_upsert
  stage: deploy
  needs: [deploy_review]
  variables:
    ELIDO_KEY: $ELIDO_PREVIEW_KEY
    ELIDO_WS: $ELIDO_PREVIEW_WS
    SLUG: "${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}"
    TARGET: "https://${CI_ENVIRONMENT_SLUG}.review.example.com"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: $SHORT_URL
    on_stop: stop_review_link
    auto_stop_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

El truco es el informe dotenv. El upsert escribe SHORT_URL en link.env, GitLab lo vuelve a leer y environment:url se convierte en el enlace corto, de modo que el botón View app de la solicitud de merge abre web-mr-142 en lugar del hostname sin acortar. La documentación de entornos describe este patrón de URL dinámica.

CI_MERGE_REQUEST_IID es único por proyecto y no cambia durante la vida de la solicitud de merge, por lo que cada push al mismo MR termina en el mismo slug y el upsert lo modifica en lugar de crear duplicados. La referencia de variables predefinidas contiene la lista completa si quieres otra clave.

La limpieza es un trabajo con action: stop. Tiene que compartir las rules del trabajo de inicio o GitLab no podrá activarlo automáticamente:

stop_review_link:
  image: alpine:3.20
  stage: deploy
  variables:
    GIT_STRATEGY: none
    SLUG: "${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}"
  script:
    - apk add --no-cache curl jq
    - API="https://api.elido.app/v1/workspaces/${ELIDO_PREVIEW_WS}"
    - ID=$(curl -sS -H "Authorization:
        Bearer ${ELIDO_PREVIEW_KEY}" "$API/links?q=${SLUG}" |
        jq -r --arg s "$SLUG" --argjson d "$ELIDO_DOMAIN_ID" '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1)
    - '[ -z "$ID" ] || curl -sS --fail-with-body -X PATCH "$API/links/$ID" -H "Authorization: Bearer ${ELIDO_PREVIEW_KEY}" -H "Content-Type: application/json" -d "{\"status\":\"disabled\"}"'
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Desactivo en lugar de eliminar. Un enlace desactivado conserva su historial de clics y, si alguien vuelve a abrir el MR, el siguiente pipeline lo devuelve a active mediante el mismo upsert. GIT_STRATEGY: none está ahí porque para entonces la rama puede haber desaparecido.

Si tus review apps superan a tus versiones en una proporción de diez a uno, ahí es donde empiezan a notarse los límites del plan. Comprueba la cantidad de enlaces permitida en la página de precios antes de conectarlo a un monorepo activo y crea un espacio de trabajo gratuito para las previsualizaciones mientras lo pruebas.

Redirigir un enlace estable de latest en los pipelines de tags

El segundo patrón se ejecuta en tags y hace lo contrario que el enlace de revisión: un slug que nunca cambia y cuyo destino avanza con cada versión. Tu README puede apuntar para siempre a go.example.com/cli-latest.

latest_link:
  extends: .elido_upsert
  stage: release
  variables:
    ELIDO_KEY: $ELIDO_RELEASE_KEY
    ELIDO_WS: $ELIDO_RELEASE_WS
    SLUG: "cli-latest"
    TARGET: "${CI_PROJECT_URL}/-/releases/${CI_COMMIT_TAG}"
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

Combina la regla con un patrón de tags protegidos como v* para que solo los mantenedores puedan crear los tags que la activan; de lo contrario, la clave protegida simplemente no estará disponible y el trabajo fallará de forma segura, que es el comportamiento que quieres. Si también quieres un enlace permanente por versión, ejecuta el mismo trabajo una segunda vez con SLUG: "cli-${CI_COMMIT_REF_SLUG}", lo que convierte v1.4.0 en cli-v1-4-0.

No establezcas redirect_status en 301 para un enlace de latest. Una 301 es una promesa permanente que los navegadores pueden almacenar en caché, y un enlace de latest rompe esa promesa en cada versión. Elido usa 302 de forma predeterminada cuando dejas el campo fuera, y nuestro artículo sobre redirecciones 301 frente a 302 repasa los casos en que esta elección realmente causa problemas.

Una salvedad honesta: un cambio de destino puede tardar unos minutos en llegar a todas las ubicaciones edge, así que una prueba rápida que consulte el enlace corto con curl justo al segundo siguiente todavía puede ver la versión anterior. Comprueba la respuesta de la API o espera antes de verificar la cabecera Location.

Principio de mínimo privilegio para un acortador de URL de GitLab CI: una clave de preview no protegida limitada a un espacio de trabajo de previews para pipelines de solicitudes de merge y una clave de release protegida que solo pueden leer los pipelines de tags protegidos para redirigir el enlace de latest

Idempotencia, reintentos y límites de velocidad

Los pipelines reintentan. Los ejecutores mueren a mitad del trabajo, alguien hace clic en Retry en un trabajo fallido y dos envíos llegan con treinta segundos de diferencia y compiten entre sí. El upsert anterior sobrevive a las tres situaciones, y merece la pena saber por qué.

La cabecera Idempotency-Key hace que un POST reintentado sea seguro: la API guarda en caché una respuesta correcta durante 24 horas y la reproduce para la misma clave, así que un reintento del mismo pipeline recupera el enlace original en lugar de recibir un error. Construir la clave a partir de CI_PIPELINE_ID y el slug significa que los reintentos dentro de un pipeline reproducen el mismo resultado, mientras que un pipeline nuevo obtiene un intento nuevo. La rama 409 gestiona la carrera entre dos pipelines distintos, y la ruta de buscar y modificar hace que una segunda ejecución no tenga efectos prácticos.

Los límites de velocidad son por clave, además de un límite por espacio de trabajo, y los espacios de trabajo recién creados también tienen un límite diario menor de creación de enlaces mientras ganan reputación. Un puñado de solicitudes de merge no lo notará. Un monorepo que levante cuarenta review apps a la vez puede notarlo, así que trata un 429 como reintentable con la palabra clave retry de GitLab y falla de forma explícita con un 402, que significa un límite del plan y no un error transitorio. Nuestro artículo más detallado sobre límites de velocidad e idempotencia para APIs de acortadores explica el backoff con más detalle del que necesita un trabajo de CI.

Omite el filtro jq de coincidencia exacta y el pipeline del MR 14 modificará silenciosamente el enlace del MR 142. El primer síntoma suele ser un diseñador confundido. Conserva el filtro.

Si el shell dentro de YAML se vuelve difícil de manejar, puedes envolver las mismas llamadas en un script que confirmes en el repositorio; la guía de la CLI del acortador de URL muestra esa estructura.

Lee el artículo principal → Enlaces cortos como Terraform: gestionar enlaces como código

Relacionado en el blog

Preguntas frecuentes

¿Puede GitLab CI crear enlaces cortos?

Sí. Cualquier trabajo que pueda ejecutar curl puede llamar a la API REST de un acortador de URL, así que un trabajo de GitLab CI puede crear un enlace corto, actualizar su destino o desactivarlo. La clave de API vive en una variable de CI/CD enmascarada y el trabajo la envía como token Bearer. No hace falta una integración nativa de GitLab.

¿Cómo guardo una clave de API de forma segura en GitLab CI?

Añádela en Settings, CI/CD, Variables con la visibilidad configurada como Masked and hidden, y marca Protect variable si solo las ramas o tags protegidos deben poder leerla. El enmascaramiento mantiene el valor fuera de los registros de los trabajos, pero la propia documentación de GitLab dice que no es una defensa garantizada, así que limita la clave a los permisos mínimos que necesita.

¿Por qué está vacía mi variable protegida en un pipeline de solicitud de merge?

Las variables protegidas solo se pasan a los pipelines que se ejecutan en ramas o tags protegidos. Un pipeline de solicitud de merge de una rama de funcionalidad no cumple ese requisito de forma predeterminada, así que la variable llega vacía. Usa una clave no protegida separada y con menos privilegios para los trabajos de revisión o reserva la clave protegida únicamente para los pipelines de tags.

¿Cómo doy a cada review app de GitLab un enlace corto?

Ejecuta un trabajo en los pipelines de solicitudes de merge que haga upsert de un slug construido a partir del nombre del proyecto y CI_MERGE_REQUEST_IID, apuntando a la URL de la review app. Escribe la URL corta resultante en un informe dotenv y úsala como environment:url, para que el widget de la solicitud de merge enlace directamente con ella. Un trabajo de parada desactiva el enlace cuando se detiene el entorno.

¿Debería usar una redirección 301 o 302 para un enlace corto de la última versión?

Usa una 302. Un enlace de latest cambia de destino en cada versión, y una 301 indica a los navegadores y las cachés que el cambio es permanente, por lo que algunos clientes seguirán enviando a la gente a la versión antigua. Elido establece 302 de forma predeterminada en los enlaces nuevos cuando no configuras redirect_status, que es la opción adecuada en este caso.

¿Existe una integración nativa de GitLab para Elido?

Todavía no. La integración nativa de GitLab está en camino y puedes apuntarte a la lista de espera en la página de integración de GitLab. Todo lo que aparece en esta guía funciona hoy mediante la API REST pública desde un trabajo del pipeline, sin instalar nada en el lado de GitLab aparte de una variable de CI/CD.

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
gitlab ci url shortener
create short link gitlab pipeline
gitlab review app short link
gitlab ci masked variable api key
short link per merge request
latest release short link

Seguir leyendo