Kubernetes MCSG/UHC plugin and controller
  • Java 91.1%
  • Go 6.4%
  • Shell 0.7%
  • Makefile 0.6%
  • Dockerfile 0.6%
  • Other 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
crypt 3ac6c7d40e fix: silence empty-lobby broadcast spam, harden game pod security, swallow 409 on pool status update
- WcMcsgPlugin: guard 'Waiting on N players' broadcast with current > 0
  so empty lobbies stop spamming the console every 30s
- instance_controller: add RunAsNonRoot + SeccompProfile (RuntimeDefault)
  to pod, RunAsNonRoot + Capabilities.Drop=ALL to both containers to
  satisfy PodSecurity restricted:latest
- pool_controller: treat 409 Conflict on status update as a silent
  requeue instead of an ERROR-level stack trace
2026-10-05 12:17:17 -05:00
.forgejo/workflows fix(ci): use mktemp for envtest bin dir to avoid stale file conflicts 2026-10-04 21:10:22 -05:00
chart release: bump chart to 0.9.1 2026-10-05 02:34:14 +00:00
controller fix: silence empty-lobby broadcast spam, harden game pod security, swallow 409 on pool status update 2026-10-05 12:17:17 -05:00
docs refactor: rename deathmatchThreshold → deathmatchPlayerThreshold 2026-10-04 21:03:39 -05:00
images feat: add UHC game type, per-game-type compass, prettified display names 2026-10-04 20:57:54 -05:00
plugin fix: silence empty-lobby broadcast spam, harden game pod security, swallow 409 on pool status update 2026-10-05 12:17:17 -05:00
scripts fix(ci): update test-chart.py for survival-games rename 2026-10-04 21:28:54 -05:00
.gitattributes ci: Forgejo Actions pipeline + release workflow + root README 2026-09-27 10:05:43 -05:00
.gitignore chore: ignore local agent/session config (.cortexkit, paseo.json) 2026-10-04 15:27:39 -05:00
.ignore chore: ignore deepwork session state 2026-10-03 21:36:49 -05:00
Makefile ci: restructure release flow to workflow_dispatch 2026-10-01 21:12:29 -05:00
README.md refactor: rename deathmatchThreshold → deathmatchPlayerThreshold 2026-10-04 21:03:39 -05:00

k8s-mcsg-uhc

A Kubernetes-managed Minecraft Survival Games network, faithful to the classic MCSG/MCGamer experience. Lobbies are ephemeral pods: an always-open lobby accepts players, the game starts, the lobby closes to new joins, the match plays out, the pod tears down, and a fresh lobby spins up to replace it. The whole system is packaged as a Helm chart so anyone can deploy it to a cluster with minimal setup.

The design is game-type-agnostic: the game type is a field on the pool CR, not hardcoded, so UHC or other minigames can be added later without restructuring (see the UHC Validation section of the design spec).

Architecture

                     Player connects (25565)
                          |
                          v
                +-------------------+
                |  Velocity Proxy   |  (LoadBalancer)
                |  + wc-velocity    |  watches MinecraftGameInstance CRs,
                |                   |  registers/deregisters servers,
                |                   |  enforces bans/mutes, staff chat,
                |                   |  announcements, fly/invis sync
                +---------+---------+
                          |
              +-----------+-----------+
              |                       |
              v                       v
    +-------------------+    +-------------------+
    |  Hub Server       |    |  Game Pod (x N)   |
    |  (Deployment)     |    |  (Pod per game)   |
    |  SportPaper 1.8 + |    |  SportPaper 1.8 + |
    |  wc-hub plugin    |    |  wc-mcsg plugin   |
    |                   |    |                   |
    |  Lists CRs,       |    |  Runs one SG      |
    |  renders signs,   |    |  match, patches   |
    |  live leaderboard |    |  own CR status    |
    |  holograms        |    |  (phase, alive)   |
    +---------+---------+    +---------+---------+
              |                        |
              |                        |
              v                        v
        +-----------+          +---------------+
        |  Redis    |          |  MongoDB      |
        |  (pub/sub |          |  (StatefulSet)|
        |   only,   |          |  stats, coins,|
        |   no PVC) |          |  moderation,  |
        +-----------+          |  staff state,  |
                               |  match history |
                               +---------------+

    +-------------------+
    |  Controller       |  (Deployment, 1 replica)
    |  Go operator      |  reconciles pools + instances,
    |                   |  creates/deletes pods + services,
    |                   |  picks lobby map per instance
    +-------------------+

