Skip to content
This repository was archived by the owner on Aug 1, 2026. It is now read-only.

Repository files navigation

bertie-ci

Important

Development has moved to the bertie monorepo. This repository is retained read-only for historical tags, releases, and issues.

Local-first Minecraft checks shared by the bertie-mc projects.

The executable is the abstraction boundary. It owns tool setup, instance assembly, process supervision and result collection. Projects own their test suites and decide which runners apply to them. Artifact assembly, JVM tests, GameTests and production runtime tests remain separate commands that can be composed locally or by any CI provider. GitHub Actions is only one adapter around them.

Run locally

From a custom mod checkout, build once and run the project's applicable suites. These lines are the Linux path; on native Windows there is no usable Nix, so follow docs/windows.md instead.

bertie_ci_package="$(nix build github:bertie-mc/bertie-ci/v4.0.0#bertie-ci \
  --no-link --print-out-paths)"
export PATH="$bertie_ci_package/bin:$PATH"

bertie-ci build --project . --output-dir .bertie-ci/artifact
bertie-ci unit-test --project .
bertie-ci gametest --project .
bertie-ci prepare-mod-instance --project . --artifact .bertie-ci/artifact \
  --side client --output-dir .bertie-ci/client
bertie-ci client-test --instance .bertie-ci/client/instance.json

build uses the Nix-pinned Gradle 8 executable to run assemble; it does not run tests. --output-dir stages exactly one releaseable JAR, preserving its filename, so publishing and runtime checks can consume the same explicit artifact. unit-test runs Gradle's ordinary JVM test task independently. gametest runs runGameTestServer in NeoForge's development runtime and fails closed unless at least one test is discovered and the GameTest server reports a clean completion. This catches mod-loading crashes that Gradle can otherwise report as a successful task. prepare-mod-instance consumes the already-built JAR and declarative dependency fixtures sourced from the canonical modpack. client-test and server-test consume only a prepared-instance descriptor; they do not know whether packwiz or a mod artifact produced it. With no extensions, they run the reusable smoke scenarios:

  • the HeadlessHQ mc-runtime-test client runner under Xvfb, which loads the real client, joins a singleplayer world, waits for the player's chunk, and exits;
  • a HeadlessMC dedicated-server runner, which waits for server readiness and stops it cleanly.

Projects extend those same runners instead of switching to another API. A production client suite can supply a test-only mod containing its own assertions, a minimum GameTest count, exact project-owned success markers, or any combination:

bertie-ci build-client-test-mod --project . \
  --output-dir .bertie-ci/client-test-mod
bertie-ci client-test --instance .bertie-ci/client/instance.json \
  --test-mod .bertie-ci/client-test-mod/client-test-mod.jar \
  --require-log EXAMPLE_CLIENT_ASSERTIONS_PASSED

build-client-test-mod runs the conventional clientTestJar task and stages its single runtime JAR under a stable name. mc-runtime-test creates and joins the world. The test-only mod owns the assertions and emits its stable marker only after they pass; bertie-ci fails closed if the marker is absent. Structure-backed suites can additionally use --minimum-game-tests; their NBT fixtures belong under the project's test resource source set, never in the release JAR. A project-specific dedicated-server suite can supply a HeadlessMC command-test document, test-only mod, log markers, or a combination. Without them, the same command remains the readiness test:

bertie-ci server-test --instance .bertie-ci/server/instance.json \
  --command-test tests/runtime/server.json

The JSON and its expected messages live in the project. bertie-ci only installs and supervises the exact server. Ordinary JVM tests and the development GameTest server are usually cheaper and should be preferred whenever they can express the behavior.

Pass --artifact path/to/mod.jar or a directory containing one runtime JAR to prepare an artifact downloaded from another build. Logs and crash reports remain below .bertie-ci/ for inspection. Minecraft downloads are cached under ${XDG_CACHE_HOME:-~/.cache}/bertie-ci by default.

Mods with external runtime dependencies select one or more canonical pack mods or aggregate fixture profiles. For example:

bertie-ci prepare-mod-instance --project . --artifact .bertie-ci/artifact \
  --fixture forbidden-arcanus,irons-spells --side client \
  --output-dir .bertie-ci/client

The Nix lock pins an immutable bertie-pack checkout. A selector resolves directly to mods/<selector>.pw.toml when that canonical mod exists. Explicit profiles are reserved for aggregate dependency closures, so profiles.json contains no one-to-one aliases. bertie-ci therefore owns only the small dependency-set mappings; mod versions, download hashes, filenames, and physical sides have one source of truth. The official packwiz installer resolves that metadata into each ephemeral side-specific instance. Selectors compose by set union, so combinations do not require a new workflow or a project branch in Python. Every client instance also gets Collective and Hide Experimental Warning from the same canonical pack snapshot.

For example, the artifacts profile installs the pack's Artifacts and Curios pins for mods such as fart-bomb.

From a packwiz checkout, validation never mutates the source tree. Side preparation resolves and installs the selected downloads, and the same universal runners consume the result:

bertie-ci pack-validate --project .
bertie-ci prepare-pack-instance --project . --side client --output-dir .bertie-ci/client
bertie-ci client-test --instance .bertie-ci/client/instance.json --max-memory 10G

GitHub Actions adapters

