10 min di letturaIntegrazioni

URL shortener GitLab CI: link brevi da ogni pipeline

Usa GitLab CI come URL shortener: crea link brevi per le review app a ogni merge request e reindirizza un link stabile latest ai tag, con una chiave mascherata e protetta.

Marius Voß
DevRel · edge infra
Una pipeline URL shortener GitLab CI disegnata come una sequenza di fasi a pixel, in cui build, test e deploy terminano con un job link che scrive un link breve per ogni merge request e reindirizza un link latest ai tag

Sì, GitLab CI può fungere da URL shortener. Un job con curl e una sola chiave API può creare un link breve a ogni merge request, puntarlo alla review app e spostare un link stabile latest sulla nuova release quando esegui il push di un tag. Sono circa quaranta righe di YAML. E funziona già oggi.

La parte che spesso viene sbagliata non è la chiamata HTTP. È la chiave: dove risiede, quali pipeline possono leggerla e quanto danno potrebbe fare se un job di branch la esponesse. Per questo questa guida dedica alle variabili e al loro ambito lo stesso spazio riservato al file .gitlab-ci.yml. Se preferisci gestire i link longevi in modo dichiarativo, l'approccio Terraform ai link brevi è più adatto; le pipeline sono ideali per link che nascono e vengono ritirati insieme al codice.

Una nota iniziale sullo stato. L'integrazione GitLab nativa di Elido è in arrivo, non è ancora live, e nulla di ciò che segue dipende da essa. Vuoi la versione gestita? La pagina dell'integrazione GitLab contiene la lista d'attesa.

Cosa fa un job URL shortener GitLab CI

Un URL shortener basato su pipeline fa tre cose, e solo tre. Crea un link quando lo slug non esiste, aggiorna la destinazione quando esiste e disabilita il link quando ciò a cui puntava non è più disponibile. Analisi e codici QR restano dal lato Elido.

La superficie dell'API è ridotta. I link risiedono sotto /v1/workspaces/{workspace_id}/links: POST ne crea uno e richiede domain_id più destination_url, PATCH /links/{link_id} modifica i campi di un link esistente e GET /links?q= cerca per slug, destinazione o titolo. L'autenticazione è un solo header: Authorization: Bearer elido_.... La chiave proviene dalla pagina delle chiavi API nella dashboard.

Questo è l'intero contratto. La panoramica di API e SDK elenca gli altri endpoint, ma una pipeline raramente ha bisogno di più di questi tre.

Memorizzare la chiave come variabile mascherata e protetta

GitLab mette a disposizione due interruttori importanti in questo caso, e svolgono funzioni diverse. Il masking nasconde un valore nei log dei job. La protezione controlla quali pipeline ricevono il valore.

In Settings, CI/CD, Variables, quando crei la variabile scegli Masked and hidden. Hidden (disponibile in generale da GitLab 17.6) significa che in seguito nessuno potrà rivelare il valore nella pagina delle impostazioni: è ciò che vuoi per una credenziale. La documentazione sulle variabili CI/CD di GitLab elenca i requisiti per un valore mascherato: una sola riga, nessuno spazio, almeno 8 caratteri. Le chiavi Elido sono elido_ seguite da base32, quindi li soddisfano.

La stessa pagina è molto chiara sul limite: il masking "is not a guaranteed way to prevent malicious users from accessing variable values." Un job che codifica la variabile in base64 e la stampa aggira direttamente il masking. Considera il masking una misura di igiene dei log, non un controllo degli accessi.

La protezione è il controllo degli accessi. Una variabile protetta arriva solo alle pipeline su branch o tag protetti, e questo crea il problema in cui si imbatte ogni team nella prima settimana: la pipeline della merge request viene eseguita su un branch di funzionalità, quindi la chiave protetta arriva come stringa vuota e il job fallisce con un 401 che sembra un refuso.

Io risolverei il problema con due chiavi invece di indebolire quella principale. Questa è la configurazione che userei:

VariableVisibilityProtectedRead by
ELIDO_PREVIEW_KEYMasked and hiddenNoMerge request pipelines
ELIDO_RELEASE_KEYMasked and hiddenYesTag pipelines on protected tags
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSVisibleNoAny job (IDs are not secrets)
ELIDO_DOMAIN_ID, SHORT_HOSTVisibleNoAny job

La chiave di preview appartiene a un workspace separato che contiene solo link di review. Chiunque possa fare push su un branch può, in linea di principio, esfiltrare una variabile non protetta, quindi assicurati che il peggio che possa raggiungere sia un mucchio di link usa e getta mr-142, mentre la chiave di release risiede nel workspace reale e viene eseguita solo sui tag che hai protetto.

