11 min leestijdIntegraties

GitHub Actions URL-verkorter: korte links vanuit je CI

Maak en update korte links vanuit GitHub Actions: API-sleutel als geheim, een werkende workflow, idempotente upserts en sleutels met minimale rechten.

Marius Voß
DevRel · edge infra

Een GitHub Actions URL-verkorterstap is een paar regels shell: lees een API-sleutel uit een versleuteld geheim, controleer of de slug al bestaat en werk dan de bestemming bij of maak hem aan. Draai hem bij elke push en dezelfde korte link wijst altijd naar de nieuwste preview, docs-build of het nieuwste artefact. Geen marketplace-actie nodig. curl en jq zitten op elke door GitHub gehoste Ubuntu-runner.

Dat is het hele antwoord, en de rest van dit artikel gaat over het deel waardoor het standhoudt over een paar honderd workflowruns. Wie zoekt hoe je een korte link maakt in GitHub Actions, komt meestal tot één POST-verzoek, en dat werkt tot de tweede push naar dezelfde pull request, wanneer de aanmaakaanroep een conflict teruggeeft en de job rood wordt. De oplossing is de stap als upsert te behandelen, niet als create. Het andere dat misgaat is de sleutel zelf, die vaak iemands persoonlijke token is met veel meer bereik dan een CI-job nodig heeft.

Beheer je links al als code, dan is korte links als Terraform de declaratieve versie van hetzelfde idee en de betere keuze voor links die op menselijk tempo veranderen. Een workflowstap wint wanneer de bestemming pas bestaat nadat een build klaar is.

Hoe een GitHub Actions URL-verkorterstap werkt

Elke run doet drie dingen tegen de REST-API op https://api.elido.app/v1. Hij vraagt de links van de werkruimte op, gefilterd op de slug. Hij stuurt een PATCH naar de link die hij vond, of een POST als hij niets vond. En hij schrijft de korte URL naar $GITHUB_OUTPUT zodat de volgende stap hem kan gebruiken.

Waarom de verkorter geen willekeurige slug laten genereren? Omdat je de link dan niet meer terugvindt. De slug moet komen uit iets wat de workflow bij elke run al weet: het pull-requestnummer, de branchnaam, een vast woord als latest. Een stabiele slug betekent een stabiele korte URL, en dat is de hele waarde voor reviewers die hem bookmarken of productmanagers die hem in een ticket plakken.

Hoe een GitHub Actions URL-verkorterstap draait: de workflow leest de API-sleutel uit een versleuteld geheim, zoekt links op slug, stuurt PATCH als de slug bestaat of POST als hij niet bestaat, en schrijft dan de korte URL naar een stap-output De GitHub-integratiepagina van Elido met de scopes en installatiestappen voor het maken van korte links vanuit een workflowrun

De API-sleutel opslaan als versleuteld geheim

Maak de sleutel in het dashboard en kopieer hem meteen, want hij wordt precies één keer getoond (hij begint met elido_), en sla hem dan op onder Settings, dan Secrets and variables, dan Actions, als ELIDO_API_KEY. De gids van GitHub over geheimen gebruiken in GitHub Actions beschrijft de niveaus repository en omgeving, plus organisatiegeheimen. Voor alles wat deployt zou ik hem op een omgeving met verplichte reviewers zetten. Dan kan een verdwaalde branch hem niet gebruiken.

Drie waarden zijn niet geheim: ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID en ELIDO_HOST. Ze horen in configuratievariabelen, waar je ze later weer kunt teruglezen, en van de drie moet je het domein-ID het eerst goed hebben, want de aanmaakaanroep vereist het. Je zoekt het één keer op met GET /v1/workspaces/{workspace_id}/domains, dat van elk domein de id en hostname teruggeeft.

Koppel het geheim aan de ene stap die de API aanroept, niet aan de hele job. Een env op stapniveau houdt het buiten elk ander proces dat de job start, inclusief de acties van derden die je niet zelf hebt geschreven. Hou het smal.

