11 min de lectureIntégrations

Raccourcisseur d'URL GitLab CI : des liens courts depuis chaque pipeline

Utilisez GitLab CI comme raccourcisseur d'URL : créez des liens courts pour les review apps de chaque merge request et réorientez un lien stable latest sur les tags, avec une clé masquée et protégée.

Marius Voß
DevRel · edge infra
Un pipeline de raccourcisseur d'URL GitLab CI représenté comme des étapes en pixels, où build, test et deploy se terminent par un job link qui écrit un lien court pour chaque merge request et réoriente un lien latest sur les tags

Oui, GitLab CI peut servir de raccourcisseur d'URL. Un job avec curl et une seule clé API peut créer un lien court à chaque merge request, le faire pointer vers la review app et déplacer un lien stable latest vers la nouvelle version lorsque vous poussez un tag. Cela représente environ quarante lignes de YAML. Et cela fonctionne dès aujourd'hui.

Ce que les gens font mal ne concerne pas l'appel HTTP. C'est la clé : où elle est stockée, quels pipelines peuvent la lire et l'ampleur des dégâts si un job de branche la divulgue. Ce guide consacre donc autant de place aux variables et à leur portée qu'au fichier .gitlab-ci.yml lui-même. Si vous préférez gérer des liens longue durée de manière déclarative, l'approche Terraform des liens courts conviendra mieux ; les pipelines sont adaptés aux liens qui naissent et disparaissent avec le code.

Commençons par une précision sur le statut. L'intégration GitLab native d'Elido arrive, mais elle n'est pas encore disponible, et rien de ce qui suit n'en dépend. Vous voulez la version gérée ? La page d'intégration GitLab propose la liste d'attente.

Ce que fait un job de raccourcisseur d'URL GitLab CI

Un raccourcisseur dans un pipeline fait trois choses, et seulement trois. Il crée un lien quand le slug n'existe pas, modifie la destination quand il existe et désactive le lien quand ce vers quoi il pointait disparaît. Les analyses et les QR codes restent du côté d'Elido.

La surface de l'API est réduite. Les liens se trouvent sous /v1/workspaces/{workspace_id}/links : POST en crée un et nécessite un domain_id ainsi qu'une destination_url, PATCH /links/{link_id} modifie les champs d'un lien existant et GET /links?q= recherche par slug, destination ou titre. L'authentification tient à un seul en-tête : Authorization: Bearer elido_.... La clé vient de la page des clés API du tableau de bord.

C'est tout le contrat. La présentation de l'API et des SDK répertorie les autres endpoints, mais un pipeline a rarement besoin de plus que ces trois-là.

Stocker la clé comme variable masquée et protégée

GitLab vous donne ici deux interrupteurs importants, qui ont des rôles différents. Le masquage cache une valeur dans les journaux de job. La protection contrôle quels pipelines reçoivent cette valeur.

Dans Settings, CI/CD, Variables, choisissez Masked and hidden lors de la création de la variable. Hidden (généralement disponible depuis GitLab 17.6) signifie que personne ne pourra ensuite révéler la valeur dans la page des paramètres, ce qui est souhaitable pour un identifiant. La documentation des variables CI/CD de GitLab énumère les exigences d'une valeur masquée : une seule ligne, sans espaces et d'au moins 8 caractères. Les clés Elido sont composées de elido_ suivi de base32, elles remplissent donc ces conditions.

La même page est très claire sur la limite : le masquage "is not a guaranteed way to prevent malicious users from accessing variable values." Un job qui encode la variable en base64 puis l'affiche passe directement au-delà du masque. Considérez le masquage comme une mesure d'hygiène des journaux, pas comme un contrôle d'accès.

La protection est le contrôle d'accès. Une variable protégée n'atteint que les pipelines des branches ou tags protégés, ce qui crée le problème que toutes les équipes rencontrent pendant leur première semaine : votre pipeline de merge request s'exécute sur une branche de fonctionnalité, la clé protégée arrive donc sous forme de chaîne vide et le job échoue avec une 401 qui ressemble à une faute de frappe.

Je réglerais cela avec deux clés plutôt qu'en affaiblissant la première. Voici la configuration que j'utiliserais :

VariableVisibilitéProtégéeLue par
ELIDO_PREVIEW_KEYMasked and hiddenNonPipelines de merge request
ELIDO_RELEASE_KEYMasked and hiddenOuiPipelines de tags protégés
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSVisibleNonTout job (les ID ne sont pas secrets)
ELIDO_DOMAIN_ID, SHORT_HOSTVisibleNonTout job

La clé de preview appartient à un workspace distinct qui ne contient que des liens de review. Toute personne capable de pousser une branche peut, en principe, exfiltrer une variable non protégée ; assurez-vous donc que le pire résultat possible se limite à un tas de liens jetables mr-142, tandis que la clé de release vit dans votre workspace réel et ne s'exécute que sur les tags que vous avez protégés.

