12 min de lectureIntégrations

Raccourcisseur d'URL GitHub Actions : des liens courts depuis votre CI

Créez et mettez à jour des liens courts depuis GitHub Actions : la clé API comme secret chiffré, un workflow fonctionnel, des upserts idempotents et des clés aux privilèges minimaux.

Marius Voß
DevRel · edge infra
Une étape de raccourcissement d'URL GitHub Actions représentée comme un pipeline : une exécution de workflow lit un secret chiffré, recherche le slug, puis met à jour le lien court existant ou en crée un nouveau

Une étape de raccourcissement d'URL GitHub Actions tient en quelques lignes de shell : lire une clé API depuis un secret chiffré, vérifier si le slug existe déjà, puis mettre à jour sa destination ou le créer. Exécutez-la à chaque push et le même lien court pointera toujours vers le dernier aperçu, le dernier build de la documentation ou le dernier artefact. Aucune action de marketplace n'est nécessaire. curl et jq sont présents sur chaque runner Ubuntu hébergé par GitHub.

C'est toute la réponse, et la suite de cet article explique comment la rendre fiable sur plusieurs centaines d'exécutions de workflow. Les personnes qui cherchent comment créer un lien court dans GitHub Actions arrivent généralement jusqu'à une seule requête POST, qui fonctionne jusqu'au deuxième push vers la même pull request, lorsque l'appel de création renvoie un conflit et que le job passe au rouge. La solution consiste à traiter l'étape comme un upsert, et non comme une création. L'autre problème vient de la clé elle-même, qui est souvent le token personnel de quelqu'un, avec une portée bien plus large que nécessaire pour un job CI.

Si vous gérez déjà vos liens comme du code, les liens courts avec Terraform sont la version déclarative de la même idée et conviennent mieux aux liens qui changent au rythme des décisions humaines. Une étape de workflow est préférable lorsque la destination n'existe qu'une fois le build terminé.

Fonctionnement d'une étape de raccourcissement d'URL GitHub Actions

À chaque exécution, les trois mêmes opérations sont effectuées sur l'API REST à l'adresse https://api.elido.app/v1. Les liens de l'espace de travail sont listés après filtrage par slug. Un PATCH est envoyé vers le lien trouvé, ou un POST si aucun lien n'a été trouvé. L'URL courte est écrite dans $GITHUB_OUTPUT afin que l'étape suivante puisse l'utiliser.

Pourquoi ne pas laisser le raccourcisseur générer un slug aléatoire ? Parce que vous ne pourriez alors plus retrouver le lien. Le slug doit provenir d'une donnée que le workflow connaît déjà à chaque exécution : le numéro de la pull request, le nom de la branche, un mot fixe comme latest. Un slug stable donne une URL courte stable, et c'est toute sa valeur pour les reviewers qui l'ajoutent à leurs favoris ou les product managers qui la copient dans un ticket.

Fonctionnement d'une étape de raccourcissement d'URL GitHub Actions : le workflow lit la clé API depuis un secret chiffré, liste les liens par slug, envoie PATCH lorsque le slug existe ou POST dans le cas contraire, puis écrit l'URL courte dans la sortie d'une étape

Stocker la clé API comme secret chiffré

Créez la clé dans le tableau de bord, copiez-la une seule fois (elle est affichée exactement une fois et commence par elido_), puis enregistrez-la dans Settings, puis Secrets and variables, puis Actions, sous le nom ELIDO_API_KEY. Le guide de GitHub sur l'utilisation des secrets dans GitHub Actions couvre les niveaux du dépôt, de l'environnement et de l'organisation. Pour tout ce qui effectue un déploiement, je la placerais dans un environnement avec des reviewers obligatoires afin qu'une branche imprévue ne puisse pas l'utiliser.

Trois valeurs ne sont pas secrètes et doivent figurer dans les variables de configuration, où vous pouvez les relire : ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID et ELIDO_HOST. L'ID du domaine est important, car l'appel de création l'exige. Vous pouvez le rechercher une fois avec GET /v1/workspaces/{workspace_id}/domains, qui renvoie l'id et le hostname de chaque domaine.

Transmettez le secret à la seule étape qui appelle l'API, et non à tout le job. Un env au niveau de l'étape l'écarte de tous les autres processus démarrés par le job, y compris des actions tierces que vous n'avez pas écrites.