The command-line operations are also exposed as independent composite actions. Each job runs actions/setup once; it installs Nix, builds the pinned package once, and adds the wrapped bertie-ci command to PATH. Operational actions call that command directly and do not reevaluate Nixpkgs.

  • bertie-mc/bertie-ci/actions/setup@v4.0.0
  • bertie-mc/bertie-ci/actions/build@v4.0.0
  • bertie-mc/bertie-ci/actions/build-client-test-mod@v4.0.0
  • bertie-mc/bertie-ci/actions/unit-test@v4.0.0
  • bertie-mc/bertie-ci/actions/gametest@v4.0.0
  • bertie-mc/bertie-ci/actions/prepare-mod-instance@v4.0.0
  • bertie-mc/bertie-ci/actions/prepare-pack-instance@v4.0.0
  • bertie-mc/bertie-ci/actions/client-test@v4.0.0
  • bertie-mc/bertie-ci/actions/server-test@v4.0.0
  • bertie-mc/bertie-ci/actions/pack-validate@v4.0.0
  • bertie-mc/bertie-ci/actions/pack-export-client@v4.0.0
  • bertie-mc/bertie-ci/actions/pack-export-server@v4.0.0
  • bertie-mc/bertie-ci/actions/github-release@v4.0.0

Each owns one operation. Except for the GitHub-only publisher, operational actions expect the setup action to have placed bertie-ci on PATH. The build and test actions do not check out source, transfer artifacts, or choose job dependencies; the GitHub publisher consumes files and never builds them. A custom workflow can compose the actions as ordinary steps.

Small reusable workflows provide the common GitHub-specific job adapters. A repository keeps its trigger and dependency graph visible while reusing the implementation. GitHub requires reusable workflows to live directly in .github/workflows, so these adapters cannot be moved alongside the composite actions in workflows/.

For example:

name: Build and test

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  build:
    uses: bertie-mc/bertie-ci/.github/workflows/build-mod.yml@v4.0.0

  unit-test:
    uses: bertie-mc/bertie-ci/.github/workflows/unit-test.yml@v4.0.0

  gametest:
    uses: bertie-mc/bertie-ci/.github/workflows/gametest.yml@v4.0.0

  client-test:
    needs: build
    uses: bertie-mc/bertie-ci/.github/workflows/client-test.yml@v4.0.0
    with:
      artifact-name: ${{ needs.build.outputs.artifact-name }}
      fixture: forbidden-arcanus,irons-spells
      build-test-mod: true
      success-marker: EXAMPLE_CLIENT_ASSERTIONS_PASSED

  server-test:
    needs: build
    uses: bertie-mc/bertie-ci/.github/workflows/server-test.yml@v4.0.0
    with:
      artifact-name: ${{ needs.build.outputs.artifact-name }}
      fixture: forbidden-arcanus,irons-spells

build-mod.yml only assembles and uploads a JAR. unit-test.yml only runs ordinary JVM tests. client-test.yml and server-test.yml compose the relevant mod-instance adapter with the universal runtime runner and optional project-owned assertions. With no extensions they run world join and readiness. gametest.yml only runs the development-runtime suite. Reusable server and pack adapters can consume an optional separately uploaded test-mod artifact. The runner accepts either its exact JAR or the artifact directory, which must contain exactly one runtime JAR; test assembly remains independent from instance preparation and execution. github-release.yml only downloads and publishes a named artifact. A release therefore composes build-mod.yml followed by github-release.yml; it has no second build recipe.

jobs:
  build:
    uses: bertie-mc/bertie-ci/.github/workflows/build-mod.yml@v4.0.0

  publish:
    needs: build
    permissions:
      contents: write
    uses: bertie-mc/bertie-ci/.github/workflows/github-release.yml@v4.0.0
    with:
      artifact-name: ${{ needs.build.outputs.artifact-name }}

Test logic does not depend on GitHub-hosted runner setup or apt-get. The Python commands are CI-provider-independent and avoid POSIX-shell assumptions; Nix is the supported Linux dependency provider, while other platforms can supply Java, HeadlessHQ artifacts, and a display backend through environment variables. On Linux, the Python runner starts and supervises Xvfb directly; it does not depend on a distribution-specific xvfb-run shell helper. On Windows the runner invokes a Gradle 8 installation directly, and every check runs natively without WSL; docs/windows.md covers the setup and the one behavioral difference, which is that client-test has no Xvfb equivalent and therefore occupies the desktop session it runs on.

Projects choose their layers

NeoForge intentionally registers ordinary @GameTest methods only in a development runtime, not in the production installation launched by HeadlessMC. A mod with server or world behavior should normally compose gametest.yml, the gametest action, or the standalone command. A production runtime test is added only when it covers a different risk, such as client rendering, side safety, optional-mod loading, or release-JAR packaging.

client-test uses mc-runtime-test because client launch, world creation, player join, timeouts and clean exit are reusable mechanics. Assertions stay in the project and can run at the lifecycle point appropriate to the behavior instead of being forced into a GameTest. A call without extensions is the zero-assertion world-join scenario; similarly, server-test defaults to readiness when no command test is supplied.

Pins

The initial toolchain is Minecraft 1.21.1, NeoForge 21.1.233, Gradle 8.14.4, Java 21, HeadlessMC 2.10.0, mc-runtime-test 4.5.1, and packwiz-installer 0.5.14. Third-party JARs are fixed-output Nix inputs with verified SHA-256 hashes. The canonical bertie-pack metadata source is also an immutable flake input; updating it is an explicit lock-file change.

About

Archived CI tooling; development moved to bertie-mc/bertie.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages