Skip to content

javachanges GitLab CI/CD Usage Guide

1. Overview

This guide explains how to use javachanges in GitLab CI/CD for:

  1. regular validation
  2. GitLab CI/CD variable management
  3. release merge request generation
  4. release tag creation from the generated release plan
  5. Maven or Gradle publishing in tag pipelines
  6. Maven and Gradle dependency caching

javachanges now has four GitLab-specific commands:

CommandPurpose
gitlab-release-planCreate or update a release branch and release merge request
gitlab-tag-from-planCreate and push the final release tag after the release plan lands
gitlab-releaseGenerate release notes and create or update the GitLab Release for the current CI tag
init-gitlab-ciGenerate the minimal GitLab CI file that wires release-plan, tag, publish, and GitLab Release jobs

2. What javachanges Can Do In GitLab CI/CD

Recommended command mapping:

GoalCommand
Check pending release statestatus
Apply a release plan locally or in CIplan --apply true
Generate Maven settings from env varswrite-settings
Preview GitLab variables from a local env filerender-vars --platform gitlab
Check platform readinessdoctor-local, doctor-platform
Sync GitLab variables through glabsync-vars --platform gitlab
Audit GitLab variables through glab variable exportaudit-vars --platform gitlab
Create or update a GitLab release MRgitlab-release-plan --write-plan-files false --execute true
Create and push a release tag after a release plan mergegitlab-tag-from-plan --fresh true --execute true
Validate a publishpreflight
Run the real Maven deploy commandpublish --execute true
Run the real Gradle publish taskgradle-publish --execute true
Create or update a GitLab Release from the tag pipelinegitlab-release --execute true

3. Variable Model

3.1 Shared Maven repository variables

javachanges understands these values from env/release.env.example:

VariableRequiredMeaning
MAVEN_RELEASE_REPOSITORY_URLYesRelease repository URL
MAVEN_SNAPSHOT_REPOSITORY_URLYesSnapshot repository URL
MAVEN_RELEASE_REPOSITORY_IDYesRelease repository id
MAVEN_SNAPSHOT_REPOSITORY_IDYesSnapshot repository id
MAVEN_REPOSITORY_USERNAMEYes, unless explicit split credentials are usedShared username
MAVEN_REPOSITORY_PASSWORDYes, unless explicit split credentials are usedShared password
MAVEN_CENTRAL_USERNAMEOptionalSonatype Central Portal token username fallback
MAVEN_CENTRAL_PASSWORDOptionalSonatype Central Portal token password fallback
MAVEN_RELEASE_REPOSITORY_USERNAMEOptionalExplicit release username
MAVEN_RELEASE_REPOSITORY_PASSWORDOptionalExplicit release password
MAVEN_SNAPSHOT_REPOSITORY_USERNAMEOptionalExplicit snapshot username
MAVEN_SNAPSHOT_REPOSITORY_PASSWORDOptionalExplicit snapshot password
GITLAB_RELEASE_TOKENOptionalExtra token for GitLab release creation flows outside CI job token fallback

When syncing to GitLab with sync-vars, secret values are written as masked and protected variables.

3.2 Extra variables for GitLab release branch and MR automation

gitlab-release-plan also depends on these runtime values:

VariableSource
CI_PROJECT_IDGitLab built-in CI variable or --project-id
CI_DEFAULT_BRANCHGitLab built-in CI variable
CI_SERVER_HOSTGitLab built-in CI variable
CI_SERVER_URLGitLab built-in CI variable
CI_PROJECT_PATHGitLab built-in CI variable
GITLAB_RELEASE_BOT_USERNAMEProject variable you provide
GITLAB_RELEASE_BOT_TOKENProject variable you provide

gitlab-tag-from-plan additionally needs:

Option or variableMeaning
--before-sha or CI_COMMIT_BEFORE_SHAPrevious commit SHA
--current-sha or CI_COMMIT_SHACurrent commit SHA

4. Local Preparation

4.1 Build the CLI

bash
mvn -q test

4.2 Initialize a local env file

bash
mvn -q -DskipTests compile exec:java -Dexec.args="init-env --target env/release.env.local"

4.3 Preview GitLab variables

bash
mvn -q -DskipTests compile exec:java -Dexec.args="render-vars --env-file env/release.env.local --platform gitlab"

4.4 Check local readiness

bash
mvn -q -DskipTests compile exec:java -Dexec.args="doctor-local --env-file env/release.env.local --gitlab-repo group/project"

4.5 Sync GitLab variables with glab

Dry-run:

bash
mvn -q -DskipTests compile exec:java -Dexec.args="sync-vars --env-file env/release.env.local --platform gitlab --repo group/project"

Apply:

bash
mvn -q -DskipTests compile exec:java -Dexec.args="sync-vars --env-file env/release.env.local --platform gitlab --repo group/project --execute true"

Audit:

bash
mvn -q -DskipTests compile exec:java -Dexec.args="audit-vars --env-file env/release.env.local --platform gitlab --gitlab-repo group/project"

Recommended stages:

  1. verify
  2. release-plan
  3. tag
  4. publish

6. Minimal .gitlab-ci.yml

If you want the shortest stable setup, let javachanges generate it:

bash
mvn -q -DskipTests compile exec:java -Dexec.args="init-gitlab-ci --directory /path/to/repo --output .gitlab-ci.yml --force true"

If you prefer to call the released Maven plugin directly from a business repository, the shortest runnable form is:

bash
mvn -B io.github.sonofmagic:javachanges:1.12.2:run -Djavachanges.args="gitlab-release-plan --directory $CI_PROJECT_DIR --write-plan-files false --execute true"

Generated template shape:

yaml
stages:
  - verify
  - release-plan
  - tag
  - publish

default:
  image: maven:3.9.9-eclipse-temurin-8
  cache:
    key:
      files:
        - pom.xml
    paths:
      - .m2/repository

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  JAVACHANGES_VERSION: "1.12.2"

verify:
  stage: verify
  script:
    - mvn -B verify
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="status --directory $CI_PROJECT_DIR"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH

release_plan_mr:
  stage: release-plan
  script:
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="gitlab-release-plan --directory $CI_PROJECT_DIR --write-plan-files false --execute true"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

release_tag:
  stage: tag
  script:
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="gitlab-tag-from-plan --directory $CI_PROJECT_DIR --fresh true --execute true"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

publish_snapshot:
  stage: publish
  script:
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="publish --directory $CI_PROJECT_DIR --execute true"
  rules:
    - if: $CI_COMMIT_BRANCH == "snapshot"

publish_release:
  stage: publish
  script:
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="publish --directory $CI_PROJECT_DIR --execute true"
    - >
      mvn -B io.github.sonofmagic:javachanges:${JAVACHANGES_VERSION}:run
      -Djavachanges.args="gitlab-release --directory $CI_PROJECT_DIR --execute true"
  rules:
    - if: $CI_COMMIT_TAG

How the example works:

JobPurpose
verifyValidates the repository and prints release state
release_plan_mrCreates or updates the release branch and merge request
release_tagCreates the final tag after the release plan manifest has changed on the default branch
publish_snapshotPublishes from the configured snapshot branch without extra shell branch parsing
publish_releasePublishes from the final Git tag and creates or updates the GitLab Release

If .changesets/config.json or .changesets/config.jsonc contains:

jsonc
{
  "snapshotBranch": "snapshot",
  "snapshotVersionMode": "plain"
}

then the same publish --directory $CI_PROJECT_DIR --execute true snapshot job automatically switches to plain snapshot mode on that branch. No extra if block or custom mvn deploy split is required in the business repository.

6.1 Minimal Gradle .gitlab-ci.yml

For Gradle repositories, use the CLI jar directly and let Gradle own artifact publishing:

yaml
stages:
  - verify
  - release-plan
  - tag
  - publish

default:
  image: eclipse-temurin:17
  cache:
    key:
      files:
        - gradle.properties
        - settings.gradle.kts
    paths:
      - .gradle/caches
      - .gradle/wrapper
      - .javachanges

