Assinar
Voltar
DevOps

Azure DevOps Pipelines: a mesma pipeline abre o PR, amarra o Work Item e completa sozinha (Parte 2, final)

Segunda e última parte da série: a pipeline que criava a branch agora abre o Pull Request, vincula o Work Item, liga o auto-complete em nome de quem executou — e cria as branch policies que fazem tudo isso significar alguma coisa.

</>

Na Parte 1 a gente resolveu o nome da branch. Um clique, parâmetros tipados, feature/squad-alpha/checkout-4821 criada no padrão certo. Governança ganha, ninguém mais inventa ajuste-final-2.

E aí o problema muda de lugar.

A branch nasce padronizada e some. Duas semanas depois o board tem uma atividade “Em desenvolvimento” que ninguém sabe onde está, e o repositório tem dezessete branches órfãs. Padronizar o nome resolveu a rastreabilidade da criação — não a do ciclo de vida. Automatizar o começo e deixar o resto na mão é trocar um trabalho manual por outro.

Este é o post que fecha a série. Vamos completar o circuito: a mesma pipeline vai abrir o Pull Request, amarrar o Work Item, ligar o auto-complete e — a parte que a maioria dos tutoriais pula — criar as políticas de branch que fazem o auto-complete significar alguma coisa.

Auto-complete sem branch policy é merge automático com nome bonito. A política não é burocracia acessória: é o que transforma “completa sozinho” em “completa sozinho depois que passou“.

Um parâmetro a mais muda tudo

A Parte 1 pedia atividade como texto livre. Para fechar o circuito, esse campo precisa virar o ID do Work Item. Parece detalhe, mas é a chave: com o ID em mãos, a pipeline consegue nomear a branch, criar o PR e amarrar os dois ao item do board — tudo a partir de uma única entrada do usuário.

parameters:
  - name: workItemId
    displayName: ID do Work Item (ex.: 4821)
    type: number

  - name: projeto
    displayName: Projeto
    type: string
    default: meu-projeto

  - name: squad
    displayName: Squad responsavel
    type: string
    values:
      - squad-alpha
      - squad-bravo
      - squad-charlie

  - name: branchDestino
    displayName: Branch de destino do PR
    type: string
    default: main

  - name: autoComplete
    displayName: Completar o PR automaticamente quando as politicas passarem
    type: boolean
    default: true

trigger: none
pr: none

Repare no type: number e no type: boolean. Parâmetro tipado não é preciosismo — o Azure Pipelines valida antes de enfileirar, então “4821a” nunca chega ao curl. É validação de graça, na fronteira certa.

Passo 1 — ler o Work Item para nomear a branch

