javachanges GitLab CI/CD Usage Guide
1. Overview
This guide explains how to use javachanges in GitLab CI/CD for:
- regular validation
- GitLab CI/CD variable management
- release merge request generation
- release tag creation from the generated release plan
- Maven or Gradle publishing in tag pipelines
- Maven and Gradle dependency caching
javachanges now has four GitLab-specific commands:
| Command | Purpose |
|---|---|
gitlab-release-plan | Create or update a release branch and release merge request |
gitlab-tag-from-plan | Create and push the final release tag after the release plan lands |
gitlab-release | Generate release notes and create or update the GitLab Release for the current CI tag |
init-gitlab-ci | Generate 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:
| Goal | Command |
|---|---|
| Check pending release state | status |
| Apply a release plan locally or in CI | plan --apply true |
| Generate Maven settings from env vars | write-settings |
| Preview GitLab variables from a local env file | render-vars --platform gitlab |
| Check platform readiness | doctor-local, doctor-platform |
Sync GitLab variables through glab | sync-vars --platform gitlab |
Audit GitLab variables through glab variable export | audit-vars --platform gitlab |
| Create or update a GitLab release MR | gitlab-release-plan --write-plan-files false --execute true |
| Create and push a release tag after a release plan merge | gitlab-tag-from-plan --fresh true --execute true |
| Validate a publish | preflight |
| Run the real Maven deploy command | publish --execute true |
| Run the real Gradle publish task | gradle-publish --execute true |
| Create or update a GitLab Release from the tag pipeline | gitlab-release --execute true |
3. Variable Model
3.1 Shared Maven repository variables
javachanges understands these values from env/release.env.example:
| Variable | Required | Meaning |
|---|---|---|
MAVEN_RELEASE_REPOSITORY_URL | Yes | Release repository URL |
MAVEN_SNAPSHOT_REPOSITORY_URL | Yes | Snapshot repository URL |
MAVEN_RELEASE_REPOSITORY_ID | Yes | Release repository id |
MAVEN_SNAPSHOT_REPOSITORY_ID | Yes | Snapshot repository id |
MAVEN_REPOSITORY_USERNAME | Yes, unless explicit split credentials are used | Shared username |
MAVEN_REPOSITORY_PASSWORD | Yes, unless explicit split credentials are used | Shared password |
MAVEN_CENTRAL_USERNAME | Optional | Sonatype Central Portal token username fallback |
MAVEN_CENTRAL_PASSWORD | Optional | Sonatype Central Portal token password fallback |
MAVEN_RELEASE_REPOSITORY_USERNAME | Optional | Explicit release username |
MAVEN_RELEASE_REPOSITORY_PASSWORD | Optional | Explicit release password |
MAVEN_SNAPSHOT_REPOSITORY_USERNAME | Optional | Explicit snapshot username |
MAVEN_SNAPSHOT_REPOSITORY_PASSWORD | Optional | Explicit snapshot password |
GITLAB_RELEASE_TOKEN | Optional | Extra 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:
| Variable | Source |
|---|---|
CI_PROJECT_ID | GitLab built-in CI variable or --project-id |
CI_DEFAULT_BRANCH | GitLab built-in CI variable |
CI_SERVER_HOST | GitLab built-in CI variable |
CI_SERVER_URL | GitLab built-in CI variable |
CI_PROJECT_PATH | GitLab built-in CI variable |
GITLAB_RELEASE_BOT_USERNAME | Project variable you provide |
GITLAB_RELEASE_BOT_TOKEN | Project variable you provide |
gitlab-tag-from-plan additionally needs:
| Option or variable | Meaning |
|---|---|
--before-sha or CI_COMMIT_BEFORE_SHA | Previous commit SHA |
--current-sha or CI_COMMIT_SHA | Current commit SHA |
4. Local Preparation
4.1 Build the CLI
mvn -q test4.2 Initialize a local env file
mvn -q -DskipTests compile exec:java -Dexec.args="init-env --target env/release.env.local"4.3 Preview GitLab variables
mvn -q -DskipTests compile exec:java -Dexec.args="render-vars --env-file env/release.env.local --platform gitlab"4.4 Check local readiness
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:
mvn -q -DskipTests compile exec:java -Dexec.args="sync-vars --env-file env/release.env.local --platform gitlab --repo group/project"Apply:
mvn -q -DskipTests compile exec:java -Dexec.args="sync-vars --env-file env/release.env.local --platform gitlab --repo group/project --execute true"Audit:
mvn -q -DskipTests compile exec:java -Dexec.args="audit-vars --env-file env/release.env.local --platform gitlab --gitlab-repo group/project"5. Recommended GitLab CI/CD Pipeline Topology
Recommended stages:
verifyrelease-plantagpublish
6. Minimal .gitlab-ci.yml
If you want the shortest stable setup, let javachanges generate it:
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:
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:
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_TAGHow the example works:
| Job | Purpose |
|---|---|
verify | Validates the repository and prints release state |
release_plan_mr | Creates or updates the release branch and merge request |
release_tag | Creates the final tag after the release plan manifest has changed on the default branch |
publish_snapshot | Publishes from the configured snapshot branch without extra shell branch parsing |
publish_release | Publishes from the final Git tag and creates or updates the GitLab Release |
If .changesets/config.json or .changesets/config.jsonc contains:
{
"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:
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_TAGThe 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 ...orjava -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 underscripts/. - Quote GitLab variables explicitly, for example
"$CI_PROJECT_DIR"and"$CI_COMMIT_TAG".
Not recommended:
script: - |blocks that contain shell heredoc such ascat <<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.ymlwhen the same logic can live in a repository script.
Why this pitfall is common:
- GitLab parses
.gitlab-ci.ymlas 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:
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:
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:
write_env:
script:
- printf 'CI_PROJECT_DIR=%s\nCI_COMMIT_SHA=%s\n' "$CI_PROJECT_DIR" "$CI_COMMIT_SHA" > release.envFor longer setup flows, move the shell into a repository script:
release_plan_mr:
script:
- ./scripts/gitlab-release-plan.sh8. How The GitLab-specific Commands Behave
8.1 gitlab-release-plan
Default behavior:
| Input | Default |
|---|---|
--project-id | CI_PROJECT_ID |
--target-branch | CI_DEFAULT_BRANCH, or main if absent |
--release-branch | changeset-release/<target-branch> |
Important behavior:
| Condition | Result |
|---|---|
| No pending changesets | Skips the release MR |
--execute true missing | Dry-run only |
| Release plan produces no staged file changes | Skips MR update |
| Open release MR already exists | Updates it instead of creating a new one |
Remote changeset-release/* branch already exists | Reuses the branch by resolving its current remote SHA, then pushes with an explicit --force-with-lease |
Notes:
gitlab-release-plantreatschangeset-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:
| Condition | Result |
|---|---|
beforeSha missing or all zeros | Skips tagging |
| release state did not change between commits | Skips tagging |
--fallback-from-release-commit true and HEAD is chore(release): release vX.Y.Z | Creates the whole-repo tag from that release commit |
| Tag already exists remotely | Skips tagging |
--execute true missing | Dry-run only |
9. Generic Maven Publish In GitLab CI/CD
The generic publish helper uses:
preflightlogic to verify revision, tag, and credentialswrite-settingslogic to generate.m2/settings.xml- repository variables such as
MAVEN_RELEASE_REPOSITORY_URL - credentials from your GitLab CI/CD variables
Snapshot mode behavior:
- default behavior stays
stamped, which rewrites1.2.3-SNAPSHOTto a unique stamped revision before deploy - if the configured
snapshotBranchmatches the current branch andsnapshotVersionModeisplain,publish --execute truekeeps the effective version at the original1.2.3-SNAPSHOT preflightandpublishlogs 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:
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_TAG10. Maven Cache Behavior In GitLab CI/CD
Recommended cache:
cache:
key:
files:
- pom.xml
paths:
- .m2/repositoryRecommended runtime option:
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"What this improves:
| Cached well | Not solved by GitLab cache alone |
|---|---|
| Maven dependencies | Git clone/fetch cost |
| Maven plugins | JDK image pull time |
Repeat pipelines with the same pom.xml | GitLab API calls for release MR creation |
| Reuse across jobs on shared cache backends | Remote repository publishing latency |
Important behavior:
| Situation | Result |
|---|---|
| New cache key | First pipeline still downloads |
pom.xml changes | Cache key may change |
| Different runners without shared cache | Cache reuse may be weak |
| Shared/distributed GitLab cache configured | Cross-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, orBEGIN PRIVATE KEY, inside.gitlab-ci.yml,Makefile, or another rule-definition file.
Recommended default strategy:
- keep secret-detection patterns in a dedicated file such as
.hygiene/secret-patterns.txt - exclude that file, plus
.gitlab-ci.ymlandMakefile, from content scanning - scan source, docs, scripts, and config that may carry real secrets
- 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:
hygiene:
stage: verify
script:
- ./scripts/secret-scan.sh
rules:
- if: $CI_COMMIT_BRANCH# 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:
| Option | Pros | Cons | Recommendation |
|---|---|---|---|
Exclude .gitlab-ci.yml / Makefile only | simple and quick | still fails if rules move into another scanned file | useful as a minimum stopgap |
| Move rules to a dedicated file and exclude it | clear ownership, easiest to explain, stable over time | needs one extra checked-in file | recommended default |
| Split patterns into fragments and concatenate them | can avoid literal self-matches without file exclusions | hurts readability, easier to break, may reduce portability across tools | avoid by default |
| Use allowlist comments | precise for a few reviewed lines | noisy, tool-specific, easy to overuse until real hits get hidden | keep for exceptional cases only |
Avoid:
- defining detector regex literals directly in
.gitlab-ci.yml - defining the same literals inline in
Makefiletargets - treating allowlists as the primary suppression mechanism for scanner-owned files
12. Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
| Release MR job fails to push | GITLAB_RELEASE_BOT_TOKEN or GITLAB_RELEASE_BOT_USERNAME missing | add the bot credentials as project variables |
Release MR job fails with stale info | another process updated changeset-release/* after javachanges resolved the remote SHA | rerun the pipeline; if the branch is shared by other automation, stop sharing that branch name |
| Release tag job never tags | release state did not change or CI_COMMIT_BEFORE_SHA is unusable | inspect the branch pipeline, version file, and changelog |
Snapshot publish job cannot see Maven credentials or GITLAB_RELEASE_TOKEN | the variables are protected but the configured snapshotBranch is not a protected branch | protect 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 key | heredoc or other multiline shell content broke YAML indentation rules | replace `script: - |
Hygiene or secret scan fails on .gitlab-ci.yml or Makefile, but no real credential was added | the scanner matched rule literals inside its own configuration | move patterns into a dedicated rules file and exclude scanner-owned files from scanning |
sync-vars does nothing | env file still contains placeholders | replace replace-me values first |
audit-vars fails with MISMATCH | local env and remote project variables diverged | resync or deliberately update one side |
| Publish job fails on missing Maven credentials | project variables were not configured | sync the variables with glab, then rerun |
13. Recommended Documentation Split
Use these docs together:
| Need | Document |
|---|---|
| Generic release commands and local preparation | Development Guide |
| GitHub-based self-release flow in this repository | GitHub Actions Release Flow |
| Maven Central-specific publishing | Publish To Maven Central |
14. Summary
The practical GitLab CI/CD path is:
- validate with
status - create or update a release MR with
gitlab-release-plan - create the final tag with
gitlab-tag-from-plan - sync and audit GitLab variables with
sync-varsandaudit-vars - publish snapshots and tags with the same
publish --execute truecommand - create or update the GitLab Release with
gitlab-release
15. References
- GitLab CI/CD YAML syntax: https://docs.gitlab.com/ci/yaml/
- GitLab CI/CD caching: https://docs.gitlab.com/ci/caching/
glab auth login: https://docs.gitlab.com/cli/auth/login/glab variable set: https://docs.gitlab.com/cli/variable/set/glab variable export: https://docs.gitlab.com/cli/variable/export/