Components:

  • Controller (Go, controller-runtime): reconciles MinecraftGamePool and MinecraftGameInstance CRDs. Creates pods/services for game instances, maintains the target lobby count, picks a lobby map per instance, tears down ended games.
  • Velocity proxy + wc-velocity plugin: player entrypoint. Watches CRs to dynamically register/deregisters servers and transfers players between hub and lobbies. Owns cross-server concerns: ban/mute enforcement, staff chat, announcements, and fly/invis state sync across game servers.
  • Hub server + wc-hub plugin: always-on lobby selector. Lists open game instances from CRs, renders clickable signs, sends transfer requests to the proxy. Displays live leaderboard holograms (armor stands) updated via Redis pub/sub. Optional (built-in or external hub).
  • Game pods + wc-mcsg plugin: ephemeral, one SG match per pod. The plugin runs the full game lifecycle (voting, countdown, match, deathmatch), patches its own CR status (phase, alivePlayers, winner), and enforces in-game staff commands.
  • MongoDB: persistent stats, coin balances, match history, moderation records, player name index, staff state, first-seen tracking.
  • Redis: ephemeral pub/sub for in-lobby map voting and live leaderboard updates. No persistence (pub/sub only; MongoDB is the source of truth).
  • Helm chart (wc-minecraft): deploys all of the above.

Repo layout

k8s-mcsg-uhc/
  plugin/            Java/Gradle multi-project
    wc-common/       shared: K8s client, MongoDB, Redis, state machine,
                     permissions, moderation, coins, stats, leaderboards
    wc-velocity/     proxy plugin: server registration, transfer, ban/mute
                     enforcement, staff chat, announcements, staff-state sync
    wc-hub/          hub plugin: lobby signs, CR listing, live leaderboard
                     holograms
    wc-mcsg/         SG game plugin: game logic, voting, MCSG features,
                     CR status patching, in-game admin commands
  controller/        Go operator (cmd/, internal/, api/)
  images/            Dockerfiles for server, hub-server, velocity, maps
  chart/             Helm chart wc-minecraft (CRDs, controller, mongodb,
                     redis, hub, velocity, staff/announcements ConfigMaps)
  docs/specs/        design documents
  docs/superpowers/  specs + implementation plans
  .forgejo/workflows/  CI (commit-check, ci, release)
  scripts/           kind-test.sh and utility scripts
  Makefile           plugin / controller / images / chart / test / lint

Game flow

Player connects -> Velocity proxy -> hub server
  -> player clicks a lobby sign (or is transferred automatically)
  -> joins a game pod (lobby state)

Lobby state:
  - Server boots on a lobby map (random from pool.lobbyMaps)
  - All game maps from the pool are available on a side volume
  - Players wait; voting starts when the lobby reaches the min-vote threshold

Voting (in-lobby):
  - 5 game maps randomly selected from the pool, numbered 1-5 in chat
  - Players type /vote 1-5 (one vote each, changeable)
  - A staff member can /veto <map> to force a specific map

Countdown:
  - Starts when minPlayers is reached
  - At countdown end, the voted map (or /veto override, or a random game map
    if fewer than the vote threshold) is loaded via WorldCreator and all
    participants are teleported to its spawn

In-game:
  - Grace period (PvP off), then PvP enabled
  - Tiered chests (tier 1/2), sponsor GUI (spectators spend coins to drop
    items at living players), supply drop at the 10-minute mark (tier 3)
  - Block interaction restricted to an allowlist; tracking compass

Deathmatch:
  - Triggers at deathmatchPlayerThreshold players remaining
  - Timer (default 120s); on expiry, overtime lightning strikes a random
    living player every 3s until one (or zero) remain
  - Border enforcement: players outside the arena radius take damage + knockback

End:
  - Winner credited +10 coins; killer credits (+1 normal, +2 deathmatch)
  - stats.updated event published to Redis -> hub leaderboard holograms refresh
  - CR status transitions to "ending"; after endingTtl the controller tears
    down the pod and spins up a replacement lobby

Features

