← Blog

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:

  1. Verifica se não há mudanças sem commit nem dependências SNAPSHOT.
  2. Muda a versão para 1.0.0 e faz commit: prepare release v1.0.0.
  3. Cria a tag v1.0.0 nesse commit.
  4. Muda a versão para a próxima de desenvolvimento, por exemplo 1.1.0-SNAPSHOT, e faz commit: prepare for next development iteration.
  5. 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:

  1. 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”.
  2. Fazer o release no branch release/* e levar o resultado à main por PR. A main fica totalmente protegida, mas a tag nasce num branch mergeado depois, e um PR rejeitado deixa uma tag órfã.
  3. 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 main está limitado à identidade do release?