Assegna a entrambe le chiavi il ruolo Editor e una scadenza; 90 giorni sono adatti a quella di preview. Editor è il preset con i privilegi minimi che può scrivere link, ma può anche eliminarli; le chiavi API usano uno dei ruoli preimpostati e io vorrei un preset che consentisse solo creazione e aggiornamento proprio per questo caso, ma non esiste ancora. È la separazione dei workspace a limitare davvero il raggio d'azione.

Ecco il componente condiviso: un upsert che cerca lo slug, crea il link se manca e lo aggiorna altrimenti. Inseriscilo in un job nascosto ed estendilo.

.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

La ricerca q è una corrispondenza per sottostringa, quindi il filtro jq la restringe allo slug esatto sul dominio esatto. Senza quel filtro, una ricerca di web-mr-14 restituirebbe tranquillamente web-mr-142. Recupera una volta il tuo domain_id con GET /v1/workspaces/{id}/domains e memorizzalo come variabile semplice; un host brandizzato configurato tramite i domini personalizzati fa una figura migliore in una merge request rispetto a uno generico.

Ciclo di vita di un link breve per una review app GitLab: una pipeline di merge request esegue l'upsert dello slug, scrive SHORT_URL in un report dotenv usato come URL dell'ambiente e un job di stop disabilita il link quando la merge request viene chiusa

Le review app sono il nome che GitLab dà a un ambiente temporaneo per branch o merge request, e la documentazione sulle review app le costruisce su ambienti dinamici. I loro URL tendono a essere sgradevoli: un hash, un namespace, l'hostname di un cloud provider. Un link breve come go.example.com/web-mr-142 è qualcosa che puoi dire ad alta voce durante uno 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"

Il punto è il report dotenv. L'upsert scrive SHORT_URL in link.env, GitLab lo rilegge e environment:url diventa il link breve, così il pulsante View app nella merge request apre web-mr-142 invece dell'hostname originale. La documentazione sugli ambienti descrive questo schema di URL dinamico.

CI_MERGE_REQUEST_IID è univoco per progetto e non cambia per tutta la durata della merge request: per questo ogni push sulla stessa MR finisce nello stesso slug e l'upsert aggiorna invece di creare duplicati. La documentazione di riferimento sulle variabili predefinite contiene l'elenco completo se vuoi usare una chiave diversa.

La pulizia è affidata a un job con action: stop. Deve condividere le rules del job di avvio, altrimenti GitLab non può attivarlo automaticamente:

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"

Io disabilito invece di eliminare. Un link disabilitato conserva la cronologia dei clic e, se qualcuno riapre la MR, la pipeline successiva lo riporta ad active tramite lo stesso upsert. GIT_STRATEGY: none è presente perché nel frattempo il branch potrebbe non esistere più.

Se le review app superano le release con un rapporto di dieci a uno, è qui che i limiti del piano iniziano a farsi sentire. Controlla la quota di link nella pagina dei prezzi prima di collegare tutto a un monorepo molto attivo e avvia un workspace gratuito per le preview mentre fai i test.

Il secondo schema viene eseguito sui tag e fa l'opposto del link di review: uno slug che non cambia mai, con una destinazione che avanza a ogni release. Il tuo README può puntare per sempre a go.example.com/cli-latest.

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

Abbina la regola a uno schema di tag protetto come v*, così solo i maintainer possono creare i tag che lo attivano; altrimenti la chiave protetta semplicemente non sarà disponibile e il job fallirà in modo sicuro, che è il comportamento desiderato. Se vuoi anche un link permanente per ogni versione, esegui una seconda volta lo stesso job con SLUG: "cli-${CI_COMMIT_REF_SLUG}", che trasforma v1.4.0 in cli-v1-4-0.

Non impostare redirect_status su 301 per un link latest. Un 301 è una promessa permanente che i browser possono memorizzare nella cache, mentre un link latest infrange quella promessa a ogni release. Elido usa 302 per impostazione predefinita quando ometti il campo e il nostro approfondimento sui redirect 301 e 302 esamina i casi in cui questa scelta conta davvero.

Una precisazione onesta: una modifica della destinazione può impiegare alcuni minuti per raggiungere ogni edge location, quindi uno smoke test che esegue curl sul link breve appena un secondo dopo potrebbe vedere ancora la release precedente. Verifica invece la risposta dell'API oppure aspetta prima di controllare l'header Location.

Principio del privilegio minimo per un URL shortener GitLab CI: una chiave di preview non protetta e limitata a un workspace di preview per le pipeline di merge request, e una chiave di release protetta che solo le pipeline di tag protetti possono leggere per reindirizzare il link latest