Een werkende workflow om bij elke pull request een URL te verkorten

Dit is het complete bestand voor de gangbaarste reden om een URL in een GitHub-workflow te verkorten, een previewlink per pull request. Zet het in .github/workflows/preview-link.yml en pas de DEST-regel aan naar waar je preview-deploys landen.

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

Een paar regels verdienen uitleg. --fail-with-body maakt van een 4xx of 5xx een mislukte stap en toont toch de foutbody, wat gewone curl -s niet doet; die sluit met 0 bij een 401 en de job gaat vrolijk door. De requestbodies worden gebouwd met jq -n in plaats van met stringinterpolatie, zodat een bestemming met aanhalingstekens of een ampersand de JSON niet kan breken. En de commentaarstap draait alleen bij opened. Omdat de korte URL nooit verandert, blijft één commentaar juist voor de hele levensduur van de PR, en krijgt niemand een melding bij elke push.

Het concurrency-blok is geen decoratie. Twee snelle pushes zouden anders twee runs starten die allebei "nog geen link" zien en allebei een link proberen te maken. De docs van GitHub over workflowconcurrency beheersen leggen de groepering uit; hier wordt de oudere run geannuleerd en treedt de race nooit op.

Onder het woord idempotent zitten twee verschillende fouten, en de workflow handelt ze apart af. De eerste is de herhaling: een tweede push, een handmatige "Re-run jobs", een heropende PR. Daar dient de lookup-dan-PATCH-tak voor. De tweede is het herhaalde verzoek, waarbij curl de POST verstuurt, het netwerk wegvalt voordat het antwoord aankomt en curl hem opnieuw stuurt. De Idempotency-Key-header dekt die. Elido bewaart het eerste geslaagde antwoord 24 uur bij de sleutel en speelt het opnieuw af voor een overeenkomend herhaald verzoek, dus de aanmaak draait één keer; de volledige werking staat in limieten, retries en idempotentie.

Beslissingsstroom voor het maken van een korte link in GitHub Actions zonder duplicaten: een exacte slugmatch in je werkruimte leidt tot PATCH, geen match leidt tot POST, en een 409 betekent dat een andere werkruimte op een gedeeld domein de slug al bezit

Slugbotsingen zijn het deel dat mensen missen. Slugs zijn uniek per redirectdomein, niet per werkruimte. Op een gedeeld domein zit elke andere Elido-klant in dezelfde naamruimte, en een slug zo gewoon als pr-12 is waarschijnlijk al door iemand geclaimd. De lookup ziet hun link niet (hij toont alleen jouw werkruimte), dus de POST gaat eruit en komt terug als 409 slug already exists for this domain. Twee oplossingen: voeg een projectwoord aan de slug toe, of zet CI-links op je eigen domein, waar de naamruimte alleen van jou is. Ik zou beide doen.

Er is een tweede, geniepiger reden waarom de reponaam aan het eind van de slug staat en niet vooraan. De parameter q is een substringmatch over slug, bestemming en titel. Met myapp-pr-1 geeft de zoekopdracht ook myapp-pr-10 tot en met myapp-pr-199 terug, meer dan de 100 resultaten die één pagina teruggeeft, en de ene link die je wilde, de oudste, valt aan het eind af. pr-1-myapp matcht alleen zichzelf. Een klein detail, en het kostte me een beschamend lange middag van "waarom krijgt PR #1 steeds een 409" om het te zien.

Drie CI-korte-linkjobs die het automatiseren waard zijn

De previewworkflow is één patroon. Verander de trigger, de slug en de bestemming, en dezelfde stap dekt het meeste wat teams echt automatiseren. (Release notes zijn een eigen onderwerp, behandeld in links verkorten in release notes.)

ToepassingTriggerSlugWat de stap doet
Preview-deploy per PRpull_requestpr-42-myappUpsert bij elke push, verwijderen bij sluiten
Docs-deploypush naar maindocs-myappPATCH naar de net gedeployde docs-URL
Nieuwste buildpush naar main of taglatest-myappPATCH via een bewaard link-ID, geen lookup
Nachtelijk artefactschedulenightly-myappPATCH naar de nieuwste artefact-URL

