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ável | Visibilidade | Protegida | Lida por |
|---|---|---|---|
ELIDO_PREVIEW_KEY | Masked and hidden | Não | Pipelines de merge request |
ELIDO_RELEASE_KEY | Masked and hidden | Sim | Pipelines de tags protegidas |
ELIDO_PREVIEW_WS, ELIDO_RELEASE_WS | Visible | Não | Qualquer job (IDs não são segredos) |
ELIDO_DOMAIN_ID, SHORT_HOST | Visible | Não | Qualquer 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.
Um job funcional do .gitlab-ci.yml para criar um link curto
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.
Links curtos de review app por merge request
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.
Redirecionando um link estável de latest em pipelines de tags
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.
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
- Guia rápido da API do encurtador de URL - a superfície REST chamada por todos os jobs acima.
- Limites de taxa e idempotência para APIs de encurtadores - por que a lógica de retry tem esse formato.
- Um encurtador de URL na linha de comando - as mesmas chamadas em um script reutilizável.
- Redirecionamentos 301 vs 302 - por que um link que muda precisa de um redirecionamento temporário.
- Monitorando redirecionamentos de links com Sentry e Datadog - detectando um destino quebrado depois que o pipeline fica verde.
- Links curtos a partir do GitHub Actions - o equivalente no GitHub, com secrets e concorrência.
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