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

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:

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

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 build task includes test, so each version is tested as part of its build
  • Permanent release artifactsbuild-release artifacts 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-jdk image does not include git. The build.gradle 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:

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_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

Note: The deploy:curseforge job is currently a stub. When implementing it, you'll need to add the CurseForgeGradle plugin to build.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

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.

Adding a New Minecraft Version

To add support for a new Minecraft version (e.g., 1.20.4):

  1. settings.gradle — Add the version to the Stonecutter block:

    stonecutter {
        create(rootProject) {
            versions '1.20.1', '1.20.4'
        }
    }
    

  2. .gitlab-ci.yml — Add the version to the .build-base matrix (shared by both build and build-release):

    .build-base:
      parallel:
        matrix:
          - MC_VERSION: ["1.20.1", "1.20.4"]
    

  3. .gitlab-ci.yml — Add release asset links for the new version in the prepare-release job's script: (dotenv URLs) and the release job's release:assets:links: block.

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.