Un workflow fonctionnel pour raccourcir une URL à chaque pull request

Voici le fichier complet pour le cas le plus courant de raccourcissement d'une URL dans un workflow GitHub : un lien d'aperçu par pull request. Déposez-le dans .github/workflows/preview-link.yml et modifiez la ligne DEST pour qu'elle pointe vers l'emplacement de vos déploiements d'aperçu.

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 }}"

Quelques lignes méritent un commentaire. --fail-with-body transforme une erreur 4xx ou 5xx en échec de l'étape tout en affichant le corps de l'erreur, ce que ne fait pas curl -s ; ce dernier sort avec le code 0 pour une 401 et le job poursuit tranquillement son exécution. Les corps des requêtes sont construits avec jq -n plutôt qu'avec une interpolation de chaînes, afin qu'une destination contenant des guillemets ou une esperluette ne puisse pas casser le JSON. Et l'étape de commentaire ne s'exécute que pour opened. Comme l'URL courte ne change jamais, un seul commentaire reste correct pendant toute la durée de la PR, et personne ne reçoit de notification à chaque push.

Le bloc concurrency n'est pas décoratif. Deux push rapprochés lanceraient sinon deux exécutions qui verraient toutes deux « aucun lien pour le moment » et tenteraient toutes deux d'en créer un. La documentation de GitHub sur le contrôle de la concurrence des workflows explique le regroupement ; ici, l'exécution la plus ancienne est annulée et la course n'a pas lieu.

Idempotence : mettre à jour le lien court plutôt que le dupliquer

Deux échecs différents se cachent derrière le mot idempotent, et le workflow les traite séparément. Le premier est la nouvelle exécution : un deuxième push, un « Re-run jobs » manuel, une PR rouverte. C'est le rôle de la branche recherche puis PATCH. Le second est la requête réessayée : curl envoie le POST, le réseau coupe avant l'arrivée de la réponse, puis curl le renvoie. L'en-tête Idempotency-Key couvre ce cas. Elido conserve la première réponse réussie associée à la clé pendant 24 heures et la rejoue pour une nouvelle tentative correspondante, si bien que la création n'a lieu qu'une fois ; le fonctionnement complet est expliqué dans limites de débit, nouvelles tentatives et idempotence.

Flux de décision pour créer un lien court dans GitHub Actions sans doublon : une correspondance exacte du slug dans votre espace de travail mène à PATCH, l'absence de correspondance mène à POST, et une 409 signifie qu'un autre espace de travail possède déjà le slug sur un domaine partagé

Les collisions de slug sont le point qui échappe souvent aux utilisateurs. Les slugs sont uniques par domaine de redirection, et non par espace de travail. Sur un domaine partagé, tous les autres clients d'Elido utilisent le même espace de noms, et un slug aussi simple que pr-12 a probablement déjà été pris. La recherche ne verra pas leur lien (elle ne liste que ceux de votre espace de travail), donc le POST est envoyé et revient avec 409 slug already exists for this domain. Deux solutions : ajouter un mot de projet au slug, ou placer les liens CI sur votre propre domaine personnalisé, où l'espace de noms vous appartient entièrement. Je ferais les deux.

Il existe une deuxième raison, plus subtile, pour laquelle le nom du dépôt se trouve à la fin du slug plutôt qu'au début. Le paramètre q effectue une recherche par sous-chaîne dans le slug, la destination et le titre. Avec myapp-pr-1, la recherche renvoie aussi myapp-pr-10 à myapp-pr-199, soit davantage que les 100 résultats renvoyés par une seule page, et le lien voulu, parce qu'il est le plus ancien, disparaît de la fin. pr-1-myapp ne correspond qu'à lui-même. Un petit détail, et il m'a fallu un après-midi ridiculement long à me demander « pourquoi la PR #1 reçoit-elle toujours une 409 ? » pour le repérer.

Trois jobs de liens courts CI qui méritent d'être automatisés

Le workflow d'aperçu n'est qu'un modèle. Changez le déclencheur, le slug et la destination, et la même étape couvre la plupart des opérations que les équipes automatisent réellement. (Les notes de version sont un sujet à part, traité dans raccourcir les liens dans les notes de version.)