Attribuez aux deux clés le rôle Editor et une date d'expiration ; 90 jours conviennent à celle de preview. Editor est le préréglage le moins élevé capable d'écrire des liens, et il peut aussi les supprimer ; les clés API utilisent l'un des rôles prédéfinis, et j'aimerais disposer d'un préréglage permettant uniquement de créer et de modifier pour ce cas précis ; il n'existe pas encore. C'est la séparation des workspaces qui limite réellement le rayon d'action.

Un job .gitlab-ci.yml fonctionnel pour créer un lien court

Voici le morceau commun : une opération de création ou de mise à jour qui recherche le slug, crée le lien s'il manque et le modifie dans le cas contraire. Placez-le dans un job masqué et étendez-le.

.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 recherche q est une correspondance partielle, donc le filtre jq la réduit au slug exact sur le domaine exact. Sans lui, une recherche de web-mr-14 renverrait volontiers web-mr-142. Obtenez une fois votre domain_id avec GET /v1/workspaces/{id}/domains et stockez-le comme variable ordinaire ; un hôte brandé configuré grâce aux domaines personnalisés est plus lisible dans une merge request qu'un hôte générique.

Cycle de vie d'un lien court de review app GitLab : un pipeline de merge request crée ou met à jour le slug, écrit SHORT_URL dans un rapport dotenv utilisé comme URL d'environnement, puis un job d'arrêt désactive le lien à la fermeture de la merge request

Des liens courts de review app pour chaque merge request

Les review apps sont le nom que GitLab donne à un environnement temporaire par branche ou merge request, et la documentation des review apps les construit avec des environnements dynamiques. Leurs URL sont généralement peu élégantes : un hash, un namespace, le nom d'hôte d'un fournisseur cloud. Un lien court comme go.example.com/web-mr-142 est quelque chose que vous pouvez prononcer à voix haute pendant un point d'équipe.

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"

L'astuce est le rapport dotenv. L'opération de création ou mise à jour écrit SHORT_URL dans link.env, GitLab le relit et environment:url devient le lien court ; le bouton View app de la merge request ouvre donc web-mr-142 au lieu du nom d'hôte brut. La documentation des environnements décrit ce modèle d'URL dynamique.

CI_MERGE_REQUEST_IID est unique par projet et ne change jamais pendant toute la durée de la merge request ; chaque push vers la même MR arrive donc sur le même slug et l'opération le modifie au lieu de créer des doublons. La référence des variables prédéfinies contient la liste complète si vous voulez utiliser une autre clé.

Le nettoyage est un job avec action: stop. Il doit partager les mêmes rules que le job de démarrage, sinon GitLab ne peut pas le déclencher automatiquement :

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"

Je désactive plutôt que de supprimer. Un lien désactivé conserve son historique de clics et, si quelqu'un rouvre la MR, le pipeline suivant le repasse à active via la même opération de création ou mise à jour. GIT_STRATEGY: none est présent parce que la branche peut avoir disparu entre-temps.

Si vos review apps sont dix fois plus nombreuses que vos releases, c'est là que les limites du plan commencent à se faire sentir. Vérifiez le quota de liens sur la page de tarification avant de brancher cela à un monorepo actif, et commencez avec un workspace gratuit pour les previews pendant vos essais.

Réorienter un lien latest stable dans les pipelines de tags

Le deuxième modèle s'exécute sur les tags et fait l'inverse du lien de review : un slug qui ne change jamais, dont la destination avance à chaque release. Votre README peut pointer indéfiniment vers 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+$/

Associez la règle à un modèle de tag protégé comme v* afin que seuls les mainteneurs puissent créer les tags qui le déclenchent ; sinon la clé protégée ne sera tout simplement pas disponible et le job échouera par défaut, ce qui est le comportement recherché. Si vous voulez également un lien permanent par version, exécutez une seconde fois le même job avec SLUG: "cli-${CI_COMMIT_REF_SLUG}", ce qui transforme v1.4.0 en cli-v1-4-0.

Ne définissez pas redirect_status sur 301 pour un lien latest. Une 301 est une promesse de permanence que les navigateurs sont autorisés à mettre en cache, et un lien latest rompt cette promesse à chaque release. Elido utilise 302 par défaut lorsque vous laissez le champ vide, et notre article sur les redirections 301 et 302 détaille les cas où ce choix a réellement des conséquences.

Une réserve honnête : une nouvelle destination peut mettre quelques minutes à atteindre chaque emplacement edge, si bien qu'un test rapide qui interroge le lien court dès la seconde suivante peut encore voir la version précédente. Vérifiez plutôt la réponse de l'API ou attendez avant de contrôler l'en-tête Location.

Principe du moindre privilège pour un raccourcisseur d'URL GitLab CI : une clé de preview non protégée limitée à un workspace de previews pour les pipelines de merge request, et une clé de release protégée que seuls les pipelines de tags protégés peuvent lire pour réorienter le lien latest

Idempotence, nouvelles tentatives et limites de débit