MCSG faithfulness

  • Tiered chests - tier 1 (common) and tier 2 (uncommon) loot tables, loaded per-map from chests.yml. Chests fill on first open.
  • Sponsors - spectators open a coin-backed GUI: pick an item from the sponsor catalogue (each with a coin cost), pick a living player, and the item drops at their feet. Insufficient coins grey out the item. Only spectators can sponsor; only living participants can be sponsored.
  • Supply drops - at the 10-minute mark of in-game time, a tier-3 chest spawns at one of 5 preset map locations. A 10-second chat warning with coordinates precedes it; a beacon beam marks the spot. Once-per-game.
  • Deathmatch - configurable timer (default 120s). On expiry with more than one player alive, overtime lightning strikes a random living player every 3 seconds until one remains (lightning is the tiebreaker, not "most kills").
  • Deathmatch border - during deathmatch, players outside the arena radius take damage and knockback toward the center every second.
  • Block restriction - during a game, non-op block breaks are limited to an allowlist (leaves, vines, grass, flowers, mushrooms, web). Placement is limited to flint and steel. Ops bypass for map maintenance.
  • Sidebar - scoreboard sidebar on every participant/spectator: server name, map name, alive/tributes count, game phase, your kills this game, your coin balance. Refreshed on phase change, kill, death, sponsor spend, and a 2-second tick.
  • Chat formatting - color-coded death broadcasts, phase announcements, sponsor notifications, supply-drop alerts, and win announcements.
  • Tracking compass - points toward the nearest living player.
  • Coins - per-player integer balance persisted in MongoDB. Earned: +1 per normal kill, +2 per deathmatch kill, +10 per win. Spent on sponsors.

