12 min de leituraIntegrações

Encurtador de URL no GitHub Actions: links curtos a partir do seu CI

Crie e atualize links curtos a partir do GitHub Actions: a chave de API como um segredo criptografado, um workflow funcional, upserts idempotentes e chaves com privilégio mínimo.

Marius Voß
DevRel · edge infra
Uma etapa de encurtador de URL do GitHub Actions desenhada como um pipeline: uma execução do workflow lê um segredo criptografado, procura o slug e então atualiza o link curto existente ou cria um novo

Uma etapa de encurtador de URL no GitHub Actions são algumas linhas de shell: ler uma chave de API de um segredo criptografado, verificar se o slug já existe e então atualizar o destino ou criar o link. Execute-a a cada push e o mesmo link curto sempre apontará para a preview, a build da documentação ou o artefato mais recente. Nenhuma action do marketplace é necessária. curl e jq vêm em todos os runners Ubuntu hospedados pelo GitHub.

Essa é a resposta inteira, e o restante deste post é a parte que faz isso funcionar ao longo de algumas centenas de execuções de workflow. Quem procura como criar um link curto no GitHub Actions geralmente chega até uma única requisição POST, que funciona até o segundo push para o mesmo pull request, quando a chamada de criação retorna um conflito e o job fica vermelho. A correção é tratar a etapa como um upsert, não como uma criação. O outro problema costuma ser a própria chave, que tende a ser o token pessoal de alguém, com um alcance muito maior do que um job de CI precisa.

Se você já gerencia links como código, links curtos como Terraform é a versão declarativa da mesma ideia e se encaixa melhor em links que mudam conforme o cronograma de uma pessoa. Uma etapa de workflow é melhor quando o destino só passa a existir depois que uma build termina.

Como funciona uma etapa de encurtador de URL no GitHub Actions

Cada execução faz as mesmas três coisas na API REST em https://api.elido.app/v1. Ela lista os links do workspace filtrados pelo slug. Envia um PATCH para o link encontrado ou um POST se não encontrou nada. Grava a URL curta em $GITHUB_OUTPUT para que a próxima etapa possa usá-la.

Por que não deixar o encurtador gerar um slug aleatório? Porque assim você não consegue encontrar o link novamente. O slug precisa vir de algo que o workflow já conheça em todas as execuções: o número do pull request, o nome da branch, uma palavra fixa como latest. Um slug estável significa uma URL curta estável, e esse é o valor principal para revisores que o salvam nos favoritos ou para gerentes de produto que colam o link em um ticket.

Como funciona uma etapa de encurtador de URL no GitHub Actions: o workflow lê a chave de API de um segredo criptografado, lista links pelo slug, envia PATCH quando o slug existe ou POST quando não existe e então grava a URL curta na saída da etapa

Armazenando a chave de API como um segredo criptografado

Crie a chave no dashboard, copie-a uma vez (ela é exibida exatamente uma vez e começa com elido_) e salve-a em Settings, depois Secrets and variables, depois Actions, como ELIDO_API_KEY. O guia do GitHub sobre como usar segredos no GitHub Actions aborda os níveis de repositório, ambiente e organização. Para qualquer coisa que faça deploy, eu a colocaria em um ambiente com revisores obrigatórios, para que uma branch perdida não possa usá-la.

Três valores não são secretos e pertencem às variáveis de configuração, onde você pode consultá-los: ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID e ELIDO_HOST. O ID do domínio importa porque a chamada de criação exige esse valor. Você pode consultá-lo uma vez com GET /v1/workspaces/{workspace_id}/domains, que retorna o id e o hostname de cada domínio.

Mapeie o segredo na única etapa que chama a API, não no job inteiro. Um env no nível da etapa o mantém fora de todos os outros processos iniciados pelo job, inclusive das actions de terceiros que você não escreveu.

Um workflow funcional para encurtar uma URL a cada pull request

Este é o arquivo completo para o motivo mais comum de encurtar uma URL em um workflow do GitHub: um link de preview por pull request. Coloque-o em .github/workflows/preview-link.yml e altere a linha DEST para o local em que seus deploys de preview ficam disponíveis.

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 }}"

Algumas linhas merecem um comentário. --fail-with-body transforma um 4xx ou 5xx em uma etapa com falha, mas ainda imprime o corpo do erro, algo que curl -s não faz; com ele, um 401 faria o processo sair com 0 e o job continuaria alegremente. Os corpos das requisições são construídos com jq -n, em vez de interpolação de strings, para que um destino contendo aspas ou um e comercial não quebre o JSON. E a etapa de comentário só é executada em opened. Como a URL curta nunca muda, um comentário continua correto durante toda a vida do PR, e ninguém recebe uma notificação a cada push.

O bloco concurrency não é enfeite. Sem ele, dois pushes rápidos iniciariam duas execuções que veriam "nenhum link ainda" e tentariam criar um link. A documentação do GitHub sobre como controlar a concorrência de workflows explica o agrupamento; aqui, a execução mais antiga é cancelada e a condição de corrida nunca acontece.

