11 min di letturaIntegrazioni

URL shortener di GitHub Actions: short link dalla CI

Crea e aggiorna short link da GitHub Actions: la chiave API come secret crittografato, un workflow funzionante, upsert idempotenti e chiavi con privilegi minimi.

Marius Voß
DevRel · edge infra
Uno step di GitHub Actions per accorciare gli URL, rappresentato come una pipeline: un'esecuzione del workflow legge un secret crittografato, cerca lo slug, poi aggiorna lo short link esistente o ne crea uno nuovo

Uno step di URL shortener per GitHub Actions richiede poche righe di shell: legge una chiave API da un secret crittografato, verifica se lo slug esiste già, poi aggiorna la destinazione oppure crea il link. Eseguendolo a ogni push, lo stesso short link punta sempre all'anteprima, alla build della documentazione o all'artefatto più recente. Non serve nessuna action del marketplace. curl e jq sono presenti su ogni runner Ubuntu ospitato da GitHub.

Questa è la risposta completa, e il resto dell'articolo serve a farla funzionare anche dopo qualche centinaio di esecuzioni del workflow. Chi cerca come creare uno short link in GitHub Actions di solito arriva fino a una singola richiesta POST, che funziona fino al secondo push sulla stessa pull request, quando la chiamata di creazione restituisce un conflitto e il job diventa rosso. La soluzione è trattare lo step come un upsert, non come una creazione. L'altro problema riguarda la chiave: spesso è il token personale di qualcuno, con molti più privilegi di quelli necessari a un job CI.

Se gestisci già i link come codice, short link come Terraform è la versione dichiarativa della stessa idea e si adatta meglio ai link che cambiano secondo una pianificazione umana. Uno step del workflow è preferibile quando la destinazione esiste solo dopo il completamento di una build.

Come funziona uno step di URL shortener per GitHub Actions

A ogni esecuzione accadono le stesse tre cose contro l'API REST su https://api.elido.app/v1. Vengono elencati i link del workspace filtrati per slug. Viene inviata una PATCH al link trovato, oppure una POST se non è stato trovato nulla. L'URL breve viene scritto in $GITHUB_OUTPUT, così lo step successivo può usarlo.

Perché non lasciare che l'URL shortener generi uno slug casuale? Perché poi non potresti ritrovare il link. Lo slug deve provenire da qualcosa che il workflow conosce già a ogni esecuzione: il numero della pull request, il nome del branch, una parola fissa come latest. Uno slug stabile significa un URL breve stabile, ed è tutto ciò che serve ai reviewer che lo aggiungono ai preferiti o ai product manager che lo incollano in un ticket.

Come funziona uno step di URL shortener per GitHub Actions: il workflow legge la chiave API da un secret crittografato, elenca i link per slug, invia PATCH quando lo slug esiste o POST quando non esiste, poi scrive l'URL breve nell'output di uno step

Salvare la chiave API come secret crittografato

Crea la chiave nella dashboard, copiala una volta (viene mostrata esattamente una volta e inizia con elido_) e salvala in Settings, poi Secrets and variables, quindi Actions, con il nome ELIDO_API_KEY. La guida di GitHub su come usare i secret in GitHub Actions copre i livelli repository, environment e organizzazione. Per tutto ciò che esegue un deploy, io la metterei in un environment con reviewer obbligatori, così un branch occasionale non può usarla.

Tre valori non sono segreti e appartengono alle variabili di configurazione, dove puoi rileggerli: ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID e ELIDO_HOST. L'ID del dominio è importante perché la chiamata di creazione lo richiede. Puoi recuperarlo una volta con GET /v1/workspaces/{workspace_id}/domains, che restituisce l'id e l'hostname di ogni dominio.

Mappa il secret nell'unico step che chiama l'API, non nell'intero job. Un env a livello di step lo tiene fuori da ogni altro processo avviato dal job, comprese le action di terze parti che non hai scritto tu.

Un workflow funzionante per accorciare un URL a ogni pull request

Questo è il file completo per il caso più comune in cui si accorcia un URL in un workflow GitHub: un link di anteprima per ogni pull request. Inseriscilo in .github/workflows/preview-link.yml e modifica la riga DEST in base a dove vengono pubblicati i tuoi deploy di anteprima.

