10 min de leituraIntegrações

Encurtador de URL no GitLab CI: links curtos em cada pipeline

Use o GitLab CI como encurtador de URL: crie links curtos de review app por merge request e redirecione um link estável de latest nas tags, com uma chave mascarada e protegida.

Marius Voß
DevRel · edge infra
Pipeline de encurtador de URL do GitLab CI desenhado como etapas em pixels, em que build, test e deploy terminam em um job de link que grava um link curto para cada merge request e redireciona um link de latest nas tags

Sim, o GitLab CI pode atuar como seu encurtador de URL. Um job com curl e uma chave de API pode criar um link curto em cada merge request, apontá-lo para o review app e mover um link estável de latest para a nova release quando você enviar uma tag. São cerca de quarenta linhas de YAML. Funciona hoje.

O que as pessoas fazem errado não é a chamada HTTP. É a chave: onde ela fica, quais pipelines podem lê-la e quanto estrago poderia causar se um job de branch a vazasse. Por isso, este guia dedica tanto espaço às variáveis e ao escopo quanto ao próprio .gitlab-ci.yml. Se você prefere gerenciar links de longa duração de forma declarativa, a abordagem do Terraform para links curtos é mais adequada; pipelines combinam com links que nascem e são aposentados junto com o código.

Uma observação de status logo de início. A integração nativa do Elido com o GitLab está a caminho, ainda não está disponível, e nada abaixo depende dela. Quer a versão gerenciada? A página de integração com o GitLab tem a lista de espera.

O que um job de encurtador de URL do GitLab CI faz

Um encurtador de pipeline faz três coisas, e apenas três. Cria um link quando um slug não existe, atualiza o destino quando ele existe e desativa o link quando aquilo para o qual ele apontava deixa de existir. As análises e os códigos QR continuam do lado do Elido.

A superfície da API é pequena. Os links ficam em /v1/workspaces/{workspace_id}/links: POST cria um link e precisa de domain_id e destination_url, PATCH /links/{link_id} altera campos de um link existente e GET /links?q= pesquisa por slug, destino ou título. A autenticação usa um único cabeçalho: Authorization: Bearer elido_.... A chave vem da página de chaves de API do painel.

Esse é todo o contrato. A visão geral da API e dos SDKs lista os demais endpoints, mas um pipeline raramente precisa de mais do que esses três.

Armazenando a chave como variável mascarada e protegida

O GitLab oferece duas opções importantes aqui, e elas têm funções diferentes. O mascaramento oculta um valor nos logs dos jobs. A proteção controla quais pipelines recebem o valor.

Em Settings, CI/CD, Variables, escolha Masked and hidden ao criar a variável. Hidden (geralmente disponível desde o GitLab 17.6) significa que ninguém poderá revelar o valor na página de configurações depois, que é exatamente o que você quer para uma credencial. A documentação das variáveis de CI/CD do GitLab lista os requisitos para um valor mascarado: uma única linha, sem espaços e com pelo menos 8 caracteres. As chaves do Elido são elido_ seguidas de base32, então se qualificam.

A mesma página é direta sobre o limite: o mascaramento "is not a guaranteed way to prevent malicious users from accessing variable values." Um job que codifica a variável em base64 e a imprime passa direto pelo mascaramento. Trate o mascaramento como higiene de logs, não como controle de acesso.

A proteção é o controle de acesso. Uma variável protegida só chega a pipelines em branches ou tags protegidas, o que cria o problema que toda equipe encontra na primeira semana: seu pipeline de merge request é executado em uma branch de funcionalidade, então a chave protegida chega como uma string vazia e o job falha com um 401 que parece um erro de digitação.

Eu resolveria isso com duas chaves, em vez de enfraquecer a primeira. Esta é a configuração que eu usaria:

VariávelVisibilidadeProtegidaLida por
ELIDO_PREVIEW_KEYMasked and hiddenNãoPipelines de merge request
ELIDO_RELEASE_KEYMasked and hiddenSimPipelines de tags protegidas
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WSVisibleNãoQualquer job (IDs não são segredos)
ELIDO_DOMAIN_ID, SHORT_HOSTVisibleNãoQualquer job

A chave de preview pertence a um workspace separado que contém apenas links de review. Qualquer pessoa que possa enviar uma branch pode, em princípio, exfiltrar uma variável não protegida, então garanta que o pior que ela possa alcançar seja uma pilha de links descartáveis mr-142, enquanto a chave de release fica no seu workspace real e só é executada nas tags que você protegeu.

