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 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:
| Variabele | Zichtbaarheid | Beschermd | Gelezen door |
|---|---|---|---|
ELIDO_PREVIEW_KEY | Masked and hidden | Nee | Merge-requestpipelines |
ELIDO_RELEASE_KEY | Masked and hidden | Ja | Tag-pipelines op beschermde tags |
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WS | Zichtbaar | Nee | Elke job (ID's zijn geen geheimen) |
ELIDO_DOMAIN_ID, SHORT_HOST | Zichtbaar | Nee | Elke 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.
Een werkende .gitlab-ci.yml-job om een korte link te maken
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.
Review-app-links per merge request
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.
Een stabiele latest-link omzetten in tag-pipelines
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.
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
- Quickstart voor de URL-verkorter-API - het REST-oppervlak dat elke job hierboven aanroept.
- Limieten en idempotentie voor verkorter-API's - waarom de retrylogica eruitziet zoals ze eruitziet.
- Een URL-verkorter vanaf de opdrachtregel - dezelfde aanroepen als herbruikbaar script.
- 301 vs 302-redirects - waarom een bewegende link een tijdelijke redirect nodig heeft.
- Linkredirects monitoren met Sentry en Datadog - een kapotte bestemming opvangen nadat de pipeline groen is geworden.
- Korte links vanuit GitHub Actions - het GitHub-equivalent, met geheimen en concurrency.
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.