Idempotenza, retry e limiti di frequenza

Le pipeline eseguono retry. I runner si interrompono a metà job, qualcuno clicca su Retry in un job rosso e due push arrivano a trenta secondi di distanza, entrando in competizione. L'upsert qui sopra sopravvive a tutti e tre i casi, e vale la pena capire perché.

L'header Idempotency-Key rende sicuro un POST ritentato: l'API memorizza una risposta riuscita per 24 ore e la riproduce per la stessa chiave, quindi un retry della stessa pipeline riceve il link originale invece di un errore. Costruire la chiave a partire da CI_PIPELINE_ID e dallo slug significa che i retry all'interno di una pipeline riproducono la richiesta, mentre una nuova pipeline ottiene un tentativo nuovo. Il ramo 409 gestisce la competizione tra due pipeline diverse e il percorso cerca-e-poi-aggiorna rende di fatto nulla una seconda esecuzione.

I limiti di frequenza valgono per chiave, oltre a un limite per workspace, e i workspace appena creati hanno anche un limite giornaliero inferiore per la creazione di link mentre costruiscono la propria reputazione. Una manciata di merge request non se ne accorgerà. Un monorepo che avvia quaranta review app contemporaneamente potrebbe farlo, quindi considera un 429 ritentabile usando la parola chiave retry di GitLab e fallisci esplicitamente su un 402, che indica un limite del piano e non un errore temporaneo. Il nostro approfondimento sui limiti di frequenza e l'idempotenza per le API di URL shortener tratta il backoff con più dettagli di quanti ne servano a un job CI.

Se salti il filtro jq per la corrispondenza esatta, la pipeline della MR 14 aggiornerà in silenzio il link della MR 142. Il primo sintomo è di solito un designer confuso. Conserva il filtro.

Se la shell nello YAML diventa ingestibile, puoi racchiudere le stesse chiamate in uno script da salvare nel repository, e la guida alla CLI dell'URL shortener mostra questa struttura.

Leggi il contenuto fondamentale → Link brevi come Terraform: gestire i link come codice

Articoli correlati sul blog

Domande frequenti

GitLab CI può creare link brevi?

Sì. Qualsiasi job in grado di eseguire curl può chiamare l'API REST di un URL shortener, quindi un job GitLab CI può creare un link breve, aggiornarne la destinazione o disabilitarlo. La chiave API risiede in una variabile CI/CD mascherata e il job la invia come token Bearer. Per farlo non serve un'integrazione GitLab nativa.

Come memorizzo in modo sicuro una chiave API in GitLab CI?

Aggiungila in Settings, CI/CD, Variables con la visibilità impostata su Masked and hidden e seleziona Protect variable se devono leggerla solo branch o tag protetti. Il masking tiene il valore fuori dai log dei job, ma la documentazione di GitLab specifica che non è una difesa garantita, quindi limita la chiave stessa a ciò che le serve davvero.

Perché la mia variabile protetta è vuota in una pipeline di merge request?

Le variabili protette vengono passate solo alle pipeline eseguite su branch o tag protetti. Per impostazione predefinita, una pipeline di merge request da un branch di funzionalità non è idonea, quindi la variabile arriva vuota. Usa una chiave separata, non protetta e con privilegi inferiori per i job di review, oppure conserva la chiave protetta solo per le pipeline dei tag.

Come assegno un link breve a ogni review app GitLab?

Esegui un job nelle pipeline di merge request che esegua l'upsert di uno slug costruito con il nome del progetto e CI_MERGE_REQUEST_IID, puntando all'URL della review app. Scrivi l'URL breve risultante in un report dotenv e usalo come environment:url, così il widget della merge request punta direttamente lì. Un job di stop disabilita il link quando l'ambiente si arresta.

Un link breve per l'ultima release dovrebbe usare un redirect 301 o 302?

Usa un 302. La destinazione di un link latest cambia a ogni release, mentre un 301 comunica a browser e cache che lo spostamento è permanente, quindi alcuni client continueranno a inviare gli utenti alla versione precedente. Elido imposta per impostazione predefinita i nuovi link su 302 quando non specifichi redirect_status: in questo caso è la scelta corretta.

Esiste un'integrazione GitLab nativa per Elido?

Non ancora. Un'integrazione GitLab nativa è in arrivo e puoi iscriverti alla lista d'attesa nella pagina dell'integrazione GitLab. Oggi tutto ciò che trovi in questa guida funziona tramite l'API REST pubblica da un job della pipeline, senza installare nulla sul lato GitLab oltre a una variabile CI/CD.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Gratis, nessuna registrazione richiesta · 2 al giorno

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
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

Continua a leggere