Duas falhas diferentes se escondem sob a palavra idempotente, e o workflow trata cada uma separadamente. A primeira é a nova execução: um segundo push, um "Re-run jobs" manual, um PR reaberto. É para isso que serve o ramo de busca seguido de PATCH. A segunda é a requisição repetida, quando o curl envia o POST, a rede cai antes de a resposta chegar e o curl o envia novamente. O cabeçalho Idempotency-Key cobre esse caso. O Elido armazena a primeira resposta bem-sucedida associada à chave por 24 horas e a reproduz para uma nova tentativa correspondente, então a criação acontece uma vez; o funcionamento completo está em limites de taxa, retries e idempotência.

Fluxo de decisão para criar um link curto no GitHub Actions sem duplicatas: uma correspondência exata de slug no seu workspace leva a PATCH, nenhuma correspondência leva a POST e um 409 significa que outro workspace em um domínio compartilhado já possui o slug

Colisões de slug são a parte que as pessoas não percebem. Os slugs são únicos por domínio de redirecionamento, não por workspace. Em um domínio compartilhado, todos os outros clientes do Elido estão no mesmo namespace, e é provável que alguém já tenha usado um slug simples como pr-12. A busca não verá o link dessa pessoa (ela lista apenas o seu workspace), então o POST é enviado e retorna 409 slug already exists for this domain. Duas soluções: adicione uma palavra do projeto ao slug ou coloque os links de CI no seu próprio domínio personalizado, onde o namespace pertence somente a você. Eu faria as duas coisas.

Há um segundo motivo, mais sutil, para o nome do repositório ficar no final do slug, e não no começo. O parâmetro q faz uma correspondência parcial em slug, destino e título. Com myapp-pr-1, a busca também retorna myapp-pr-10 até myapp-pr-199, mais do que as 100 respostas que uma única página retorna, e o link que você queria, por ser o mais antigo, desaparece no final. pr-1-myapp não corresponde a nada além de si mesmo. É um detalhe pequeno, e levei uma tarde constrangedoramente longa perguntando "por que o PR #1 continua recebendo um 409" até perceber isso.

O workflow de preview é um padrão. Altere o gatilho, o slug e o destino, e a mesma etapa cobre a maior parte do que as equipes realmente automatizam. (Notas de release são um assunto à parte, abordado em encurtando links nas notas de release.)

Caso de usoGatilhoSlugO que a etapa faz
Deploy de preview por PRpull_requestpr-42-myappUpsert a cada push, exclui ao fechar
Deploy da documentaçãopush para maindocs-myappPATCH para a URL recém-publicada da documentação
Build mais recentepush para main ou uma taglatest-myappPATCH por um ID de link armazenado, sem busca
Artefato noturnoschedulenightly-myappPATCH para a URL do artefato mais recente

O caso da build mais recente é o mais simples de todos. Crie o link manualmente uma vez, salve seu ID numérico como uma variável e o job passa a ter uma única chamada:

- 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}')"

Mantenha esses links variáveis em um 302, que é o padrão quando você não define redirect_status. Um 301 informa aos navegadores que eles podem armazenar a resposta em cache, e as pessoas que clicaram ontem continuarão chegando à build de ontem; nosso texto sobre redirecionamentos 301 e 302 traz a versão longa.

Para links de preview, faça a limpeza quando o PR for fechado. Adicione closed aos tipos do gatilho, reutilize a busca e envie DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Um slug excluído fica livre para ser reutilizado. Se preferir manter o histórico de cliques, use PATCH com {"status": "disabled"}; o upsert acima define status: "active" a cada execução, então um PR reaberto reativa seu link.

Pronto para experimentar em um repositório? Comece um workspace gratuito, crie uma chave e o workflow acima funcionará como está assim que as três variáveis forem definidas.

Chaves de API de CI com privilégio mínimo

A chave em um segredo de CI deve conseguir fazer exatamente o que o workflow faz, nada além disso. Isso é mais difícil do que parece por causa do funcionamento das chaves pessoais.

Uma chave de API pessoal autentica como a pessoa que a criou. Tudo o que essa pessoa pode fazer, a chave pode fazer, e quando ela deixa a empresa a chave vai junto com a conta. Para CI, eu usaria um usuário de máquina: uma conta de serviço que pertence a um workspace, tem sua própria função e possui tokens que somente um administrador humano autenticado pode criar ou revogar. Crie-o em Machine users no dashboard com a função de editor, que é a função integrada mais baixa capaz de criar, editar e excluir links, e depois gere um token com data de expiração. Desativar o usuário de máquina invalida todos os tokens que ele possui de uma vez, exatamente o botão que você quer ter no dia em que um segredo vazar.