Cas d'usageDéclencheurSlugAction de l'étape
Déploiement d'aperçu par PRpull_requestpr-42-myappUpsert à chaque push, suppression à la fermeture
Déploiement de la documentationpush vers maindocs-myappPATCH vers l'URL de documentation qui vient d'être déployée
Dernier buildpush vers main ou un taglatest-myappPATCH par ID de lien enregistré, sans recherche
Artefact nocturneschedulenightly-myappPATCH vers l'URL du dernier artefact

Le cas du dernier build est le plus simple de tous. Créez le lien manuellement une fois, enregistrez son ID numérique comme variable, et le job se réduit à un seul appel :

- 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}')"

Gardez ces liens mobiles en 302, la valeur par défaut lorsque vous ne définissez pas redirect_status. Une 301 indique aux navigateurs qu'ils peuvent mettre la réponse en cache, et les personnes qui ont cliqué hier continueront d'arriver sur le build d'hier ; notre article sur les redirections 301 et 302 donne la version longue.

Pour les liens d'aperçu, faites le ménage lorsque la PR est fermée. Ajoutez closed aux types de déclencheur, réutilisez la recherche et envoyez DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Un slug supprimé peut être réutilisé. Si vous préférez conserver l'historique des clics, utilisez plutôt PATCH avec {"status": "disabled"} ; l'upsert ci-dessus définit status: "active" à chaque exécution, si bien qu'une PR rouverte réactive son lien.

Prêt à essayer sur un dépôt ? Commencez avec un espace de travail gratuit, créez une clé, et le workflow ci-dessus s'exécutera tel quel une fois les trois variables définies.

Des clés API CI aux privilèges minimaux

La clé stockée dans un secret CI doit pouvoir faire exactement ce que fait le workflow, et rien de plus. C'est plus difficile qu'il n'y paraît, à cause du fonctionnement des clés personnelles.

Une clé API personnelle s'authentifie comme la personne qui l'a créée. Tout ce que cette personne peut faire, la clé peut le faire, et lorsqu'elle quitte l'entreprise, la clé reste attachée à son compte. Pour la CI, j'utiliserais plutôt un utilisateur machine : un compte de service qui appartient à un seul espace de travail, possède son propre rôle et des tokens qu'un administrateur humain connecté est le seul à pouvoir créer ou révoquer. Créez-le sous Machine users dans le tableau de bord avec le rôle editor, qui est le rôle intégré le moins élevé capable de créer, modifier et supprimer des liens, puis générez un token avec une date d'expiration. Désactiver l'utilisateur machine invalide instantanément tous ses tokens, ce qui est exactement le bouton qu'il vous faut le jour où un secret fuit.

Quatre autres habitudes ne coûtent rien :

  • Un token par dépôt, nommé d'après celui-ci, afin que la piste d'audit indique quel dépôt a créé quel lien.
  • Des secrets d'environnement avec des reviewers obligatoires pour tout workflow qui modifie un lien sur lequel des personnes comptent.
  • permissions: défini explicitement en haut du workflow, comme dans l'exemple, afin que le GITHUB_TOKEN n'obtienne que les droits nécessaires au job.
  • N'utilisez jamais pull_request_target pour accéder au secret depuis des PR de forks. L'article de GitHub Security Lab sur la prévention des pwn requests montre pourquoi exécuter du code non fiable à côté d'un token en écriture finit mal.

Les espaces de travail peuvent également restreindre l'accès à l'API avec une liste d'autorisation IP. C'est un contrôle solide pour les runners auto-hébergés avec une sortie fixe, et presque inutile pour les runners hébergés par GitHub, dont les adresses proviennent d'un très grand pool changeant. La référence de GitHub sur l'utilisation sécurisée mérite une heure de lecture si vos workflows touchent à la production.

Ce qui casse en pratique

La plupart des échecs viennent de quatre endroits, et chacun apparaît sous la forme d'une erreur lisible si --fail-with-body est activé. Une 404 à chaque appel signifie généralement que la variable d'ID de l'espace de travail est incorrecte ou que la clé appartient à un autre espace de travail. Une 400 indiquant domain_id is required signifie que la variable est vide, le plus souvent parce qu'elle a été définie dans un autre environnement que celui utilisé par le job. Une 409 est la collision d'espace de noms partagé décrite dans la section sur l'idempotence. Et une 429 signifie que vous dépassez la limite de débit par clé, ce qu'un seul upsert par exécution n'atteindra pas, mais qu'une matrice de cinquante jobs peut atteindre.

