Skip to content

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):

  1. settings.gradle.kts โ€” Uncomment or add the version in the stonecutter block
  2. .gitlab-ci.yml โ€” Add the target to all MC_VERSION_LOADER matrix arrays (search the file for MC_VERSION_LOADER)
  3. .gitlab-ci.yml โ€” Add release asset URLs in the prepare-release loop and release asset links
  4. versions/<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:

git-cliff --latest --strip header > release_notes.md

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-jdk image does not include git. The build.gradle.kts is 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_HOME is 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

ERROR: 'v0.2.0' is a lightweight tag.

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:

  1. Check that the Minecraft server starts successfully (look for "GameTest server started" in logs)
  2. Verify heap size is sufficient (-Xmx2G is configured via GRADLE_OPTS)
  3. 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 reads mkdocs.yml natively for backward compatibility.
  • Broken relative links in copied files โ€” The sed commands in the pages job 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 in docs/ but isn't listed in the nav: section of mkdocs.yml. Add new files to the nav to include them in the site.
  • pip install fails โ€” The python:3.12-slim image needs network access to install zensical from PyPI.

To debug locally, run zensical serve from the project root and check the terminal output for warnings.