variables:
  GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
  JAVACHANGES_VERSION: "1.12.2"

before_script:
  - ./gradlew --version
  - mkdir -p .javachanges
  - >
    test -f ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar" ||
    curl -fsSL
    "https://repo1.maven.org/maven2/io/github/sonofmagic/javachanges/${JAVACHANGES_VERSION}/javachanges-${JAVACHANGES_VERSION}.jar"
    -o ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar"

verify:
  stage: verify
  script:
    - ./gradlew --no-daemon build
    - java -jar ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar" status --directory "$CI_PROJECT_DIR"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH

release_plan_mr:
  stage: release-plan
  script:
    - >
      java -jar ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar"
      gitlab-release-plan
      --directory "$CI_PROJECT_DIR"
      --project-id "$CI_PROJECT_ID"
      --write-plan-files false
      --execute true
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

release_tag:
  stage: tag
  script:
    - java -jar ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar" gitlab-tag-from-plan --directory "$CI_PROJECT_DIR" --fresh true --execute true
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

publish_release:
  stage: publish
  script:
    - java -jar ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar" gradle-publish --directory "$CI_PROJECT_DIR" --execute true
    - java -jar ".javachanges/javachanges-${JAVACHANGES_VERSION}.jar" gitlab-release --directory "$CI_PROJECT_DIR" --execute true
  rules:
    - if: $CI_COMMIT_TAG

The release-plan job stages gradle.properties, CHANGELOG.md, and .changesets/ for Gradle repositories. gradle-publish renders and executes the Gradle publish task with the release or snapshot version resolved from the same manifest. For a simpler jar download step, you can also use mvn dependency:copy if Maven is available in your runner image.

7. Safe script: Patterns For GitLab CI

Recommended:

  • Keep each script: item to one command when possible.
  • Use YAML folded scalars like - > for long single commands that need line wrapping.
  • Prefer direct mvn ... or java -jar ... invocation over inline shell program generation.
  • Prefer the official Maven plugin entrypoint over custom shell wrappers or ad hoc runner POM files in business repositories.
  • If CI must write a file, prefer printf, echo, or a checked-in script under scripts/.
  • Quote GitLab variables explicitly, for example "$CI_PROJECT_DIR" and "$CI_COMMIT_TAG".

Not recommended:

  • script: - | blocks that contain shell heredoc such as cat <<EOF.
  • Heredoc bodies whose lines can be misread as YAML keys or list items after indentation changes.
  • Large inline shell programs embedded directly in .gitlab-ci.yml when the same logic can live in a repository script.

Why this pitfall is common:

  • GitLab parses .gitlab-ci.yml as YAML before the shell runs.
  • Heredoc syntax needs indentation that remains valid for both YAML and the shell terminator.
  • A small reindent can make YAML treat heredoc content as a new mapping key, which causes errors such as could not find expected ':' while scanning a simple key.
  • This is easy to trigger when users copy a working shell snippet into script: - | and then adjust indentation by hand.

Recommended pattern:

yaml
release_tag:
  stage: tag
  script:
    - mvn -B -DskipTests compile
    - >
      mvn -B -DskipTests compile exec:java
      -Dexec.args="gitlab-tag-from-plan --directory $CI_PROJECT_DIR --fresh true --before-sha $CI_COMMIT_BEFORE_SHA --current-sha $CI_COMMIT_SHA --execute true"

Avoid:

yaml
release_tag:
  stage: tag
  script:
    - |
      cat <<EOF > release.env
      CI_PROJECT_DIR=$CI_PROJECT_DIR
      CI_COMMIT_SHA=$CI_COMMIT_SHA
      EOF
      mvn -B -DskipTests compile exec:java -Dexec.args="gitlab-tag-from-plan --directory $CI_PROJECT_DIR --fresh true --execute true"

Safer file-generation pattern:

yaml
write_env:
  script:
    - printf 'CI_PROJECT_DIR=%s\nCI_COMMIT_SHA=%s\n' "$CI_PROJECT_DIR" "$CI_COMMIT_SHA" > release.env

