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ção —
0609b952-1397-4640-95ec-e00a01b2c241
- Número mínimo de aprovações —
fa4e907d-c16b-4a4c-9dfa-4906e5d171dd
- Vínculo com Work Item —
40e92b44-2fe1-4dd6-b3d8-74a9c21d0c6e
- Revisores obrigatórios —
fd2167ab-b0be-447a-8ec8-39368250530e
- Estratégia de merge —
fa4e907d-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:
Contribute no repositório — para o git push da branch. Sem isso o push falha ou a REST API devolve createBranchPermissionRequired.
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.
- 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í:
- A pipeline lê o Work Item e monta o nome padronizado da branch.
- Cria e publica a branch.
- Abre o Pull Request com título derivado da atividade.
- Vincula o PR ao Work Item — visível dos dois lados.
- Liga o auto-complete em nome de quem executou.
- 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