CI/CD Pipeline¶
This document describes the GitLab CI/CD pipeline for Ansible Crafting, including all stages, jobs, triggers, and required configuration.
See also: Release Process for the local release workflow that triggers this pipeline.
Pipeline Overview¶
The pipeline builds, tests, and deploys Ansible Crafting across all supported Minecraft version ร loader combinations. Each versionรloader target (e.g., 1.20.1-fabric, 1.21.1-neoforge) is built and tested as a separate parallel job.
flowchart LR
subgraph Triggers
MR["Merge Request"]
MAIN["Push to main"]
TAG["Version Tag<br/>v*.*.*"]
end
subgraph Lint Stage
LINT_COMMITS["lint:commits<br/>Conventional Commits"]
VALIDATE["validate:tag<br/>Check annotated tag"]
end
subgraph Build Stage
BUILD_MATRIX["build: VERSION-LOADER<br/>All targets in parallel<br/>Compile + Test + Package<br/>Artifacts expire 1 week"]
BUILD_RELEASE["build-release: VERSION-LOADER<br/>Non-dev targets in parallel<br/>Compile + Test + Package<br/>Artifacts never expire"]
end
subgraph Game Test Stage
GAMETEST["gametest: VERSION-LOADER<br/>Subset in parallel<br/>Headless server tests"]
GAMETEST_REL["gametest-release: VERSION-LOADER<br/>Subset in parallel<br/>Headless server tests"]
end
subgraph Release Stage
PREPARE["prepare-release<br/>Generate notes + dotenv<br/>Asset URLs for all targets"]
RELEASE["release<br/>GitLab Release + notes<br/>2 asset links per target"]
end
subgraph Deploy Stage
MODRINTH["deploy:modrinth<br/>6 targets in parallel"]
CURSEFORGE["deploy:curseforge<br/>6 targets in parallel"]
end
subgraph Pages Stage
PAGES["pages<br/>Zensical build<br/>GitLab Pages deploy"]
end
MR --> LINT_COMMITS --> BUILD_MATRIX --> GAMETEST
MAIN --> BUILD_MATRIX
MAIN --> PAGES
TAG --> VALIDATE --> BUILD_RELEASE --> GAMETEST_REL --> PREPARE --> RELEASE
RELEASE --> MODRINTH
RELEASE --> CURSEFORGE
Build Matrix¶
The pipeline builds all versionรloader targets in parallel, matching the Stonecutter configuration in settings.gradle.kts:
| MC Version | Java | Fabric | NeoForge | Forge |
|---|---|---|---|---|
| 1.20.1 | 21 (--release 17) |
โ | โ | โ |
| 1.21.1 | 21 | โ | โ | โ |
| 1.21.4 | 21 | โ | โ | โ |
| 1.21.10 | 21 | โ | โ | โ |
All targets compile on JDK 21. The 1.20.1 targets use --release 17 (via javaTarget in Modstitch) to produce Java 17 bytecode, so a single JDK 21 image handles the entire matrix.
Adding a New VersionรLoader Target¶
To add a new target (e.g., 1.21.10-fabric):
settings.gradle.ktsโ Uncomment or add the version in thestonecutterblock.gitlab-ci.ymlโ Add the target to allMC_VERSION_LOADERmatrix arrays (search the file forMC_VERSION_LOADER).gitlab-ci.ymlโ Add release asset URLs in theprepare-releaseloop andreleaseasset linksversions/<version-loader>/gradle.propertiesโ Create with correct dependency versions
Stages¶
1. Lint¶
| Job | Trigger | Description |
|---|---|---|
lint:commits |
MRs only | Validates that all MR commits follow Conventional Commits format. Mirrors the pattern from .githooks/commit-msg to catch commits from developers who haven't configured the local hook. |
validate:tag |
Version tags only | Verifies the tag is annotated (not lightweight). Rejects lightweight tags. |
The lint:commits job iterates over every commit in the MR range (CI_MERGE_REQUEST_DIFF_BASE_SHA..CI_COMMIT_SHA) and checks each subject line against the allowed types: feat, fix, refactor, perf, docs, revert, chore, test, ci, style, build. Merge commits and git-generated reverts are allowed through.
2. Build¶
| Job | Trigger | Description |
|---|---|---|
build |
MRs + main | Compiles, tests, and packages the mod JAR for each versionรloader in parallel. Artifacts expire after 1 week. |
build-release |
Version tags only | Same as build but artifacts never expire, ensuring release asset download links remain valid permanently. Only non-dev targets are built (dev targets like 1.21.10-* are excluded since they are not deployed or attached to releases). |
Both build jobs use Stonecutter subprojects and Gradle's build lifecycle task, which runs assemble (compile + package) and check (test + lint) in a single invocation:
./gradlew ":1.20.1-fabric:build"
# Equivalent to: compile โ test โ spotlessCheck โ jar โ remapJar โ sourcesJar
The pipeline UI shows each target as a separate job:
build: [1.20.1-fabric] โ
build: [1.20.1-forge] โ
build: [1.21.1-fabric] โ
build: [1.21.1-neoforge] โ
build: [1.21.4-fabric] โ โ easy to see which target failed
build: [1.21.4-neoforge] โ
build: [1.21.10-fabric] โ
build: [1.21.10-neoforge] โ
Artifacts:
- JAR files from versions/<version-loader>/build/libs/ (mod JAR + sources JAR)
- Test reports from versions/<version-loader>/build/reports/tests/
- JUnit XML from versions/<version-loader>/build/test-results/test/ (published to GitLab MR UI)
MR/main artifacts expire after 1 week. Tag build artifacts never expire (release downloads must remain valid). Test artifacts are uploaded even on failure (when: always).
3. Game Test¶
| Job | Trigger | Description |
|---|---|---|
gametest |
MRs + main | Runs headless game server integration tests on a subset of the build matrix. |
gametest-release |
Version tags only | Same tests (minus 1.21.10-fabric), but depends on build-release. Runs before release creation. |
Game tests run on a subset of the build matrix. Two targets are excluded for technical reasons:
| Excluded Target | Reason |
|---|---|
1.20.1-forge |
EMI Forge 1.20.1 SRG mapping incompatibility prevents the game test mod from loading |
1.21.10-neoforge |
NeoForge removed @GameTestHolder in 1.21.10; test class is conditionally compiled out |
The gametest-release job runs the same subset minus 1.21.10-fabric (which is too new for the release pipeline). The full build matrix still runs unit tests for all targets.
Game test jobs have a 10-minute timeout and use -Xmx2G heap. They run headless Minecraft server instances that exercise scanning, crafting, and state persistence.
4. Release¶
| Job | Trigger | Description |
|---|---|---|
prepare-release |
Version tags only | Generates release notes with git-cliff, computes asset link URLs for all targets (2 URLs per target: mod JAR + sources JAR), and exports them as a dotenv artifact. |
release |
Version tags only | Creates a GitLab Release using the built-in release: keyword. Consumes the dotenv artifact from prepare-release to populate the asset link URLs. |
The release stage uses a two-job split because GitLab's dotenv artifacts only inject variables into downstream jobs, not the job that produces them. The prepare-release job computes the asset URLs and writes them to variables.env, which the release job then consumes.
Dependencies:
- prepare-release requires validate:tag, all build-release matrix instances, and all gametest-release matrix instances to pass first.
- release requires prepare-release to pass (and consumes its artifacts).
Release notes are generated by installing git-cliff in the prepare-release container:
The prepare-release job generates asset URLs using a shell loop over all versionรloader targets. Each target produces two URLs (mod JAR + sources JAR) using the GitLab "artifacts by ref + job name" URL format.
5. Deploy¶
| Job | Trigger | Description |
|---|---|---|
deploy:modrinth |
Version tags (automatic) | Publishes each non-dev target to Modrinth via ./gradlew ":VERSION-LOADER:modrinth". Runs 6 targets in parallel. |
deploy:curseforge |
Version tags (automatic) | Publishes each non-dev target to CurseForge via ./gradlew ":VERSION-LOADER:publishCurseForge". Runs 6 targets in parallel. |
Deploy uses direct jobs in the parent pipeline. Each deploy job downloads build artifacts from build-release and runs the per-target Gradle publish task. The deploy matrix is static in .gitlab-ci.yml โ only targets with release_channel != dev in their versions/*/gradle.properties are included. When promoting a target from dev to release (or vice versa), update the deploy matrix AND the target's gradle.properties.
Both deploy jobs depend on the release job (ordering only, no artifact download) to ensure the GitLab Release is created before publishing to external platforms. All deploy jobs have allow_failure: true to prevent flaky external API issues from blocking the pipeline.
Version type mapping: The mod_version property determines the release channel on both Modrinth and CurseForge. Stable versions (no suffix) publish as release, -beta/-rc suffixes publish as beta, and -alpha publishes as alpha. Any other suffix causes the build to fail. See Version Format for details.
Each deploy job installs git-cliff to generate release notes, then runs the versionรloader-specific Gradle task:
# Modrinth
./gradlew ":1.20.1-fabric:modrinth"
# CurseForge
./gradlew ":1.20.1-fabric:publishCurseForge"
The Modrinth task uploads the mod JAR, attaches release notes, sets the correct MC version and loader tags, and syncs the PROJECT.md content to the Modrinth project description page.
The CurseForge task uploads the mod JAR, attaches release notes, and sets the game version, mod loader, and Java version metadata. Unlike Modrinth, CurseForge does not support automatic project description syncing โ the project description must be updated manually through the CurseForge author dashboard.
6. Pages¶
| Job | Trigger | Description |
|---|---|---|
pages |
Main branch (when docs change) + version tags | Builds the documentation site with Zensical (successor to MkDocs Material, by the same team) and deploys to GitLab Pages. |
The pages job copies root-level Markdown files (README.md, CONTRIBUTING.md, etc.) into the docs/ directory, rewrites their relative links, then runs zensical build to generate a static HTML site. The site is deployed to GitLab Pages automatically.
The job runs when documentation files are modified (via changes: filter) and also on release tags (since the release scripts use ci.skip for the branch push). Allowed to fail โ a Pages build failure does not block the pipeline.
See also: MkDocs configuration, design plan
Job Trigger Matrix¶
| Job | Merge Request | Main Branch | Version Tag |
|---|---|---|---|
lint:commits |
โ | ||
validate:tag |
โ | ||
build (all targets) |
โ | โ | |
build-release (non-dev targets) |
โ | ||
gametest (subset) |
โ | โ | |
gametest-release (subset) |
โ | ||
prepare-release |
โ | ||
release |
โ | ||
deploy:modrinth (6 targets) |
โ | ||
deploy:curseforge (6 targets) |
โ | ||
pages |
โ (when docs change) | โ |
Release Asset Naming¶
Release JARs follow the naming convention:
ansiblecrafting-<version>-mc<minecraft_version>-<loader>.jar
ansiblecrafting-<version>-mc<minecraft_version>-<loader>-sources.jar
Example release:
ansiblecrafting-0.3.0-mc1.20.1-fabric.jar
ansiblecrafting-0.3.0-mc1.20.1-fabric-sources.jar
ansiblecrafting-0.3.0-mc1.20.1-forge.jar
ansiblecrafting-0.3.0-mc1.20.1-forge-sources.jar
ansiblecrafting-0.3.0-mc1.21.1-fabric.jar
ansiblecrafting-0.3.0-mc1.21.1-fabric-sources.jar
ansiblecrafting-0.3.0-mc1.21.1-neoforge.jar
ansiblecrafting-0.3.0-mc1.21.1-neoforge-sources.jar
ansiblecrafting-0.3.0-mc1.21.4-fabric.jar
ansiblecrafting-0.3.0-mc1.21.4-fabric-sources.jar
ansiblecrafting-0.3.0-mc1.21.4-neoforge.jar
ansiblecrafting-0.3.0-mc1.21.4-neoforge-sources.jar
ansiblecrafting-0.3.0-mc1.21.10-fabric.jar
ansiblecrafting-0.3.0-mc1.21.10-fabric-sources.jar
ansiblecrafting-0.3.0-mc1.21.10-neoforge.jar
ansiblecrafting-0.3.0-mc1.21.10-neoforge-sources.jar
The sources JARs are included to satisfy MPL-2.0 source availability requirements (Section 3.2a).
Infrastructure¶
Base Image¶
All JDK jobs use eclipse-temurin:21-jdk with the Gradle Wrapper (./gradlew). Java 17 targets (1.20.1) use --release 17 configured via javaTarget in Modstitch, so a single JDK 21 image handles all targets.
Note: The
eclipse-temurin:21-jdkimage does not includegit. Thebuild.gradle.ktsis designed to handle this gracefully โ git-dependent features (version SHA, git hooks setup) are skipped when git is unavailable.
Caching¶
Gradle caches are stored per-job with a key based on gradle.properties + build.gradle.kts:
cache:
key:
files:
- gradle.properties
- build.gradle.kts
prefix: "${CI_JOB_NAME}"
paths:
- .gradle-home/caches/
- .gradle-home/wrapper/
- .gradle/caches/
- .loom-cache/
Gradle Options¶
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true"
GRADLE_USER_HOME: "${CI_PROJECT_DIR}/.gradle-home"
- The daemon is disabled in CI (single-use containers), but parallel execution and build caching are enabled.
GRADLE_USER_HOMEis set to a project-local directory so the Gradle wrapper distribution and dependency caches are within the build directory and can be cached by GitLab CI. Without this, Gradle uses~/.gradle/which is outside the cacheable project directory.
Required CI/CD Variables¶
These variables must be configured in GitLab โ Settings โ CI/CD โ Variables for deployment:
| Variable | Required For | Description |
|---|---|---|
MODRINTH_TOKEN |
deploy:modrinth |
Modrinth PAT with CREATE_VERSION and WRITE_PROJECT scopes. Must be masked and protected. See Modrinth PATs. PATs expire โ set a calendar reminder to rotate before expiration. |
CURSEFORGE_TOKEN |
deploy:curseforge |
CurseForge API token for publishing. Must be masked and protected. |
Pipeline Configuration File¶
The full pipeline is defined in .gitlab-ci.yml at the project root.
Troubleshooting¶
Tag Validation Fails¶
The release pipeline requires annotated tags. The release script creates these automatically. If you created a tag manually, use:
# Delete the lightweight tag
git tag -d v0.2.0
git push origin :refs/tags/v0.2.0
# Create an annotated tag
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
Cache Issues¶
If builds fail with stale cache, clear the cache in GitLab โ CI/CD โ Pipelines โ Clear Runner Caches.
Loom Cache¶
Fabric Loom downloads Minecraft assets and mappings to .loom-cache/. This is cached in CI to avoid re-downloading on every build. If mappings change, the cache key (based on gradle.properties) will invalidate automatically.
Game Test Timeouts¶
Game test jobs have a 10-minute timeout. If tests are timing out:
- Check that the Minecraft server starts successfully (look for "GameTest server started" in logs)
- Verify heap size is sufficient (
-Xmx2Gis configured viaGRADLE_OPTS) - Check for deadlocks in test logic (e.g., waiting for ticks that never fire)
Pages Build Fails¶
Common causes:
- Missing
mkdocs.ymlโ The configuration file must exist at the project root. Zensical readsmkdocs.ymlnatively for backward compatibility. - Broken relative links in copied files โ The
sedcommands in thepagesjob rewrite links in root-level Markdown files. If a new cross-reference is added, the sed rules may need updating. - New documentation file not in
nav:โ Zensical will warn (but not fail) if a file exists indocs/but isn't listed in thenav:section ofmkdocs.yml. Add new files to the nav to include them in the site. - pip install fails โ The
python:3.12-slimimage needs network access to installzensicalfrom PyPI.
To debug locally, run zensical serve from the project root and check the terminal output for warnings.