For longer setup flows, move the shell into a repository script:

yaml
release_plan_mr:
  script:
    - ./scripts/gitlab-release-plan.sh

8. How The GitLab-specific Commands Behave

8.1 gitlab-release-plan

Default behavior:

InputDefault
--project-idCI_PROJECT_ID
--target-branchCI_DEFAULT_BRANCH, or main if absent
--release-branchchangeset-release/<target-branch>

Important behavior:

ConditionResult
No pending changesetsSkips the release MR
--execute true missingDry-run only
Release plan produces no staged file changesSkips MR update
Open release MR already existsUpdates it instead of creating a new one
Remote changeset-release/* branch already existsReuses the branch by resolving its current remote SHA, then pushes with an explicit --force-with-lease

Notes:

  • gitlab-release-plan treats changeset-release/<target-branch> as an automation-owned branch.
  • If the remote branch exists but no open MR matches it, the command still refreshes that branch and then creates a new MR.
  • This keeps repeated default-branch pipelines idempotent without requiring manual branch deletion.

8.2 gitlab-tag-from-plan

Important behavior:

ConditionResult
beforeSha missing or all zerosSkips tagging
release state did not change between commitsSkips tagging
--fallback-from-release-commit true and HEAD is chore(release): release vX.Y.ZCreates the whole-repo tag from that release commit
Tag already exists remotelySkips tagging
--execute true missingDry-run only

9. Generic Maven Publish In GitLab CI/CD

The generic publish helper uses:

  1. preflight logic to verify revision, tag, and credentials
  2. write-settings logic to generate .m2/settings.xml
  3. repository variables such as MAVEN_RELEASE_REPOSITORY_URL
  4. credentials from your GitLab CI/CD variables

Snapshot mode behavior:

  • default behavior stays stamped, which rewrites 1.2.3-SNAPSHOT to a unique stamped revision before deploy
  • if the configured snapshotBranch matches the current branch and snapshotVersionMode is plain, publish --execute true keeps the effective version at the original 1.2.3-SNAPSHOT
  • preflight and publish logs now print the resolved snapshot mode so pipeline logs make the choice explicit
  • even in plain mode, Maven snapshot repositories still normally produce timestamped artifact filenames on the server side; that is repository-standard snapshot expansion, not a second rewrite by javachanges

Typical tag-pipeline split:

yaml
publish_preflight:
  stage: publish
  script:
    - mvn -B -DskipTests compile
    - >
      mvn -B -DskipTests compile exec:java
      -Dexec.args="preflight --directory $CI_PROJECT_DIR --tag $CI_COMMIT_TAG"
  rules:
    - if: $CI_COMMIT_TAG

publish_execute:
  stage: publish
  script:
    - mvn -B -DskipTests compile
    - >
      mvn -B -DskipTests compile exec:java
      -Dexec.args="publish --directory $CI_PROJECT_DIR --tag $CI_COMMIT_TAG --execute true"
  rules:
    - if: $CI_COMMIT_TAG

10. Maven Cache Behavior In GitLab CI/CD

Recommended cache:

yaml
cache:
  key:
    files:
      - pom.xml
  paths:
    - .m2/repository

Recommended runtime option:

yaml
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

What this improves:

Cached wellNot solved by GitLab cache alone
Maven dependenciesGit clone/fetch cost
Maven pluginsJDK image pull time
Repeat pipelines with the same pom.xmlGitLab API calls for release MR creation
Reuse across jobs on shared cache backendsRemote repository publishing latency

Important behavior:

SituationResult
New cache keyFirst pipeline still downloads
pom.xml changesCache key may change
Different runners without shared cacheCache reuse may be weak
Shared/distributed GitLab cache configuredCross-runner reuse improves

11. Optional Hygiene And Secret Scanning

If you add a hygiene or secret-scanning job to the same repository, distinguish two different outcomes:

  • A real secret hit means repository content contains a value that looks like an actual credential or private key.
  • A rule self-hit means the scanner matched its own detection patterns, such as ghp_, glpat-, AKIA, or BEGIN PRIVATE KEY, inside .gitlab-ci.yml, Makefile, or another rule-definition file.

Recommended default strategy:

  1. keep secret-detection patterns in a dedicated file such as .hygiene/secret-patterns.txt
  2. exclude that file, plus .gitlab-ci.yml and Makefile, from content scanning
  3. scan source, docs, scripts, and config that may carry real secrets
  4. use allowlist comments only for one-off reviewed exceptions

Why this is the safest default:

  • scanner configuration is not business content and should not be scanned like application files
  • excluding one dedicated rules file is easier to reason about than scattering regex literals through CI YAML
  • it avoids fragile pattern splitting that makes rule maintenance harder

Recommended example:

yaml
hygiene:
  stage: verify
  script:
    - ./scripts/secret-scan.sh
  rules:
    - if: $CI_COMMIT_BRANCH
bash
# scripts/secret-scan.sh
set -eu

scanner scan \
  --rules .hygiene/secret-patterns.txt \
  --exclude .hygiene/secret-patterns.txt \
  --exclude .gitlab-ci.yml \
  --exclude Makefile \
  .

Option comparison:

OptionProsConsRecommendation
Exclude .gitlab-ci.yml / Makefile onlysimple and quickstill fails if rules move into another scanned fileuseful as a minimum stopgap
Move rules to a dedicated file and exclude itclear ownership, easiest to explain, stable over timeneeds one extra checked-in filerecommended default
Split patterns into fragments and concatenate themcan avoid literal self-matches without file exclusionshurts readability, easier to break, may reduce portability across toolsavoid by default
Use allowlist commentsprecise for a few reviewed linesnoisy, tool-specific, easy to overuse until real hits get hiddenkeep for exceptional cases only

Avoid:

  • defining detector regex literals directly in .gitlab-ci.yml
  • defining the same literals inline in Makefile targets
  • treating allowlists as the primary suppression mechanism for scanner-owned files

12. Common Mistakes

ProblemCauseFix
Release MR job fails to pushGITLAB_RELEASE_BOT_TOKEN or GITLAB_RELEASE_BOT_USERNAME missingadd the bot credentials as project variables
Release MR job fails with stale infoanother process updated changeset-release/* after javachanges resolved the remote SHArerun the pipeline; if the branch is shared by other automation, stop sharing that branch name
Release tag job never tagsrelease state did not change or CI_COMMIT_BEFORE_SHA is unusableinspect the branch pipeline, version file, and changelog
Snapshot publish job cannot see Maven credentials or GITLAB_RELEASE_TOKENthe variables are protected but the configured snapshotBranch is not a protected branchprotect the snapshotBranch, then rerun doctor-platform --platform gitlab and the pipeline
GitLab rejects the pipeline before any job starts with could not find expected ':' while scanning a simple keyheredoc or other multiline shell content broke YAML indentation rulesreplace `script: -
Hygiene or secret scan fails on .gitlab-ci.yml or Makefile, but no real credential was addedthe scanner matched rule literals inside its own configurationmove patterns into a dedicated rules file and exclude scanner-owned files from scanning
sync-vars does nothingenv file still contains placeholdersreplace replace-me values first
audit-vars fails with MISMATCHlocal env and remote project variables divergedresync or deliberately update one side
Publish job fails on missing Maven credentialsproject variables were not configuredsync the variables with glab, then rerun

Use these docs together:

NeedDocument
Generic release commands and local preparationDevelopment Guide
GitHub-based self-release flow in this repositoryGitHub Actions Release Flow
Maven Central-specific publishingPublish To Maven Central

14. Summary

The practical GitLab CI/CD path is:

  1. validate with status
  2. create or update a release MR with gitlab-release-plan
  3. create the final tag with gitlab-tag-from-plan
  4. sync and audit GitLab variables with sync-vars and audit-vars
  5. publish snapshots and tags with the same publish --execute true command
  6. create or update the GitLab Release with gitlab-release

15. References

Released under the Apache-2.0 License.