Map voting

  • The server boots onto a lobby map (randomly selected from the pool's lobbyMaps, preferring maps not already in use).
  • All game maps from the pool are copied to a side volume at boot.
  • Voting starts when the lobby reaches the vote threshold (default 6 players). Players type /vote 1 through /vote 5 for one of 5 randomly selected game maps. One vote per player, changeable.
  • At countdown end, the most-voted map is loaded at runtime via WorldCreator from the side volume, and all participants teleport to its spawn. The sidebar and CR status reflect the actually-played map.
  • With fewer than the vote threshold at countdown, a random game map is played (chat: "Not enough players for voting; playing on .").
  • /veto <map> (staff) forces a specific map, canceling the vote.

Real-time leaderboards

  • Game pods publish stats.updated events to Redis on every kill and win.
  • The hub subscribes via Redis pub/sub, debounces 5 seconds, re-fetches the top-N from MongoDB, and updates armor-stand holograms at configured locations.
  • Holograms are custom (no external plugin): each line is an invisible armor stand with a custom name, stacked 0.25 blocks apart.
  • Redis is ephemeral (no PVC). If Redis is down, game pods are unaffected (stats still write to MongoDB) and the hub shows the last-known leaderboard until the next successful event.
  • If redis.enabled=false, the hub shows a static leaderboard fetched once on startup (no live updates).

Clans

  • Players spend 1000 coins to create a clan (/clan create <name> [tag]). Clan names are 3-16 chars (alphanumeric + underscore), tags are 1-5 chars (alphanumeric). Both are case-insensitive (stored lowercase).
  • Anyone can join a clan (/clan join <name>) up to a max of 10 members.
  • The leader can appoint up to 3 elders (/clan promote <player>). Elders can kick regular members but not other elders or the leader.
  • Kills and wins earn clan points alongside personal coins: +1 per normal kill, +2 per deathmatch kill, +10 per win. Clan leaderboards (points, kills, wins) display on the hub via holograms.
  • Clan tags appear in chat (death broadcasts, win announcements) and the scoreboard sidebar.
  • /clan subcommands: create, join, leave, disband, info, promote, demote, kick, list, top, tag. Leader leaving disbands the clan automatically.
  • Clans are stored in MongoDB (clans collection); membership is a clanId field on the player document.

Admin and staff tooling

Permissions - staff.yml ConfigMap maps UUIDs to permission nodes (* is the only wildcard). Loaded by PermissionManager (wc-common) on the proxy, game pods, and the hub. Console always bypasses permission checks; ops (from the OPS env var, game pods only) get implicit * permission. Bukkit attachments are applied on join and refreshed on /staffreload, which refreshes the proxy, game pods, and the hub.

Moderation - MongoDB-backed bans, mutes, and kicks. Moderation documents use a compound _id (<uuid>:<type>); unbanning/unmuting sets expiresAt to now (history is retained). Ban enforcement lives on the Velocity proxy (PreLoginEvent) and fails open on MongoDB errors (a transient blip must not lock everyone out). Mute enforcement is defense in depth: the proxy cancels chat for muted players, and the game pod also checks on PlayerChatEvent.

Staff commands - generic commands are implemented once in wc-common and declared in each host plugin's plugin.yml; SG-specific commands stay in wc-mcsg. /fly and /invis are also registered on the hub:

Command Description
/sc <msg> Staff chat (proxy-relayed to all staff)
/fly Toggle flight; state synced cross-server via wc:staffstate (game pods + hub)
/invis Toggle invisibility; state synced cross-server (game pods + hub)
/tp <player> Teleport to target
/heal Full health + food, extinguish
/feed Full food + saturation
/clearchat Clear chat for all online players
/lockchat Toggle chat lock (non-staff blocked when locked)
/invsee <player> Open target inventory (read-only)
/spectate <player> Spectate mode; excluded from alive set and game-end logic
/veto <map> Override map vote
/sgsetup <sub> Author a map's spawn points (load/center/add/dm/drop/finish) and export map.yml
/gamestate Show phase, map, alive count/names, time, deathmatch status
/resetstats <player> Delete a player's stats document
/setcoins <player> <n> Set a player's coin balance

Proxy commands (wc-velocity): /ban, /kick, /mute, /unban, /unmute, /modhistory, /announce [scope] <msg>, /sc, /staffreload.

Announcements - timed rotating messages (sequential or random) with per-message scope (all/hub/game) and prefix overrides. First-join welcome message fires once per player (atomic firstSeen upsert in MongoDB). Manual /announce <msg> and /announce game|hub <msg> for scoped broadcasts.

Staff state sync - fly and invis state persist in MongoDB (StaffStateStore) and sync across game servers and the hub via the wc:staffstate plugin message channel on ServerConnectedEvent. Every host (game pods and the hub) runs the shared wc-common StaffStateReceiver, so persisted state applies on join everywhere.

alivePlayers CRD status - the game plugin patches status.alivePlayers (list of UUIDs) on every alive-set change (join, death, quit-eliminate). The controller passes it through without logic; it is observable via kubectl get minecraftgameinstances -o yaml.

OPS and whitelist

  • game.ops (chart values): comma-joined and injected as the OPS env var on every game pod. The itzg minecraft-server image uses this for server ops; the wc-mcsg plugin treats op IGNs as implicit * permission holders.
  • game.whitelist (chart values): when enabled, injects ENABLE_WHITELIST=true and WHITELIST=<players> on every game pod. The plugin enforces the whitelist server-side (Velocity proxying already gates ingress).

Installation

Helm install

helm install wc-minecraft ./chart \
  --namespace wc-minecraft --create-namespace \
  --set velocity.type=LoadBalancer \
  --set velocity.loadBalancerIP=192.168.1.60 \
  --set pools[0].desiredOpenLobbies=5

Or, once published, from the Forgejo Helm registry:

helm repo add willowcrest https://git.willowcrest.world/api/packages/crypt/helm
helm install wc-minecraft willowcrest/wc-minecraft \
  --namespace wc-minecraft --create-namespace

The release namespace is set with --namespace; the chart does not create a Namespace resource (use --create-namespace or pre-create it). All chart resources are namespaced into .Release.Namespace.

Verify:

kubectl -n wc-minecraft get minecraftgamepools
kubectl -n wc-minecraft get minecraftgameinstances
kubectl -n wc-minecraft get pods

See chart/README.md for the full values reference, external-hub setup, RBAC, NetworkPolicy, secrets, and CRD upgrade notes.

Key values.yaml sections

Section Purpose
registry Container registry prefix for all chart-managed images
controller Operator image, replicas, resources
hub Built-in hub Deployment (set enabled: false for external hub)
velocity Proxy Service type (LoadBalancer/NodePort/ClusterIP), forwarding secret
game Default server/world images, resources, OPS, whitelist, velocity secret
staff staff.yml ConfigMap (UUID -> permission nodes); configMap for bring-your-own
announcements announcements.yml ConfigMap (interval, mode, messages, welcome)
mongodb StatefulSet, auth secret, PVC size/storageClass
redis Ephemeral pub/sub StatefulSet (no PVC); enabled: false to disable
pools Game pools (gameType, worldSource, capacity, maps, per-pool gameConfig)
networkPolicy NetworkPolicy enable + API server CIDR
imagePullSecrets Applied to all chart-managed workloads + the wc-game SA

SportPaper setup

The server and hub images require sportpaper.jar (SportPaper 1.8.8, gitignored, not vendored). SportPaper is published to the Forgejo Maven registry at world.willowcrest:SportPaper:1.8.8-R0.1.

  • In CI: the SPORTPAPER_VERSION repository variable (Settings > Actions > Variables) controls which version is fetched from the Forgejo Maven registry. If unset, server/hub image builds are skipped (velocity and maps always build).
  • For local builds: source sportpaper.jar from the Forgejo Maven registry or a local SportPaper build, then place it in both images/server/ and images/hub-server/. See the Dockerfiles for details.

Map images

Map files (.mca) are tracked via Git LFS (.gitattributes). The maps image (images/maps/) has two subdirectories: lobby/ (lobby map variations) and game/ (game maps). The init container copies one lobby map to /data/ (for server startup) and all game maps to a side volume for runtime world loading during voting. images/maps/ ships with one game map (alaskan-village), one lobby map (lobby1), and a hub world; add more maps under images/maps/maps/game/ and images/maps/maps/lobby/ as needed.

CI

Forgejo Actions (.forgejo/workflows/). ci.yml is build/lint/test only and runs on every pull request; publishing happens on tag pushes via release.yml.

  • commit-check.yml - conventional commit format on every PR.
  • ci.yml - path-filtered stages:
    • plugin - Spotless check, Gradle build + test.
    • controller - go vet, build (static, CGO disabled), golangci-lint, unit tests (envtest-dependent tests skipped in CI).
    • chart - helm lint, kubeconform on rendered manifests, package (validates the chart packages).
  • release.yml - triggered by semver tag push (v*.*.*). Builds all artifacts (plugin jars, controller image, the 4 Docker images, Helm chart), tags them with the clean semver, publishes to Forgejo registries (Maven, container, Helm), and creates a Forgejo release with the chart package attached.

KinD integration test

make kind-test runs scripts/kind-test.sh, which spins up a KinD cluster, builds the controller image, installs the chart with mock busybox images for the Java components (no SportPaper needed), and verifies the controller creates game instances and pods.

make kind-test              # run, clean up after
KEEP=1 make kind-test       # leave the cluster running for debugging
FULL=1 make kind-test       # build and load real images (needs SportPaper)

Deployment

The chart is designed to be self-contained in its own namespace (wc-minecraft) so it can be installed, upgraded, and removed independently of other cluster workloads. MongoDB data lives on a PVC and survives undeploy by default.

Quick start

# 1. Build and publish all images + chart (triggered by a semver tag)
make release VERSION=0.1.0

# 2. Deploy to the cluster (creates namespace wc-minecraft)
make deploy

# 3. Get the proxy IP (players connect here)
kubectl -n wc-minecraft get svc wc-minecraft-velocity -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

# 4. Configure staff (edit values.yaml or use --set)
helm upgrade wc-minecraft chart/ --namespace wc-minecraft \
  --set 'staff.staff[0].uuid=YOUR-UUID' \
  --set 'staff.staff[0].name=YOUR-IGN' \
  --set 'staff.staff[0].role=admin' \
  --set 'staff.staff[0].permissions[0]=*'

Homelab deployment (ArgoCD GitOps)

The willowcrest-homelab repo has an ArgoCD Application at k8s/infra/argocd/manifests/applications/wc-minecraft.yaml that deploys this chart from main with homelab-specific overrides (MetalLB IP, node selectors, pool config). ArgoCD syncs automatically with self-heal.

Deploy order (important):

  1. Publish images first: make release VERSION=0.1.0 (triggers release workflow which builds and pushes all 5 images to the Forgejo registry)
  2. Create the image pull secret in the wc-minecraft namespace:
    kubectl -n forgejo get secret forgejo-pull -o yaml | \
      sed 's/namespace: forgejo/namespace: wc-minecraft/' | \
      kubectl apply -f -
    
  3. Merge the homelab PR - ArgoCD syncs automatically, pods start with the already-published images

Upgrade flow:

  1. Merge changes to main in the k8s-mcsg-uhc repo (chart/code changes)
  2. Publish a new version: make release VERSION=0.2.0
  3. Update chart.AppVersion and the ArgoCD app values (if needed) to reference the new version
  4. ArgoCD self-heals with the new image tags

Deploy from local chart (dev)

make deploy                                    # local chart, default values
make deploy REGISTRY=git.willowcrest.world/crypt  # use Forgejo container registry

Deploy a released version

make deploy VERSION=0.1.0    # pulls chart from Forgejo Helm registry

Undeploy

make undeploy           # remove all pods/services, KEEP MongoDB data
make undeploy PURGE=1   # remove everything including MongoDB data (irreversible)

Data export / import (for cluster migration or backup)

make export-data                                          # exports to ./backups/
make export-data OUTPUT=/tmp/my-backup.tar.gz             # custom path
make import-data INPUT=backups/wc-minecraft-20260927.tar.gz  # restore

The export runs mongodump inside the MongoDB pod via kubectl exec - no local MongoDB tools needed. The archive is a gzipped BSON dump of all collections (players, moderation, playerNames, matches, counters). Use this to migrate between clusters or back up before teardown.

Maps

Place Minecraft 1.8 world folders in images/maps/maps/lobby/ and images/maps/maps/game/. See images/maps/maps/README.md for the expected layout. Add map names to pools[].maps (game) and pools[].lobbyMaps (lobby) in chart/values.yaml.

Configuration

Key values.yaml sections:

Section What it controls
pools Game pools: world source, maps, capacity, per-pool game rules, desiredOpenLobbies
game.ops Bukkit ops (IGN list, injected via OPS env var)
game.whitelist Server whitelist (enabled + player IGN list)
staff.staff Staff permissions (UUID + name + role + permissions list)
announcements Timed rotating broadcasts + first-join welcome
mongodb.persistence PVC storage class + size
hub.signs Hub sign locations for lobby listing
hub.leaderboards Hologram locations for live leaderboards

Development

Prerequisites: JDK 17-21 (Gradle 8.5 cannot run on JDK 25), Go 1.22+, Helm 3 or 4, Docker or buildah, and controller-gen (for CRD regeneration).

make plugin      # build all Java jars (Gradle wrapper, no installed Gradle needed)
make controller  # build the Go operator binary (static, CGO disabled)
make images       # stage jars + fetch SportPaper + build all 4 Docker images
make push-images  # push all images to the registry
make release-images  # build controller + all images + push (one command)
make chart       # helm lint + kubeconform + package
make test        # plugin + controller unit tests
make lint        # spotless + golangci-lint + helm lint
make crd         # regenerate CRDs from Go types (controller-gen)
make kind-test   # KinD integration test

The controller tests use controller-runtime envtest (a real kube-apiserver

  • etcd). Install the assets once with:
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
"$(go env GOPATH)/bin/setup-envtest" use 1.30.0 --bin-dir /tmp/opencode/envtest-bin

The test harness finds them under /tmp/opencode/envtest-bin automatically. In CI, envtest-dependent integration tests are skipped (the runner cannot download envtest binaries); they run locally where setup-envtest is available.

Java formatting

Spotless with google-java-format enforces consistent formatting. Run make lint (or cd plugin && ./gradlew spotlessCheck) to verify; run cd plugin && ./gradlew spotlessApply to auto-format. Java 8 is used for the game plugins (SportPaper 1.8) and Java 17 for wc-velocity.

Notes

  • JAVA_HOME must point at JDK 17-21 for make plugin / make images. The default PATH JDK (often 8 or 25) will not work. Example: export JAVA_HOME=/usr/lib/jvm/java-21-openjdk.
  • The controller image (controller/Dockerfile) runs the binary under distroless static, so it must be built with CGO_ENABLED=0. make controller sets this; a bare go build produces a dynamically-linked binary that distroless cannot exec.
  • The K8s Java client is io.kubernetes:client-java:25.0.0-legacy (not 27.x - the legacy variant was dropped after 25.x). Gradle Shadow shades wc-common and its dependencies into each plugin jar with package relocation, so each plugin jar is self-contained.

Documentation

  • Design spec: docs/specs/2026-09-26-mcsg-k8s-design.md
  • MCSG faithfulness spec: docs/superpowers/specs/2026-09-27-mcsg-faithfulness-design.md
  • Map voting + leaderboard spec: docs/superpowers/specs/2026-09-27-map-voting-leaderboard-design.md
  • Admin/staff tooling plan: docs/superpowers/plans/2026-09-27-admin-staff-tooling.md
  • Chart reference: chart/README.md

License

Willowcrest internal. Not for redistribution.