Dê às duas chaves o papel Editor e uma data de expiração; 90 dias servem para a de preview. Editor é o preset mais baixo capaz de gravar links, e também pode excluí-los; as chaves de API usam um dos papéis predefinidos, e eu gostaria de um preset que permitisse apenas criar e atualizar para este caso exato; ainda não existe um. A separação de workspaces é o que realmente limita o raio de impacto.

Esta é a parte compartilhada: um upsert que pesquisa o slug, cria o link se ele não existir e faz um patch caso contrário. Coloque-o em um job oculto e estenda-o.

.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

A pesquisa por q é uma correspondência parcial, então o filtro jq a restringe ao slug exato no domínio exato. Sem isso, uma pesquisa por web-mr-14 retornaria tranquilamente web-mr-142. Obtenha seu domain_id uma vez com GET /v1/workspaces/{id}/domains e armazene-o como uma variável comum; um host de marca configurado por meio de domínios personalizados fica melhor em um merge request do que um host genérico.

Ciclo de vida de um link curto de review app do GitLab: um pipeline de merge request faz upsert do slug, grava SHORT_URL em um relatório dotenv usado como URL do ambiente, e um job de parada desativa o link quando o merge request é fechado

Review apps é o nome que o GitLab dá a um ambiente temporário por branch ou merge request, e a documentação de review apps os cria em ambientes dinâmicos. As URLs tendem a ser feias: um hash, um namespace, o hostname de um provedor de nuvem. Um link curto como go.example.com/web-mr-142 é algo que você pode dizer em voz alta numa daily.

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"

O truque é o relatório dotenv. O upsert grava SHORT_URL em link.env, o GitLab o lê de volta e environment:url se torna o link curto, então o botão View app no merge request abre web-mr-142 em vez do hostname bruto. A documentação de ambientes descreve esse padrão de URL dinâmica.

CI_MERGE_REQUEST_IID é único por projeto e nunca muda durante a vida do merge request, por isso cada push para o mesmo MR usa o mesmo slug e o upsert faz patch em vez de criar duplicatas. A referência de variáveis predefinidas traz a lista completa caso você queira outra chave.

A limpeza é feita por um job com action: stop. Ele precisa compartilhar as mesmas rules do job de início, ou o GitLab não conseguirá acioná-lo 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"

Eu desativo em vez de excluir. Um link desativado mantém seu histórico de cliques e, se alguém reabrir o MR, o pipeline seguinte o transforma novamente em active por meio do mesmo upsert. GIT_STRATEGY: none está ali porque a branch pode ter sido excluída até então.

Se seus review apps superarem suas releases na proporção de dez para um, é aí que os limites do plano começam a pesar. Verifique a quantidade de links permitida na página de preços antes de conectar isso a um monorepo movimentado e crie um workspace gratuito para os previews enquanto você testa.

O segundo padrão é executado em tags e faz o oposto do link de review: um slug que nunca muda, cujo destino avança a cada release. Seu README pode apontar para go.example.com/cli-latest para sempre.

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

Combine a regra com um padrão de tag protegida como v* para que apenas mantenedores possam criar as tags que a acionam; caso contrário, a chave protegida simplesmente não estará presente e o job falhará fechado, que é o comportamento desejado. Se você também quiser um link permanente por versão, execute o mesmo job uma segunda vez com SLUG: "cli-${CI_COMMIT_REF_SLUG}", o que transforma v1.4.0 em cli-v1-4-0.

Não defina redirect_status como 301 em um link de latest. Um 301 é uma promessa permanente que os navegadores podem armazenar em cache, e um link de latest quebra essa promessa a cada release. O Elido usa 302 por padrão quando você deixa o campo de fora, e nosso texto sobre redirecionamentos 301 vs 302 passa pelos casos em que essa escolha realmente causa problemas.

Uma ressalva honesta: um destino alterado pode levar alguns minutos para chegar a todos os locais de edge, então um smoke test que faça curl do link curto no segundo seguinte ainda pode ver a release anterior. Verifique a resposta da API, ou aguarde antes de conferir o cabeçalho Location.

Princípio do menor privilégio para um encurtador de URL no GitLab CI: uma chave de preview não protegida limitada a um workspace de previews para pipelines de merge request, e uma chave de release protegida que apenas pipelines de tags protegidas podem ler para redirecionar o link de latest