Quatro hábitos adicionais não custam nada:

  • Um token por repositório, nomeado de acordo com ele, para que o registro de auditoria mostre qual repositório criou cada link.
  • Segredos de ambiente com revisores obrigatórios para qualquer workflow que altere um link do qual as pessoas dependam.
  • permissions: definido explicitamente no topo do workflow, como no exemplo, para que o GITHUB_TOKEN receba apenas o que o job precisa.
  • Nunca use pull_request_target para alcançar o segredo a partir de PRs de forks. O texto do GitHub Security Lab sobre como evitar pwn requests mostra por que executar código não confiável ao lado de um token de escrita acaba mal.

Os workspaces também podem restringir o acesso à API por uma lista de IPs permitidos. É um controle forte para runners self-hosted com saída fixa e quase inútil para runners hospedados pelo GitHub, cujos endereços vêm de um pool muito grande e variável. A própria referência do GitHub sobre uso seguro vale uma hora do seu tempo se seus workflows tocarem em produção.

O que quebra na prática

A maioria das falhas vem de quatro lugares, e cada uma aparece como um erro legível quando --fail-with-body está ativado. Um 404 em todas as chamadas geralmente significa que a variável do ID do workspace está errada ou que a chave pertence a outro workspace. Um 400 dizendo domain_id is required significa que a variável está vazia, normalmente porque foi definida em um ambiente diferente daquele usado pelo job. Um 409 é a colisão de namespace compartilhado da seção sobre idempotência. E um 429 significa que você ultrapassou o limite de taxa por chave, algo que um único upsert por execução não atinge, mas uma matriz de cinquenta jobs pode atingir.

Uma coisa nem sequer é um erro. Depois de um PATCH, um visitante ainda pode chegar ao destino antigo por um curto período, porque os redirecionamentos são armazenados em cache perto do visitante para continuar rápidos. Um smoke test que verifica o novo destino imediatamente após a atualização pode falhar de forma intermitente. Faça polling com um backoff curto ou verifique a resposta da API.

Se quiser que a etapa comunique a mudança para fora, combine-a com webhooks para eventos de link, disparados quando um link muda, ou com os padrões de curl e jq do guia da CLI para testes locais antes de confirmar o workflow. A referência da API e dos SDKs lista todos os campos aceitos pelos endpoints de links.

Leia o conteúdo principal → Gerencie seus links curtos como Terraform

Relacionados no blog

Perguntas frequentes

O GitHub Actions pode criar links curtos?

Sim. Uma etapa do workflow pode chamar a API REST de qualquer encurtador com curl, que já vem instalado nos runners hospedados pelo GitHub junto com jq. A etapa lê a chave de API de um segredo criptografado, envia a URL de destino e grava a URL curta resultante na saída da etapa, para que as etapas seguintes possam publicá-la em um comentário de pull request ou no resumo de um job.

Como armazeno uma chave de API de encurtador de URL no GitHub Actions?

Salve-a como um segredo criptografado do repositório ou do ambiente e depois mapeie-a na única etapa que precisa dela com uma entrada env como ELIDO_API_KEY: secrets.ELIDO_API_KEY dentro da sintaxe de expressões. O GitHub mascara o valor nos logs. Deixe valores que não são secretos, como o ID do workspace e o ID do domínio, em variáveis de configuração para que continuem legíveis.

Como evito criar links curtos duplicados a cada execução do workflow?

Transforme a etapa em um upsert. Derive o slug de algo estável, como o número do pull request, procure-o primeiro e envie um PATCH para alterar o destino quando ele já existir. Só crie quando a busca não retornar nada. Um cabeçalho Idempotency-Key na chamada de criação cobre o caso separado de uma requisição repetida após um timeout de rede.

Por que meu workflow recebe um 409 ao criar um link curto?

O slug já está ocupado nesse domínio. No Elido, os slugs são únicos por domínio de redirecionamento, e um domínio compartilhado é usado por todos os outros workspaces, então é provável que um slug genérico como pr-12 já exista. Adicione um prefixo ou sufixo do projeto ao slug ou use seu próprio domínio personalizado, onde todo o namespace pertence a você.

As etapas de links curtos funcionam em pull requests de forks?

Não com o gatilho pull_request simples, porque o GitHub não passa segredos do repositório para workflows iniciados por um fork. Ignore o job para forks com uma condição if no repositório de origem. Trocar para pull_request_target para obter o segredo é arriscado, pois ele executa com acesso de escrita ao lado de código que você não revisou.

Um link que aponta para a build mais recente deve usar um redirecionamento 301 ou 302?

Use 302 ou 307. Os navegadores podem armazenar um 301 em cache indefinidamente, então visitantes recorrentes continuariam chegando a uma build antiga depois que seu workflow tivesse movido o link. Os links do Elido usam 302 por padrão quando você não define redirect_status, que é a escolha certa para qualquer link cujo destino seja alterado por um pipeline.

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

Continuar lendo