Da, GitLab CI poate funcționa ca scurtător de URL. Un job cu curl și o cheie API poate crea un link scurt la fiecare merge request, îl poate îndrepta spre review app și poate muta un link latest stabil la noua lansare când dai push unui tag. Sunt vreo patruzeci de linii de YAML. Funcționează azi.
Partea pe care o greșesc oamenii nu este apelul HTTP. Este cheia: unde stă, ce pipeline-uri o pot citi și cât de mult rău ar putea face dacă un job de pe un branch ar scurge-o. De aceea ghidul acesta petrece la fel de mult timp pe variabile și pe restrângerea accesului ca pe .gitlab-ci.yml în sine. Dacă preferi să gestionezi declarativ linkurile de lungă durată, abordarea Terraform pentru linkuri scurte se potrivește mai bine; pipeline-urile se potrivesc linkurilor care se nasc și se retrag odată cu codul.
O notă de stare de la început. Integrarea nativă GitLab a Elido urmează să apară, nu este live, iar nimic din cele de mai jos nu depinde de ea. Vrei versiunea gestionată? Pagina integrării GitLab are lista de așteptare.
Ce face un job de scurtător de URL în GitLab CI
Un scurtător într-un pipeline face trei lucruri și doar trei. Creează un link când slugul nu există, actualizează destinația când există și dezactivează linkul când ceea ce indica dispare. Analitica și codurile QR rămân de partea Elido.
Suprafața API este mică. Linkurile se află sub /v1/workspaces/{workspace_id}/links: POST creează unul și cere un domain_id plus un destination_url, PATCH /links/{link_id} schimbă câmpuri ale unuia existent, iar GET /links?q= caută după slug, destinație sau titlu. Autentificarea este un singur header: Authorization: Bearer elido_.... Cheia vine de pe pagina de chei API din dashboard.
Acesta e tot contractul. Prezentarea API și SDK listează restul endpointurilor, dar un pipeline rareori are nevoie de mai mult decât aceste trei.
Stocarea cheii ca variabilă mascată și protejată
GitLab îți dă aici două comutatoare care contează și care fac lucruri diferite. Mascarea ascunde o valoare în logurile jobului. Protecția controlează ce pipeline-uri primesc valoarea, în general.
La Settings, CI/CD, Variables, alege Masked and hidden când creezi variabila. Hidden (disponibil general din GitLab 17.6) înseamnă că nimeni nu poate dezvălui ulterior valoarea în pagina de setări, ceea ce vrei pentru un credențial. Documentația variabilelor GitLab CI/CD listează cerințele pentru o valoare mascată: o singură linie, fără spații, cel puțin 8 caractere. Cheile Elido sunt elido_ urmat de base32, deci se califică.
Aceeași pagină este tranșantă în privința limitei: mascarea „is not a guaranteed way to prevent malicious users from accessing variable values." Un job care codează variabila în base64 și o afișează trece direct pe lângă mască. Tratează mascarea ca igienă a logurilor, nu ca pe un control de acces.
Protecția este controlul de acces. O variabilă protejată ajunge doar la pipeline-urile de pe branch-uri sau tag-uri protejate, ceea ce creează singura problemă pe care o lovește fiecare echipă în prima săptămână: pipeline-ul tău de merge request rulează pe un feature branch, așa că cheia protejată sosește ca șir gol, iar jobul eșuează cu un 401 care arată ca o greșeală de tastare.
Eu aș rezolva asta cu două chei, nu slăbind-o pe cea existentă. Iată configurația pe care aș folosi-o:
| Variabilă | Vizibilitate | Protejată | Citită de |
|---|---|---|---|
ELIDO_PREVIEW_KEY | Masked and hidden | Nu | Pipeline-urile de merge request |
ELIDO_RELEASE_KEY | Masked and hidden | Da | Pipeline-urile de tag pe tag-uri protejate |
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WS | Vizibilă | Nu | Orice job (ID-urile nu sunt secrete) |
ELIDO_DOMAIN_ID, SHORT_HOST | Vizibilă | Nu | Orice job |
Cheia de preview aparține unui workspace separat, care nu conține nimic în afară de linkuri de review. Oricine poate da push unui branch poate, în principiu, exfiltra o variabilă neprotejată, deci asigură-te că cel mai rău lucru la care ajunge este un morman de linkuri mr-142 de unică folosință, în timp ce cheia de release trăiește în workspace-ul tău real și rulează doar pe tag-uri pe care le-ai protejat.
Dă ambelor chei rolul Editor și o dată de expirare; 90 de zile se potrivesc celei de preview. Editor este cel mai mic preset care poate scrie linkuri, și le poate și șterge; cheile API primesc unul dintre rolurile prestabilite, iar mi-aș dori un preset doar pentru creare și actualizare exact pentru acest caz; încă nu există. Separarea workspace-urilor este ceea ce limitează de fapt raza de impact.
Un job .gitlab-ci.yml funcțional pentru crearea unui link scurt
Iată piesa comună: un upsert care caută slugul, creează linkul dacă lipsește și îl actualizează altfel. Pune-l într-un job ascuns și extinde-l.
.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
Căutarea q este o potrivire de subșir, deci filtrul jq o restrânge la slugul exact de pe domeniul exact. Fără el, o căutare pentru web-mr-14 ar returna fără probleme web-mr-142. Obține domain_id o singură dată cu GET /v1/workspaces/{id}/domains și stochează-l ca variabilă simplă; un host de brand configurat prin domenii personalizate arată mai bine într-un merge request decât unul generic.
Linkuri scurte de review app per merge request
Review apps este numele GitLab pentru un mediu temporar per branch sau merge request, iar documentația review apps le construiește pe medii dinamice. URL-urile lor tind să fie urâte: un hash, un namespace, hostname-ul unui furnizor cloud. Un link scurt precum go.example.com/web-mr-142 este ceva ce poți spune cu voce tare la un 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"
Trucul este raportul dotenv. Upsertul scrie SHORT_URL în link.env, GitLab îl citește înapoi, iar environment:url devine linkul scurt, deci butonul View app din merge request deschide web-mr-142 în loc de hostname-ul brut. Documentația mediilor descrie acest tipar de URL dinamic.
CI_MERGE_REQUEST_IID este unic per proiect și nu se schimbă niciodată cât trăiește merge requestul, motiv pentru care fiecare push în același MR ajunge pe același slug, iar upsertul actualizează în loc să duplice. Referința variabilelor predefinite are lista completă, dacă vrei o altă cheie.
Curățenia este un job cu action: stop. Trebuie să împartă rules cu jobul de start, altfel GitLab nu îl poate declanșa automat:
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"
Dezactivez, nu șterg. Un link dezactivat își păstrează istoricul clicurilor, iar dacă cineva redeschide MR-ul, următorul pipeline îl readuce pe active prin același upsert. GIT_STRATEGY: none este acolo pentru că branch-ul poate să fi dispărut până atunci.
Dacă review apps le depășesc de zece ori lansările, acolo încep să muște limitele planului. Verifică alocația de linkuri pe pagina de prețuri înainte să conectezi asta la un monorepo aglomerat și pornește un workspace gratuit pentru preview-uri cât testezi.
Repoziționarea unui link latest stabil la pipeline-urile de tag
Al doilea tipar rulează pe tag-uri și face opusul linkului de review: un singur slug care nu se schimbă niciodată, a cărui destinație avansează cu fiecare lansare. README-ul tău poate indica go.example.com/cli-latest pentru totdeauna.
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+$/
Asociază regula cu un tipar de tag protejat precum v*, ca doar maintainerii să poată crea tag-urile care o declanșează; altfel cheia protejată pur și simplu nu va fi acolo, iar jobul eșuează închis, ceea ce este comportamentul pe care îl vrei. Dacă vrei și un link permanent per versiune, rulează același job a doua oară cu SLUG: "cli-${CI_COMMIT_REF_SLUG}", care transformă v1.4.0 în cli-v1-4-0.
Nu seta redirect_status pe 301 la un link latest. Un 301 este o promisiune permanentă pe care browserele au voie să o rețină în cache, iar un link latest încalcă promisiunea la fiecare lansare. Elido folosește implicit 302 când omiți câmpul, iar articolul nostru despre redirecționările 301 vs 302 parcurge cazurile în care alegerea chiar mușcă.
O rezervă sinceră: o destinație schimbată poate dura câteva minute să ajungă la fiecare locație edge, deci un smoke test care apelează cu curl linkul scurt chiar în secunda următoare poate vedea încă lansarea anterioară. Verifică în schimb răspunsul API sau așteaptă puțin înainte să verifici headerul Location.
Idempotență, reîncercări și limite de rată
Pipeline-urile se reîncearcă. Runnerele mor în mijlocul jobului, cineva apasă Retry pe un job roșu, iar două push-uri ajung la treizeci de secunde unul de altul și se întrec. Upsertul de mai sus le supraviețuiește pe toate trei, și merită să știi de ce.
Headerul Idempotency-Key face sigur un POST reîncercat: API-ul reține un răspuns reușit 24 de ore și îl redă pentru aceeași cheie, deci o reîncercare a aceluiași pipeline primește înapoi linkul original, nu o eroare. Construirea cheii din CI_PIPELINE_ID și slug înseamnă că reîncercările din cadrul unui pipeline sunt redate, în timp ce un pipeline nou primește o încercare proaspătă. Ramura 409 tratează cursa dintre două pipeline-uri diferite, iar calea căutare-apoi-actualizare face ca o a doua rulare să fie, în efect, o operație nulă.
Limitele de rată sunt per cheie, pe lângă o limită per workspace, iar workspace-urile noi au și un plafon zilnic mai mic de creare de linkuri cât își construiesc reputația. Câteva merge requesturi nu o vor simți. Un monorepo care pornește patruzeci de review apps deodată poate, deci tratează un 429 ca reîncercabil cu cuvântul cheie retry din GitLab și eșuează zgomotos la un 402, care înseamnă o limită de plan, nu o eroare tranzitorie. Articolul nostru mai amplu despre limite de rată și idempotență pentru API-uri de scurtătoare acoperă backoff-ul mai detaliat decât are nevoie un job CI.
Sari peste filtrul jq cu potrivire exactă și pipeline-ul MR 14 va actualiza în tăcere linkul MR 142. Primul simptom este de obicei un designer nedumerit. Păstrează filtrul.
Dacă shellul în YAML devine greu de stăpânit, aceleași apeluri se împachetează frumos într-un script pe care îl comiți în repository, iar ghidul CLI pentru scurtător de URL arată forma aceasta.
Citește articolul-pilon → Linkuri scurte ca Terraform: gestionarea linkurilor ca cod
Pe același subiect pe blog
- Quickstart API pentru scurtător de URL - suprafața REST pe care o apelează fiecare job de mai sus.
- Limite de rată și idempotență pentru API-uri de scurtătoare - de ce arată logica de reîncercare așa cum arată.
- Un scurtător de URL din linia de comandă - aceleași apeluri ca script reutilizabil.
- Redirecționări 301 vs 302 - de ce un link mobil are nevoie de o redirecționare temporară.
- Monitorizarea redirecționărilor de linkuri cu Sentry și Datadog - prinderea unei destinații stricate după ce pipeline-ul devine verde.
- Linkuri scurte din GitHub Actions - echivalentul GitHub, cu secrete și concurență.
Întrebări frecvente
Poate GitLab CI să creeze linkuri scurte?
Da. Orice job care poate rula curl poate apela API-ul REST al unui scurtător de URL, deci un job GitLab CI poate crea un link scurt, poate actualiza destinația lui sau îl poate dezactiva. Cheia API stă într-o variabilă CI/CD mascată, iar jobul o trimite ca token Bearer. Pentru asta nu e nevoie de nicio integrare nativă GitLab.
Cum stochez în siguranță o cheie API în GitLab CI?
Adaugă-o la Settings, CI/CD, Variables, cu vizibilitatea setată la Masked and hidden, și bifează Protect variable dacă doar branch-urile sau tag-urile protejate trebuie să o citească. Mascarea ține valoarea departe de logurile jobului, dar documentația GitLab spune că nu este o apărare garantată, așa că restrânge cheia însăși la minimul de care are nevoie.
De ce este goală variabila mea protejată într-un pipeline de merge request?
Variabilele protejate sunt transmise doar pipeline-urilor care rulează pe branch-uri sau tag-uri protejate. Un pipeline de merge request dintr-un feature branch nu se califică implicit, deci variabila sosește goală. Fie folosești o cheie separată, cu privilegii mai mici și neprotejată, pentru joburile de review, fie păstrezi cheia protejată doar pentru pipeline-urile de tag.
Cum dau fiecărui review app GitLab un link scurt?
Rulează un job pe pipeline-urile de merge request care face upsert unui slug construit din numele proiectului și CI_MERGE_REQUEST_IID, indicând spre URL-ul review app-ului. Scrie URL-ul scurt rezultat într-un raport dotenv și folosește-l ca environment:url, astfel încât widgetul merge requestului să trimită direct la el. Un job de oprire dezactivează linkul când mediul se oprește.
Ar trebui un link scurt către ultima lansare să folosească redirecționare 301 sau 302?
Folosește 302. Un link latest își schimbă destinația la fiecare lansare, iar un 301 le spune browserelor și cache-urilor că mutarea este permanentă, deci unii clienți vor continua să trimită oamenii la versiunea veche. Elido setează implicit 302 pentru linkurile noi când nu setezi redirect_status, ceea ce este alegerea corectă aici.
Există o integrare nativă GitLab pentru Elido?
Încă nu. O integrare nativă GitLab este în drum și te poți înscrie pe lista de așteptare de pe pagina integrării GitLab. Tot ce e în acest ghid funcționează azi prin API-ul REST public, dintr-un job de pipeline, fără nimic de instalat în GitLab în afara unei variabile CI/CD.
Încearcă Elido
Lipește un URL, obții un link scurt funcțional
Fără înregistrare. Linkul este activ timp de 30 de zile. Înregistrează-te ca să-l păstrezi pentru totdeauna.
Fără înregistrare. Creează un cont gratuit pentru 50 linkuri pe lună.