Les pipelines réessaient. Les runners s'arrêtent au milieu d'un job, quelqu'un clique sur Retry dans un job en échec, et deux pushes arrivent à trente secondes d'intervalle en se disputant la même ressource. L'opération de création ou mise à jour ci-dessus résiste aux trois situations, et il vaut la peine de comprendre pourquoi.

L'en-tête Idempotency-Key rend un POST relancé sûr : l'API met en cache une réponse réussie pendant 24 heures et la rejoue pour la même clé, si bien qu'une nouvelle tentative du même pipeline récupère le lien d'origine au lieu d'une erreur. Construire la clé à partir de CI_PIPELINE_ID et du slug signifie que les nouvelles tentatives au sein d'un pipeline sont rejouées, tandis qu'un nouveau pipeline obtient une nouvelle tentative. La branche 409 gère la course entre deux pipelines différents, et le chemin recherche puis modification rend une seconde exécution effectivement sans effet.

Les limites de débit s'appliquent par clé, en plus d'une limite par workspace, et les workspaces tout juste créés ont aussi un plafond quotidien plus bas de création de liens pendant qu'ils construisent leur réputation. Quelques merge requests ne le remarqueront pas. Un monorepo qui lance quarante review apps à la fois pourrait le remarquer ; traitez donc une 429 comme réessayable avec le mot-clé retry de GitLab et échouez explicitement sur une 402, qui signifie une limite de plan plutôt qu'une erreur transitoire. Notre article approfondi sur les limites de débit et l'idempotence des API de raccourcisseurs couvre le backoff plus en détail que nécessaire dans un job CI.

Ignorez le filtre jq de correspondance exacte et le pipeline de la MR 14 modifiera discrètement le lien de la MR 142. Le premier symptôme est généralement un designer perplexe. Gardez le filtre.

Si le shell dans YAML devient difficile à manier, les mêmes appels s'intègrent proprement dans un script que vous validez dans le dépôt, et le guide du CLI de raccourcisseur d'URL montre cette forme.

Consultez l'article pilier → Liens courts avec Terraform : gérer les liens comme du code

Articles associés du blog

Questions fréquentes

GitLab CI peut-il créer des liens courts ?

Oui. Tout job capable d'exécuter curl peut appeler l'API REST d'un raccourcisseur d'URL, donc un job GitLab CI peut créer un lien court, modifier sa destination ou le désactiver. La clé API est stockée dans une variable CI/CD masquée et le job l'envoie comme jeton Bearer. Aucune intégration GitLab native n'est nécessaire pour cela.

Comment stocker une clé API en toute sécurité dans GitLab CI ?

Ajoutez-la sous Settings, CI/CD, Variables avec la visibilité définie sur Masked and hidden, et cochez Protect variable si seuls les branches ou tags protégés doivent pouvoir la lire. Le masquage empêche la valeur d'apparaître dans les journaux de job, mais la documentation de GitLab précise que ce n'est pas une défense garantie ; limitez donc la clé elle-même au strict nécessaire.

Pourquoi ma variable protégée est-elle vide dans un pipeline de merge request ?

Les variables protégées ne sont transmises qu'aux pipelines exécutés sur des branches ou tags protégés. Par défaut, un pipeline de merge request issu d'une branche de fonctionnalité ne remplit pas cette condition, donc la variable arrive vide. Utilisez soit une clé distincte, non protégée et moins privilégiée pour les jobs de review, soit gardez la clé protégée uniquement pour les pipelines de tags.

Comment donner un lien court à chaque review app GitLab ?

Exécutez un job sur les pipelines de merge request qui crée ou met à jour un slug construit à partir du nom du projet et de CI_MERGE_REQUEST_IID, en le faisant pointer vers l'URL de la review app. Écrivez l'URL courte obtenue dans un rapport dotenv et utilisez-la comme environment:url, afin que le widget de la merge request y mène directement. Un job d'arrêt désactive le lien quand l'environnement s'arrête.

Un lien court vers la dernière version doit-il utiliser une redirection 301 ou 302 ?

Utilisez une 302. La destination d'un lien latest change à chaque version, tandis qu'une 301 indique aux navigateurs et aux caches que le déplacement est permanent ; certains clients continueront donc à envoyer les visiteurs vers l'ancienne version. Elido définit par défaut les nouveaux liens sur 302 quand vous ne renseignez pas redirect_status, ce qui convient ici.

Existe-t-il une intégration GitLab native pour Elido ?

Pas encore. Une intégration GitLab native est en préparation et vous pouvez vous inscrire sur la liste d'attente depuis la page d'intégration GitLab. Tout ce guide fonctionne dès aujourd'hui via l'API REST publique depuis un job de pipeline, sans rien installer côté GitLab au-delà d'une variable CI/CD.

Essayer Elido

Collez une URL, obtenez un lien court

Sans inscription. Lien actif 30 jours. Inscrivez-vous pour le garder pour toujours.

Gratuit, sans inscription · 2 par jour

Essayer Elido

Raccourcisseur d'URL hébergé en UE : domaines personnalisés, analyses approfondies et API ouverte. Forfait gratuit - sans carte bancaire.

Tags
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

Lire la suite