Versionando aplicações Mule com o Maven release plugin no Azure Pipelines
Referência:
templates/jobs/release.yml· ADR 2 · ADR 3
Uma versão liberada deve ser construída uma única vez, a partir de um commit com tag, e nunca reconstruída com outro código. O maven-release-plugin garante isso e elimina o erro de versionamento mais comum em times Mule: gente editando <version> à mão. Este artigo mostra o que o plugin faz, como rodá-lo dentro do Azure Pipelines e os três problemas que aparecem no caminho.
O que o plugin faz
Dois goals fazem o trabalho.
release:prepare, partindo de 1.0.0-SNAPSHOT:
- Verifica se não há mudanças sem commit nem dependências SNAPSHOT.
- Muda a versão para
1.0.0e faz commit: prepare release v1.0.0. - Cria a tag
v1.0.0nesse commit. - Muda a versão para a próxima de desenvolvimento, por exemplo
1.1.0-SNAPSHOT, e faz commit: prepare for next development iteration. - Faz push dos dois commits e da tag.
release:perform faz checkout da tag em target/checkout e roda ali os goals configurados — aqui, deploy, que publica a 1.0.0 no Anypoint Exchange. Como ele constrói a partir da tag num diretório limpo, o artefato liberado é exatamente o que a tag contém.
A configuração do plugin no pom.xml da aplicação:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<scmCommentPrefix>[skip ci] [maven-release-plugin] </scmCommentPrefix>
<tagNameFormat>v@{project.version}</tagNameFormat>
<goals>deploy</goals>
<pushChanges>true</pushChanges>
</configuration>
</plugin>
Ele também precisa de um bloco <scm> apontando para o repositório e de um distributionManagement apontando para o Exchange (https://maven.anypoint.mulesoft.com/api/v3/organizations/${project.groupId}/maven, com o ID da organização como groupId).
Rodando no pipeline
O job de release roda na main, depois de um merge. O essencial:
current=$(mvn $(MAVEN_ARGS) -q help:evaluate -Dexpression=project.version -DforceStdout)
release="${current%-SNAPSHOT}"
IFS=. read -r major minor patch <<< "$release"
case "$BUMP" in
major) next="$((major + 1)).0.0" ;;
minor) next="${major}.$((minor + 1)).0" ;;
patch) next="${major}.${minor}.$((patch + 1))" ;;
esac
mvn $(MAVEN_ARGS) release:prepare release:perform \
-DreleaseVersion="$release" \
-DdevelopmentVersion="$next-SNAPSHOT" \
-Darguments="-DskipMunitTests -s $(MAVEN_SETTINGS) -Danypoint.orgId=$(ANYPOINT_ORG_ID)"
echo "##vso[task.setvariable variable=appVersion;isOutput=true]$release"
A versão liberada é a que a main carrega, sem o -SNAPSHOT; a próxima é decidida por um parâmetro do pipeline (nextVersionBump), e não por quem roda. A versão liberada sai como variável de saída, e os stages de deploy usam exatamente essa versão. O -Darguments passa opções para o Maven que o release:perform executa dentro de target/checkout.
Problema 1: detached HEAD
O Azure Pipelines faz checkout de um commit específico, não de um branch. O plugin precisa commitar num branch, então falha. O job muda para o branch de verdade antes:
branch="${BUILD_SOURCEBRANCH#refs/heads/}"
git switch -C "$branch" "origin/$branch"
Problema 2: push sem chave pessoal
O plugin precisa fazer push. Muitas configurações — inclusive a que eu usei por anos — clonam o repositório de novo por SSH, com uma chave guardada como secure file. Funciona, mas acrescenta um segredo para rotacionar e amarra o release a quem é dono da chave.
O pipeline já tem um token para o repositório. Mantê-lo depois do checkout é uma linha:
- checkout: self
persistCredentials: true
fetchDepth: 0
Não bastou. O primeiro release de verdade falhou com:
fatal: could not read Username for 'https://github.com': terminal prompts disabled
O checkout guarda o token para a URL exata que clonou (https://github.com/brunosouzas/mulesoft-orders-api). O plugin faz push para a URL do <scm>, que termina em .git. O Git associa credenciais à URL, então para ele são remotos diferentes e o token não é enviado. A correção aplica o mesmo cabeçalho ao host inteiro:
header=$(git config --get-regexp '^http\..*\.extraheader$' | head -1 | cut -d' ' -f2- || true)
host=$(git remote get-url origin | sed -E 's#^(https://[^/]+/).*#\1#')
if [[ -n "$header" && "$host" == https://* ]]; then
git config "http.${host}.extraheader" "$header"
fi
Ela saiu na versão 1.0.1 dos templates e funciona também com o Azure Repos.
Problema 3: o pipeline disparando a si mesmo
Os dois commits do plugin caem na main, e um push na main dispara o pipeline — que faria outro release. O scmCommentPrefix começa com [skip ci], que o Azure Pipelines respeita em gatilhos de push. Depois do release, a main mostra os dois commits do plugin e nenhuma execução extra.
Proteção de branch versus release automatizado
A main é protegida: só pull requests podem alterá-la. O plugin faz push direto. Algo precisa ceder, e há três opções:
- Liberar a identidade do release para contornar a regra na
main. É a opção mais estreita quando essa identidade é o próprio pipeline — por exemplo, o GitHub App do Azure Pipelines, ou o build service no Azure Repos com “Bypass policies when pushing”. - Fazer o release no branch
release/*e levar o resultado àmainpor PR. Amainfica totalmente protegida, mas a tag nasce num branch mergeado depois, e um PR rejeitado deixa uma tag órfã. - Fazer push com um token ou chave pessoal. Funciona em qualquer lugar, acrescenta um segredo e amarra o release a uma pessoa.
O projeto de referência usa a opção 1. Com uma ressalva, dita com honestidade: a conexão com o GitHub usa OAuth, então o pipeline age como o meu usuário, e o bypass precisa ser dado ao papel de admin do repositório. Com uma conexão via GitHub App, só o app ficaria na lista de bypass, que é a configuração melhor para um time.
Depois do release
Faça o merge da main de volta no develop por PR. Se o develop subiu de versão quando o branch de release foi criado (veja o artigo sobre GitFlow), os dois branches estão no mesmo próximo -SNAPSHOT e o back-merge não tem conflito.
Checklist
- Alguém edita
<version>à mão? Não deveria precisar. - O formato da tag é consistente (
v1.2.3) e a tag é a fonte da verdade sobre o que foi liberado? - O job de release roda num branch, faz push com a identidade do pipeline e ignora os próprios commits?
- O bypass na
mainestá limitado à identidade do release?