9 Min. LesezeitIntegrationen

GitLab CI URL-Shortener: Short Links aus jeder Pipeline

Verwenden Sie GitLab CI als URL-Shortener: Erstellen Sie pro Merge Request Short Links für Review-Apps und setzen Sie einen stabilen Latest-Link bei Tags auf ein neues Ziel, mit einem maskierten, geschützten Schlüssel.

Marius Voß
DevRel · edge infra
Eine GitLab-CI-URL-Shortener-Pipeline als Pixel-Stufen dargestellt, bei der Build, Test und Deploy in einem Link-Job enden, der für jeden Merge Request einen Short Link schreibt und einen Latest-Link bei Tags neu ausrichtet

Ja, GitLab CI kann als Ihr URL-Shortener dienen. Ein Job mit curl und einem API-Schlüssel kann bei jedem Merge Request einen Short Link erstellen, auf die Review-App zeigen lassen und einen stabilen latest-Link beim Push eines Tags auf das neue Release verschieben. Das sind etwa vierzig Zeilen YAML. Es funktioniert schon heute.

Der Teil, bei dem sich viele vertun, ist nicht der HTTP-Aufruf. Es ist der Schlüssel: Wo liegt er, welche Pipelines können ihn lesen, und wie viel Schaden könnte er anrichten, wenn ein Branch-Job ihn nach außen gibt? Deshalb widmet sich dieser Leitfaden den Variablen und ihrer Eingrenzung genauso ausführlich wie der .gitlab-ci.yml selbst. Wenn Sie langlebige Links lieber deklarativ verwalten möchten, passt der Terraform-Ansatz für Short Links besser; Pipelines eignen sich für Links, die mit dem Code entstehen und mit ihm ausgemustert werden.

Zunächst ein Hinweis zum Status. Elidos native GitLab-Integration kommt, ist aber noch nicht live, und nichts weiter unten hängt von ihr ab. Sie möchten die verwaltete Version? Auf der GitLab-Integrationsseite finden Sie die Warteliste.

Was ein GitLab-CI-URL-Shortener-Job tut

Ein Pipeline-Shortener erledigt drei Dinge, und nur drei. Er erstellt einen Link, wenn ein Slug noch nicht existiert, aktualisiert das Ziel, wenn er bereits existiert, und deaktiviert den Link, wenn das Ziel nicht mehr vorhanden ist. Analytics und QR-Codes bleiben auf der Elido-Seite.

Die API-Oberfläche ist klein. Links liegen unter /v1/workspaces/{workspace_id}/links: POST erstellt einen und benötigt domain_id sowie destination_url, PATCH /links/{link_id} ändert Felder eines bestehenden Links, und GET /links?q= sucht nach Slug, Ziel oder Titel. Die Authentifizierung besteht aus einem Header: Authorization: Bearer elido_.... Der Schlüssel kommt von der API-Keys-Seite im Dashboard.

Das ist der gesamte Vertrag. Die Übersicht zu API und SDKs listet die übrigen Endpunkte auf, aber eine Pipeline braucht selten mehr als diese drei.

Den Schlüssel als maskierte, geschützte Variable speichern

GitLab gibt Ihnen hier zwei wichtige Schalter, die unterschiedliche Aufgaben erfüllen. Masking blendet einen Wert in Job-Logs aus. Protection steuert, welche Pipelines den Wert überhaupt erhalten.

Wählen Sie beim Anlegen der Variable unter Settings, CI/CD, Variables die Optionen Masked and hidden. Hidden (allgemein verfügbar seit GitLab 17.6) bedeutet, dass später niemand den Wert auf der Einstellungsseite anzeigen lassen kann, was Sie für ein Zugangsdaten-Secret möchten. Die Dokumentation zu GitLab-CI/CD-Variablen nennt die Anforderungen für einen maskierten Wert: eine einzelne Zeile, keine Leerzeichen, mindestens 8 Zeichen. Elido-Schlüssel bestehen aus elido_ gefolgt von Base32 und erfüllen diese Bedingungen.

Auf derselben Seite wird die Einschränkung unverblümt benannt: masking "is not a guaranteed way to prevent malicious users from accessing variable values." Ein Job, der die Variable Base64-kodiert und ausgibt, umgeht die Maske problemlos. Betrachten Sie Masking als Hygiene für Logs, nicht als Zugriffskontrolle.

Protection ist die Zugriffskontrolle. Eine geschützte Variable erreicht nur Pipelines auf geschützten Branches oder geschützten Tags. Daraus entsteht das eine Problem, auf das jedes Team in der ersten Woche stößt: Ihre Merge-Request-Pipeline läuft auf einem Feature-Branch, daher kommt der geschützte Schlüssel als leerer String an und der Job scheitert mit einem 401, der wie ein Tippfehler aussieht.

Ich würde das mit zwei Schlüsseln lösen, statt den einen abzuschwächen. So würde ich es einrichten:

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

Der Preview-Schlüssel gehört in einen separaten Workspace, der ausschließlich Review-Links enthält. Jeder, der einen Branch pushen kann, kann eine ungeschützte Variable theoretisch nach außen geben. Stellen Sie also sicher, dass im schlimmsten Fall nur ein Haufen wegwerfbarer mr-142-Links erreichbar ist, während der Release-Schlüssel in Ihrem echten Workspace liegt und nur bei von Ihnen geschützten Tags ausgeführt wird.

Geben Sie beiden Schlüsseln die Rolle Editor und ein Ablaufdatum; 90 Tage passen für den Preview-Schlüssel. Editor ist die niedrigste vordefinierte Rolle, mit der sich Links schreiben lassen, und sie kann sie auch löschen. API-Schlüssel verwenden eine der vordefinierten Rollen, und für genau diesen Fall hätte ich gern eine Voreinstellung, die nur Erstellen und Aktualisieren erlaubt; die gibt es noch nicht. Die Trennung der Workspaces begrenzt den möglichen Schaden tatsächlich.

Hier ist der gemeinsame Baustein: ein Upsert, das nach dem Slug sucht, den Link bei Fehlen erstellt und ihn andernfalls patcht. Legen Sie ihn in einem versteckten Job ab und erweitern Sie ihn.

.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

Die q-Suche findet Teilzeichenfolgen, daher grenzt der jq-Filter das Ergebnis auf den exakten Slug in der exakten Domain ein. Ohne ihn würde eine Suche nach web-mr-14 problemlos web-mr-142 zurückgeben. Ermitteln Sie Ihre domain_id einmal mit GET /v1/workspaces/{id}/domains und speichern Sie sie als normale Variable. Ein über Custom Domains eingerichteter gebrandeter Host wirkt in einem Merge Request besser als ein generischer.

Lebenszyklus eines GitLab-Review-App-Short-Links: Eine Merge-Request-Pipeline erstellt oder aktualisiert den Slug, schreibt SHORT_URL in einen als Environment-URL verwendeten dotenv-Report, und ein Stop-Job deaktiviert den Link, wenn der Merge Request geschlossen wird

Review-Apps ist GitLabs Bezeichnung für eine temporäre Umgebung pro Branch oder Merge Request. Die Dokumentation zu Review-Apps erstellt sie in dynamischen Umgebungen. Ihre URLs sind meist unschön: ein Hash, ein Namespace, der Hostname eines Cloud-Anbieters. Einen Short Link wie go.example.com/web-mr-142 können Sie in einem Stand-up laut sagen.

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"

Der Kniff ist der dotenv-Report. Das Upsert schreibt SHORT_URL in link.env, GitLab liest den Wert wieder ein und environment:url wird zum Short Link. Dadurch öffnet der View app-Button im Merge Request web-mr-142 statt des rohen Hostnamens. Die Dokumentation zu Umgebungen beschreibt dieses Muster für dynamische URLs.

CI_MERGE_REQUEST_IID ist pro Projekt eindeutig und ändert sich während der gesamten Lebensdauer des Merge Requests nicht. Deshalb landet jeder Push auf denselben MR unter demselben Slug, und das Upsert patcht den bestehenden Link, statt Duplikate zu erzeugen. Die Referenz der vordefinierten Variablen enthält die vollständige Liste, falls Sie einen anderen Schlüssel benötigen.

Das Aufräumen übernimmt ein Job mit action: stop. Er muss dieselben rules wie der Start-Job verwenden, sonst kann GitLab ihn nicht automatisch auslösen:

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"

Ich deaktiviere den Link, statt ihn zu löschen. Ein deaktivierter Link behält seine Klick-Historie, und wenn jemand den MR wieder öffnet, setzt die nächste Pipeline ihn über dasselbe Upsert wieder auf active. GIT_STRATEGY: none ist dort nötig, weil der Branch zu diesem Zeitpunkt verschwunden sein kann.

Wenn Ihre Review-Apps im Verhältnis zehn zu eins zahlreicher sind als Ihre Releases, beginnen hier die Planlimits zu greifen. Prüfen Sie das Link-Kontingent auf der Pricing-Seite, bevor Sie dies in ein stark ausgelastetes Monorepo einbauen, und starten Sie zum Testen einen kostenlosen Workspace für die Previews.

Das zweite Muster läuft auf Tags und macht das Gegenteil des Review-Links: ein Slug, der sich nie ändert, dessen Ziel aber mit jedem Release weiterwandert. Ihr README kann dauerhaft auf go.example.com/cli-latest zeigen.

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

Kombinieren Sie die Regel mit einem geschützten Tag-Muster wie v*, damit nur Maintainer die Tags erstellen können, die diesen Job auslösen. Andernfalls ist der geschützte Schlüssel schlicht nicht vorhanden und der Job schlägt sicherheitshalber fehl, statt unsicher weiterzulaufen - genau das gewünschte Verhalten. Wenn Sie zusätzlich einen dauerhaften Link pro Version möchten, führen Sie denselben Job ein zweites Mal mit SLUG: "cli-${CI_COMMIT_REF_SLUG}" aus. So wird aus v1.4.0 cli-v1-4-0.

Setzen Sie redirect_status bei einem Latest-Link nicht auf 301. Ein 301 ist ein dauerhaftes Versprechen, das Browser cachen dürfen, und ein Latest-Link bricht dieses Versprechen bei jedem Release. Elido verwendet standardmäßig 302, wenn Sie das Feld auslassen, und unser Beitrag zu 301- und 302-Redirects geht die Fälle durch, in denen diese Wahl tatsächlich problematisch wird.

Ein ehrlicher Vorbehalt: Es kann einige Minuten dauern, bis ein geändertes Ziel jede Edge-Location erreicht. Ein Smoke-Test, der den Short Link schon in der nächsten Sekunde per curl abfragt, kann daher noch das vorherige Release sehen. Prüfen Sie stattdessen die API-Antwort oder warten Sie vor der Kontrolle des Location-Headers.

Prinzip der geringsten Berechtigung für einen GitLab-CI-URL-Shortener: ein ungeschützter Preview-Schlüssel, der auf einen Preview-Workspace für Merge-Request-Pipelines beschränkt ist, und ein geschützter Release-Schlüssel, den nur geschützte Tag-Pipelines lesen können, um den Latest-Link neu auszurichten

Idempotenz, Retries und Rate Limits

Pipelines führen Retries aus. Runner sterben mitten im Job, jemand klickt bei einem roten Job auf Retry, und zwei Pushes treffen im Abstand von dreißig Sekunden ein und konkurrieren miteinander. Das obige Upsert übersteht alle drei Fälle, und es lohnt sich zu verstehen, warum.

Der Idempotency-Key-Header macht einen wiederholten POST sicher: Die API cached eine erfolgreiche Antwort 24 Stunden lang und gibt sie bei demselben Schlüssel erneut aus. Ein Retry derselben Pipeline erhält also den ursprünglichen Link zurück statt eines Fehlers. Wenn Sie den Schlüssel aus CI_PIPELINE_ID und dem Slug bilden, werden Retries innerhalb einer Pipeline wiedergegeben, während eine neue Pipeline einen neuen Versuch erhält. Der 409-Zweig behandelt das Wettrennen zwischen zwei verschiedenen Pipelines, und der Pfad aus Suchen und anschließenden Patchen macht einen zweiten Lauf faktisch zum No-op.

Rate Limits gelten pro Schlüssel zusätzlich zu einem Workspace-Limit. Ganz neue Workspaces haben außerdem ein niedrigeres tägliches Limit für die Link-Erstellung, während sie Vertrauen aufbauen. Ein paar Merge Requests werden das nicht bemerken. Ein Monorepo, das vierzig Review-Apps auf einmal startet, möglicherweise schon. Behandeln Sie einen 429 daher mit dem GitLab-Schlüsselwort retry als wiederholbar und brechen Sie bei einem 402 deutlich ab, denn dieser bedeutet ein Planlimit und keinen vorübergehenden Fehler. Unser ausführlicherer Beitrag zu Rate Limits und Idempotenz für Shortener-APIs erklärt Backoff detaillierter, als es ein CI-Job braucht.

