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¶
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: MC_VERSION<br/>Compile + Test + Package<br/>Artifacts expire 1 week"]
BUILD_RELEASE["build-release: MC_VERSION<br/>Compile + Test + Package<br/>Artifacts never expire"]
end
subgraph Release Stage
PREPARE["prepare-release<br/>Generate notes + dotenv"]
RELEASE["release<br/>GitLab Release + notes<br/>release: keyword"]
end
subgraph Deploy Stage
MODRINTH["deploy:modrinth<br/>Automatic"]
CURSEFORGE["deploy:curseforge<br/>Manual"]
end
subgraph Pages Stage
PAGES["pages<br/>MkDocs Material build<br/>GitLab Pages deploy"]
end
MR --> LINT_COMMITS --> BUILD_MATRIX
MAIN --> BUILD_MATRIX
MAIN --> PAGES
TAG --> VALIDATE --> BUILD_RELEASE --> PREPARE --> RELEASE --> MODRINTH
RELEASE --> CURSEFORGE
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. |
This 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 |
|---|---|---|
validate:tag |
Version tags only | Verifies the tag is annotated (not lightweight). Rejects lightweight tags. |
build |
MRs + main | Compiles, tests, and packages the mod JAR for each MC version in parallel using GitLab CI's parallel:matrix. Each MC version spawns a separate job instance (e.g., build: [1.20.1]). 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. |
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:build
# Equivalent to: compile → test → spotlessCheck → jar → remapJar → sourcesJar
Adding a new Minecraft version requires two changes:
1. Add the version to settings.gradle in the stonecutter block
2. Add the version to the MC_VERSION matrix array in .gitlab-ci.yml
Artifacts:
- JAR files from versions/<version>/build/libs/ (mod JAR + sources JAR)
- Test reports from versions/<version>/build/reports/tests/
- JUnit XML from versions/<version>/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. Release¶
| Job | Trigger | Description |
|---|---|---|
prepare-release |
Version tags only | Generates release notes with git-cliff, computes asset link URLs, and exports them as a dotenv artifact for the downstream release job. |
release |
Version tags only | Creates a GitLab Release using the built-in release: keyword. Consumes the dotenv artifact from prepare-release to populate 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 and all build-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 and running:
The release job uses GitLab's declarative release: keyword. Asset link URLs are injected from the prepare-release job's dotenv artifact, and the release notes file is passed as a regular artifact.
4. Deploy¶
| Job | Trigger | Description |
|---|---|---|
deploy:modrinth |
Version tags (automatic) | Publishes to Modrinth using Minotaur. Uploads the mod JAR + sources JAR, attaches release notes, and syncs the PROJECT.md content to the Modrinth project description. |
deploy:curseforge |
Version tags (manual) | Deploys to CurseForge. Currently a TODO stub. |
The deploy:modrinth job runs automatically after the GitLab Release is created. The deploy:curseforge job requires manual trigger (click in GitLab UI). Both are allowed to fail — a deploy failure does not block the pipeline.
5. Pages¶
| Job | Trigger | Description |
|---|---|---|
pages |
Main branch (when docs change) | Builds the documentation site with Zensical (successor to MkDocs Material, by the same team) and deploys to GitLab Pages. Uses a dual-theme approach: Enderman/End-inspired Minecraft theme for mod documentation, clean dark professional theme for project governance 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 only runs when documentation files are modified (via changes: filter) and is 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 (per MC version) |
✅ | ✅ | |
build-release (per MC version) |
✅ | ||
prepare-release |
✅ | ||
release |
✅ | ||
deploy:modrinth |
✅ (automatic) | ||
deploy:curseforge |
🔲 (manual) | ||
pages |
✅ (when docs change) |
Parallel Matrix Build¶
The build jobs use GitLab CI's parallel:matrix to build each Minecraft version as a separate job instance. Both build (MR/main) and build-release (tags) extend a shared .build-base template. This provides:
- Per-version visibility — each MC version shows as a separate job in the pipeline UI
- Independent failure — if one version fails, others continue
- Future-proof — adding a new MC version is just adding it to the matrix array
- Integrated testing — Gradle's
buildtask includestest, so each version is tested as part of its build - Permanent release artifacts —
build-releaseartifacts never expire, so release download links remain valid
.build-base:
parallel:
matrix:
- MC_VERSION: ["1.20.1"]
script:
- ./gradlew ":${MC_VERSION}:build"
build: # MR + main — artifacts expire after 1 week
extends: .build-base
build-release: # Tags — artifacts never expire
extends: .build-base
When more versions are added (e.g., 1.20.4, 1.21.1), the pipeline automatically spawns parallel jobs:
build-release: [1.20.1] ✅
build-release: [1.20.4] ✅
build-release: [1.21.1] ❌ ← easy to see which version failed
Infrastructure¶
Base Image¶
All JDK jobs use eclipse-temurin:17-jdk with the Gradle Wrapper (./gradlew).
Note: The
eclipse-temurin:17-jdkimage does not includegit. Thebuild.gradleis 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:
cache:
key:
files:
- gradle.properties
- build.gradle
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 |
Note: The
deploy:curseforgejob is currently a stub. When implementing it, you'll need to add the CurseForgeGradle plugin tobuild.gradle.
Release Asset Naming¶
The release job attaches the mod JAR and sources JAR with the naming convention:
ansiblecrafting-<version>-mc<minecraft_version>-fabric.jar
ansiblecrafting-<version>-mc<minecraft_version>-fabric-sources.jar
Example: ansiblecrafting-0.2.0-mc1.20.1-fabric.jar, ansiblecrafting-0.2.0-mc1.20.1-fabric-sources.jar
The sources JAR is included to satisfy MPL-2.0 source availability requirements (Section 3.2a).
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.
Adding a New Minecraft Version¶
To add support for a new Minecraft version (e.g., 1.20.4):
-
settings.gradle— Add the version to the Stonecutter block: -
.gitlab-ci.yml— Add the version to the.build-basematrix (shared by bothbuildandbuild-release): -
.gitlab-ci.yml— Add release asset links for the new version in theprepare-releasejob'sscript:(dotenv URLs) and thereleasejob'srelease:assets:links:block.
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.