De nieuwste-buildcase is de eenvoudigste van allemaal. Maak de link één keer met de hand, sla zijn numerieke ID op als variabele, en de job krimpt tot één aanroep:

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

Houd deze bewegende links op een 302, de standaard als je redirect_status niet instelt. Een 301 vertelt browsers dat ze het antwoord mogen cachen, en mensen die gisteren klikten blijven op de build van gisteren landen; ons artikel over 301- versus 302-redirects heeft de lange versie.

Ruim previewlinks op als de PR sluit. Voeg closed toe aan de triggertypes, hergebruik de lookup en stuur DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Een verwijderde slug is vrij voor hergebruik. Wil je liever de klikgeschiedenis houden, gebruik dan PATCH met {"status": "disabled"}; de upsert hierboven zet bij elke run status: "active", dus een heropende PR brengt zijn link terug.

Wil je het op één repo proberen? Start een gratis werkruimte, maak een sleutel, en de workflow hierboven draait zoals geschreven zodra de drie variabelen zijn ingesteld.

API-sleutels met minimale rechten voor CI

De sleutel in een CI-geheim moet precies kunnen wat de workflow doet, niet meer. Dat is moeilijker dan het klinkt, door de manier waarop persoonlijke sleutels werken.

Een persoonlijke API-sleutel authenticeert als de persoon die hem maakte. Wat die persoon kan, kan de sleutel, en als diegene het bedrijf verlaat verdwijnt de sleutel met zijn account. Voor CI zou ik in plaats daarvan een machinegebruiker gebruiken: een serviceaccount dat bij één werkruimte hoort, een eigen rol heeft en tokens die alleen een ingelogde menselijke beheerder kan aanmaken of intrekken. Maak het aan onder Machine users in het dashboard met de rol editor, de laagste ingebouwde rol die links kan maken, bewerken en verwijderen, en maak er dan een token voor met een vervaldatum. Een machinegebruiker uitschakelen doodt alle tokens die hij heeft in één keer, precies de knop die je wilt op de dag dat een geheim lekt.

Vier gewoonten kosten niets:

  • Eén token per repository, naar de repository genoemd, zodat het auditspoor zegt welke repo welke link maakte.
  • Omgevingsgeheimen met verplichte reviewers voor elke workflow die een link wijzigt waar mensen op vertrouwen.
  • permissions: expliciet bovenaan de workflow, zoals in het voorbeeld, zodat de GITHUB_TOKEN alleen krijgt wat de job nodig heeft.
  • Nooit pull_request_target om het geheim te bereiken vanuit fork-PR's. Het stuk van GitHub Security Lab over pwn requests voorkomen laat zien waarom onvertrouwde code naast een schrijftoken slecht afloopt.

Werkruimtes kunnen API-toegang ook beperken met een IP-toegangslijst. Dat is een sterke maatregel voor zelf gehoste runners met vaste uitgaande adressen, en bijna nutteloos voor door GitHub gehoste runners, waarvan de adressen uit een zeer grote, wisselende pool komen. De eigen referentie voor veilig gebruik van GitHub is een uur waard als je workflows productie raken.

Wat er in de praktijk misgaat

De meeste fouten komen uit vier plekken, en elk toont zich als een leesbare fout als --fail-with-body aanstaat. Een 404 op elke aanroep betekent meestal dat de variabele met het werkruimte-ID fout is of dat de sleutel bij een andere werkruimte hoort. Een 400 met domain_id is required betekent dat de variabele leeg is, meestal omdat hij is ingesteld op een andere omgeving dan de job gebruikt. Een 409 is de botsing in de gedeelde naamruimte uit de sectie over idempotentie. En een 429 betekent dat je boven de limiet per sleutel zit, wat één upsert per run niet haalt maar een matrix van vijftig jobs wel.