Idempotência, retries e limites de taxa

Pipelines fazem retries. Runners morrem no meio do job, alguém clica em Retry em um job vermelho, e dois pushes chegam com trinta segundos de diferença e entram em uma corrida. O upsert acima sobrevive aos três casos, e vale entender por quê.

O cabeçalho Idempotency-Key torna seguro repetir um POST: a API armazena em cache uma resposta bem-sucedida por 24 horas e a reproduz para a mesma chave, então um retry do mesmo pipeline recebe o link original de volta em vez de um erro. Montar a chave a partir de CI_PIPELINE_ID e do slug significa que retries dentro de um pipeline são reproduzidos, enquanto um novo pipeline recebe uma tentativa nova. O ramo 409 lida com a corrida entre dois pipelines diferentes, e o caminho de pesquisa seguida de patch faz com que uma segunda execução não tenha efeito.

Os limites de taxa são por chave, além de um limite por workspace, e workspaces recém-criados também têm um limite diário menor para criação de links enquanto constroem reputação. Alguns merge requests não notarão nada. Um monorepo que inicia quarenta review apps de uma vez pode notar, então trate um 429 como passível de retry com a palavra-chave retry do GitLab e falhe explicitamente em um 402, que significa limite do plano, não erro transitório. Nosso texto mais aprofundado sobre limites de taxa e idempotência para APIs de encurtadores cobre o backoff com mais detalhes do que um job de CI precisa.

Ignore o filtro jq de correspondência exata e o pipeline do MR 14 fará silenciosamente um patch no link do MR 142. O primeiro sintoma costuma ser um designer confuso. Mantenha o filtro.

Se o shell no YAML ficar difícil de manejar, as mesmas chamadas cabem perfeitamente em um script que você grava no repositório, e o guia da CLI do encurtador de URL mostra esse formato.

Leia o artigo principal → Links curtos como Terraform: gerenciando links como código

Relacionado no blog

Perguntas frequentes

O GitLab CI pode criar links curtos?

Sim. Qualquer job que consiga executar curl pode chamar a API REST de um encurtador de URL, então um job do GitLab CI pode criar um link curto, atualizar o destino ou desativá-lo. A chave de API fica em uma variável de CI/CD mascarada, e o job a envia como token Bearer. Não é necessária uma integração nativa com o GitLab para isso.

Como armazeno uma chave de API com segurança no GitLab CI?

Adicione-a em Settings, CI/CD, Variables com a visibilidade definida como Masked and hidden e marque Protect variable se apenas branches ou tags protegidas devem poder lê-la. O mascaramento mantém o valor fora dos logs dos jobs, mas a própria documentação do GitLab diz que isso não é uma defesa garantida; portanto, limite a própria chave ao mínimo necessário.

Por que minha variável protegida está vazia em um pipeline de merge request?

Variáveis protegidas só são passadas para pipelines executados em branches ou tags protegidas. Um pipeline de merge request vindo de uma branch de funcionalidade não se qualifica por padrão, então a variável chega vazia. Use uma chave não protegida separada, com menos privilégios, para os jobs de review ou mantenha a chave protegida apenas para pipelines de tags.

Como dou um link curto a cada review app do GitLab?

Execute um job nos pipelines de merge request que faça upsert de um slug criado a partir do nome do projeto e de CI_MERGE_REQUEST_IID, apontando para a URL do review app. Grave a URL curta resultante em um relatório dotenv e use-a como environment:url, para que o widget do merge request aponte diretamente para ela. Um job de parada desativa o link quando o ambiente é interrompido.

Um link curto de latest release deve usar redirecionamento 301 ou 302?

Use 302. Um link de latest muda de destino a cada release, e um 301 informa aos navegadores e caches que a mudança é permanente, então alguns clientes continuarão enviando as pessoas para a versão antiga. O Elido define novos links como 302 por padrão quando você não informa redirect_status, que é a escolha certa aqui.

Existe uma integração nativa do GitLab com o Elido?

Ainda não. Uma integração nativa com o GitLab está a caminho, e você pode entrar na lista de espera na página de integração com o GitLab. Tudo neste guia funciona hoje por meio da API REST pública a partir de um job de pipeline, sem nada para instalar no lado do GitLab além de uma variável de CI/CD.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Grátis, sem necessidade de registo · 2 por dia

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

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

Continuar lendo