Antes de criar qualquer coisa, buscamos o título do Work Item. Ele vai virar o nome da branch e o título do PR, o que elimina a divergência clássica entre “o que a branch diz” e “o que a tarefa era”.

  - script: |
      set -euo pipefail

      WI=$(curl -sf \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/wit/workitems/${{ parameters.workItemId }}?api-version=7.1")

      TITULO=$(echo "$WI" | jq -r '.fields["System.Title"]')
      echo "Work Item: $TITULO"

      # slug: minusculas, sem acento, hifens no lugar de espaco, 40 chars
      SLUG=$(echo "$TITULO" \
        | iconv -f utf8 -t ascii//TRANSLIT \
        | tr '[:upper:]' '[:lower:]' \
        | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' \
        | cut -c1-40 | sed -E 's/-+$//')

      BRANCH="feature/${{ parameters.squad }}/${{ parameters.workItemId }}-$SLUG"

      # exporta para os proximos steps do job
      echo "##vso[task.setvariable variable=novaBranch]$BRANCH"
      echo "##vso[task.setvariable variable=tituloWI]$TITULO"
    displayName: "Ler Work Item e montar o nome da branch"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Duas coisas aqui merecem atenção.

O ##vso[task.setvariable] é o mecanismo de logging command do Azure Pipelines: um echo num formato especial que o agente intercepta e converte em variável para os passos seguintes. É a única forma de um script devolver valor para o resto do job — variável de shell morre no fim do passo.

E o set -euo pipefail não é enfeite. Sem o -e, um curl que falha não derruba o passo, o jq recebe vazio, e você acaba com uma branch chamada feature/squad-alpha/4821-null criada com sucesso. Pipeline que falha silenciosamente é pior que pipeline que quebra.

Passo 2 — criar a branch e abrir o Pull Request

A criação da branch é a mesma da Parte 1. O que muda é o que vem logo depois: um POST em /pullrequests.

  - checkout: self
    persistCredentials: true

  - script: |
      set -euo pipefail

      # --- cria e publica a branch (igual a Parte 1) ---
      git checkout -b "$(novaBranch)"
      git push origin "$(novaBranch)"

      # --- abre o Pull Request ---
      BODY=$(jq -n \
        --arg src  "refs/heads/$(novaBranch)" \
        --arg dst  "refs/heads/${{ parameters.branchDestino }}" \
        --arg tit  "AB#${{ parameters.workItemId }} $(tituloWI)" \
        --arg desc "PR aberto automaticamente pela pipeline de criacao de branch." \
        '{sourceRefName:$src, targetRefName:$dst, title:$tit, description:$desc}')

      PR=$(curl -sf -X POST \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        -H "Content-Type: application/json" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests?api-version=7.1" \
        -d "$BODY")

      PR_ID=$(echo "$PR" | jq -r '.pullRequestId')
      echo "Pull Request #$PR_ID criado"
      echo "##vso[task.setvariable variable=prId]$PR_ID"
    displayName: "Criar branch e abrir o Pull Request"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Três detalhes que economizam uma tarde de depuração:

  • Use Build.Repository.ID, não Build.Repository.Name. A API aceita os dois, mas o nome quebra quando alguém renomeia o repositório — e o ID, segundo a própria documentação de variáveis predefinidas, não muda nunca.
  • sourceRefName e targetRefName exigem o caminho completo (refs/heads/main, não main). Passar o nome curto devolve um 400 com mensagem pouco útil.
  • Monte o JSON com jq -n, não com echo e aspas. Título de Work Item vem com aspas, acento, cedilha e barra. Concatenar string à mão funciona até o primeiro item chamado Corrigir “N/A” no relatório — e aí você tem um JSON inválido e um erro que não aponta para a causa.

Por que o prefixo AB# no título

O Azure DevOps reconhece a sintaxe AB#1234 em títulos, descrições e mensagens de commit e a transforma em link para o Work Item. É um detalhe de dois caracteres que faz o PR ficar navegável na direção contrária — de quem está no código para quem está no board.

Passo 3 — amarrar o Work Item de verdade

A API de criação de PR aceita um campo workItemRefs, e é tentador resolver tudo ali. Na prática, a ligação que aparece no board — na aba Development do Work Item, que é onde o time olha — é uma relação do tipo ArtifactLink no lado do Work Item. Fazer o vínculo explícito é uma chamada a mais e elimina a categoria inteira de “o PR existe mas o board não sabe”.

  - script: |
      set -euo pipefail

      # formato do artefato: vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{prId}
      ARTIFACT="vstfs:///Git/PullRequestId/$(System.TeamProjectId)%2F$(Build.Repository.ID)%2F$(prId)"

      PATCH=$(jq -n --arg url "$ARTIFACT" \
        '[{op:"add", path:"/relations/-", value:{
            rel:"ArtifactLink",
            url:$url,
            attributes:{name:"Pull Request"}
          }}]')

      curl -sf -X PATCH \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        -H "Content-Type: application/json-patch+json" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/wit/workitems/${{ parameters.workItemId }}?api-version=7.1" \
        -d "$PATCH"

      echo "Work Item ${{ parameters.workItemId }} vinculado ao PR $(prId)"
    displayName: "Vincular Work Item ao Pull Request"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Dois pontos costumam derrubar esse passo na primeira execução. O Content-Type é application/json-patch+json, não application/json — a API de Work Items fala JSON Patch (RFC 6902), e o corpo é um array de operações. E as barras dentro do identificador do artefato precisam vir codificadas como %2F: o valor inteiro é um único token, não um caminho de URL.

Passo 4 — auto-complete em nome de quem rodou

Aqui está a decisão de design mais interessante desta pipeline.

Ligar o auto-complete é um PATCH no PR informando autoCompleteSetBy — e esse campo pede uma identidade. O reflexo é usar a identidade de build. Mas a build identity é um robô: se ela é quem “pediu” a conclusão automática, o histórico do PR fica sem dono e as políticas que dependem de voto de pessoa ficam sem sentido.

A resposta elegante é Build.RequestedForId — o GUID de quem clicou em Run pipeline. O PR passa a se auto-completar em nome da pessoa que iniciou a atividade, que é exatamente a semântica certa:

  - ${{ if eq(parameters.autoComplete, true) }}:
    - script: |
        set -euo pipefail

        BODY=$(jq -n --arg quem "$(Build.RequestedForId)" \
          '{autoCompleteSetBy:{id:$quem},
            completionOptions:{
              mergeStrategy:"squash",
              deleteSourceBranch:true,
              transitionWorkItems:true,
              bypassPolicy:false
            }}')

        curl -sf -X PATCH \
          -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
          -H "Content-Type: application/json" \
          "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests/$(prId)?api-version=7.1" \
          -d "$BODY"

        echo "Auto-complete ligado por $(Build.RequestedFor)"
      displayName: "Ligar auto-complete no PR"
      env:
        SYSTEM_ACCESSTOKEN: $(System.AccessToken)

As completionOptions merecem leitura atenta, porque cada uma é uma decisão de processo disfarçada de campo JSON:

  • mergeStrategy: "squash". Um commit por PR no histórico da main. Se o seu time prefere preservar os commits individuais, use noFastForward.
  • deleteSourceBranch: true. É o que resolve as dezessete branches órfãs do começo do texto. Sem isso, você automatizou a criação e não a limpeza.
  • transitionWorkItems: true. Move o Work Item vinculado para o estado seguinte quando o PR completa. É o fechamento do circuito: a atividade sai de “Em desenvolvimento” sozinha.
  • bypassPolicy: false. Deixe explícito, mesmo sendo o padrão. Ver false escrito no YAML é o que impede alguém de trocar para true “só para destravar” e nunca mais voltar atrás.

Passo 5 — as políticas, que é onde isso vira engenharia

Auto-complete só faz sentido se existir algo a esperar. Sem política nenhuma, “completar quando tudo passar” completa imediatamente — e você construiu uma máquina de dar merge direto na main.

Políticas de branch também são API. Dá para versionar a configuração do repositório junto com o código, o que acaba com o “alguém desligou a validação no console e ninguém viu”.

POST $(System.CollectionUri)$(System.TeamProject)/_apis/policy/configurations?api-version=7.1
Content-Type: application/json

{
  "isEnabled": true,
  "isBlocking": true,
  "type": { "id": "0609b952-1397-4640-95ec-e00a01b2c241" },
  "settings": {
    "buildDefinitionId": 42,
    "displayName": "Build de validacao",
    "queueOnSourceUpdateOnly": true,
    "manualQueueOnly": false,
    "validDuration": 720,
    "scope": [
      { "repositoryId": "<repo-id>", "refName": "refs/heads/main", "matchKind": "exact" }
    ]
  }
}

O type.id é o que define qual política você está criando. Os identificadores são fixos e globais:

  • Build de validação0609b952-1397-4640-95ec-e00a01b2c241
  • Número mínimo de aprovaçõesfa4e907d-c16b-4a4c-9dfa-4906e5d171dd
  • Vínculo com Work Item40e92b44-2fe1-4dd6-b3d8-74a9c21d0c6e
  • Revisores obrigatóriosfd2167ab-b0be-447a-8ec8-39368250530e
  • Estratégia de mergefa4e907d-c16b-4a4c-9dfa-4916e5d171ab

Não decore essa lista — e principalmente não a copie de um post antigo. A fonte confiável é a própria instalação:

curl -sf -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
  "$(System.CollectionUri)$(System.TeamProject)/_apis/policy/types?api-version=7.1" \
  | jq -r '.value[] | "\(.id)  \(.displayName)"'

Uma combinação que funciona bem na prática: exigir uma aprovação com creatorVoteCounts: false (quem abriu não aprova o próprio PR), build de validação bloqueante e vínculo com Work Item obrigatório. Essa terceira política tem um efeito colateral simpático: como a nossa pipeline já vincula o item no passo 3, o PR nasce em conformidade — a política vira uma rede de proteção contra o PR aberto na mão, não um obstáculo ao fluxo automatizado.

As permissões — onde isso quebra na primeira execução

Praticamente todo mundo tropeça aqui, e o erro raramente diz a verdade. Três coisas precisam estar certas para a identidade de build fazer esse trajeto inteiro:

  1. Contribute no repositório — para o git push da branch. Sem isso o push falha ou a REST API devolve createBranchPermissionRequired.
  2. Contribute to pull requests — permissão separada da anterior. Criar branch e criar PR são direitos distintos, e é comum liberar só o primeiro e passar meia hora depurando um 403 no POST /pullrequests.
  3. Escopo de autorização do job — se a organização tem Limit job authorization scope to current project ativado (e deveria ter), o System.AccessToken só alcança recursos do projeto atual. Repositório em outro projeto exige token próprio.

A identidade a procurar nas permissões do repositório é <Nome do Projeto> Build Service (<organização>). Para escrever no Work Item, ela também precisa de Edit work items in this node na área correspondente.

Armadilha de rodapé: a branch nova pode disparar a sua CI, que dispara a build de validação, que roda de novo a cada push. Se a pipeline de criação de branch estiver no mesmo arquivo da CI sem trigger: none, você monta um laço que consome agente até alguém perceber a conta.

O circuito fechado

Vale olhar o que a série construiu, de ponta a ponta. Uma pessoa abre o Azure DevOps, clica em Run pipeline, informa o ID do Work Item e a squad. A partir daí:

  1. A pipeline lê o Work Item e monta o nome padronizado da branch.
  2. Cria e publica a branch.
  3. Abre o Pull Request com título derivado da atividade.
  4. Vincula o PR ao Work Item — visível dos dois lados.
  5. Liga o auto-complete em nome de quem executou.
  6. Quando a build passa e a aprovação sai, o merge acontece, a branch é apagada e o Work Item avança de estado.

Um clique no início, um clique de aprovação no fim. Entre os dois, nenhuma etapa depende de alguém lembrar de fazer.

A lição que fica não é sobre Azure Pipelines. É sobre onde a automação começa a valer: automatizar uma etapa de um processo de seis normalmente só empurra o trabalho manual para a etapa seguinte. O ganho aparece quando o circuito fecha — quando não sobra nenhum ponto em que o fluxo depende de disciplina humana para continuar. Padronizar o nome da branch foi bom. Fazer o board se atualizar sozinho é o que muda a rotina do time.

Perguntas frequentes

Preciso mesmo criar as políticas pela API?

Não. Configurar pelo console funciona igual. A vantagem da API é que a configuração vira código versionado, revisado em PR e replicável em outro repositório — em vez de um estado invisível que alguém pode alterar sem deixar rastro.

O auto-complete pode dar merge sem ninguém revisar?

Pode, se não houver política bloqueante. Com isBlocking: true em pelo menos uma política de aprovação, o PR fica esperando. É por isso que o passo 5 não é opcional: sem política, “auto-complete” é literalmente “merge agora”.

E se o Work Item não existir?

Com set -euo pipefail e curl -sf, o passo falha com 404 antes de criar qualquer coisa — que é o comportamento desejado. O -f do curl é o que transforma resposta de erro HTTP em código de saída diferente de zero; sem ele o corpo do erro segue adiante como se fosse dado válido.

Dá para adaptar isso para GitHub Actions?

Dá, e a estrutura é a mesma: ler a issue, criar a branch, abrir o PR (gh pr create), ligar o auto-merge (gh pr merge --auto) e proteger a branch com rulesets. Muda a API, não o raciocínio — e o ponto sobre “auto-merge sem proteção é merge direto” vale igual.

Uso Jira em vez de Azure Boards. Perdi a série?

Só os passos 1 e 3. A branch continua sendo nomeada a partir da chave da issue, o PR continua sendo aberto pela mesma API do Azure Repos, e o vínculo passa a ser feito pelo smart commit do Jira ou pela integração dele com o Azure DevOps. O circuito fecha do mesmo jeito.

Fontes

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *