9 min leestijdIntegraties

GitLab CI URL-verkorter: korte links uit elke pipeline

Gebruik GitLab CI als URL-verkorter: maak review-app-links per merge request en een stabiele latest-link bij tags, met een gemaskeerde sleutel.

Marius Voß
DevRel · edge infra

Ja, GitLab CI kan fungeren als je URL-verkorter. Een job met curl en één API-sleutel kan bij elke merge request een korte link maken, die naar de review-app laten wijzen en een stabiele latest-link naar de nieuwe release verplaatsen als je een tag pusht. Het zijn zo'n veertig regels YAML. Het werkt vandaag.

Het deel dat mensen verkeerd doen is niet de HTTP-aanroep. Het is de sleutel: waar hij staat, welke pipelines hem kunnen lezen en hoeveel schade hij kan aanrichten als een branchjob hem lekt. Deze gids besteedt dus net zoveel tijd aan variabelen en scoping als aan de .gitlab-ci.yml zelf. Beheer je liever langlevende links declaratief, dan past de Terraform-aanpak voor korte links beter; pipelines passen bij links die samen met de code worden geboren en gepensioneerd.

Eén statusnotitie vooraf. De native GitLab-integratie van Elido komt eraan, is nog niet live, en niets hieronder hangt ervan af. Wil je de beheerde versie? De GitLab-integratiepagina heeft de wachtlijst.

Wat een GitLab CI URL-verkorterjob doet

Een pipelineverkorter doet drie dingen en alleen die. Hij maakt een link als een slug niet bestaat, werkt de bestemming bij als hij wel bestaat en schakelt de link uit als het ding waar hij naar wees verdwijnt. Analytics en QR-codes blijven aan de Elido-kant.

Het API-oppervlak is klein. Links staan onder /v1/workspaces/{workspace_id}/links: POST maakt er een aan en heeft een domain_id plus een destination_url nodig, PATCH /links/{link_id} wijzigt velden van een bestaande en GET /links?q= zoekt op slug, bestemming of titel. Authenticatie is één header: Authorization: Bearer elido_.... De sleutel komt van de pagina met API-sleutels in het dashboard.

Dat is het hele contract. Het API- en SDK-overzicht toont de overige endpoints, maar een pipeline heeft zelden meer nodig dan deze drie.

De GitLab-integratiepagina van Elido met de installatiestappen voor het maken van korte links vanuit een CI-pipeline

De sleutel opslaan als gemaskeerde, beschermde variabele

GitLab geeft je hier twee schakelaars die ertoe doen, en ze doen verschillend werk. Maskeren verbergt een waarde in joblogs. Bescherming bepaalt welke pipelines de waarde überhaupt ontvangen.

Kies in Settings, CI/CD, Variables bij het aanmaken van de variabele Masked and hidden. Hidden (algemeen beschikbaar sinds GitLab 17.6) betekent dat niemand de waarde later op de instellingenpagina kan onthullen, wat je wilt voor een credential. De docs over GitLab CI/CD-variabelen noemen de eisen aan een gemaskeerde waarde: één regel, geen spaties, minstens 8 tekens. Elido-sleutels zijn elido_ gevolgd door base32, dus ze voldoen.

Dezelfde pagina is duidelijk over de grens: maskeren "is not a guaranteed way to prevent malicious users from accessing variable values." Een job die de variabele base64-codeert en afdrukt, loopt zo langs het masker. Zie maskeren als loghygiëne, niet als toegangscontrole.

Bescherming is de toegangscontrole. Een beschermde variabele bereikt alleen pipelines op beschermde branches of beschermde tags, wat het ene probleem veroorzaakt dat elk team in de eerste week tegenkomt: je merge-requestpipeline draait op een featurebranch, dus de beschermde sleutel komt aan als lege string en de job faalt met een 401 die op een typefout lijkt.

Ik zou dat oplossen met twee sleutels in plaats van de ene te verzwakken. Dit is de opzet die ik zou gebruiken:

VariabeleZichtbaarheidBeschermdGelezen door
ELIDO_PREVIEW_KEYMasked and hiddenNeeMerge-requestpipelines
ELIDO_RELEASE_KEYMasked and hiddenJaTag-pipelines op beschermde tags
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSZichtbaarNeeElke job (ID's zijn geen geheimen)
ELIDO_DOMAIN_ID, SHORT_HOSTZichtbaarNeeElke job

De previewsleutel hoort bij een aparte werkruimte die niets bevat dan reviewlinks. Iedereen die een branch kan pushen kan in principe een onbeschermde variabele exfiltreren, dus zorg dat het ergste wat ze kunnen bereiken een hoop wegwerplinks als mr-142 is, terwijl de releasesleutel in je echte werkruimte staat en alleen draait op tags die je hebt beschermd.

Geef beide sleutels de rol Editor en een vervaldatum; 90 dagen past bij de previewsleutel. Editor is de laagste voorinstelling die links kan schrijven, en kan ze ook verwijderen; API-sleutels krijgen een van de voorgedefinieerde rollen, en ik zou precies voor dit geval graag een voorinstelling hebben die alleen kan maken en bijwerken; die is er nog niet. Scheiding van werkruimtes is wat de schade echt beperkt.

Dit is het gedeelde onderdeel: een upsert die de slug opzoekt, de link maakt als hij ontbreekt en anders patcht. Zet hem in een verborgen job en breid hem uit.

.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

De zoekopdracht met q is een substringmatch, dus het jq-filter beperkt hem tot de exacte slug op het exacte domein. Zonder dat geeft een zoekopdracht naar web-mr-14 gewoon web-mr-142 terug. Haal je domain_id één keer op met GET /v1/workspaces/{id}/domains en bewaar hem als gewone variabele; een eigen host die je via eigen domeinen hebt ingesteld leest in een merge request beter dan een generieke.

Levenscyclus van een korte link voor een GitLab review-app: een merge-requestpipeline upsert de slug, schrijft SHORT_URL naar een dotenv-rapport dat als omgevings-URL dient, en een stopjob schakelt de link uit als de merge request sluit

Review-apps is de naam die GitLab geeft aan een tijdelijke omgeving per branch of merge request, en de docs over review-apps bouwen ze op dynamische omgevingen. Hun URL's zijn meestal lelijk: een hash, een namespace, de hostnaam van een cloudprovider. Een korte link als go.example.com/web-mr-142 kun je hardop zeggen in een standup.

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"

De truc is het dotenv-rapport. De upsert schrijft SHORT_URL naar link.env, GitLab leest het terug en environment:url wordt de korte link, zodat de knop View app op de merge request web-mr-142 opent in plaats van de ruwe hostnaam. De docs over omgevingen beschrijven dit patroon met dynamische URL's.

CI_MERGE_REQUEST_IID is uniek per project en verandert nooit gedurende het leven van de merge request, daarom landt elke push naar dezelfde MR op dezelfde slug en patcht de upsert in plaats van te dupliceren. De referentie met voorgedefinieerde variabelen heeft de volledige lijst als je een andere sleutel wilt.

Opruimen is een job met action: stop. Die moet de rules van de startjob delen, anders kan GitLab hem niet automatisch activeren:

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"

Ik schakel uit in plaats van te verwijderen. Een uitgeschakelde link houdt zijn klikgeschiedenis, en als iemand de MR heropent zet de volgende pipeline hem via dezelfde upsert terug op active. GIT_STRATEGY: none staat er omdat de branch dan misschien weg is.

Als je review-apps de releases tien tegen één overtreffen, beginnen daar de abonnementslimieten te knellen. Bekijk de linkruimte op de prijzenpagina voordat je dit aan een drukke monorepo koppelt, en start een gratis werkruimte voor de previews terwijl je het test.

Het tweede patroon draait op tags en doet het omgekeerde van de reviewlink: één slug die nooit verandert, waarvan de bestemming bij elke release vooruit schuift. Je README kan voor altijd naar go.example.com/cli-latest wijzen.

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+$/

Combineer de regel met een beschermd tagpatroon als v*, zodat alleen maintainers de tags kunnen maken die hem triggeren; anders is de beschermde sleutel er gewoon niet en faalt de job gesloten, wat het gedrag is dat je wilt. Wil je ook een permanente link per versie, draai dan dezelfde job een tweede keer met SLUG: "cli-${CI_COMMIT_REF_SLUG}", wat v1.4.0 omzet in cli-v1-4-0.

Zet redirect_status bij een latest-link niet op 301. Een 301 is een permanente belofte die browsers mogen cachen, en een latest-link breekt die belofte bij elke release. Elido gebruikt standaard 302 als je het veld weglaat, en ons artikel over 301- versus 302-redirects loopt de gevallen door waarin de keuze echt uitmaakt.

Eén eerlijke kanttekening: een gewijzigde bestemming kan een paar minuten nodig hebben om elke edge-locatie te bereiken, dus een smoketest die de korte link in de allereerstvolgende seconde met curl opvraagt, ziet misschien nog de vorige release. Toets in plaats daarvan op het API-antwoord, of wacht even voordat je de Location-header controleert.

Minimale rechten voor een GitLab CI URL-verkorter: een onbeschermde previewsleutel beperkt tot een werkruimte voor previews voor merge-requestpipelines, en een beschermde releasesleutel die alleen tag-pipelines op beschermde tags kunnen lezen om de latest-link om te zetten

Idempotentie, retries en limieten

Pipelines worden herhaald. Runners sterven midden in een job, iemand klikt op Retry bij een rode job, en twee pushes komen dertig seconden na elkaar binnen en racen tegen elkaar. De upsert hierboven overleeft alle drie, en het is goed om te weten waarom.

De Idempotency-Key-header maakt een herhaalde POST veilig: de API cachet een geslaagd antwoord 24 uur en speelt het opnieuw af voor dezelfde sleutel, zodat een herhaling van dezelfde pipeline de oorspronkelijke link terugkrijgt in plaats van een fout. Als je de sleutel opbouwt uit CI_PIPELINE_ID en de slug, worden herhalingen binnen een pipeline opnieuw afgespeeld terwijl een nieuwe pipeline een verse poging krijgt. De 409-tak vangt de race tussen twee verschillende pipelines op, en het pad van opzoeken en dan patchen maakt een tweede run in feite een no-op.

Limieten gelden per sleutel, bovenop een limiet per werkruimte, en gloednieuwe werkruimtes hebben ook een lager dagelijks plafond voor het maken van links terwijl ze reputatie opbouwen. Een handvol merge requests merkt dat niet. Een monorepo die veertig review-apps tegelijk opstart misschien wel, dus behandel een 429 als herhaalbaar met het retry-sleutelwoord van GitLab en laat het luid falen bij een 402, wat een abonnementslimiet betekent en geen tijdelijke fout. Ons diepere artikel over limieten en idempotentie voor verkorter-API's behandelt backoff uitgebreider dan een CI-job nodig heeft.

Sla het jq-filter op exacte match over en de pipeline van MR 14 patcht stilletjes de link van MR 142. Het eerste symptoom is meestal een verwarde designer. Houd het filter.

Wordt shell in YAML onhandig, dan passen dezelfde aanroepen netjes in een script dat je in de repo commit, en de gids over de URL-verkorter-CLI toont die vorm.

Lees de cornerstone → Korte links als Terraform: links beheren als code

Gerelateerd op de blog

Veelgestelde vragen

Kan GitLab CI korte links maken?

Ja. Elke job die curl kan draaien kan de REST-API van een URL-verkorter aanroepen, dus een GitLab CI-job kan een korte link maken, de bestemming bijwerken of hem uitschakelen. De API-sleutel staat in een gemaskeerde CI/CD-variabele en de job stuurt hem mee als Bearer-token. Hiervoor is geen native GitLab-integratie nodig.

Hoe bewaar ik een API-sleutel veilig in GitLab CI?

Voeg hem toe onder Settings, CI/CD, Variables met de zichtbaarheid op Masked and hidden, en vink Protect variable aan als alleen beschermde branches of tags hem mogen lezen. Maskeren houdt de waarde uit joblogs, maar de eigen docs van GitLab zeggen dat het geen gegarandeerde verdediging is, dus beperk de sleutel zelf tot het minimum dat hij nodig heeft.

Waarom is mijn beschermde variabele leeg in een merge-requestpipeline?

Beschermde variabelen worden alleen doorgegeven aan pipelines op beschermde branches of beschermde tags. Een merge-requestpipeline vanaf een featurebranch komt standaard niet in aanmerking, dus de variabele komt leeg binnen. Gebruik ofwel een aparte, onbeschermde sleutel met minder rechten voor reviewjobs, ofwel bewaar de beschermde sleutel alleen voor tag-pipelines.

Hoe geef ik elke GitLab review-app een korte link?

Draai een job op merge-requestpipelines die een slug upsert, opgebouwd uit de projectnaam en CI_MERGE_REQUEST_IID, wijzend naar de URL van de review-app. Schrijf de resulterende korte URL naar een dotenv-rapport en gebruik hem als environment:url, zodat de merge-requestwidget er direct naar linkt. Een stopjob schakelt de link uit als de omgeving stopt.

Moet een korte link naar de nieuwste release een 301 of 302 gebruiken?

Gebruik een 302. Een latest-link wijzigt bij elke release van bestemming, en een 301 vertelt browsers en caches dat de verhuizing permanent is, dus sommige clients blijven mensen naar de oude versie sturen. Elido geeft nieuwe links standaard 302 als je redirect_status niet instelt, wat hier de juiste keuze is.

Is er een native GitLab-integratie voor Elido?

Nog niet. Een native GitLab-integratie is onderweg en je kunt je op de wachtlijst zetten op de GitLab-integratiepagina. Alles in deze gids werkt vandaag al via de publieke REST-API vanuit een pipelinejob, zonder iets te installeren aan de GitLab-kant behalve een CI/CD-variabele.

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

Verder lezen