Lassen Sie den jq-Filter für exakte Treffer weg, wird die Pipeline von MR 14 stillschweigend den Link von MR 142 patchen. Das erste Symptom ist gewöhnlich ein verwirrter Designer. Behalten Sie den Filter bei.

Wenn Shell in YAML unhandlich wird, lassen sich dieselben Aufrufe sauber in ein Skript verpacken, das Sie ins Repository committen. Der CLI-Leitfaden für URL-Shortener zeigt dieses Muster.

Lesen Sie den Cornerstone-Artikel → Short Links als Terraform: Links als Code verwalten

Verwandte Beiträge im Blog

Häufig gestellte Fragen

Kann GitLab CI Short Links erstellen?

Ja. Jeder Job, der curl ausführen kann, kann die REST-API eines URL-Shorteners aufrufen. Deshalb kann ein GitLab-CI-Job einen Short Link erstellen, sein Ziel aktualisieren oder ihn deaktivieren. Der API-Schlüssel liegt in einer maskierten CI/CD-Variable, und der Job sendet ihn als Bearer-Token. Dafür ist keine native GitLab-Integration erforderlich.

Wie speichere ich einen API-Schlüssel sicher in GitLab CI?

Fügen Sie ihn unter Settings, CI/CD, Variables hinzu, setzen Sie die Sichtbarkeit auf Masked and hidden und aktivieren Sie Protect variable, wenn nur geschützte Branches oder Tags ihn lesen dürfen. Masking hält den Wert aus Job-Logs heraus, aber die GitLab-eigene Dokumentation sagt, dass dies keine garantierte Abwehr ist. Beschränken Sie den Schlüssel daher selbst auf die geringstmöglichen Berechtigungen.

Warum ist meine geschützte Variable in einer Merge-Request-Pipeline leer?

Geschützte Variablen werden nur an Pipelines übergeben, die auf geschützten Branches oder geschützten Tags laufen. Eine Merge-Request-Pipeline aus einem Feature-Branch erfüllt diese Bedingung standardmäßig nicht, daher kommt die Variable als leerer Wert an. Verwenden Sie entweder einen separaten Schlüssel mit geringeren Berechtigungen ohne Schutz für Review-Jobs oder behalten Sie den geschützten Schlüssel ausschließlich für Tag-Pipelines.

Wie gebe ich jeder GitLab-Review-App einen Short Link?

Führen Sie in Merge-Request-Pipelines einen Job aus, der aus dem Projektnamen und CI_MERGE_REQUEST_IID einen Slug erstellt oder aktualisiert und auf die URL der Review-App zeigt. Schreiben Sie die resultierende Short URL in einen dotenv-Report und verwenden Sie sie als environment:url, damit das Merge-Request-Widget direkt dorthin verlinkt. Ein Stop-Job deaktiviert den Link, wenn die Umgebung beendet wird.

Sollte ein Short Link für das neueste Release einen 301- oder 302-Redirect verwenden?

Verwenden Sie 302. Ein Latest-Link ändert sein Ziel bei jedem Release. Ein 301 teilt Browsern und Caches dagegen mit, dass der Umzug dauerhaft ist, sodass einige Clients weiterhin Personen an die alte Version senden. Elido verwendet für neue Links standardmäßig 302, wenn Sie redirect_status nicht setzen. Das ist hier die richtige Wahl.

Gibt es eine native GitLab-Integration für Elido?

Noch nicht. Eine native GitLab-Integration ist in Vorbereitung, und Sie können sich auf der GitLab-Integrationsseite in die Warteliste eintragen. Alles in diesem Leitfaden funktioniert schon heute über die öffentliche REST-API aus einem Pipeline-Job, ohne dass Sie auf der GitLab-Seite außer einer CI/CD-Variable etwas installieren müssen.

Elido testen

URL einfügen, kurzer Link in Sekunden

Kein Konto nötig. Link bleibt 30 Tage aktiv. Konto erstellen, um ihn dauerhaft zu behalten.

Kostenlos, keine Anmeldung erforderlich · 2 pro Tag

Elido testen

URL-Shortener mit EU-Hosting: eigene Domains, tiefe Analytik und eine offene API. Kostenloser Tarif - keine Kreditkarte nötig.

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

Weiterlesen