Une chose n'est pas une erreur du tout. Après un PATCH, un visiteur peut encore atteindre l'ancienne destination pendant un court moment, car les redirections sont mises en cache près de lui pour rester rapides. Un smoke test qui vérifie la nouvelle destination immédiatement après la mise à jour sera instable. Faites des requêtes répétées avec un court délai croissant, ou vérifiez plutôt la réponse de l'API.

Si vous voulez que l'étape envoie un rapport vers l'extérieur, associez-la aux webhooks pour les événements de lien, qui se déclenchent lorsqu'un lien change, ou aux modèles curl et jq du guide CLI pour effectuer des tests locaux avant de valider le workflow. La référence de l'API et des SDK répertorie tous les champs acceptés par les endpoints de liens.

Lisez l'article pilier → Gérez vos liens courts avec Terraform

À lire également sur le blog

Questions fréquentes

GitHub Actions peut-il créer des liens courts ?

Oui. Une étape de workflow peut appeler l'API REST de n'importe quel raccourcisseur avec curl, qui est préinstallé sur les runners hébergés par GitHub avec jq. L'étape lit la clé API depuis un secret chiffré, envoie l'URL de destination et écrit l'URL courte obtenue dans la sortie de l'étape afin que les étapes suivantes puissent la publier dans un commentaire de pull request ou dans un résumé de job.

Comment stocker une clé API de raccourcisseur d'URL dans GitHub Actions ?

Enregistrez-la comme secret chiffré du dépôt ou de l'environnement, puis transmettez-la à la seule étape qui en a besoin avec une entrée env telle que ELIDO_API_KEY: secrets.ELIDO_API_KEY dans la syntaxe d'expression. GitHub masque la valeur dans les journaux. Gardez plutôt les valeurs non secrètes, comme l'ID de l'espace de travail et l'ID du domaine, dans des variables de configuration afin qu'elles restent lisibles.

Comment éviter de créer des liens courts en double à chaque exécution du workflow ?

Faites de l'étape un upsert. Déduisez le slug d'une donnée stable, comme le numéro de la pull request, recherchez-le d'abord et envoyez un PATCH pour modifier la destination lorsqu'il existe déjà. Ne créez le lien que si la recherche ne renvoie rien. Un en-tête Idempotency-Key sur l'appel de création couvre le cas distinct d'une requête réessayée après un délai d'attente réseau.

Pourquoi mon workflow reçoit-il une erreur 409 lors de la création d'un lien court ?

Le slug est déjà pris sur ce domaine. Dans Elido, les slugs sont uniques par domaine de redirection, et un domaine partagé l'est avec tous les autres espaces de travail, si bien qu'un slug générique comme pr-12 existe probablement déjà. Ajoutez un préfixe ou un suffixe de projet au slug, ou utilisez votre propre domaine personnalisé, dont tout l'espace de noms vous appartient.

Les étapes de liens courts fonctionnent-elles sur les pull requests provenant de forks ?

Pas avec le déclencheur pull_request standard, car GitHub ne transmet pas les secrets du dépôt aux workflows démarrés par un fork. Ignorez le job pour les forks avec une condition if sur le dépôt source. Passer à pull_request_target pour obtenir le secret est risqué, car cela exécute le workflow avec un accès en écriture à côté de code que vous n'avez pas contrôlé.

Un lien qui pointe vers le dernier build doit-il utiliser une redirection 301 ou 302 ?

Utilisez une 302 ou une 307. Les navigateurs peuvent mettre une 301 en cache indéfiniment, si bien que les visiteurs récurrents continueraient d'arriver sur un ancien build après que votre workflow a déplacé le lien. Les liens Elido utilisent par défaut une 302 lorsque vous ne définissez pas redirect_status, ce qui convient à tout lien dont la destination est modifiée par un pipeline.

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
github actions url shortener
create short link in github actions
shorten url github workflow
preview deployments
ci/cd
api keys

Lire la suite