Eén ding is helemaal geen fout. Na een PATCH kan een bezoeker nog even de oude bestemming bereiken, omdat redirects dicht bij de bezoeker worden gecachet om ze snel te houden. Een smoketest die direct na de update de nieuwe bestemming afdwingt, wordt onbetrouwbaar. Poll met een korte backoff of controleer tegen het API-antwoord.

Wil je dat de stap naar buiten rapporteert, combineer hem dan met webhooks voor linkevents, die afgaan als een link verandert, of met de curl- en jq-patronen uit de CLI-gids om lokaal te testen voordat je de workflow commit. De API- en SDK-referentie toont elk veld dat de link-endpoints accepteren.

Lees de cornerstone → Beheer je korte links als Terraform

Gerelateerd op de blog

Veelgestelde vragen

Kan GitHub Actions korte links maken?

Ja. Een workflowstap kan de REST-API van elke verkorter aanroepen met curl, dat samen met jq vooraf is geïnstalleerd op door GitHub gehoste runners. De stap leest de API-sleutel uit een versleuteld geheim, stuurt de bestemmings-URL mee en schrijft de resulterende korte URL naar een stap-output, zodat latere stappen hem in een pull-requestcommentaar of een jobsamenvatting kunnen plaatsen.

Hoe bewaar ik een API-sleutel van een URL-verkorter in GitHub Actions?

Sla hem op als versleuteld repository- of omgevingsgeheim en koppel hem dan in de ene stap die hem nodig heeft aan een env-regel zoals ELIDO_API_KEY: secrets.ELIDO_API_KEY binnen de expressiesyntaxis. GitHub maskeert de waarde in logs. Houd niet-geheime waarden zoals de werkruimte-ID en het domein-ID in configuratievariabelen, zodat ze leesbaar blijven.

Hoe voorkom ik dat elke workflowrun dubbele korte links maakt?

Maak van de stap een upsert. Leid de slug af van iets stabiels, zoals het pull-requestnummer, zoek hem eerst op en stuur een PATCH om de bestemming te wijzigen als hij al bestaat. Maak alleen aan als de lookup leeg terugkomt. Een Idempotency-Key-header op de aanmaakaanroep dekt het aparte geval van een herhaald verzoek na een netwerktime-out.

Waarom krijgt mijn workflow een 409 bij het maken van een korte link?

De slug is al bezet op dat domein. Bij Elido zijn slugs uniek per redirectdomein, en een gedeeld domein deel je met alle andere werkruimtes, dus een generieke slug als pr-12 bestaat waarschijnlijk al. Voeg een projectvoorvoegsel of -achtervoegsel aan de slug toe, of gebruik je eigen domein waar de hele naamruimte van jou is.

Werken korte-linkstappen bij pull requests vanaf forks?

Niet met de gewone pull_request-trigger, omdat GitHub geen repositorygeheimen doorgeeft aan workflows die door een fork zijn gestart. Sla de job voor forks over met een if-voorwaarde op de head-repository. Overstappen op pull_request_target om het geheim te krijgen is riskant, omdat die draait met schrijfrechten naast code die je niet hebt beoordeeld.

Moet een link naar de nieuwste build een 301- of een 302-redirect gebruiken?

Gebruik een 302 of 307. Browsers mogen een 301 onbeperkt cachen, dus terugkerende bezoekers blijven op een oude build landen nadat je workflow de link heeft verplaatst. Elido-links hebben standaard 302 als je geen redirect_status instelt, de juiste keuze voor elke link waarvan een pipeline de bestemming wijzigt.

Probeer Elido

Plak een URL, krijg een werkende korte link

Geen aanmelding nodig. Link blijft 30 dagen actief. Meld je aan om hem voor altijd te bewaren.

Geen account nodig. Meld je gratis aan voor 50 links per maand.

Probeer Elido

In de EU gehoste URL-shortener met aangepaste domeinen, uitgebreide analyses en een open API. Gratis abonnement - geen creditcard nodig.

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

Verder lezen