diff --git a/.gitignore b/.gitignore index 3f37d6c..6c99f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle/ +.docker/ build/ out/ .idea/ diff --git a/README.md b/README.md index cf05a42..7ac7afa 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,40 @@ The approved behavior and implementation record are in the [OKF design bundle](d The plugin JAR is written to `build/libs/`. +## Local Docker test server + +Build the current plugin and start it on an isolated Spigot 26.2 server: + +```bash +./scripts/start-test-server.sh +``` + +Connect to `localhost:25565` as `WindMagi`, which the harness configures as an operator. The server uses offline mode for local convenience and binds only to the loopback interface. + +The first startup may take several minutes while the image downloads and Spigot is prepared. Override the 15-minute wait or local RCON password when needed: + +```bash +TREE_FELLER_START_TIMEOUT=1200 \ +TREE_FELLER_RCON_PASSWORD='local-secret' \ +./scripts/start-test-server.sh +``` + +Common lifecycle commands: + +```bash +# Follow server logs +docker compose -f compose.test.yml logs -f minecraft + +# Stop while preserving the world and server configuration +docker compose -f compose.test.yml down + +# Stop and permanently reset all generated local test state +docker compose -f compose.test.yml down +rm -rf .docker/minecraft +``` + +Re-run the start script after code changes to rebuild the JAR and recreate the test container. Generated server state remains under `.docker/minecraft/` and is ignored by Git. + ## Player commands ```text diff --git a/compose.test.yml b/compose.test.yml new file mode 100644 index 0000000..4e1b42f --- /dev/null +++ b/compose.test.yml @@ -0,0 +1,32 @@ +name: tree-feller-test + +services: + minecraft: + image: itzg/minecraft-server:java25 + container_name: tree-feller-test + ports: + - "127.0.0.1:25565:25565" + environment: + EULA: "TRUE" + TYPE: SPIGOT + VERSION: "26.2" + MEMORY: 2G + OPS: WindMagi + ONLINE_MODE: "false" + ENABLE_RCON: "true" + RCON_PASSWORD: "${TREE_FELLER_RCON_PASSWORD:-tree-feller-local-test}" + VIEW_DISTANCE: "6" + SIMULATION_DISTANCE: "6" + SPAWN_PROTECTION: "0" + volumes: + - ./.docker/minecraft:/data + - ./build/libs/tree-feller-0.1.0-SNAPSHOT.jar:/plugins/TreeFeller.jar:ro + healthcheck: + test: ["CMD", "mc-health"] + start_period: 15m + interval: 10s + timeout: 5s + retries: 90 + restart: "no" + stdin_open: true + tty: true diff --git a/design/log.md b/design/log.md index 35a2194..37bad1e 100644 --- a/design/log.md +++ b/design/log.md @@ -2,6 +2,14 @@ ## 2026-08-11 +### US-010 local Docker test server completed + +- Added a repository-local Docker Compose harness using `itzg/minecraft-server:java25` with Spigot 26.2, persistent ignored state, offline local testing, and loopback-only port binding. +- Added one executable script that runs the strict Gradle build, recreates the isolated container with the latest JAR, waits for health, verifies Tree Feller through RCON, and confirms `WindMagi` as an operator. +- Documented start, log, stop, persistent-state reset, timeout, and RCON-password controls. +- Live verification completed in 23 seconds: the container was healthy, RCON reported `TreeFeller`, `ops.json` contained `WindMagi` at level 4, and port 25565 was bound only to `127.0.0.1`. +- The test server remains running at `localhost:25565` for gameplay testing. + ### US-009 build and release completed - Added maintainer documentation for requirements, builds, commands, configuration, supported species, persistence, safety, compatibility, and releases. diff --git a/design/user-stories/index.md b/design/user-stories/index.md index 1c49f41..bd0dff0 100644 --- a/design/user-stories/index.md +++ b/design/user-stories/index.md @@ -9,3 +9,4 @@ 7. [US-007: Administer Tree Feller](us-007-administer-tree-feller.md) 8. [US-008: Configure and persist Tree Feller](us-008-configure-and-persist-tree-feller.md) 9. [US-009: Build and release Tree Feller](us-009-build-and-release-tree-feller.md) +10. [US-010: Run a local Docker test server](us-010-run-local-docker-test-server.md) diff --git a/design/user-stories/us-010-run-local-docker-test-server.md b/design/user-stories/us-010-run-local-docker-test-server.md new file mode 100644 index 0000000..9568480 --- /dev/null +++ b/design/user-stories/us-010-run-local-docker-test-server.md @@ -0,0 +1,33 @@ +--- +type: User Story +title: "US-010: Run a local Docker test server" +description: Provide a repeatable Docker Compose harness that builds and loads Tree Feller on an isolated Spigot 26.2 server. +status: done +--- + +# US-010: Run a local Docker test server + +As a **plugin developer**, I want one command to run the current Tree Feller build on a local containerized server so that I can perform repeatable integration and gameplay testing. + +## Acceptance criteria + +- [x] One repository script builds and verifies the plugin before starting an isolated Spigot 26.2 server through Docker Compose. +- [x] The current Tree Feller JAR is mounted into the server automatically whenever the container is recreated. +- [x] `WindMagi` is configured and verified as a server operator. +- [x] The Minecraft port binds only to `127.0.0.1:25565` by default. +- [x] Startup waits for server health and verifies Tree Feller through RCON. +- [x] Generated worlds, logs, configuration, and downloaded server artifacts are stored under `.docker/minecraft/` and excluded from Git. +- [x] Stopping Compose preserves generated test state unless the operator explicitly deletes it. +- [x] Existing unrelated containers are not modified. +- [x] Maintainer documentation explains start, logs, stop, reset, timeout, and RCON-password controls. + +## Scope + +This harness is for local development only. It does not define production deployment, publish an image, expose Minecraft or RCON publicly, or persist its runtime state in Git. + +## Related + +- [US-003: Fell unlocked trees](us-003-fell-unlocked-trees.md) +- [US-005: Undo the last felled tree](us-005-undo-the-last-felled-tree.md) +- [US-009: Build and release Tree Feller](us-009-build-and-release-tree-feller.md) +- [User-story catalog](index.md) diff --git a/scripts/start-test-server.sh b/scripts/start-test-server.sh new file mode 100755 index 0000000..1f7093d --- /dev/null +++ b/scripts/start-test-server.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +compose_file="$project_root/compose.test.yml" +wait_seconds="${TREE_FELLER_START_TIMEOUT:-900}" + +if ! command -v docker >/dev/null 2>&1; then + echo "Required command not found: docker" >&2 + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + echo "Docker is not running or is not accessible." >&2 + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose v2 is required." >&2 + exit 1 +fi + +cd "$project_root" +./gradlew clean check jar +mkdir -p .docker/minecraft + +# Recreate the container so the image installs the newly built read-only plugin JAR. +docker compose -f "$compose_file" up -d --force-recreate +container_id="$(docker compose -f "$compose_file" ps -q minecraft)" +if [[ -z "$container_id" ]]; then + echo "The Tree Feller test container was not created." >&2 + exit 1 +fi + +echo "Waiting up to ${wait_seconds}s for Spigot 26.2..." +deadline=$((SECONDS + wait_seconds)) +while (( SECONDS < deadline )); do + container_state="$(docker inspect --format '{{.State.Status}}' "$container_id")" + health_state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")" + if [[ "$health_state" == "healthy" ]]; then + break + fi + if [[ "$container_state" == "exited" || "$container_state" == "dead" ]]; then + docker compose -f "$compose_file" logs --tail=150 minecraft >&2 + echo "The Minecraft server exited during startup." >&2 + exit 1 + fi + sleep 5 +done + +health_state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")" +if [[ "$health_state" != "healthy" ]]; then + docker compose -f "$compose_file" logs --tail=150 minecraft >&2 + echo "Timed out waiting for Spigot 26.2 (status: $health_state)." >&2 + exit 1 +fi + +plugins="$(docker compose -f "$compose_file" exec -T minecraft rcon-cli plugins)" +if [[ "$plugins" != *"TreeFeller"* ]]; then + echo "$plugins" >&2 + echo "TreeFeller was not reported by the running server." >&2 + exit 1 +fi + +# This is idempotent and confirms the requested local test account is an operator. +docker compose -f "$compose_file" exec -T minecraft rcon-cli 'op WindMagi' >/dev/null + +cat <<'MESSAGE' +Tree Feller test server is ready at localhost:25565. +Operator: WindMagi +View logs: docker compose -f compose.test.yml logs -f minecraft +Stop: docker compose -f compose.test.yml down +Reset: docker compose -f compose.test.yml down && rm -rf .docker/minecraft +MESSAGE