name: Preview short link

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: preview-link-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  short-link:
    # Forks get no secrets; skip them instead of failing.
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    env:
      API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
      DOMAIN_ID: ${{ vars.ELIDO_DOMAIN_ID }}
      HOST: ${{ vars.ELIDO_HOST }}
      SLUG: pr-${{ github.event.pull_request.number }}-myapp
      DEST: https://pr-${{ github.event.pull_request.number }}.preview.example.com
    steps:
      - name: Create or update the short link
        id: link
        env:
          ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
        run: |
          set -euo pipefail
          auth=(-H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json")

          # 1. Find an existing link with exactly this slug on this domain.
          link_id=$(curl -sS --fail-with-body "${auth[@]}" "$API/links?q=$SLUG&limit=100" \
            | jq -r --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1)

          if [ -n "$link_id" ]; then
            # 2a. Found: point it at the new destination.
            curl -sS --fail-with-body -X PATCH "${auth[@]}" "$API/links/$link_id" \
              -d "$(jq -n --arg u "$DEST" '{destination_url: $u, status: "active"}')" > /dev/null
          else
            # 2b. Not found: create it. The key makes curl's retries safe.
            curl -sS --fail-with-body --retry 3 -X POST "${auth[@]}" "$API/links" \
              -H "Idempotency-Key: $GITHUB_REPOSITORY-$SLUG-$GITHUB_RUN_ID" \
              -d "$(jq -n --arg u "$DEST" --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                  '{domain_id: $d, slug: $s, destination_url: $u, tags: ["ci", "preview"]}')" > /dev/null
          fi

          echo "url=https://$HOST/$SLUG" >> "$GITHUB_OUTPUT"

      - name: Comment once, when the pull request opens
        if: github.event.action == 'opened'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh pr comment "${{ github.event.pull_request.number }}" \
            --repo "${{ github.repository }}" \
            --body "Preview: ${{ steps.link.outputs.url }}"

Alcune righe meritano un commento. --fail-with-body trasforma un 4xx o 5xx in uno step fallito continuando però a stampare il corpo dell'errore, cosa che curl -s semplice non fa; quest'ultimo restituisce 0 su un 401 e il job procede tranquillamente. I corpi delle richieste vengono costruiti con jq -n invece che con l'interpolazione di stringhe, così una destinazione contenente virgolette o una e commerciale non può rompere il JSON. Inoltre lo step del commento viene eseguito solo su opened. Poiché l'URL breve non cambia mai, un commento resta corretto per tutta la vita della PR e nessuno riceve una notifica a ogni push.

Il blocco concurrency non è decorativo. Senza di esso, due push ravvicinati avvierebbero due esecuzioni che vedrebbero entrambe "nessun link ancora presente" e proverebbero entrambe a crearne uno. La documentazione di GitHub su come controllare la concorrenza dei workflow spiega il raggruppamento; qui l'esecuzione più vecchia viene annullata e la race condition non si verifica.

Sotto la parola idempotente si nascondono due errori diversi, che il workflow gestisce separatamente. Il primo è il nuovo avvio: un secondo push, un "Re-run jobs" manuale, una PR riaperta. A questo serve il ramo che prima cerca e poi invia una PATCH. Il secondo è la richiesta ritentata, quando curl invia la POST, la rete cade prima che arrivi la risposta e curl la invia di nuovo. L'header Idempotency-Key copre questo caso. Elido conserva per 24 ore la prima risposta riuscita associata alla chiave e la ripropone per un retry corrispondente, quindi la creazione viene eseguita una volta sola; la meccanica completa è descritta in limiti di frequenza, retry e idempotenza.

Flusso decisionale per creare uno short link in GitHub Actions senza duplicati: una corrispondenza esatta dello slug nel tuo workspace porta a PATCH, nessuna corrispondenza porta a POST e un 409 indica che un altro workspace su un dominio condiviso possiede già lo slug

Le collisioni degli slug sono la parte che spesso sfugge. Gli slug sono unici per dominio di redirect, non per workspace. Su un dominio condiviso, ogni altro cliente Elido usa lo stesso namespace, e probabilmente qualcuno ha già riservato uno slug semplice come pr-12. La ricerca non vedrà il suo link (elenca solo il tuo workspace), quindi la POST parte e torna con 409 slug already exists for this domain. Due soluzioni: aggiungere una parola del progetto allo slug oppure mettere i link CI su un tuo dominio personalizzato, dove il namespace appartiene solo a te. Io farei entrambe le cose.

C'è un secondo motivo, più subdolo, per cui il nome del repository si trova alla fine dello slug invece che all'inizio. Il parametro q cerca una corrispondenza parziale in slug, destinazione e titolo. Con myapp-pr-1, la ricerca restituisce anche myapp-pr-10 fino a myapp-pr-199, più dei 100 risultati restituiti da una singola pagina, e il link che volevi, essendo il più vecchio, resta fuori. pr-1-myapp corrisponde solo a se stesso. Un dettaglio piccolo, che mi è costato un pomeriggio imbarazzante passato a chiedermi "perché la PR #1 continua a ricevere un 409?" prima di accorgermene.

Il workflow di anteprima è un modello. Cambia il trigger, lo slug e la destinazione, e lo stesso step copre la maggior parte di ciò che i team automatizzano davvero. (Le release note sono un argomento a parte, trattato in accorciare i link nelle release note.)

Caso d'usoTriggerSlugCosa fa lo step
Deploy di anteprima per PRpull_requestpr-42-myappUpsert a ogni push, elimina alla chiusura
Deploy della documentazionepush su maindocs-myappPATCH al nuovo URL della documentazione
Ultima buildpush su main o un taglatest-myappPATCH tramite ID del link salvato, senza ricerca
Artefatto notturnoschedulenightly-myappPATCH al nuovo URL dell'artefatto

Il caso dell'ultima build è il più semplice. Crea il link manualmente una volta, salva il suo ID numerico come variabile e il job si riduce a una sola chiamata:

- name: Point the latest link at this build
  env:
    ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
    API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
    DEST: https://builds.example.com/${{ github.sha }}/
  run: |
    curl -sS --fail-with-body -X PATCH \
      -H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json" \
      "$API/links/${{ vars.ELIDO_LATEST_LINK_ID }}" \
      -d "$(jq -n --arg u "$DEST" '{destination_url: $u}')"

Mantieni questi link mobili su un 302, che è il valore predefinito quando non imposti redirect_status. Un 301 dice ai browser che possono memorizzare la risposta nella cache, e chi ha fatto clic ieri continuerà ad arrivare alla build di ieri; il nostro articolo su redirect 301 e 302 ne offre la versione completa.

Per i link di anteprima, fai pulizia quando la PR viene chiusa. Aggiungi closed ai tipi del trigger, riusa la ricerca e invia DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Uno slug eliminato torna disponibile. Se preferisci conservare la cronologia dei clic, usa invece PATCH con {"status": "disabled"}; l'upsert precedente imposta status: "active" a ogni esecuzione, quindi una PR riaperta riattiva il suo link.

Vuoi provarlo su un repository? Avvia un workspace gratuito, crea una chiave e il workflow qui sopra verrà eseguito così com'è dopo aver impostato le tre variabili.

Chiavi API CI con privilegi minimi

La chiave contenuta in un secret CI dovrebbe poter fare esattamente ciò che fa il workflow, e niente di più. È più difficile di quanto sembri, per via del funzionamento delle chiavi personali.

Una chiave API personale esegue l'autenticazione come la persona che l'ha creata. La chiave può fare tutto ciò che quella persona può fare e, quando la persona lascia l'azienda, la chiave resta legata al suo account. Per la CI userei invece un machine user: un account di servizio appartenente a un solo workspace, con il proprio ruolo e token che solo un amministratore umano autenticato può creare o revocare. Crealo in Machine users nella dashboard con il ruolo editor, il ruolo integrato più basso che può creare, modificare ed eliminare link, poi genera un token con una data di scadenza. Disabilitando il machine user invalidi immediatamente tutti i token che possiede: è esattamente il pulsante che vuoi avere il giorno in cui un secret finisce nelle mani sbagliate.

Quattro altre abitudini non costano nulla:

  • Un token per repository, con il nome del repository, così l'audit trail indica quale repository ha creato quale link.
  • Secret di environment con reviewer obbligatori per ogni workflow che modifica un link da cui dipendono delle persone.
  • Imposta esplicitamente permissions: all'inizio del workflow, come nell'esempio, così GITHUB_TOKEN riceve solo ciò che serve al job.
  • Non usare mai pull_request_target per raggiungere il secret dalle PR dei fork. L'articolo di GitHub Security Lab su come prevenire le pwn request mostra perché eseguire codice non attendibile accanto a un token con accesso in scrittura finisce male.

I workspace possono anche limitare l'accesso all'API con un allowlist di IP. È un controllo efficace per i runner self-hosted con egress fisso e quasi inutile per i runner ospitati da GitHub, i cui indirizzi provengono da un pool molto ampio e variabile. La guida di riferimento di GitHub sull'uso sicuro merita un'ora se i tuoi workflow toccano la produzione.

Cosa si rompe nella pratica

La maggior parte dei problemi proviene da quattro punti, e ognuno si presenta come un errore leggibile se --fail-with-body è attivo. Un 404 su ogni chiamata di solito significa che la variabile dell'ID del workspace è errata o che la chiave appartiene a un workspace diverso. Un 400 con il messaggio domain_id is required significa che la variabile è vuota, in genere perché è stata impostata in un environment diverso da quello usato dal job. Un 409 è la collisione del namespace condiviso descritta nella sezione sull'idempotenza. Un 429 significa invece che hai superato il limite di frequenza per chiave: una sola operazione di upsert per esecuzione non lo raggiungerà, ma una matrice di cinquanta job può farlo.

Una cosa non è affatto un errore. Dopo una PATCH, per un breve periodo un visitatore potrebbe ancora raggiungere la vecchia destinazione, perché i redirect vengono memorizzati nella cache vicino al visitatore per mantenerli veloci. Un test smoke che verifica la nuova destinazione subito dopo l'aggiornamento può diventare intermittente. Esegui il polling con un breve backoff oppure verifica la risposta dell'API.

Se vuoi che lo step invii notifiche all'esterno, abbinalo ai webhook per gli eventi dei link, che si attivano quando un link cambia, oppure ai pattern curl e jq della guida CLI per i test locali prima di fare il commit del workflow. Il riferimento API e SDK elenca tutti i campi accettati dagli endpoint dei link.

Leggi il cornerstone → Gestisci gli short link come Terraform

Correlati nel blog

Domande frequenti

GitHub Actions può creare short link?

Sì. Uno step del workflow può chiamare l'API REST di qualsiasi servizio di URL shortener con curl, già preinstallato sui runner ospitati da GitHub insieme a jq. Lo step legge la chiave API da un secret crittografato, invia l'URL di destinazione e scrive l'URL breve risultante nell'output dello step, così gli step successivi possono pubblicarlo in un commento a una pull request o in un riepilogo del job.

Come salvo una chiave API di un URL shortener in GitHub Actions?

Salvala come secret crittografato del repository o dell'environment, poi mappala nell'unico step che ne ha bisogno con una voce env come ELIDO_API_KEY: secrets.ELIDO_API_KEY nella sintassi delle espressioni. GitHub maschera il valore nei log. Tieni invece i valori non segreti, come l'ID del workspace e l'ID del dominio, nelle variabili di configurazione, così restano leggibili.

Come evito di creare short link duplicati a ogni esecuzione del workflow?

Trasforma lo step in un upsert. Ricava lo slug da qualcosa di stabile, come il numero della pull request, cercalo prima e invia una PATCH per cambiare la destinazione quando esiste già. Crea il link solo se la ricerca non restituisce risultati. Un header Idempotency-Key nella chiamata di creazione copre il caso separato di una richiesta ritentata dopo un timeout di rete.

Perché il mio workflow riceve un 409 quando crea uno short link?

Lo slug è già occupato su quel dominio. Su Elido, gli slug sono unici per dominio di redirect, e un dominio condiviso è condiviso con tutti gli altri workspace, quindi è probabile che uno slug generico come pr-12 esista già. Aggiungi un prefisso o un suffisso di progetto allo slug, oppure usa un tuo dominio personalizzato, dove l'intero namespace appartiene a te.

Gli step per gli short link funzionano sulle pull request provenienti da fork?

Non con il trigger pull_request semplice, perché GitHub non passa i secret del repository ai workflow avviati da un fork. Salta il job per i fork con una condizione if sul repository head. Passare a pull_request_target per ottenere il secret è rischioso, perché viene eseguito con accesso in scrittura accanto a codice che non hai esaminato.

Un link che punta all'ultima build deve usare un redirect 301 o 302?

Usa un 302 o un 307. I browser possono memorizzare in cache un 301 a tempo indefinito, quindi chi torna sul link continuerebbe ad arrivare a una build vecchia dopo che il workflow ha spostato il link. I link Elido usano 302 per impostazione predefinita quando non imposti redirect_status: è la scelta giusta per qualsiasi link la cui destinazione cambi a opera di una pipeline.

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
github actions url shortener
create short link in github actions
shorten url github workflow
preview deployments
ci/cd
api keys

Continua a leggere