From af0c871c688b93068385dc7409a22394e6042777 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 16 Aug 2026 10:42:50 +0200 Subject: [PATCH 1/8] Configure agent skill conventions --- AGENTS.md | 9 ++++++++ docs/agents/domain.md | 35 ++++++++++++++++++++++++++++ docs/agents/issue-tracker.md | 45 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/agents/domain.md create mode 100644 docs/agents/issue-tracker.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..094a905 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +## Agent skills + +### Issue tracker + +Issues and specs are tracked in GitHub Issues using the `gh` CLI. See `docs/agents/issue-tracker.md`. + +### Domain docs + +This repository uses the single-context domain-doc layout. See `docs/agents/domain.md`. diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000..529bbc5 --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,35 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +This repository uses the single-context layout: `CONTEXT.md` and `docs/adr/` live at the repository root. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +```text +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000..bf595e2 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,45 @@ +# Issue tracker: GitHub + +Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## Pull requests as a triage surface + +**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ + +When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: + +- **Read a PR**: `gh pr view --comments` and `gh pr diff ` for the diff. +- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`). +- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`. + +GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. + +## Wayfinding operations + +Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets. + +- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`. +- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #` at the top of the child body. Labels: `wayfinder:` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev. +- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. +- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins. +- **Claim**: `gh issue edit --add-assignee @me` — the session's first write. +- **Resolve**: `gh issue comment --body ""`, then `gh issue close `, then append a context pointer (gist + link) to the map's Decisions-so-far. From 5f5cfab60b6e061fe9df6d0256a5b2dcd3def92a Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 23 Aug 2026 09:58:43 +0200 Subject: [PATCH 2/8] Fix Gradle CI build Replace the stale Maven workflow with a pinned Java 25 and Gradle build. Resolve BlueMapAPI and PlaceholderAPI from their official repositories so clean checkouts can compile and package the plugin without local server jars. --- .github/workflows/main.yml | 55 +++++++++++++++++++++++------- README.md | 4 ++- build.gradle | 68 ++++++++++++++++++++------------------ docs/installation.md | 3 ++ docs/integrations.md | 4 +++ gradle.properties | 2 ++ settings.gradle | 10 ++++++ 7 files changed, 101 insertions(+), 45 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c8101c1..3e74220 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,25 +1,56 @@ -name: Java CI with Maven +name: Java CI with Gradle on: push: pull_request: workflow_dispatch: +permissions: + contents: read + jobs: build: + name: Build plugin jar runs-on: ubuntu-latest + timeout-minutes: 15 + steps: - - uses: actions/checkout@v4 - - name: Set up JDK - uses: actions/setup-java@v4 + - name: Check out repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Set up Java 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - java-version: '21' distribution: 'temurin' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + java-version: '25' + + - name: Set up Gradle 9.6.1 + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + with: + gradle-version: '9.6.1' + cache-provider: basic + + - name: Read release build number + id: build-number + shell: bash + run: | + build_number="$(tr -d '\r\n' < build-number.txt)" + if [[ ! "$build_number" =~ ^[0-9]{3}$ ]]; then + echo "build-number.txt must contain exactly three digits" >&2 + exit 1 + fi + echo "value=$build_number" >> "$GITHUB_OUTPUT" + + - name: Build and verify plugin jar + run: >- + gradle --no-daemon + -PreleaseBuildNumber=${{ steps.build-number.outputs.value }} + clean check -x test + + - name: Upload plugin jar + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - path: | - target/*.jar - !target/original-*.jar + name: 1MB-MapHide-${{ github.sha }} + path: build/libs/*.jar + if-no-files-found: error + retention-days: 14 diff --git a/README.md b/README.md index 17e967d..b188507 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,9 @@ Requirements: - Gradle 9.6.1 or newer - JDK 25.0.4 at `/Library/Java/JavaVirtualMachines/jdk-25.0.4.jdk/Contents/Home` - The exact Maven dependency `io.papermc.paper:paper-api:26.2.build.84-stable` -- The local `servers/Paper-26.2` folder with `plugins/bluemap-5.22-paper.jar` and optional `plugins/PlaceholderAPI-2.12.3.jar` +- The Maven dependencies `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3` + +A clean checkout can compile without a local Paper server. The local `servers/Paper-26.2` folder is only needed for `deployServers` and `verifyLocalPaperServer`. Build release `028` and deploy it to the maintained Paper 26.2 test server: diff --git a/build.gradle b/build.gradle index a2faa77..62818af 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,8 @@ def paperTarget = providers.gradleProperty("paperTarget").get() def paperApiVersion = providers.gradleProperty("paperApiVersion").get() def paperBuild = providers.gradleProperty("paperBuild").get().toInteger() def paperChannel = providers.gradleProperty("paperChannel").get().toUpperCase() +def blueMapApiVersion = providers.gradleProperty("blueMapApiVersion").get() +def placeholderApiVersion = providers.gradleProperty("placeholderApiVersion").get() version = pluginVersion def buildNumberFile = file("build-number.txt") @@ -20,11 +22,6 @@ def maintainedServerDirs = [ file("servers/Paper-26.2") ] -def localCompileJars = files( - "servers/Paper-26.2/plugins/bluemap-5.22-paper.jar", - "servers/Paper-26.2/plugins/PlaceholderAPI-2.12.3.jar" -) - java { toolchain { languageVersion = JavaLanguageVersion.of(javaTarget) @@ -83,7 +80,8 @@ def releaseVersion = providers.provider { dependencies { compileOnly "io.papermc.paper:paper-api:${paperApiVersion}" - compileOnly localCompileJars + compileOnly "de.bluecolored:bluemap-api:${blueMapApiVersion}" + compileOnly "me.clip:placeholderapi:${placeholderApiVersion}" } tasks.processResources { @@ -156,32 +154,38 @@ tasks.register("verifyReleaseMetadata") { }).unique().collect { it.text }.join(System.lineSeparator()) [ - (readme): [ - "Version `${pluginVersion}`", - expectedJar, - "`io.papermc.paper:paper-api:${paperApiVersion}`" - ], - (installation): [ - "1MB-BlueMap-MapHide-v${pluginVersion}--j${javaTarget}-${paperTarget}.jar", - "`io.papermc.paper:paper-api:${paperApiVersion}`" - ], - (integrations): [ - "Paper ${paperTarget}", - "build ${paperBuild} ${paperChannel.toLowerCase()}" - ], - (docsMetadata): [ - "java_target: ${javaTarget}", - "paper_target: ${paperTarget}" - ], - (pluginMetadata): [ - "version: '\${version}'", - "api-version: '\${paperTarget}'" - ], - (buildInfoTemplate): [ - "paperApiVersion=\${paperApiVersion}", - "paperBuild=\${paperBuild}", - "paperChannel=\${paperChannel}" - ] + (readme): [ + "Version `${pluginVersion}`", + expectedJar, + "`io.papermc.paper:paper-api:${paperApiVersion}`", + "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", + "`me.clip:placeholderapi:${placeholderApiVersion}`" + ], + (installation): [ + "1MB-BlueMap-MapHide-v${pluginVersion}--j${javaTarget}-${paperTarget}.jar", + "`io.papermc.paper:paper-api:${paperApiVersion}`", + "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", + "`me.clip:placeholderapi:${placeholderApiVersion}`" + ], + (integrations): [ + "Paper ${paperTarget}", + "build ${paperBuild} ${paperChannel.toLowerCase()}", + "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", + "`me.clip:placeholderapi:${placeholderApiVersion}`" + ], + (docsMetadata): [ + "java_target: ${javaTarget}", + "paper_target: ${paperTarget}" + ], + (pluginMetadata): [ + "version: '\${version}'", + "api-version: '\${paperTarget}'" + ], + (buildInfoTemplate): [ + "paperApiVersion=\${paperApiVersion}", + "paperBuild=\${paperBuild}", + "paperChannel=\${paperChannel}" + ] ].each { File target, List expectedValues -> expectedValues.each { String expectedValue -> if (!target.text.contains(expectedValue)) { diff --git a/docs/installation.md b/docs/installation.md index dcc09c2..ef8e385 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,6 +6,7 @@ - Java 25 bytecode, built with JDK 25.0.4. - Paper 26.2 build 84 stable is the maintained compile and release-test target. - The exact compile coordinate is `io.papermc.paper:paper-api:26.2.build.84-stable`. +- BlueMapAPI `de.bluecolored:bluemap-api:2.8.0` and PlaceholderAPI `me.clip:placeholderapi:2.12.3` are resolved from their official Maven repositories. - PlaceholderAPI is optional. This release declares `api-version: 26.2` and is not intended for older Paper versions. @@ -62,6 +63,8 @@ The jar name follows this pattern: The Gradle deploy task copies the jar only into the maintained local `servers/Paper-26.2/plugins/` folder and appends `.disabled` to older active MapHide jars. +The local server folder is not required to compile or package the plugin. It is only used by `deployServers` and `verifyLocalPaperServer`. + `gradle check` runs release-documentation and generated-artifact drift checks. `gradle verifyLocalPaperServer` validates PaperScript's stable channel, Paper build, installed jar, and saved SHA-256. ## Readiness Check diff --git a/docs/integrations.md b/docs/integrations.md index a6311b6..37e93c8 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -4,12 +4,16 @@ BlueMap is required. MapHide uses the BlueMap API to read and update player marker visibility in the BlueMap web app. +Compilation uses the official `de.bluecolored:bluemap-api:2.8.0` artifact matching BlueMap 5.22. + If BlueMap is not ready yet, MapHide sends a "BlueMap is not ready yet" message and does not change visibility. ## PlaceholderAPI PlaceholderAPI is optional. When present, MapHide registers the `maphide` expansion and exposes `%maphide_*%` placeholders. +Compilation uses the official `me.clip:placeholderapi:2.12.3` artifact; it is not bundled into the MapHide jar. + Use `/bmpc debug placeholders` to confirm whether PlaceholderAPI is enabled and to list placeholder meanings. ## Permissions Plugins diff --git a/gradle.properties b/gradle.properties index 1961769..9d3cf90 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,5 @@ paperTarget=26.2 paperApiVersion=26.2.build.84-stable paperBuild=84 paperChannel=STABLE +blueMapApiVersion=2.8.0 +placeholderApiVersion=2.12.3 diff --git a/settings.gradle b/settings.gradle index b1cd147..091afcd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,16 @@ dependencyResolutionManagement { maven { name = "bluecolored" url = uri("https://repo.bluecolored.de/releases") + content { + includeGroup("de.bluecolored") + } + } + maven { + name = "placeholderapi" + url = uri("https://repo.extendedclip.com/releases/") + content { + includeGroup("me.clip") + } } } } From 91b0e5de5b4d50060c9cdda72abe634187aeb719 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 15 Sep 2026 00:27:39 +0200 Subject: [PATCH 3/8] Build release 029 with JDK 25.0.4.1 Select the exact release JDK through JAVA_HOME for Gradle, compilation, tests, and CI while retaining Java 25 bytecode and Paper 26.2 build 84. Add isolated dual-runtime smoke checks and preserve historical verification records. Handle current and staged PaperScript checksums independently. --- .github/workflows/main.yml | 8 +- README.md | 17 +- build-number.txt | 2 +- build.gradle | 76 ++++++- docs/installation.md | 33 ++- docs/integrations.md | 13 +- docs/verification/2026-09-15-jdk-update.md | 67 ++++++ gradle.properties | 4 + scripts/smoke-test.py | 228 +++++++++++++++++++++ 9 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 docs/verification/2026-09-15-jdk-update.md create mode 100644 scripts/smoke-test.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e74220..0f6ca4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,11 +18,11 @@ jobs: - name: Check out repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Set up Java 25 + - name: Set up JDK 25.0.4.1 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: 'temurin' - java-version: '25' + distribution: 'oracle' + java-version: '25.0.4.1' - name: Set up Gradle 9.6.1 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -45,7 +45,7 @@ jobs: run: >- gradle --no-daemon -PreleaseBuildNumber=${{ steps.build-number.outputs.value }} - clean check -x test + clean build - name: Upload plugin jar uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/README.md b/README.md index b188507..65191d1 100644 --- a/README.md +++ b/README.md @@ -137,35 +137,38 @@ PlaceholderAPI is optional. When installed, 1MB-MapHide registers: Requirements: - Gradle 9.6.1 or newer -- JDK 25.0.4 at `/Library/Java/JavaVirtualMachines/jdk-25.0.4.jdk/Contents/Home` +- JDK 25.0.4.1 at `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` - The exact Maven dependency `io.papermc.paper:paper-api:26.2.build.84-stable` - The Maven dependencies `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3` A clean checkout can compile without a local Paper server. The local `servers/Paper-26.2` folder is only needed for `deployServers` and `verifyLocalPaperServer`. -Build release `028` and deploy it to the maintained Paper 26.2 test server: +Build release `029` and deploy it to the maintained Paper 26.2 test server: ```sh -JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.jdk/Contents/Home \ - gradle --no-daemon -PreleaseBuildNumber=028 clean build +export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home +export PATH="$JAVA_HOME/bin:$PATH" +gradle --no-daemon -PreleaseBuildNumber=029 clean build verifyLocalPaperServer ``` Jars are named like: ```text -1MB-BlueMap-MapHide-v2.0.1-028-j25-26.2.jar +1MB-BlueMap-MapHide-v2.0.1-029-j25-26.2.jar ``` The plugin version comes from `gradle.properties`. The three-digit build number is stored in `build-number.txt` and advances for a new unpinned jar build. Pin `releaseBuildNumber` when repeating a release build so retries reuse the intended number instead of incrementing it again. The Java and Paper parts of the jar name are target identifiers, not the semantic plugin version. -`verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and Java 25 class version. Both run as part of `check`. +`verifyBuildJava` checks that Gradle, the compiler, and the test launcher use JDK 25.0.4.1 from `JAVA_HOME`. `verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and every class for Java 25 bytecode without preview features. All run as part of `check`. After a successful `build`, `deployServers` copies the jar into `servers/Paper-26.2/plugins/`. Before copying, it renames active older MapHide jars in that folder by appending `.disabled`. +Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on both JDK 25.0.4.1 and JDK 26.0.2.1. It uses isolated copies of the local server and saves new logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for prerequisites and coverage. Live uses Java 26; the plugin retains Java 25 bytecode. + ## Installation 1. Stop the Paper server. -2. Put `1MB-BlueMap-MapHide-v2.0.1-028-j25-26.2.jar` in `plugins/`. +2. Put `1MB-BlueMap-MapHide-v2.0.1-029-j25-26.2.jar` in `plugins/`. 3. Make sure BlueMap is also in `plugins/`. 4. Remove or disable old `BlueMapPlayerControl-*.jar` copies. 5. Start the server. diff --git a/build-number.txt b/build-number.txt index 445e2e7..9722268 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -028 +029 diff --git a/build.gradle b/build.gradle index 62818af..33387be 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ group = "com.onemb" def pluginVersion = providers.gradleProperty("pluginVersion").get() def javaTarget = providers.gradleProperty("javaTarget").get().toInteger() +def buildJavaVersion = providers.gradleProperty("buildJavaVersion").get() def paperTarget = providers.gradleProperty("paperTarget").get() def paperApiVersion = providers.gradleProperty("paperApiVersion").get() def paperBuild = providers.gradleProperty("paperBuild").get().toInteger() @@ -28,7 +29,30 @@ java { } } +tasks.register("verifyBuildJava") { + group = "verification" + description = "Verifies Gradle, compilation, and tests use the release JDK selected through JAVA_HOME." + + doLast { + String javaHome = providers.environmentVariable("JAVA_HOME").orNull + if (!javaHome || file(javaHome).canonicalFile != file(System.getProperty("java.home")).canonicalFile) { + throw new GradleException("Set JAVA_HOME and prepend its bin directory to PATH before building") + } + if (System.getProperty("java.version") != buildJavaVersion) { + throw new GradleException("Build with JDK ${buildJavaVersion}; running ${System.getProperty('java.version')}") + } + [tasks.compileJava.javaCompiler.get().metadata, tasks.test.javaLauncher.get().metadata].each { metadata -> + if (metadata.javaRuntimeVersion.split('\\+')[0] != buildJavaVersion + || metadata.installationPath.asFile.canonicalFile != file(javaHome).canonicalFile) { + throw new GradleException("Compiler and tests must use JDK ${buildJavaVersion} from JAVA_HOME") + } + } + logger.lifecycle("Verified build/compiler/test JDK ${System.getProperty('java.runtime.version')} at ${javaHome}") + } +} + tasks.withType(JavaCompile).configureEach { + dependsOn(tasks.named("verifyBuildJava")) options.encoding = "UTF-8" options.release = javaTarget options.compilerArgs.add("-Xlint:deprecation") @@ -147,6 +171,7 @@ tasks.register("verifyReleaseMetadata") { File installation = file("docs/installation.md") File integrations = file("docs/integrations.md") File docsMetadata = file("docs/plugin-docs.yml") + File workflow = file(".github/workflows/main.yml") File pluginMetadata = file("src/main/resources/plugin.yml") File buildInfoTemplate = file("src/main/resources/build-info.properties") String documentation = ([readme, installation, integrations] + fileTree("docs") { @@ -156,12 +181,14 @@ tasks.register("verifyReleaseMetadata") { [ (readme): [ "Version `${pluginVersion}`", + "JDK ${buildJavaVersion}", expectedJar, "`io.papermc.paper:paper-api:${paperApiVersion}`", "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", "`me.clip:placeholderapi:${placeholderApiVersion}`" ], (installation): [ + "JDK ${buildJavaVersion}", "1MB-BlueMap-MapHide-v${pluginVersion}--j${javaTarget}-${paperTarget}.jar", "`io.papermc.paper:paper-api:${paperApiVersion}`", "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", @@ -177,6 +204,9 @@ tasks.register("verifyReleaseMetadata") { "java_target: ${javaTarget}", "paper_target: ${paperTarget}" ], + (workflow): [ + "java-version: '${buildJavaVersion}'" + ], (pluginMetadata): [ "version: '\${version}'", "api-version: '\${paperTarget}'" @@ -248,16 +278,23 @@ tasks.register("verifyArtifactMetadata") { throw new GradleException("Generated plugin.yml metadata is stale") } - byte[] mainClass = zip.getInputStream(zip.getEntry( - "com/technicjelle/bluemapplayercontrol/BlueMapPlayerControl.class" - )).readAllBytes() - int classMajorVersion = ((mainClass[6] & 0xff) << 8) | (mainClass[7] & 0xff) int expectedClassMajorVersion = 44 + javaTarget - if (classMajorVersion != expectedClassMajorVersion) { - throw new GradleException( - "Artifact class version ${classMajorVersion}; expected Java ${javaTarget} (${expectedClassMajorVersion})" - ) + if (zip.getEntry("com/technicjelle/bluemapplayercontrol/BlueMapPlayerControl.class") == null) { + throw new GradleException("Artifact is missing the plugin main class") + } + def classes = zip.entries().findAll { it.name.endsWith(".class") } + if (classes.isEmpty()) { + throw new GradleException("Artifact contains no Java classes") + } + classes.each { entry -> + byte[] bytecode = zip.getInputStream(entry).readAllBytes() + int minor = ((bytecode[4] & 0xff) << 8) | (bytecode[5] & 0xff) + int major = ((bytecode[6] & 0xff) << 8) | (bytecode[7] & 0xff) + if (major != expectedClassMajorVersion || minor != 0) { + throw new GradleException("${entry.name} has class version ${major}.${minor}; expected ${expectedClassMajorVersion}.0 without preview features") + } } + logger.lifecycle("Verified ${classes.size()} Java ${javaTarget} classes without preview features") } finally { zip.close() } @@ -269,7 +306,7 @@ tasks.register("verifyLocalPaperServer") { description = "Checks the maintained local PaperScript config, Paper build, and saved SHA-256." doLast { - File serverDir = maintainedServerDirs.first() + File serverDir = file(providers.gradleProperty("localPaperServerDir").getOrElse("servers/Paper-26.2")) File configFile = new File(serverDir, "paperscript/config.json") File stateFile = new File(serverDir, "paperscript/state.json") if (!configFile.isFile() || !stateFile.isFile()) { @@ -283,7 +320,8 @@ tasks.register("verifyLocalPaperServer") { if (config.default_channel?.toString()?.toUpperCase() != "STABLE" || config.check_latest_channel_only?.toString()?.toUpperCase() != "STABLE" || config.allow_same_version_build_upgrade != true - || config.download_filename_pattern != "Paper-{version}.jar") { + || (config.containsKey("download_filename_pattern") + && config.download_filename_pattern != "Paper-{version}.jar")) { throw new GradleException("Maintained PaperScript config does not match the stable release policy") } if (state.current_version != paperTarget || state.current_build.toString().toInteger() != paperBuild) { @@ -298,9 +336,24 @@ tasks.register("verifyLocalPaperServer") { .digest(paperJar.bytes) .collect { String.format("%02x", it & 0xff) } .join() - if (actualSha256 != state.current_sha256 || actualSha256 != state.expected_sha256) { + if (actualSha256 != state.current_sha256) { throw new GradleException("Maintained Paper jar SHA-256 does not match PaperScript state") } + // Newer PaperScript records expected_sha256 for a staged upgrade, not the current jar. + if (state.staged_jar) { + File stagedJar = new File(serverDir, state.staged_jar.toString()) + if (!stagedJar.isFile()) { + throw new GradleException("Staged Paper jar is missing: ${stagedJar}") + } + String stagedSha256 = java.security.MessageDigest.getInstance("SHA-256") + .digest(stagedJar.bytes).collect { String.format("%02x", it & 0xff) }.join() + if (stagedSha256 != state.staged_sha256 || stagedSha256 != state.expected_sha256) { + throw new GradleException("Staged Paper jar SHA-256 does not match PaperScript state") + } + logger.lifecycle("Verified separately staged ${stagedJar.name}; release tests still use ${paperJar.name}") + } else if (actualSha256 != state.expected_sha256) { + throw new GradleException("Maintained Paper jar SHA-256 does not match the expected download checksum") + } logger.lifecycle("Verified Paper ${paperTarget} build ${paperBuild} ${paperChannel}: ${paperJar}") } } @@ -349,6 +402,7 @@ tasks.register("deployServers") { } tasks.named("check") { + dependsOn(tasks.named("verifyBuildJava")) dependsOn(tasks.named("verifyReleaseMetadata")) dependsOn(tasks.named("verifyArtifactMetadata")) } diff --git a/docs/installation.md b/docs/installation.md index ef8e385..0844386 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,7 +3,8 @@ ## Requirements - Paper server with BlueMap installed. -- Java 25 bytecode, built with JDK 25.0.4. +- Java 25 bytecode, built with JDK 25.0.4.1. +- Supported runtimes: Java 25.0.4.1 and Java 26.0.2.1. Live uses Java 26. - Paper 26.2 build 84 stable is the maintained compile and release-test target. - The exact compile coordinate is `io.papermc.paper:paper-api:26.2.build.84-stable`. - BlueMapAPI `de.bluecolored:bluemap-api:2.8.0` and PlaceholderAPI `me.clip:placeholderapi:2.12.3` are resolved from their official Maven repositories. @@ -49,8 +50,9 @@ Existing `config.yml` values are preserved. Missing defaults and managed comment The normal local build command is: ```sh -JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.jdk/Contents/Home \ - gradle --no-daemon -PreleaseBuildNumber=028 clean build +export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home +export PATH="$JAVA_HOME/bin:$PATH" +gradle --no-daemon -PreleaseBuildNumber=029 clean build verifyLocalPaperServer ``` The jar name follows this pattern: @@ -59,13 +61,34 @@ The jar name follows this pattern: 1MB-BlueMap-MapHide-v2.0.1--j25-26.2.jar ``` -`2.0.1` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=028` makes release retries reuse build `028`. The `j25` and `26.2` parts identify the Java bytecode and Paper target. +`2.0.1` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=029` makes release retries reuse build `029`. The `j25` and `26.2` parts identify the Java bytecode and Paper target. The Gradle deploy task copies the jar only into the maintained local `servers/Paper-26.2/plugins/` folder and appends `.disabled` to older active MapHide jars. The local server folder is not required to compile or package the plugin. It is only used by `deployServers` and `verifyLocalPaperServer`. -`gradle check` runs release-documentation and generated-artifact drift checks. `gradle verifyLocalPaperServer` validates PaperScript's stable channel, Paper build, installed jar, and saved SHA-256. +`gradle check` runs JDK selection, release-documentation, generated-artifact, and test tasks. The repository currently has no unit-test sources, so Gradle reports `test NO-SOURCE`; CI includes the task so future tests are not skipped. Compilation and test toolchains come from `JAVA_HOME`, with automatic discovery/download disabled. `verifyBuildJava` requires the configured JDK 25.0.4.1 for Gradle, the compiler, and the test launcher. CI installs that exact Oracle JDK and runs `clean build`. + +`gradle verifyLocalPaperServer` validates PaperScript's stable channel, Paper build, installed jar, and saved SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. + +## Runtime Verification + +After the full build, run: + +```sh +python3 scripts/smoke-test.py +``` + +The script requires Python 3.11 or newer and the maintained `servers/Paper-26.2` fixture: Paper build 84, its existing accepted EULA, cached server files/world, BlueMap 5.22 and its cached assets, and PlaceholderAPI 2.12.3. It copies these into a fresh timestamped `servers/verification/` directory for each run. The Minecraft listener binds to localhost and the copied BlueMap HTTP listener is disabled. Existing worlds, configuration, logs, historical results, and staged Paper jars are preserved. + +By default it selects these exact local JDKs, setting both `JAVA_HOME` and `PATH` for each server process: + +- `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` +- `/Library/Java/JavaVirtualMachines/jdk-26.0.2.1.jdk/Contents/Home` + +Use `--java25-home` and `--java26-home` for equivalent installations elsewhere, `--server` for a different fixture, and `--output` for a new results directory. The script validates the runtime versions and artifact/Paper metadata, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI expansion values, config edits and reloads, preserved comments/custom keys, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. + +These are local console smoke tests. Player joins, actual marker toggles, forced player permissions, and timer expiry require a connected player and are outside this automated check. ## Readiness Check diff --git a/docs/integrations.md b/docs/integrations.md index 37e93c8..943d241 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -36,8 +36,15 @@ The default `/map hide` alias is handled by MapHide itself and runs the same per The project is compiled for Java 25 and `paper-api:26.2.build.84-stable`. The current jar has been smoke-tested locally on: -| Server | BlueMap | Result | +| Server | BlueMap | Java runtime | | --- | --- | --- | -| Paper 26.2 build 84 stable | BlueMap 5.22 | Primary target. | +| Paper 26.2 build 84 stable | BlueMap 5.22 | 25.0.4.1 | +| Paper 26.2 build 84 stable | BlueMap 5.22 | 26.0.2.1 (live runtime version) | -Paper 26.2 build 84 stable is the maintained Paper API and runtime target for this release. The generated plugin metadata declares `api-version: 26.2`, so older Paper versions require an older MapHide build. Java 25.0.4 is the build and primary runtime; Java 26.0.2 is also smoke-tested without changing the Java 25 bytecode target. +Paper 26.2 build 84 stable is the maintained Paper API and runtime target for this release. The generated plugin metadata declares `api-version: 26.2`, so older Paper versions require an older MapHide build. JDK 25.0.4.1 is the build and test JDK. Both Java 25.0.4.1 and Java 26.0.2.1 are supported runtimes; live uses Java 26. Java 25 bytecode is retained without preview features. + +See the [build 029 verification record](verification/2026-09-15-jdk-update.md) for exact runtime builds, checksums, results, and test scope. + +### Historical verification + +Build 028 was smoke-tested with Java 25.0.4 and Java 26.0.2. Those results describe the earlier release environment; the old installations have been removed. diff --git a/docs/verification/2026-09-15-jdk-update.md b/docs/verification/2026-09-15-jdk-update.md new file mode 100644 index 0000000..31607ff --- /dev/null +++ b/docs/verification/2026-09-15-jdk-update.md @@ -0,0 +1,67 @@ +# JDK update verification, 2026-09-15 + +## Release + +- Plugin version: `2.0.1`, build `029` (advanced once from `028`; retries pinned to `029`). +- Artifact: `1MB-BlueMap-MapHide-v2.0.1-029-j25-26.2.jar`. +- Built and deployed copies: `build/libs/` and `servers/Paper-26.2/plugins/`. +- SHA-256: `7ce1dccb3f5eec4ca6785f870d629a45e0ff94658fe1e33a4dca00b670a82a31`. +- Paper API/runtime: `26.2.build.84-stable` / `26.2-84-26e81c4`. +- Paper jar SHA-256: `defe82c1c89067186895de34cf32983e9f5a2ea387cfe7597c020faebb98ca16`. +- BlueMap `5.22`, compile API `2.8.0`; this installed BlueMap reports runtime API label `DEV`. +- PlaceholderAPI `2.12.3`. + +## Build and automated checks + +```sh +export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home +export PATH="$JAVA_HOME/bin:$PATH" +gradle --no-daemon -PreleaseBuildNumber=029 clean build verifyLocalPaperServer +python3 scripts/smoke-test.py +``` + +The canonical full rebuild passed with Gradle `9.7.1`. Gradle, compilation, and the test launcher all selected Oracle JDK `25.0.4.1+1-LTS-5`. The semantic plugin version and Paper target were retained. Build retries reused `029`. + +Release/documentation metadata, generated plugin metadata, and all 12 class files passed verification. Every class is version `69.0` (Java 25), with no preview bytecode. The built and deployed copies have the same checksum as the artifact used in both runtime tests. + +Gradle ran the test task and reported `NO-SOURCE`: this repository has no unit-test sources. CI now runs `clean build` with Oracle JDK `25.0.4.1` and includes the test task. The existing Gradle `9.6.1` CI pin remains unchanged. + +Six additional build-guard checks passed using separate fixtures: + +- Legacy PaperScript state and filename configuration accepted. +- Incorrect current Paper checksum rejected. +- Incorrect staged Paper checksum rejected. +- Incorrect current Paper build rejected. +- Non-stable Paper channel rejected. +- Building with JDK `26.0.2.1` rejected with the required JDK `25.0.4.1` message. Java 26 remains a supported server runtime. + +The maintained PaperScript state has build `121` staged. Its checksum is checked independently, without promoting it or changing the build `84` release target. + +## Runtime matrix + +| Runtime | Exact Oracle runtime/VM build | Paper startup and plugin loading | Smoke checks | Shutdown | +| --- | --- | --- | --- | --- | +| Java 25.0.4.1 | `25.0.4.1+1-LTS-5` | Passed | 30 passed | Graceful, exit 0 | +| Java 26.0.2.1 | `26.0.2.1+1-7` | Passed | 30 passed | Graceful, exit 0 | + +Both JDKs identify their release date as `2026-08-18`. The Java 25 installation is `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home`; Java 26 is `/Library/Java/JavaVirtualMachines/jdk-26.0.2.1.jdk/Contents/Home`. Live uses Java 26; these checks ran locally. + +Coverage includes MapHide enable/disable, BlueMap readiness, PlaceholderAPI registration, exact build/Paper/Java diagnostics, info/version alias, paged help, all debug categories, config listing, console-only player guards (including `/map hide`), missing-player handling, invalid inputs, placeholder evaluation, config updates, reloads, comment/custom-key preservation, final diagnostics, world saves, and clean process exit. No server errors or linkage exceptions occurred. + +The first smoke attempt failed because the new test expected BlueMap's runtime API label to equal the compile dependency version. The assertion was corrected to verify BlueMap readiness/version, and the complete matrix passed on rerun. Logs preserve both attempts. + +Existing upstream warnings remained: JOML's deprecated Unsafe call, OSHI's unrecognized macOS 27 name, the copied server's manual-save warning, and Paper's newer-build notice. They did not prevent loading or clean shutdown. No plugin runtime code changed. + +These console smoke tests do not simulate connected players. Actual marker toggling, player permission enforcement, join behavior, and timer expiry were not exercised. + +## Local evidence and preservation + +- Build and guard logs: `servers/verification/jdk-update-029/`. +- Passing runtime report: `servers/verification/build-029-20260914T222329Z/results.json`. +- Per-runtime console and Java-version logs: subdirectories of that passing runtime report. +- Prior test logs, maintained world/config files, staged Paper jars, and disabled plugin artifacts were preserved. New runtime tests used fresh copied fixtures. The ignored local launcher comments were refreshed to the installed Java patch versions. +- This record is historical; later releases should add a new record rather than rewrite these results. + +## Documentation checked + +The official [Paper LLM index](https://docs.papermc.io/llms.txt) was the discovery source for [project setup](https://docs.papermc.io/paper/dev/project-setup/), [Java runtime requirements](https://docs.papermc.io/paper/getting-started/), and the [Paper 26.2 plugin lifecycle API](https://jd.papermc.io/paper/26.2/org/bukkit/plugin/java/JavaPlugin.html). The public 26.2 Javadoc alias currently describes build 123; build 84 was retained and verified from the pinned dependency, artifact metadata, saved checksum, and actual startup diagnostics. diff --git a/gradle.properties b/gradle.properties index 9d3cf90..c5eee50 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,9 @@ pluginVersion=2.0.1 javaTarget=25 +buildJavaVersion=25.0.4.1 +org.gradle.java.installations.fromEnv=JAVA_HOME +org.gradle.java.installations.auto-detect=false +org.gradle.java.installations.auto-download=false paperTarget=26.2 paperApiVersion=26.2.build.84-stable paperBuild=84 diff --git a/scripts/smoke-test.py b/scripts/smoke-test.py new file mode 100644 index 0000000..6180e43 --- /dev/null +++ b/scripts/smoke-test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Exercise the release jar on both installed JDKs in isolated local Paper copies.""" + +import argparse +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import socket +import subprocess +import threading +import time +import zipfile + + +ROOT = Path(__file__).resolve().parents[1] +JAVA_ROOT = Path("/Library/Java/JavaVirtualMachines") +RUNTIMES = ("25.0.4.1", "26.0.2.1") + + +def properties(text): + return dict(line.split("=", 1) for line in text.splitlines() + if "=" in line and not line.startswith("#")) + + +def sha256(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def free_port(): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def prepare_server(source, destination, artifact, metadata): + """Copy fixtures; never start or edit the maintained world's files.""" + state = json.loads((source / "paperscript/state.json").read_text()) + if (state["current_version"] != metadata["paperTarget"] + or str(state["current_build"]) != metadata["paperBuild"]): + raise RuntimeError("Maintained Paper version/build does not match the artifact") + paper = source / state["current_jar"] + if sha256(paper) != state["current_sha256"]: + raise RuntimeError("Maintained Paper checksum mismatch") + if not re.search(r"(?m)^eula=true$", (source / "eula.txt").read_text()): + raise RuntimeError("The maintained test server must already have an accepted EULA") + destination.mkdir(parents=True) + shutil.copy2(paper, destination / "paper.jar") + server_properties = properties((source / "server.properties").read_text()) + world = server_properties.get("level-name", "world") + if Path(world).name != world: + raise RuntimeError("Smoke fixtures require a world folder inside the server directory") + for name in ("libraries", "versions", "cache", "config", world, "bluemap"): + if (source / name).is_dir(): + shutil.copytree(source / name, destination / name, + ignore=shutil.ignore_patterns("session.lock", "logs", "*.log")) + for name in ("eula.txt", "bukkit.yml", "spigot.yml"): + shutil.copy2(source / name, destination / name) + server_properties.update({"server-ip": "127.0.0.1", "server-port": str(free_port()), + "enable-rcon": "false", "enable-query": "false", + "online-mode": "true", "pause-when-empty-seconds": "-1"}) + (destination / "server.properties").write_text( + "".join(f"{key}={value}\n" for key, value in server_properties.items())) + plugins = destination / "plugins" + plugins.mkdir() + for pattern in ("bluemap-*.jar", "PlaceholderAPI-*.jar"): + matches = list((source / "plugins").glob(pattern)) + if len(matches) != 1: + raise RuntimeError(f"Expected exactly one {pattern} in the maintained server") + shutil.copy2(matches[0], plugins / matches[0].name) + shutil.copy2(artifact, plugins / artifact.name) + shutil.copytree(source / "plugins/BlueMap", plugins / "BlueMap") + # Rendering/API checks do not require a second HTTP listener. + (plugins / "BlueMap/webserver.conf").write_text("enabled: false\n") + (plugins / "PlaceholderAPI").mkdir() + (plugins / "PlaceholderAPI/config.yml").write_text("check_updates: false\n") + (plugins / "1MB-MapHide").mkdir() + with zipfile.ZipFile(artifact) as jar: + config = jar.read("config.yml").decode() + (plugins / "1MB-MapHide/config.yml").write_text( + "# Preserve this smoke-test comment.\n" + config + "\nsmoke-custom-key: preserved\n") + return state["current_sha256"] + + +def run_server(directory, java_home, version, metadata): + env = dict(os.environ, JAVA_HOME=str(java_home)) + env["PATH"] = str(java_home / "bin") + os.pathsep + env.get("PATH", "") + runtime = subprocess.run(["java", "-version"], env=env, text=True, + capture_output=True, check=True).stderr.strip() + if f'java version "{version}"' not in runtime: + raise RuntimeError(f"Expected Java {version}: {runtime}") + (directory / "java-version.txt").write_text(runtime + "\n") + print(f"Starting Paper {metadata['paperTarget']} build {metadata['paperBuild']} on {version}", flush=True) + lines = [] + checks = [] + with (directory / "console.log").open("w") as log: + process = subprocess.Popen( + ["java", "-Xms512M", "-Xmx2G", "-XX:ActiveProcessorCount=4", + "-Dfile.encoding=UTF-8", "-Dterminal.ansi=false", "-Dterminal.jline=false", + "-Dapple.awt.UIElement=true", "-jar", "paper.jar", "--nogui"], + cwd=directory, env=env, text=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1) + + def read_output(): + for line in process.stdout: + lines.append(line) + log.write(line) + log.flush() + + reader = threading.Thread(target=read_output, daemon=True) + reader.start() + + def expect(label, expected, start=0, timeout=30): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + output = "".join(lines[start:]) + if all(value in output for value in expected): + checks.append(label) + print(f" PASS {version}: {label}", flush=True) + return + if process.poll() is not None: + break + time.sleep(0.05) + raise RuntimeError(f"{label}: missing {expected}\n" + "".join(lines[-25:])) + + def command(value, *expected): + start = len(lines) + process.stdin.write(value + "\n") + process.stdin.flush() + expect(value, expected, start) + + try: + expect("startup and plugin loading", ["Done (", "[1MB-MapHide] 1MB-MapHide enabled", + "PlaceholderAPI expansion registered"], timeout=180) + expect("BlueMap ready", ["[BlueMap] Loaded!"], timeout=180) + command("bmpc status", f"Build: {metadata['buildNumber']}", + f"API {metadata['paperApiVersion']}", f"Java: {version}", + "BlueMap: 5.22, API:", f"{metadata['paperTarget']}-{metadata['paperBuild']}-") + command("bmpc info", f"build {metadata['buildNumber']}", "Target: Java 25 / Paper 26.2") + command("bmpc version", f"build {metadata['buildNumber']}", "1MB-MapHide info") + for value, expected in (("bmpc help", "(page 1/"), + ("bmpc help 2", "(page 2/"), + ("bmpc debug status", "Force perms:"), + ("bmpc debug commands", "Command routes"), + ("bmpc debug permissions", "Permission debug"), + ("bmpc debug placeholders", "PlaceholderAPI enabled: true"), + ("bmpc config", "Config values")): + command(value, expected) + for value in ("bmpc", "bmpc toggle", "bmpc show", "bmpc hide", "map hide"): + command(value, "Only players can use this command.") + command("bmpc hide MapHideMissing", 'Player "MapHideMissing" was not found.') + command("bmpc help invalid", "Invalid page:") + command("bmpc config set unknown-key invalid", "Unknown config key:") + command("papi parse --null %maphide_state%|%maphide_default_visibility%|%maphide_toggle_back_seconds%|%maphide_language%", + "unknown|visible|0|EN") + command("bmpc config set default-visibility hide", "Updated default-visibility to hide.") + command("bmpc config set toggle-back-after-seconds 2", "Updated toggle-back-after-seconds to 2.") + command("bmpc reload", "Configuration and translations reloaded.") + command("papi parse --null %maphide_default_visibility%|%maphide_toggle_back_seconds%", + "hidden|2") + command("bmpc config set default-visibility show", "Updated default-visibility to show.") + command("bmpc config set toggle-back-after-seconds 0", "Updated toggle-back-after-seconds to 0.") + config = (directory / "plugins/1MB-MapHide/config.yml").read_text() + if "# Preserve this smoke-test comment." not in config or "smoke-custom-key: preserved" not in config: + raise RuntimeError("Config reload discarded a custom comment or unknown key") + checks.append("config comments and custom keys preserved") + command("bmpc status", f"Java: {version}", "BlueMap: 5.22, API:") + finally: + if process.poll() is None: + process.stdin.write("stop\n") + process.stdin.flush() + try: + process.wait(timeout=60) + except subprocess.TimeoutExpired: + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise RuntimeError("Paper did not shut down cleanly within 60 seconds") + reader.join(timeout=5) + expect("clean shutdown", ["[1MB-MapHide] 1MB-MapHide disabled", "[BlueMap] Saved and stopped!", + "All dimensions are saved"]) + if process.returncode != 0: + raise RuntimeError(f"Paper exited with {process.returncode}") + failures = [line.strip() for line in lines if re.search( + r"(?:/|\s)(?:ERROR|SEVERE)\]|Exception|UnsupportedClassVersionError|NoClassDefFoundError|NoSuchMethodError", line)] + if failures: + raise RuntimeError("Errors in server log:\n" + "\n".join(failures)) + return {"runtime": runtime, "checks": checks, "exit_code": process.returncode, + "console_log": str(directory / "console.log")} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--server", type=Path, default=ROOT / "servers/Paper-26.2") + parser.add_argument("--output", type=Path) + for major, version in zip((25, 26), RUNTIMES): + parser.add_argument(f"--java{major}-home", type=Path, + default=JAVA_ROOT / f"jdk-{version}.jdk/Contents/Home") + args = parser.parse_args() + config = properties((ROOT / "gradle.properties").read_text()) + build = (ROOT / "build-number.txt").read_text().strip() + artifact = ROOT / "build/libs" / ( + f"1MB-BlueMap-MapHide-v{config['pluginVersion']}-{build}-j{config['javaTarget']}-{config['paperTarget']}.jar") + with zipfile.ZipFile(artifact) as jar: + metadata = properties(jar.read("build-info.properties").decode()) + if any(metadata[key] != config[key] for key in ("pluginVersion", "javaTarget", "paperTarget", "paperBuild", "paperApiVersion")) or metadata["buildNumber"] != build: + raise RuntimeError("Rebuild first: artifact metadata does not match the checkout") + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + output = (args.output or ROOT / f"servers/verification/build-{build}-{stamp}").resolve() + output.mkdir(parents=True, exist_ok=False) + report = {"artifact": str(artifact), "sha256": sha256(artifact), "metadata": metadata, "runs": []} + for java_home, version in zip((args.java25_home, args.java26_home), RUNTIMES): + directory = output / f"java-{version}" + report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) + report["runs"].append(run_server(directory, java_home.resolve(), version, metadata)) + (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") + print(f"PASS: both runtimes. Results: {output / 'results.json'}", flush=True) + + +if __name__ == "__main__": + main() From a91c4256dd94bbee34cebf5c5f32f410562c68e7 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Fri, 25 Sep 2026 10:57:43 +0200 Subject: [PATCH 4/8] Release MapHide 2.0.2 build 030 for Paper 26.3 --- AGENTS.md | 10 +++ README.md | 24 +++--- build-number.txt | 2 +- build.gradle | 30 +++++--- docs/installation.md | 29 ++++---- docs/integrations.md | 15 ++-- docs/plugin-docs.yml | 2 +- docs/verification/2026-09-25-paper-26.3.md | 87 ++++++++++++++++++++++ gradle.properties | 11 +-- scripts/smoke-test.py | 49 +++++++----- 10 files changed, 186 insertions(+), 73 deletions(-) create mode 100644 docs/verification/2026-09-25-paper-26.3.md diff --git a/AGENTS.md b/AGENTS.md index 094a905..4be98a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,13 @@ Issues and specs are tracked in GitHub Issues using the `gh` CLI. See `docs/agen ### Domain docs This repository uses the single-context domain-doc layout. See `docs/agents/domain.md`. + +## Maintained Paper release + +- Target Paper 26.3 build 41 ALPHA and `io.papermc.paper:paper-api:26.3.build.41-alpha`. This experimental channel is explicitly authorized; do not silently substitute a stable 26.2 build. +- Build with `JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` and its `bin` prepended to `PATH`. Keep the Java 25 toolchain and bytecode target. +- Release 2.0.2 reserves build `030`. Run `gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer`; reuse that number on retries. The prepareBuildNumber task owns increments. +- Maintain `servers/Paper-26.3` on `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`; keep `servers/Paper-26.2` unchanged as rollback material. Stop the local launcher before building because build deploys locally. +- Run `python3 scripts/smoke-test.py` after building. See `docs/installation.md` and `docs/verification/2026-09-25-paper-26.3.md` for runtime scope and evidence. Historical records describe their original targets. +- Read the live Paper LLM index, official docs, roadmap, and exact target Javadocs for future upgrades. Pin the server checksum and actual channel, and review compiler deprecations and exploit-protection defaults. +- The shared test server must stay stopped during artifact replacement. Match this project's `1MB-MapHide` plugin identity and main class, replace only its JARs with backups and checksum verification, and never launch the shared server as part of deployment. diff --git a/README.md b/README.md index 65191d1..3926d75 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,17 @@ https://docs.1moreblock.com/custom-server-plugins/maphide/ ## Changelog Summary -Version `2.0.1` is the Paper 26.2 stable compatibility and housekeeping release. It pins `io.papermc.paper:paper-api:26.2.build.84-stable`, keeps Java 25 bytecode, adds the `/bmpc version` compatibility alias, reports the exact compiled Paper API in diagnostics, makes release-build retries deterministic, validates generated metadata and documented jar names, and deploys only to the maintained Paper 26.2 test server. The existing `/bmpc`, `/map hide`, MiniMessage translations, comment-preserving configuration, permissions, timers, default visibility, admin tools, and PlaceholderAPI behavior remain intact. +Version `2.0.2` targets Paper 26.3 build 41 on its experimental ALPHA channel, with the exact API `io.papermc.paper:paper-api:26.3.build.41-alpha`. Build `030` retains Java 25 bytecode and is tested on Java 27. The Paper 26.2 baseline is preserved at annotated tag `v2.0.1-paper-26.2` and in `servers/Paper-26.2`. Plugin identity, commands, permissions, configuration, player visibility, and PlaceholderAPI behavior are unchanged. ## Supported Servers -This build targets Java 25 bytecode and the exact stable Paper API coordinate `io.papermc.paper:paper-api:26.2.build.84-stable`. Paper 26.2 build 84 stable is the maintained release-test server: +This build targets Java 25 bytecode and the exact Paper API coordinate `io.papermc.paper:paper-api:26.3.build.41-alpha`. Paper 26.3 build 41 alpha is the maintained release-test server: | Server | BlueMap plugin | | --- | --- | -| Paper 26.2 build 84 stable | `bluemap-5.22-paper.jar` | +| Paper 26.3 build 41 alpha | `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26) | -The generated plugin metadata declares `api-version: 26.2`, so this release is not intended for older Paper versions. Use an older MapHide build for Paper 26.1.2 or 1.21.11. +The generated plugin metadata declares `api-version: 26.3`, so this release is not intended for older Paper versions. Use an older MapHide build for Paper 26.1.2 or 1.21.11. ## Configuration @@ -138,37 +138,37 @@ Requirements: - Gradle 9.6.1 or newer - JDK 25.0.4.1 at `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` -- The exact Maven dependency `io.papermc.paper:paper-api:26.2.build.84-stable` +- The exact Maven dependency `io.papermc.paper:paper-api:26.3.build.41-alpha` - The Maven dependencies `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3` -A clean checkout can compile without a local Paper server. The local `servers/Paper-26.2` folder is only needed for `deployServers` and `verifyLocalPaperServer`. +A clean checkout can compile without a local Paper server. The local `servers/Paper-26.3` folder is only needed for `deployServers` and `verifyLocalPaperServer`. -Build release `029` and deploy it to the maintained Paper 26.2 test server: +Build release `030` and deploy it to the maintained Paper 26.3 test server: ```sh export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home export PATH="$JAVA_HOME/bin:$PATH" -gradle --no-daemon -PreleaseBuildNumber=029 clean build verifyLocalPaperServer +gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer ``` Jars are named like: ```text -1MB-BlueMap-MapHide-v2.0.1-029-j25-26.2.jar +1MB-BlueMap-MapHide-v2.0.2-030-j25-26.3.jar ``` The plugin version comes from `gradle.properties`. The three-digit build number is stored in `build-number.txt` and advances for a new unpinned jar build. Pin `releaseBuildNumber` when repeating a release build so retries reuse the intended number instead of incrementing it again. The Java and Paper parts of the jar name are target identifiers, not the semantic plugin version. `verifyBuildJava` checks that Gradle, the compiler, and the test launcher use JDK 25.0.4.1 from `JAVA_HOME`. `verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and every class for Java 25 bytecode without preview features. All run as part of `check`. -After a successful `build`, `deployServers` copies the jar into `servers/Paper-26.2/plugins/`. Before copying, it renames active older MapHide jars in that folder by appending `.disabled`. +After a successful `build`, `deployServers` copies the jar into `servers/Paper-26.3/plugins/`. Before copying, it renames active older MapHide jars in that folder by appending `.disabled`. -Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on both JDK 25.0.4.1 and JDK 26.0.2.1. It uses isolated copies of the local server and saves new logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for prerequisites and coverage. Live uses Java 26; the plugin retains Java 25 bytecode. +Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on JDK 27. It uses an isolated copy of `servers/Paper-26.3` and saves logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for coverage and [build 030 evidence](docs/verification/2026-09-25-paper-26.3.md) for results. Compile and test tasks use JDK 25.0.4.1; the maintained server launcher uses `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`. ## Installation 1. Stop the Paper server. -2. Put `1MB-BlueMap-MapHide-v2.0.1-029-j25-26.2.jar` in `plugins/`. +2. Put `1MB-BlueMap-MapHide-v2.0.2-030-j25-26.3.jar` in `plugins/`. 3. Make sure BlueMap is also in `plugins/`. 4. Remove or disable old `BlueMapPlayerControl-*.jar` copies. 5. Start the server. diff --git a/build-number.txt b/build-number.txt index 9722268..cadb715 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -029 +030 diff --git a/build.gradle b/build.gradle index 33387be..b9fd048 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ def paperTarget = providers.gradleProperty("paperTarget").get() def paperApiVersion = providers.gradleProperty("paperApiVersion").get() def paperBuild = providers.gradleProperty("paperBuild").get().toInteger() def paperChannel = providers.gradleProperty("paperChannel").get().toUpperCase() +def paperSha256 = providers.gradleProperty("paperSha256").get() def blueMapApiVersion = providers.gradleProperty("blueMapApiVersion").get() def placeholderApiVersion = providers.gradleProperty("placeholderApiVersion").get() version = pluginVersion @@ -20,7 +21,7 @@ def releaseBuildNumber = new java.util.concurrent.atomic.AtomicReference def pinnedBuildNumber = providers.gradleProperty("releaseBuildNumber") def maintainedServerDirs = [ - file("servers/Paper-26.2") + file("servers/Paper-26.3") ] java { @@ -163,8 +164,11 @@ tasks.register("verifyReleaseMetadata") { if (paperApiVersion != expectedPaperApi) { throw new GradleException("paperApiVersion ${paperApiVersion} does not match ${expectedPaperApi}") } - if (paperChannel != "STABLE") { - throw new GradleException("Paper release channel must be STABLE") + if (!(paperChannel in ["STABLE", "BETA", "ALPHA"])) { + throw new GradleException("Paper release channel must explicitly select STABLE, BETA, or ALPHA") + } + if (!(paperSha256 ==~ /[0-9a-f]{64}/)) { + throw new GradleException("paperSha256 must pin the official server download checksum") } File readme = file("README.md") @@ -306,7 +310,7 @@ tasks.register("verifyLocalPaperServer") { description = "Checks the maintained local PaperScript config, Paper build, and saved SHA-256." doLast { - File serverDir = file(providers.gradleProperty("localPaperServerDir").getOrElse("servers/Paper-26.2")) + File serverDir = file(providers.gradleProperty("localPaperServerDir").getOrElse("servers/Paper-26.3")) File configFile = new File(serverDir, "paperscript/config.json") File stateFile = new File(serverDir, "paperscript/state.json") if (!configFile.isFile() || !stateFile.isFile()) { @@ -317,16 +321,19 @@ tasks.register("verifyLocalPaperServer") { def parser = new groovy.json.JsonSlurper() Map config = parser.parse(configFile) as Map Map state = parser.parse(stateFile) as Map - if (config.default_channel?.toString()?.toUpperCase() != "STABLE" - || config.check_latest_channel_only?.toString()?.toUpperCase() != "STABLE" + if (config.default_channel?.toString()?.toUpperCase() != paperChannel + || config.check_latest_channel_only?.toString()?.toUpperCase() != paperChannel || config.allow_same_version_build_upgrade != true || (config.containsKey("download_filename_pattern") && config.download_filename_pattern != "Paper-{version}.jar")) { - throw new GradleException("Maintained PaperScript config does not match the stable release policy") + throw new GradleException("Maintained PaperScript config does not match the explicit ${paperChannel} release channel") } if (state.current_version != paperTarget || state.current_build.toString().toInteger() != paperBuild) { throw new GradleException("Maintained Paper server is not ${paperTarget} build ${paperBuild}") } + if (state.current_channel != paperChannel || file(state.server_dir.toString()).canonicalFile != serverDir.canonicalFile) { + throw new GradleException("Maintained Paper state has a stale channel or server directory") + } File paperJar = new File(serverDir, state.current_jar.toString()) if (!paperJar.isFile()) { @@ -336,8 +343,8 @@ tasks.register("verifyLocalPaperServer") { .digest(paperJar.bytes) .collect { String.format("%02x", it & 0xff) } .join() - if (actualSha256 != state.current_sha256) { - throw new GradleException("Maintained Paper jar SHA-256 does not match PaperScript state") + if (actualSha256 != state.current_sha256 || actualSha256 != paperSha256) { + throw new GradleException("Maintained Paper jar SHA-256 does not match PaperScript state and the pinned official checksum") } // Newer PaperScript records expected_sha256 for a staged upgrade, not the current jar. if (state.staged_jar) { @@ -360,12 +367,15 @@ tasks.register("verifyLocalPaperServer") { tasks.register("deployServers") { group = "build" - description = "Copies the built jar into the maintained Paper 26.2 plugins folder and disables older active copies." + description = "Copies the built jar into the maintained Paper 26.3 plugins folder and disables older active copies." dependsOn(tasks.jar) doLast { File builtJar = tasks.jar.archiveFile.get().asFile maintainedServerDirs.each { File serverDir -> + if (new File(serverDir, "paperscript/locks/server-launch").exists()) { + throw new GradleException("Stop the maintained launcher before deploying: ${serverDir}") + } File pluginsDir = new File(serverDir, "plugins") if (!pluginsDir.isDirectory()) { logger.lifecycle("Skipping ${pluginsDir}: directory does not exist") diff --git a/docs/installation.md b/docs/installation.md index 0844386..3e68bde 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,13 +4,13 @@ - Paper server with BlueMap installed. - Java 25 bytecode, built with JDK 25.0.4.1. -- Supported runtimes: Java 25.0.4.1 and Java 26.0.2.1. Live uses Java 26. -- Paper 26.2 build 84 stable is the maintained compile and release-test target. -- The exact compile coordinate is `io.papermc.paper:paper-api:26.2.build.84-stable`. +- Tested server runtime: Java 27 (`27+35-2325`). +- Paper 26.3 build 41 alpha is the maintained compile and release-test target. +- The exact compile coordinate is `io.papermc.paper:paper-api:26.3.build.41-alpha`. - BlueMapAPI `de.bluecolored:bluemap-api:2.8.0` and PlaceholderAPI `me.clip:placeholderapi:2.12.3` are resolved from their official Maven repositories. - PlaceholderAPI is optional. -This release declares `api-version: 26.2` and is not intended for older Paper versions. +This release declares `api-version: 26.3` and is not intended for older Paper versions. ## Runtime Dependencies @@ -52,24 +52,24 @@ The normal local build command is: ```sh export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home export PATH="$JAVA_HOME/bin:$PATH" -gradle --no-daemon -PreleaseBuildNumber=029 clean build verifyLocalPaperServer +gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer ``` The jar name follows this pattern: ```text -1MB-BlueMap-MapHide-v2.0.1--j25-26.2.jar +1MB-BlueMap-MapHide-v2.0.2--j25-26.3.jar ``` -`2.0.1` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=029` makes release retries reuse build `029`. The `j25` and `26.2` parts identify the Java bytecode and Paper target. +`2.0.2` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=030` makes release retries reuse build `030`. The `j25` and `26.3` parts identify the Java bytecode and Paper target. -The Gradle deploy task copies the jar only into the maintained local `servers/Paper-26.2/plugins/` folder and appends `.disabled` to older active MapHide jars. +The Gradle deploy task copies the jar only into the maintained local `servers/Paper-26.3/plugins/` folder and appends `.disabled` to older active MapHide jars. The local server folder is not required to compile or package the plugin. It is only used by `deployServers` and `verifyLocalPaperServer`. `gradle check` runs JDK selection, release-documentation, generated-artifact, and test tasks. The repository currently has no unit-test sources, so Gradle reports `test NO-SOURCE`; CI includes the task so future tests are not skipped. Compilation and test toolchains come from `JAVA_HOME`, with automatic discovery/download disabled. `verifyBuildJava` requires the configured JDK 25.0.4.1 for Gradle, the compiler, and the test launcher. CI installs that exact Oracle JDK and runs `clean build`. -`gradle verifyLocalPaperServer` validates PaperScript's stable channel, Paper build, installed jar, and saved SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. +`gradle verifyLocalPaperServer` validates PaperScript's explicitly selected ALPHA channel, Paper build, server directory, installed jar, and both saved and pinned official SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. ## Runtime Verification @@ -79,14 +79,15 @@ After the full build, run: python3 scripts/smoke-test.py ``` -The script requires Python 3.11 or newer and the maintained `servers/Paper-26.2` fixture: Paper build 84, its existing accepted EULA, cached server files/world, BlueMap 5.22 and its cached assets, and PlaceholderAPI 2.12.3. It copies these into a fresh timestamped `servers/verification/` directory for each run. The Minecraft listener binds to localhost and the copied BlueMap HTTP listener is disabled. Existing worlds, configuration, logs, historical results, and staged Paper jars are preserved. +The script requires Python 3.11 or newer and the maintained `servers/Paper-26.3` fixture: Paper build 41 ALPHA, its existing accepted EULA, cached server files/world, `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26), cached BlueMap assets, and PlaceholderAPI 2.12.3. It copies these into a fresh timestamped `servers/verification/` directory. The Minecraft listener binds to localhost on an available port; the copied BlueMap HTTP listener, RCON, and query listener are disabled. Only MapHide, BlueMap, and PlaceholderAPI are copied into this isolated smoke fixture, keeping external messaging integrations inactive. -By default it selects these exact local JDKs, setting both `JAVA_HOME` and `PATH` for each server process: +The runtime is `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`. Use `--java27-home` for an equivalent installation, `--server` for another fixture, and `--output` for a new results directory. The script validates runtime, artifact, channel, and official Paper checksum, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI values, config edits and reloads, comment/custom-key preservation, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. -- `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` -- `/Library/Java/JavaVirtualMachines/jdk-26.0.2.1.jdk/Contents/Home` +The optional CMI, CMILib, and Vault JARs are disabled in the maintained fixture after a third-party startup deadlock. Their data remains intact; see the verification record before re-enabling them. DiscordSRV is absent and other external messaging integrations stay inactive. -Use `--java25-home` and `--java26-home` for equivalent installations elsewhere, `--server` for a different fixture, and `--output` for a new results directory. The script validates the runtime versions and artifact/Paper metadata, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI expansion values, config edits and reloads, preserved comments/custom keys, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. +The maintained launcher `servers/Paper-26.3/1MB-minecraft.sh` selects Java 27 and Paper 26.3. Its PaperScript session name is `maphide-26-3-030`. Minecraft TCP uses `52227`; reserved query UDP, RCON TCP, and BlueMap HTTP ports are `52228`, `52229`, and `52230` respectively (those listeners are disabled for smoke testing). Check availability before subsequent manual launches. Start from this directory with `./1MB-minecraft.sh` and stop through the console with `stop`. + +`servers/Paper-26.2` remains untouched rollback material. Its annotated tag is `v2.0.1-paper-26.2`. The new ALPHA target is deliberate; neither the build nor the launcher falls back to Paper 26.2. Use PaperScript's explicit `download --version 26.3 --build 41 --channel ALPHA` command for this pin; its `update` shortcut selects stable builds and is not the release workflow. These are local console smoke tests. Player joins, actual marker toggles, forced player permissions, and timer expiry require a connected player and are outside this automated check. diff --git a/docs/integrations.md b/docs/integrations.md index 943d241..51a72fd 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -4,7 +4,7 @@ BlueMap is required. MapHide uses the BlueMap API to read and update player marker visibility in the BlueMap web app. -Compilation uses the official `de.bluecolored:bluemap-api:2.8.0` artifact matching BlueMap 5.22. +Compilation uses the official `de.bluecolored:bluemap-api:2.8.0` artifact; the existing visibility API is retained for the installed BlueMap runtime 5.27 / manifest version 5.26 (`bluemap-5.27-paper.jar`). If BlueMap is not ready yet, MapHide sends a "BlueMap is not ready yet" message and does not change visibility. @@ -34,17 +34,12 @@ The default `/map hide` alias is handled by MapHide itself and runs the same per ## Compatibility Notes -The project is compiled for Java 25 and `paper-api:26.2.build.84-stable`. The current jar has been smoke-tested locally on: +The project is compiled with JDK 25.0.4.1 for Java 25 and `paper-api:26.3.build.41-alpha`. The maintained test target is Paper 26.3 build 41 alpha on Java 27 (`27+35-2325`). The generated metadata declares `api-version: 26.3`; earlier Paper versions require an older MapHide release. Java 25 bytecode is retained without preview features. -| Server | BlueMap | Java runtime | -| --- | --- | --- | -| Paper 26.2 build 84 stable | BlueMap 5.22 | 25.0.4.1 | -| Paper 26.2 build 84 stable | BlueMap 5.22 | 26.0.2.1 (live runtime version) | +The new local fixture contains BlueMap runtime 5.27 / manifest version 5.26 (file `bluemap-5.27-paper.jar`), PlaceholderAPI 2.12.3, LuckPerms 5.5.85, CMI 9.8.10.1, CMILib 1.6.0.0, and Vault 1.7.3-CMI (file `Vault-1.7.4.jar`). CMI, CMILib, and Vault are disabled in the final local smoke fixture after a startup class-initialization deadlock involving CMILib and BlueMap; their configurations and data remain preserved. The passing maintained-server check covers MapHide, BlueMap, PlaceholderAPI, and LuckPerms. Only JARs were copied from the shared test server; existing project-local configurations and data were retained. MapHide does not dispatch LuckPerms commands or perform external player identity lookups. -Paper 26.2 build 84 stable is the maintained Paper API and runtime target for this release. The generated plugin metadata declares `api-version: 26.2`, so older Paper versions require an older MapHide build. JDK 25.0.4.1 is the build and test JDK. Both Java 25.0.4.1 and Java 26.0.2.1 are supported runtimes; live uses Java 26. Java 25 bytecode is retained without preview features. - -See the [build 029 verification record](verification/2026-09-15-jdk-update.md) for exact runtime builds, checksums, results, and test scope. +See the [build 030 verification record](verification/2026-09-25-paper-26.3.md) for exact checksums, runtime results, third-party notices, and manual checks. ### Historical verification -Build 028 was smoke-tested with Java 25.0.4 and Java 26.0.2. Those results describe the earlier release environment; the old installations have been removed. +Version 2.0.1 build 029 remains preserved at `v2.0.1-paper-26.2`. Its [Java 25/26 verification record](verification/2026-09-15-jdk-update.md) applies only to that baseline and its Paper 26.2 environment. diff --git a/docs/plugin-docs.yml b/docs/plugin-docs.yml index 1be6d76..437e53e 100644 --- a/docs/plugin-docs.yml +++ b/docs/plugin-docs.yml @@ -7,6 +7,6 @@ docs_url: https://docs.1moreblock.com/custom-server-plugins/maphide/ player_guide: player-guide.md technical_readme: ../README.md java_target: 25 -paper_target: 26.2 +paper_target: 26.3 official_project: true repository: mrfdev/1MB-MapHide diff --git a/docs/verification/2026-09-25-paper-26.3.md b/docs/verification/2026-09-25-paper-26.3.md new file mode 100644 index 0000000..75a34ea --- /dev/null +++ b/docs/verification/2026-09-25-paper-26.3.md @@ -0,0 +1,87 @@ +# Paper 26.3 verification, 2026-09-25 + +## Release and rollback + +- Previous snapshot: commit `91b0e5de5b4d50060c9cdda72abe634187aeb719`, annotated tag `v2.0.1-paper-26.2`, pushed before release metadata changed. +- Previous release: version `2.0.1`, build `029`, Paper 26.2 build 84 STABLE. Its [historical verification](2026-09-15-jdk-update.md) records 30 passing console checks on each of Java 25.0.4.1 and 26.0.2.1; it does not establish Paper 26.3 compatibility. +- New release: version `2.0.2`, build `030`, release tag `v2.0.2`. The canonical `prepareBuildNumber` task advanced the number once; all subsequent checks pinned `-PreleaseBuildNumber=030`. +- Complete distributable set: one JAR, `1MB-BlueMap-MapHide-v2.0.2-030-j25-26.3.jar`. +- Artifact SHA-256: `faefc562ef8175565783859e7b29614b30d71f14dd6ff88346f3b24045fe1af9`. +- Identity remains `1MB-MapHide`, main class `com.technicjelle.bluemapplayercontrol.BlueMapPlayerControl`, data folder `plugins/1MB-MapHide`. No plugin Java source, permissions, commands, player identity handling, or gameplay behavior changed. + +## Exact target and documentation review + +The official [Paper LLM index](https://docs.papermc.io/llms.txt) was the primary discovery source. On this date the [26.3 build metadata](https://fill.papermc.io/v3/projects/paper/versions/26.3/builds) listed build **41 ALPHA** as the latest build; all 39 returned builds were ALPHA. Experimental 26.3 was explicitly authorized. No stable 26.2 fallback was used. + +- Server: `26.3-41-main@a15fed9`, built `2026-09-25T03:26:20Z`. +- Compile API: `io.papermc.paper:paper-api:26.3.build.41-alpha`, confirmed in official [Maven metadata](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/maven-metadata.xml) and its exact POM. +- Server JAR: `Paper-26.3-41.jar`. +- Official and downloaded SHA-256: `2b77166ee61886a9bc9ab33dc9e4847fa3538b36d9ba6e5f2fa7ed90973aa748`. +- PaperScript verified the download size, SHA-256, all 342 ZIP entries, and executable main class. Gradle and the smoke script also check the server against the checksum pinned in `gradle.properties`. +- Both PaperScript channel settings explicitly select `ALPHA`. Its current and staged state identify this same verified JAR and the new directory. Old directory/version/download state was archived inside the new instance and reinitialized. The launcher marker confirms the actual 26.3 JAR used. + +Reviewed official [project setup](https://docs.papermc.io/paper/dev/project-setup/), [runtime requirements](https://docs.papermc.io/paper/getting-started/), [plugin descriptors](https://docs.papermc.io/paper/dev/plugin-yml/), [roadmap](https://docs.papermc.io/paper/dev/roadmap/), [scheduling](https://docs.papermc.io/paper/dev/scheduler/), and [event cancellation and priority](https://docs.papermc.io/paper/dev/event-listeners/). The exact [JavaPlugin](https://jd.papermc.io/paper/26.3/org/bukkit/plugin/java/JavaPlugin.html), [BukkitScheduler](https://jd.papermc.io/paper/26.3/org/bukkit/scheduler/BukkitScheduler.html), [EventHandler](https://jd.papermc.io/paper/26.3/org/bukkit/event/EventHandler.html), [PlayerCommandPreprocessEvent](https://jd.papermc.io/paper/26.3/org/bukkit/event/player/PlayerCommandPreprocessEvent.html), and [deprecation list](https://jd.papermc.io/paper/26.3/deprecated-list.html) pages all identified `26.3.build.41-alpha` during review. + +The existing JavaPlugin lifecycle, synchronous scheduler Runnable overloads, Bukkit permission checks, and command-event behavior remain supported. MapHide uses no NMS or custom item/inventory APIs affected by the roadmap. Paper's API POM now uses Adventure 5.2.0; the [Adventure 5 migration guide](https://docs.papermc.io/adventure/migration/adventure-4.x/) was checked against MapHide's MiniMessage and component usage. Compilation emitted no deprecation warnings. Console message rendering was exercised on the actual runtime. + +The [global](https://docs.papermc.io/paper/reference/global-configuration/) and [world configuration references](https://docs.papermc.io/paper/reference/world-configuration/) were reviewed. Local exploit protections remain at safe defaults: piston duplication, unsafe end-portal teleportation, headless pistons, permanent-block exploits, tripwire update disabling, and tripwire placement-validation bypass are false; oversized-component sanitizer exclusions remain empty. + +## Build and automated checks + +```sh +export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home +export PATH="$JAVA_HOME/bin:$PATH" +gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer +python3 scripts/smoke-test.py +``` + +- Clean build passed with Gradle 9.7.1 and Oracle JDK `25.0.4.1+1-LTS-5`. Gradle, compiler, and test launcher all selected the required installation. +- The test task ran and reported `NO-SOURCE`; this repository has no unit-test sources. +- Release/documentation checks, generated descriptors and build information, artifact naming, Paper state, and checksums passed. All 12 classes are version `69.0` (Java 25), without preview features. `api-version` is `26.3`. +- Five focused negative fixtures passed: wrong configured channel, wrong checksum, stale directory, wrong state channel, and a mismatched Paper API coordinate were all rejected. +- The local Gradle deployment points only at `servers/Paper-26.3`, preserves older MapHide JARs as disabled files, and rejects deployment while the maintained launcher lock exists. +- The 26.3 shutdown log changed: the smoke assertion now checks completed RegionFile I/O plus plugin shutdown and exit code 0, replacing the old “All dimensions are saved” marker. + +## Runtime results and limits + +Tested runtime: Oracle Java **27**, `27+35-2325`, installed at `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`. The server launcher exports this `JAVA_HOME` and prepends its `bin` to `PATH`; compilation remains on Java 25. + +| Test | Active plugins | Result | +| --- | --- | --- | +| Maintained 26.3 instance through `1MB-minecraft.sh`, tmux session `maphide-26-3-030` | MapHide, BlueMap, PlaceholderAPI, LuckPerms | Six startup/health/shutdown checks passed; clean launcher exit 0 | +| Isolated copy via `scripts/smoke-test.py` | MapHide, BlueMap, PlaceholderAPI | 30 checks passed; clean server exit 0 | + +Coverage includes enable/disable, BlueMap readiness, PlaceholderAPI registration and values, exact release/Paper/Java diagnostics, info/version alias, help and debug pages, config listing, console-only player guards, missing players, invalid inputs, config changes and reloads, comment/custom-key preservation, world saves, and clean shutdown. Passing runs contained no server errors or linkage exceptions. The exact tested JAR remains installed in the maintained fixture. + +**The wider CMI plugin combination did not pass startup.** The first maintained-server attempt, with CMI 9.8.10.1, CMILib 1.6.0.0, and Vault enabled, stalled before MapHide enabled. A JVM thread dump showed the server thread in CMILib reflection waiting for `CraftBlockData` initialization, while `BlueMap-Load` held that initialization and waited for `CraftBeehive`. Console stop and SIGTERM could not complete, so only that local test JVM was force-stopped. Its log, thread dump, and copied world were preserved. The new fixture's world was restored from the untouched baseline before retrying. CMI, CMILib, and Vault remain disabled only in the new local fixture; their data and the shared server's plugins/configuration were not altered to work around this issue. Full-stack compatibility needs a separate upstream investigation. + +The next startup and shutdown succeeded, but its harness still expected the old shutdown marker. After updating that assertion, the complete maintained check passed again. These attempts consumed no extra release/build numbers. + +Remaining non-fatal notices: JOML's deprecated Unsafe call in the isolated run, OSHI not recognizing macOS 27, BlueMap's manual-save warning, and LuckPerms/Commodore's Java 27 reflective final-field mutation warning in the maintained fixture. No permissive JVM workaround was added for the LuckPerms notice. + +Manual checks still needed: connected-player joins, Java/Bedrock marker toggles and actual map visibility, forced permission enforcement, timer expiry, reconnects/world changes, and interactions with the complete shared plugin stack. Folia behavior was not tested. External messaging integrations were inactive throughout local smoke testing. + +## Local fixtures and dependencies + +- Preserved: `/Users/floris/Projects/Codex/1MB-MapHide/servers/Paper-26.2`. All 847 files were SHA-256 compared before and after the upgrade; contents and file set are unchanged, including the old staged build 121. +- Maintained: `/Users/floris/Projects/Codex/1MB-MapHide/servers/Paper-26.3`. +- Distinct available ports were reserved and checked against other local fixtures: Minecraft TCP `52227`, query UDP `52228`, RCON TCP `52229`, BlueMap HTTP `52230`. Only Minecraft listens, on localhost; the other listeners are disabled. + +JARs were copied from `/Users/floris/MinecraftServer/test-1mb-3.14-mc-26.3/plugins`, without importing its configuration or player data: + +| JAR | Previous local version | Inspected version / final local status | +| --- | --- | --- | +| `bluemap-5.27-paper.jar` | 5.22 | Embedded runtime version 5.27, manifest 5.26, API label DEV; enabled | +| `LuckPerms-Bukkit-5.5.85.jar` | 5.5.81 | 5.5.85; enabled | +| `PlaceholderAPI-2.12.3.jar` | 2.12.3 | 2.12.3; refreshed, enabled | +| `CMI-9.8.10.1.jar` | 9.8.9.9 | 9.8.10.1; disabled after startup deadlock | +| `CMILib1.6.0.0.jar` | 1.5.9.9 | 1.6.0.0; disabled after startup deadlock | +| `Vault-1.7.4.jar` | 1.7.3-CMI | Manifest 1.7.3-CMI; refreshed, disabled with the optional CMI stack | + +Compile dependencies remain the supported API artifacts `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3`. BlueMap's embedded version JSON, rather than its stale plugin manifest, supplies the smoke test's expected runtime version. + +## Evidence and shared deployment + +Ignored local evidence is retained under `servers/verification/paper-26.3-030/`: build log, negative fixtures/results, dependency checksums, baseline inventory, port reservations, failed-start evidence, and passing `maintained-results.json`. The full isolated report is `servers/verification/build-030-20260925T085246Z/results.json` with its Java-version and console logs. + +The release artifact above is prepared for replacement of the shared server's old `1MB-BlueMap-MapHide-v2.0.0-017-j25-26.1.2.jar` after commit/tag publication. The shared deployment operation must verify the server is stopped, acquire an atomic deployment lock, match the MapHide plugin identity and main class, retain the old JAR in a recovery directory, and verify the installed checksum and absence of duplicate active MapHide copies. Its completion receipt is saved locally as `servers/verification/paper-26.3-030/shared-deployment.json` and beside the shared backup. The shared server must not be started by this operation. diff --git a/gradle.properties b/gradle.properties index c5eee50..e9bf5e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,13 @@ -pluginVersion=2.0.1 +pluginVersion=2.0.2 javaTarget=25 buildJavaVersion=25.0.4.1 org.gradle.java.installations.fromEnv=JAVA_HOME org.gradle.java.installations.auto-detect=false org.gradle.java.installations.auto-download=false -paperTarget=26.2 -paperApiVersion=26.2.build.84-stable -paperBuild=84 -paperChannel=STABLE +paperTarget=26.3 +paperApiVersion=26.3.build.41-alpha +paperBuild=41 +paperChannel=ALPHA +paperSha256=2b77166ee61886a9bc9ab33dc9e4847fa3538b36d9ba6e5f2fa7ed90973aa748 blueMapApiVersion=2.8.0 placeholderApiVersion=2.12.3 diff --git a/scripts/smoke-test.py b/scripts/smoke-test.py index 6180e43..cefd16a 100644 --- a/scripts/smoke-test.py +++ b/scripts/smoke-test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Exercise the release jar on both installed JDKs in isolated local Paper copies.""" +"""Exercise the release jar on Java 27 in an isolated local Paper 26.3 copy.""" import argparse from datetime import datetime, timezone @@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parents[1] JAVA_ROOT = Path("/Library/Java/JavaVirtualMachines") -RUNTIMES = ("25.0.4.1", "26.0.2.1") +RUNTIME_VERSION = "27" def properties(text): @@ -41,10 +41,12 @@ def prepare_server(source, destination, artifact, metadata): """Copy fixtures; never start or edit the maintained world's files.""" state = json.loads((source / "paperscript/state.json").read_text()) if (state["current_version"] != metadata["paperTarget"] - or str(state["current_build"]) != metadata["paperBuild"]): + or str(state["current_build"]) != metadata["paperBuild"] + or state["current_channel"] != metadata["paperChannel"]): raise RuntimeError("Maintained Paper version/build does not match the artifact") paper = source / state["current_jar"] - if sha256(paper) != state["current_sha256"]: + pinned_sha256 = properties((ROOT / "gradle.properties").read_text())["paperSha256"] + if sha256(paper) != state["current_sha256"] or state["current_sha256"] != pinned_sha256: raise RuntimeError("Maintained Paper checksum mismatch") if not re.search(r"(?m)^eula=true$", (source / "eula.txt").read_text()): raise RuntimeError("The maintained test server must already have an accepted EULA") @@ -97,6 +99,13 @@ def run_server(directory, java_home, version, metadata): print(f"Starting Paper {metadata['paperTarget']} build {metadata['paperBuild']} on {version}", flush=True) lines = [] checks = [] + bluemap = list((directory / "plugins").glob("bluemap-*.jar")) + if len(bluemap) != 1: + raise RuntimeError("Expected exactly one BlueMap runtime jar") + with zipfile.ZipFile(bluemap[0]) as jar: + manifest = jar.read("plugin.yml").decode() + bluemap_version = json.loads(jar.read("de/bluecolored/bluemap/version.json"))["version"] + bluemap_plugin_version = re.search(r"(?m)^version:\s*['\"]?([^'\"\s]+)", manifest).group(1) with (directory / "console.log").open("w") as log: process = subprocess.Popen( ["java", "-Xms512M", "-Xmx2G", "-XX:ActiveProcessorCount=4", @@ -136,11 +145,13 @@ def command(value, *expected): try: expect("startup and plugin loading", ["Done (", "[1MB-MapHide] 1MB-MapHide enabled", "PlaceholderAPI expansion registered"], timeout=180) - expect("BlueMap ready", ["[BlueMap] Loaded!"], timeout=180) + expect("BlueMap ready", [f"[BlueMap] Enabling BlueMap v{bluemap_plugin_version}", + "[BlueMap] Loaded!"], timeout=180) command("bmpc status", f"Build: {metadata['buildNumber']}", f"API {metadata['paperApiVersion']}", f"Java: {version}", - "BlueMap: 5.22, API:", f"{metadata['paperTarget']}-{metadata['paperBuild']}-") - command("bmpc info", f"build {metadata['buildNumber']}", "Target: Java 25 / Paper 26.2") + f"BlueMap: {bluemap_version}, API:", f"{metadata['paperTarget']}-{metadata['paperBuild']}-") + command("bmpc info", f"build {metadata['buildNumber']}", + f"Target: Java {metadata['javaTarget']} / Paper {metadata['paperTarget']}") command("bmpc version", f"build {metadata['buildNumber']}", "1MB-MapHide info") for value, expected in (("bmpc help", "(page 1/"), ("bmpc help 2", "(page 2/"), @@ -168,7 +179,7 @@ def command(value, *expected): if "# Preserve this smoke-test comment." not in config or "smoke-custom-key: preserved" not in config: raise RuntimeError("Config reload discarded a custom comment or unknown key") checks.append("config comments and custom keys preserved") - command("bmpc status", f"Java: {version}", "BlueMap: 5.22, API:") + command("bmpc status", f"Java: {version}", f"BlueMap: {bluemap_version}, API:") finally: if process.poll() is None: process.stdin.write("stop\n") @@ -185,7 +196,7 @@ def command(value, *expected): raise RuntimeError("Paper did not shut down cleanly within 60 seconds") reader.join(timeout=5) expect("clean shutdown", ["[1MB-MapHide] 1MB-MapHide disabled", "[BlueMap] Saved and stopped!", - "All dimensions are saved"]) + "All RegionFile I/O tasks to complete"]) if process.returncode != 0: raise RuntimeError(f"Paper exited with {process.returncode}") failures = [line.strip() for line in lines if re.search( @@ -198,11 +209,10 @@ def command(value, *expected): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--server", type=Path, default=ROOT / "servers/Paper-26.2") + parser.add_argument("--server", type=Path, default=ROOT / "servers/Paper-26.3") parser.add_argument("--output", type=Path) - for major, version in zip((25, 26), RUNTIMES): - parser.add_argument(f"--java{major}-home", type=Path, - default=JAVA_ROOT / f"jdk-{version}.jdk/Contents/Home") + parser.add_argument("--java27-home", type=Path, + default=JAVA_ROOT / "jdk-27.jdk/Contents/Home") args = parser.parse_args() config = properties((ROOT / "gradle.properties").read_text()) build = (ROOT / "build-number.txt").read_text().strip() @@ -210,18 +220,17 @@ def main(): f"1MB-BlueMap-MapHide-v{config['pluginVersion']}-{build}-j{config['javaTarget']}-{config['paperTarget']}.jar") with zipfile.ZipFile(artifact) as jar: metadata = properties(jar.read("build-info.properties").decode()) - if any(metadata[key] != config[key] for key in ("pluginVersion", "javaTarget", "paperTarget", "paperBuild", "paperApiVersion")) or metadata["buildNumber"] != build: + if any(metadata[key] != config[key] for key in ("pluginVersion", "javaTarget", "paperTarget", "paperBuild", "paperApiVersion", "paperChannel")) or metadata["buildNumber"] != build: raise RuntimeError("Rebuild first: artifact metadata does not match the checkout") stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") output = (args.output or ROOT / f"servers/verification/build-{build}-{stamp}").resolve() output.mkdir(parents=True, exist_ok=False) report = {"artifact": str(artifact), "sha256": sha256(artifact), "metadata": metadata, "runs": []} - for java_home, version in zip((args.java25_home, args.java26_home), RUNTIMES): - directory = output / f"java-{version}" - report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) - report["runs"].append(run_server(directory, java_home.resolve(), version, metadata)) - (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") - print(f"PASS: both runtimes. Results: {output / 'results.json'}", flush=True) + directory = output / f"java-{RUNTIME_VERSION}" + report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) + report["runs"].append(run_server(directory, args.java27_home.resolve(), RUNTIME_VERSION, metadata)) + (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") + print(f"PASS: Java {RUNTIME_VERSION}. Results: {output / 'results.json'}", flush=True) if __name__ == "__main__": From 3c80f5c6236e6780b1ef4d7b25b650f706beff99 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 25 Oct 2026 11:06:25 +0100 Subject: [PATCH 5/8] Use Oracle JDK 25 for compilation and JDK 27 for tests Resolve and validate installed Oracle JDKs dynamically, enforce compiler and test toolchains, and reject automatic provisioning or conflicting overrides. Update local build commands, CI configuration, smoke runtime selection, and documentation. Verified the standalone selection with clean check: 3 Java tests, 15 Python tests, and Java 25 bytecode checks. Keep the pre-existing Paper upgrade and fixture work outside this commit. --- .github/workflows/main.yml | 21 ++- .gitignore | 1 + AGENTS.md | 9 +- README.md | 13 +- build.gradle | 76 +++++++++-- build.sh | 5 + docs/installation.md | 31 +++-- docs/integrations.md | 2 +- .../2026-10-25-oracle-toolchains.md | 61 +++++++++ gradle.properties | 7 +- gradle/oracle-jdks.settings.gradle | 59 +++++++++ scripts/java_toolchains.py | 87 ++++++++++++ scripts/smoke-test.py | 50 ++++--- scripts/tests/test_java_toolchains.py | 125 ++++++++++++++++++ scripts/tests/test_smoke_java.py | 47 +++++++ settings.gradle | 3 + .../onemb/toolchain/JavaToolchainTest.java | 95 +++++++++++++ 17 files changed, 636 insertions(+), 56 deletions(-) create mode 100755 build.sh create mode 100644 docs/verification/2026-10-25-oracle-toolchains.md create mode 100644 gradle/oracle-jdks.settings.gradle create mode 100644 scripts/java_toolchains.py create mode 100644 scripts/tests/test_java_toolchains.py create mode 100644 scripts/tests/test_smoke_java.py create mode 100644 src/test/java/com/onemb/toolchain/JavaToolchainTest.java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f6ca4a..db32f21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,16 +18,24 @@ jobs: - name: Check out repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Set up JDK 25.0.4.1 + - name: Set up Oracle JDK 27 for tests + id: test-java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: 'oracle' - java-version: '25.0.4.1' + java-version: '27' - - name: Set up Gradle 9.6.1 + - name: Set up Oracle JDK 25 for Gradle and compilation + id: build-java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: 'oracle' + java-version: '25' + + - name: Set up Gradle 9.8.2 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 with: - gradle-version: '9.6.1' + gradle-version: '9.8.2' cache-provider: basic - name: Read release build number @@ -42,8 +50,11 @@ jobs: echo "value=$build_number" >> "$GITHUB_OUTPUT" - name: Build and verify plugin jar + env: + ONE_MB_BUILD_JAVA_HOME: ${{ steps.build-java.outputs.path }} + ONE_MB_TEST_JAVA_HOME: ${{ steps.test-java.outputs.path }} run: >- - gradle --no-daemon + ./build.sh --no-daemon -PreleaseBuildNumber=${{ steps.build-number.outputs.value }} clean build diff --git a/.gitignore b/.gitignore index 1b08ef1..fe4513b 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,4 @@ run/ build/ servers/ build/libs/ +.scratch/ diff --git a/AGENTS.md b/AGENTS.md index 4be98a5..1d3ba1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,9 +11,10 @@ This repository uses the single-context domain-doc layout. See `docs/agents/doma ## Maintained Paper release - Target Paper 26.3 build 41 ALPHA and `io.papermc.paper:paper-api:26.3.build.41-alpha`. This experimental channel is explicitly authorized; do not silently substitute a stable 26.2 build. -- Build with `JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` and its `bin` prepended to `PATH`. Keep the Java 25 toolchain and bytecode target. -- Release 2.0.2 reserves build `030`. Run `gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer`; reuse that number on retries. The prepareBuildNumber task owns increments. -- Maintain `servers/Paper-26.3` on `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`; keep `servers/Paper-26.2` unchanged as rollback material. Stop the local launcher before building because build deploys locally. -- Run `python3 scripts/smoke-test.py` after building. See `docs/installation.md` and `docs/verification/2026-09-25-paper-26.3.md` for runtime scope and evidence. Historical records describe their original targets. +- Use installed Gradle 9.8.0+ through `./build.sh`. It resolves `/usr/libexec/java_home -F -v 25` and `-v 27` on every invocation and validates actual versions, Oracle Corporation vendor, and matching javac. Gradle and all Java compilation use Oracle JDK 25 with `--release 25`; every Java test task and disposable Paper smoke server uses Oracle JDK 27. +- Select another installed patch with `ONE_MB_BUILD_JAVA_HOME` / `ONE_MB_TEST_JAVA_HOME`; never pin patch-version folders in executable configuration. Missing, wrong-major, or non-Oracle selections fail. Automatic toolchain detection/downloads are disabled. Do not override the validated toolchain properties or add daemon JVM criteria. Use `./build.sh verifyBuildJava` to print the actual paths, versions, and vendors. Configuration caching is disabled to preserve validation on every invocation. +- Release 2.0.2 reserves build `030`. Run `./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer`; reuse that number on retries. The prepareBuildNumber task owns increments. +- Maintain `servers/Paper-26.3` on dynamically selected installed Oracle JDK 27; keep `servers/Paper-26.2` unchanged as rollback material. Stop the local launcher before building because build deploys locally. +- Run `python3 scripts/smoke-test.py` after building. It resolves and validates installed Oracle JDK 27 independently and uses disposable localhost servers. See `docs/installation.md` and `docs/verification/2026-10-25-oracle-toolchains.md` for the workflow and verification evidence. Historical records describe their original targets. - Read the live Paper LLM index, official docs, roadmap, and exact target Javadocs for future upgrades. Pin the server checksum and actual channel, and review compiler deprecations and exploit-protection defaults. - The shared test server must stay stopped during artifact replacement. Match this project's `1MB-MapHide` plugin identity and main class, replace only its JARs with backups and checksum verification, and never launch the shared server as part of deployment. diff --git a/README.md b/README.md index 3926d75..c66e27a 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,9 @@ PlaceholderAPI is optional. When installed, 1MB-MapHide registers: Requirements: -- Gradle 9.6.1 or newer -- JDK 25.0.4.1 at `/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home` +- Installed Gradle 9.8.0 or newer +- Python 3.11 or newer on PATH +- Installed Oracle JDK 25 for Gradle and compilation, and Oracle JDK 27 for automated tests - The exact Maven dependency `io.papermc.paper:paper-api:26.3.build.41-alpha` - The Maven dependencies `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3` @@ -146,9 +147,7 @@ A clean checkout can compile without a local Paper server. The local `servers/Pa Build release `030` and deploy it to the maintained Paper 26.3 test server: ```sh -export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home -export PATH="$JAVA_HOME/bin:$PATH" -gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer +./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer ``` Jars are named like: @@ -159,11 +158,11 @@ Jars are named like: The plugin version comes from `gradle.properties`. The three-digit build number is stored in `build-number.txt` and advances for a new unpinned jar build. Pin `releaseBuildNumber` when repeating a release build so retries reuse the intended number instead of incrementing it again. The Java and Paper parts of the jar name are target identifiers, not the semantic plugin version. -`verifyBuildJava` checks that Gradle, the compiler, and the test launcher use JDK 25.0.4.1 from `JAVA_HOME`. `verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and every class for Java 25 bytecode without preview features. All run as part of `check`. +`build.sh` dynamically resolves installed Oracle JDK 25 and Oracle JDK 27 with macOS `java_home -F -v`, validates their major versions and Oracle vendor, and runs the installed Gradle on JDK 25. All plugin and test sources compile with JDK 25 and `--release 25`; all Java test tasks and their Java subprocesses use JDK 27. `verifyBuildJava` checks the actual toolchain paths. No JDKs are downloaded or silently substituted. Explicit installed-home overrides use `ONE_MB_BUILD_JAVA_HOME` and `ONE_MB_TEST_JAVA_HOME`; see the [build workflow](docs/installation.md#build). `verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and every class for Java 25 bytecode without preview features. All run as part of `check`. After a successful `build`, `deployServers` copies the jar into `servers/Paper-26.3/plugins/`. Before copying, it renames active older MapHide jars in that folder by appending `.disabled`. -Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on JDK 27. It uses an isolated copy of `servers/Paper-26.3` and saves logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for coverage and [build 030 evidence](docs/verification/2026-09-25-paper-26.3.md) for results. Compile and test tasks use JDK 25.0.4.1; the maintained server launcher uses `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`. +Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on JDK 27. It uses an isolated copy of `servers/Paper-26.3` and saves logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for coverage and [build 030 evidence](docs/verification/2026-09-25-paper-26.3.md) for results. Compilation uses Oracle JDK 25; Java tests and smoke servers use dynamically selected Oracle JDK 27. Set `ONE_MB_TEST_JAVA_HOME` or pass `--java-home` to select another installed Oracle 27 patch. ## Installation diff --git a/build.gradle b/build.gradle index b9fd048..0e85ffc 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,8 @@ group = "com.onemb" def pluginVersion = providers.gradleProperty("pluginVersion").get() def javaTarget = providers.gradleProperty("javaTarget").get().toInteger() def buildJavaVersion = providers.gradleProperty("buildJavaVersion").get() +def testJavaVersion = providers.gradleProperty("testJavaVersion").get() +def oracleJdks = gradle.ext.oracleJdks def paperTarget = providers.gradleProperty("paperTarget").get() def paperApiVersion = providers.gradleProperty("paperApiVersion").get() def paperBuild = providers.gradleProperty("paperBuild").get().toInteger() @@ -27,38 +29,70 @@ def maintainedServerDirs = [ java { toolchain { languageVersion = JavaLanguageVersion.of(javaTarget) + vendor = JvmVendorSpec.ORACLE + } +} + +def buildCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.ORACLE +} +def testLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(27) + vendor = JvmVendorSpec.ORACLE +} +def verifyToolchain = { metadata, expected -> + if (metadata.languageVersion.asInt() != expected.major + || metadata.vendor != "Oracle" + || metadata.installationPath.asFile.canonicalPath != expected.home) { + throw new GradleException("Expected Oracle JDK ${expected.major} at ${expected.home}; selected ${metadata}") } } tasks.register("verifyBuildJava") { group = "verification" - description = "Verifies Gradle, compilation, and tests use the release JDK selected through JAVA_HOME." + description = "Verifies Oracle 25 compilers and Oracle 27 test launchers, including exact installed paths." doLast { - String javaHome = providers.environmentVariable("JAVA_HOME").orNull - if (!javaHome || file(javaHome).canonicalFile != file(System.getProperty("java.home")).canonicalFile) { - throw new GradleException("Set JAVA_HOME and prepend its bin directory to PATH before building") - } - if (System.getProperty("java.version") != buildJavaVersion) { - throw new GradleException("Build with JDK ${buildJavaVersion}; running ${System.getProperty('java.version')}") + tasks.withType(JavaCompile).each { task -> + verifyToolchain(task.javaCompiler.get().metadata, oracleJdks.build) + if (task.options.release.get() != 25) { + throw new GradleException("${task.path} must use --release 25") + } } - [tasks.compileJava.javaCompiler.get().metadata, tasks.test.javaLauncher.get().metadata].each { metadata -> - if (metadata.javaRuntimeVersion.split('\\+')[0] != buildJavaVersion - || metadata.installationPath.asFile.canonicalFile != file(javaHome).canonicalFile) { - throw new GradleException("Compiler and tests must use JDK ${buildJavaVersion} from JAVA_HOME") + tasks.withType(Test).each { task -> + verifyToolchain(task.javaLauncher.get().metadata, oracleJdks.test) + if (task.executable != null && file(task.executable).canonicalPath != oracleJdks.test.java) { + throw new GradleException("${task.path} must launch ${oracleJdks.test.java}") } } - logger.lifecycle("Verified build/compiler/test JDK ${System.getProperty('java.runtime.version')} at ${javaHome}") + logger.lifecycle("Gradle: ${System.getProperty('java.vendor')} ${System.getProperty('java.runtime.version')} at ${System.getProperty('java.home')}") + logger.lifecycle("Compiler (all sources, --release 25): ${oracleJdks.build.vendor} ${oracleJdks.build.runtime_version} at ${oracleJdks.build.javac}") + logger.lifecycle("Tests (all Test tasks): ${oracleJdks.test.vendor} ${oracleJdks.test.runtime_version} at ${oracleJdks.test.java}") } } tasks.withType(JavaCompile).configureEach { dependsOn(tasks.named("verifyBuildJava")) + javaCompiler = buildCompiler + javaCompiler.disallowChanges() options.encoding = "UTF-8" - options.release = javaTarget + options.release = 25 + options.release.disallowChanges() options.compilerArgs.add("-Xlint:deprecation") } +tasks.withType(Test).configureEach { + dependsOn(tasks.named("verifyBuildJava")) + useJUnit() + javaLauncher = testLauncher + javaLauncher.disallowChanges() + environment "JAVA_HOME", oracleJdks.test.home + environment "PATH", "${oracleJdks.test.home}/bin${File.pathSeparator}${System.getenv('PATH')}" + systemProperty "oneMb.testJavaHome", oracleJdks.test.home + testLogging { events "passed", "skipped", "failed" } +} + tasks.register("prepareBuildNumber") { group = "build" description = "Advances the persistent build number, or reuses -PreleaseBuildNumber for repeatable release builds." @@ -107,6 +141,7 @@ dependencies { compileOnly "io.papermc.paper:paper-api:${paperApiVersion}" compileOnly "de.bluecolored:bluemap-api:${blueMapApiVersion}" compileOnly "me.clip:placeholderapi:${placeholderApiVersion}" + testImplementation "junit:junit:4.13.2" } tasks.processResources { @@ -186,6 +221,7 @@ tasks.register("verifyReleaseMetadata") { (readme): [ "Version `${pluginVersion}`", "JDK ${buildJavaVersion}", + "JDK ${testJavaVersion}", expectedJar, "`io.papermc.paper:paper-api:${paperApiVersion}`", "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", @@ -193,6 +229,7 @@ tasks.register("verifyReleaseMetadata") { ], (installation): [ "JDK ${buildJavaVersion}", + "JDK ${testJavaVersion}", "1MB-BlueMap-MapHide-v${pluginVersion}--j${javaTarget}-${paperTarget}.jar", "`io.papermc.paper:paper-api:${paperApiVersion}`", "`de.bluecolored:bluemap-api:${blueMapApiVersion}`", @@ -209,7 +246,11 @@ tasks.register("verifyReleaseMetadata") { "paper_target: ${paperTarget}" ], (workflow): [ - "java-version: '${buildJavaVersion}'" + "java-version: '${buildJavaVersion}'", + "java-version: '${testJavaVersion}'", + "./build.sh --no-daemon", + "ONE_MB_BUILD_JAVA_HOME:", + "ONE_MB_TEST_JAVA_HOME:" ], (pluginMetadata): [ "version: '\${version}'", @@ -365,6 +406,12 @@ tasks.register("verifyLocalPaperServer") { } } +tasks.register("testVerificationScripts", Exec) { + group = "verification" + description = "Tests Java toolchain selection and verification helpers." + commandLine "python3", "-m", "unittest", "discover", "-s", "scripts/tests", "-v" +} + tasks.register("deployServers") { group = "build" description = "Copies the built jar into the maintained Paper 26.3 plugins folder and disables older active copies." @@ -412,6 +459,7 @@ tasks.register("deployServers") { } tasks.named("check") { + dependsOn(tasks.named("testVerificationScripts")) dependsOn(tasks.named("verifyBuildJava")) dependsOn(tasks.named("verifyReleaseMetadata")) dependsOn(tasks.named("verifyArtifactMetadata")) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..51f37ed --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# Use the installed Gradle after selecting the project's validated Oracle JDKs. +set -eu +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +exec python3 "$project_dir/scripts/java_toolchains.py" "$@" diff --git a/docs/installation.md b/docs/installation.md index 3e68bde..3da6745 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,8 +3,9 @@ ## Requirements - Paper server with BlueMap installed. -- Java 25 bytecode, built with JDK 25.0.4.1. -- Tested server runtime: Java 27 (`27+35-2325`). +- Installed Oracle JDK 25 for Gradle and compilation, targeting Java 25 bytecode. +- Installed Oracle JDK 27 for all automated Java tests and Paper runtime verification. +- Installed Gradle 9.8.0+ and Python 3.11+ on PATH. - Paper 26.3 build 41 alpha is the maintained compile and release-test target. - The exact compile coordinate is `io.papermc.paper:paper-api:26.3.build.41-alpha`. - BlueMapAPI `de.bluecolored:bluemap-api:2.8.0` and PlaceholderAPI `me.clip:placeholderapi:2.12.3` are resolved from their official Maven repositories. @@ -50,9 +51,7 @@ Existing `config.yml` values are preserved. Missing defaults and managed comment The normal local build command is: ```sh -export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-25.0.4.1.jdk/Contents/Home -export PATH="$JAVA_HOME/bin:$PATH" -gradle --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer +./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer ``` The jar name follows this pattern: @@ -67,9 +66,25 @@ The Gradle deploy task copies the jar only into the maintained local `servers/Pa The local server folder is not required to compile or package the plugin. It is only used by `deployServers` and `verifyLocalPaperServer`. -`gradle check` runs JDK selection, release-documentation, generated-artifact, and test tasks. The repository currently has no unit-test sources, so Gradle reports `test NO-SOURCE`; CI includes the task so future tests are not skipped. Compilation and test toolchains come from `JAVA_HOME`, with automatic discovery/download disabled. `verifyBuildJava` requires the configured JDK 25.0.4.1 for Gradle, the compiler, and the test launcher. CI installs that exact Oracle JDK and runs `clean build`. +`build.sh` is a local launcher for the already installed Gradle, not a downloading Gradle wrapper. It resolves `/usr/libexec/java_home -F -v 25` and `/usr/libexec/java_home -F -v 27` on every invocation, probes each installation's actual major version, Oracle Corporation vendor and matching `javac`, then sets Gradle's `JAVA_HOME` and daemon home to Oracle JDK 25. The shell's default Java does not select the build runtime. -`gradle verifyLocalPaperServer` validates PaperScript's explicitly selected ALPHA channel, Paper build, server directory, installed jar, and both saved and pinned official SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. +All `JavaCompile` tasks, including test sources, explicitly select Oracle JDK 25 and `--release 25`. All `Test` tasks explicitly select Oracle JDK 27; their `JAVA_HOME` and `PATH` also select JDK 27 for child Java processes. `./build.sh --no-daemon verifyBuildJava` reports the actual paths, versions and vendors. Missing or invalid JDKs fail before any build tasks run. + +To select a particular installed patch or use another machine, set `ONE_MB_BUILD_JAVA_HOME` and `ONE_MB_TEST_JAVA_HOME` to the installed Oracle homes. Both overrides undergo the same checks. For example, without embedding patch-version folders: + +```sh +ONE_MB_BUILD_JAVA_HOME="$(/usr/libexec/java_home -F -v 25)" \ +ONE_MB_TEST_JAVA_HOME="$(/usr/libexec/java_home -F -v 27)" \ +./build.sh --no-daemon -PreleaseBuildNumber=030 clean build +``` + +Automatic Gradle toolchain discovery and downloads are disabled. Toolchains come only from the two validated environment variables. Settings reject conflicting project/system properties, alternate installation paths, and wrong bytecode targets before resolving toolchains. The launcher pins the Gradle daemon home and rejects JDK command-line overrides; use the two environment variables instead. Stale `gradle/gradle-daemon-jvm.properties` files are rejected because daemon criteria override `JAVA_HOME`. Configuration caching is disabled so validation runs each time. Raw `gradle` invocations must already have the same validated homes exported and run on Oracle 25; use `build.sh` for normal builds. + +`./build.sh check` runs JDK selection, release/documentation drift checks, artifact/bytecode validation, Java toolchain regression tests, and Python resolver/smoke-selection regression tests. The Java tests execute a real Gradle test worker and a child JVM on Oracle 27 and inspect Java 25 class headers in plugin and test classes. Paper integration tests remain the separate smoke command below. The local GitHub Actions definition uses Oracle 25/27, Gradle 9.8.2, and the same launcher with explicit installed-home overrides from its setup steps. No remote CI run was triggered for this local change. + +The selected Gradle must support Java 27 toolchains, which starts at 9.8.0 in the [official compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html). Toolchain tasks use the [official Gradle compiler and launcher APIs](https://docs.gradle.org/current/userguide/toolchains.html). See the [local verification record](verification/2026-10-25-oracle-toolchains.md) for the installed versions used. + +`./build.sh verifyLocalPaperServer` validates PaperScript's explicitly selected ALPHA channel, Paper build, server directory, installed jar, and both saved and pinned official SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. ## Runtime Verification @@ -81,7 +96,7 @@ python3 scripts/smoke-test.py The script requires Python 3.11 or newer and the maintained `servers/Paper-26.3` fixture: Paper build 41 ALPHA, its existing accepted EULA, cached server files/world, `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26), cached BlueMap assets, and PlaceholderAPI 2.12.3. It copies these into a fresh timestamped `servers/verification/` directory. The Minecraft listener binds to localhost on an available port; the copied BlueMap HTTP listener, RCON, and query listener are disabled. Only MapHide, BlueMap, and PlaceholderAPI are copied into this isolated smoke fixture, keeping external messaging integrations inactive. -The runtime is `/Library/Java/JavaVirtualMachines/jdk-27.jdk/Contents/Home`. Use `--java27-home` for an equivalent installation, `--server` for another fixture, and `--output` for a new results directory. The script validates runtime, artifact, channel, and official Paper checksum, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI values, config edits and reloads, comment/custom-key preservation, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. +The runtime is installed Oracle JDK 27, discovered using `java_home -F -v 27` or selected with `ONE_MB_TEST_JAVA_HOME`. Repeat `--java-home` to select installed Oracle 27 patches, use `--server` for another fixture, and `--output` for a new results directory. Other vendors and majors are rejected. The script validates runtime, artifact, channel, and official Paper checksum, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI values, config edits and reloads, comment/custom-key preservation, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. The optional CMI, CMILib, and Vault JARs are disabled in the maintained fixture after a third-party startup deadlock. Their data remains intact; see the verification record before re-enabling them. DiscordSRV is absent and other external messaging integrations stay inactive. diff --git a/docs/integrations.md b/docs/integrations.md index 51a72fd..a3d11e1 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -34,7 +34,7 @@ The default `/map hide` alias is handled by MapHide itself and runs the same per ## Compatibility Notes -The project is compiled with JDK 25.0.4.1 for Java 25 and `paper-api:26.3.build.41-alpha`. The maintained test target is Paper 26.3 build 41 alpha on Java 27 (`27+35-2325`). The generated metadata declares `api-version: 26.3`; earlier Paper versions require an older MapHide release. Java 25 bytecode is retained without preview features. +The project is compiled with dynamically selected installed Oracle JDK 25 for Java 25 and `paper-api:26.3.build.41-alpha`. The maintained test target is Paper 26.3 build 41 alpha on installed Oracle JDK 27. The generated metadata declares `api-version: 26.3`; earlier Paper versions require an older MapHide release. Java 25 bytecode is retained without preview features. Automated Java tests use installed Oracle JDK 27. The new local fixture contains BlueMap runtime 5.27 / manifest version 5.26 (file `bluemap-5.27-paper.jar`), PlaceholderAPI 2.12.3, LuckPerms 5.5.85, CMI 9.8.10.1, CMILib 1.6.0.0, and Vault 1.7.3-CMI (file `Vault-1.7.4.jar`). CMI, CMILib, and Vault are disabled in the final local smoke fixture after a startup class-initialization deadlock involving CMILib and BlueMap; their configurations and data remain preserved. The passing maintained-server check covers MapHide, BlueMap, PlaceholderAPI, and LuckPerms. Only JARs were copied from the shared test server; existing project-local configurations and data were retained. MapHide does not dispatch LuckPerms commands or perform external player identity lookups. diff --git a/docs/verification/2026-10-25-oracle-toolchains.md b/docs/verification/2026-10-25-oracle-toolchains.md new file mode 100644 index 0000000..3a78616 --- /dev/null +++ b/docs/verification/2026-10-25-oracle-toolchains.md @@ -0,0 +1,61 @@ +# Oracle JDK toolchain verification, 2026-10-25 + +Verification below was performed on the combined working tree, including the separate, pre-existing Paper 26.3 build 175 STABLE / release 031 upgrade. The Oracle-only commit leaves that upgrade uncommitted and retains the previous committed Paper build 41 ALPHA / release 030 pins. The four-lifecycle smoke suite, fixture tests, and dependency lockfile belong to the separate upgrade; their results below describe the combined working tree. Gradle and all Java compilation use Oracle 25; automated Java tests and disposable Paper integration servers use Oracle 27. + +## Installed toolchains observed + +These paths are evidence from this run, not installation pins. The launcher discovers both major versions afresh with `/usr/libexec/java_home -F -v 25` and `-v 27`, or validates explicit `ONE_MB_BUILD_JAVA_HOME` / `ONE_MB_TEST_JAVA_HOME` selections. + +| Role | Actual runtime version | Vendor | Observed installation | +| --- | --- | --- | --- | +| Gradle 9.8.2 and all Java compilers | 25.0.5.0.3+1-LTS-2 | Oracle Corporation | `/Library/Java/JavaVirtualMachines/jdk-25.0.5.0.3.jdk/Contents/Home` | +| Gradle test worker, child JVM, and Paper smoke servers | 27.0.1.0.3+1-2 | Oracle Corporation | `/Library/Java/JavaVirtualMachines/jdk-27.0.1.0.3.jdk/Contents/Home` | + +Actual compiler: `/Library/Java/JavaVirtualMachines/jdk-25.0.5.0.3.jdk/Contents/Home/bin/javac`. Actual test executable: `/Library/Java/JavaVirtualMachines/jdk-27.0.1.0.3.jdk/Contents/Home/bin/java`. Both `javac -version` probes matched their installation's `java.version`. + +Gradle 9.8.2 was already installed; no build-tool or JDK installation was required. Gradle 9.8.0 is the minimum for Java 27 toolchains in the [official compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html). The build uses supported [compiler and test launcher toolchains](https://docs.gradle.org/current/userguide/toolchains.html). + +## Verification + +The initial clean build refreshed the dependency lock only to include JUnit 4.13.2, Hamcrest 1.3, and the test configurations. The final run used the normal locked workflow: + +```sh +./build.sh --no-daemon --warning-mode all -PreleaseBuildNumber=031 clean build verifyLocalPaperServer +python3 scripts/smoke-test.py +``` + +- Clean build: passed, 12 tasks executed. No compiler or Gradle deprecation warnings appeared in the final build log. +- Java tests: 3 passed. They checked the real Gradle worker's Oracle vendor, Java 27 runtime and canonical installation path; plugin/test class headers; and a separately forked Oracle 27 JVM at the selected home. +- Python regression suite: 28 passed, covering fixture validation, smoke error handling, JDK discovery and rejection, and smoke runtime selection. +- Plugin artifact: all 12 classes have major version 69, minor version 0 (Java 25, no preview features). Both generated test classes also have version 69.0. All JavaCompile tasks explicitly use `--release 25` and the Oracle 25 compiler. +- Isolated Paper fixture: checksum and release-pin verification passed. +- Paper smoke suite: all 4 lifecycles passed, with 93 recorded checks. Initial startup, same-world restart/persistence, operation without PlaceholderAPI, and expected missing-BlueMap rejection all used the validated Oracle 27 installation and exited cleanly. +- Selection guards: 15/15 passed. Missing and wrong-major homes, project major/target overrides, launcher `-P`/`-D` download attempts, direct Gradle download attempts, and direct Gradle on Oracle 27 failed before build tasks. Unset or invalid shell `JAVA_HOME` still selected Oracle 25 for Gradle. Explicit installed-home overrides passed. +- `git diff --check` and `sh -n build.sh`: passed. + +Artifact: `build/libs/1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar` + +SHA-256: `b46e7edb3f2c67a8467cc504f635d51df4252cf993d8bf93c3548944cb65a87e` + +Local evidence: + +- [Clean build log](../../.scratch/java-toolchains/clean-build.log) +- [Java test report](../../build/reports/tests/test/index.html) +- [Java test XML with actual worker/fork metadata](../../build/test-results/test/TEST-com.onemb.toolchain.JavaToolchainTest.xml) +- [Guard results](../../.scratch/java-toolchains/guard-checks.json) +- [Smoke results](../../servers/verification/build-031-20261025T095855Z/results.json) +- [Smoke command log](../../.scratch/java-toolchains/paper-smoke.log) + +## Configuration audit and limits + +The stale project patch pin and `fromEnv=JAVA_HOME` single-JDK selection were replaced. No user-level Gradle properties, initialization scripts, daemon JVM criteria, IDE JVM overrides, or Maven build configuration needed migration. The shell selected Oracle 27, and Homebrew Gradle would otherwise default to Homebrew OpenJDK when JAVA_HOME was absent; the launcher explicitly selects Oracle 25 before invoking Gradle. + +Toolchain auto-detection and auto-download are disabled. Conflicting project/system properties and daemon criteria fail clearly. Configuration caching is disabled so installation validation runs on every invocation. No JDK was downloaded, no server deployment occurred, and no existing shared or historical server was launched. + +The active GitHub Actions definition was updated locally to supply Oracle 25/27 homes and Gradle 9.8.2 through the same launcher. It was not pushed or run remotely. No issues or pull requests were created. A subsequent user request authorized a local commit of only the Oracle setup, with the Paper upgrade kept separate. + +Only one installed Oracle patch per required major was available during the final override checks, so switching between two real patch installations could not be exercised. Explicit selection of the installed homes and mocked patch-independent discovery passed. Historical verification records retain their original runtimes. Player-driven marker, permission, timer, reconnect, and full third-party-stack behavior remains covered by the existing manual verification scope in the separate Paper upgrade, not these console tests. + +## Standalone commit verification + +Before committing, the Oracle-only changes were applied to an isolated copy of the original committed Paper build 41 ALPHA / release 030 tree. `./build.sh --no-daemon -PreleaseBuildNumber=030 clean check` passed there: 3 Java tests, 15 Python toolchain/selection tests, release metadata checks, and all 12 plugin classes at Java 25 bytecode. No deployment or Paper server launch was performed from that snapshot. Its [check log](../../.scratch/java-toolchains/commit/selected-check.log) is retained locally. The existing stable upgrade, release-number change, fixture tooling/tests, stable verification record, and dependency lockfile were excluded from the commit. diff --git a/gradle.properties b/gradle.properties index e9bf5e8..c16b2d3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,12 @@ pluginVersion=2.0.2 javaTarget=25 -buildJavaVersion=25.0.4.1 -org.gradle.java.installations.fromEnv=JAVA_HOME +buildJavaVersion=25 +testJavaVersion=27 +org.gradle.java.installations.fromEnv=ONE_MB_BUILD_JAVA_HOME,ONE_MB_TEST_JAVA_HOME +org.gradle.java.installations.paths= org.gradle.java.installations.auto-detect=false org.gradle.java.installations.auto-download=false +org.gradle.configuration-cache=false paperTarget=26.3 paperApiVersion=26.3.build.41-alpha paperBuild=41 diff --git a/gradle/oracle-jdks.settings.gradle b/gradle/oracle-jdks.settings.gradle new file mode 100644 index 0000000..fa79001 --- /dev/null +++ b/gradle/oracle-jdks.settings.gradle @@ -0,0 +1,59 @@ +import groovy.json.JsonSlurper +import org.gradle.api.configuration.BuildFeatures +import org.gradle.util.GradleVersion + +abstract class OracleSettingsFeatures implements Plugin { + @Inject abstract BuildFeatures getBuildFeatures() + + void apply(Settings settings) { + if (buildFeatures.configurationCache.requested.getOrElse(false)) { + throw new GradleException("Configuration caching is disabled so installed JDKs are validated on every invocation") + } + } +} + +if (GradleVersion.current() < GradleVersion.version("9.8.0")) { + throw new GradleException("Oracle JDK 27 tests require installed Gradle 9.8.0+; run ./build.sh") +} +apply plugin: OracleSettingsFeatures + +def requiredProperties = [ + "javaTarget": "25", + "buildJavaVersion": "25", + "testJavaVersion": "27", + "org.gradle.java.installations.auto-detect": "false", + "org.gradle.java.installations.auto-download": "false", + "org.gradle.java.installations.paths": "", + "org.gradle.java.installations.fromEnv": "ONE_MB_BUILD_JAVA_HOME,ONE_MB_TEST_JAVA_HOME" +] +requiredProperties.each { key, expected -> + String actual = providers.gradleProperty(key).orNull + String systemOverride = providers.systemProperty(key).orNull + if (actual != expected || (systemOverride != null && systemOverride != expected)) { + throw new GradleException("${key} must be '${expected}'; remove stale Gradle/command-line overrides and use ./build.sh") + } +} +if (new File(settingsDir, "gradle/gradle-daemon-jvm.properties").exists()) { + throw new GradleException("Remove stale gradle/gradle-daemon-jvm.properties; use ./build.sh for dynamic Oracle selection") +} + +def probe = providers.exec { + commandLine "python3", new File(settingsDir, "scripts/java_toolchains.py").absolutePath, "--json" + ignoreExitValue = true +} +if (probe.result.get().exitValue != 0) { + throw new GradleException(probe.standardError.asText.get().trim()) +} +def jdks = new JsonSlurper().parseText(probe.standardOutput.asText.get()) +["ONE_MB_BUILD_JAVA_HOME": jdks.build.home, "ONE_MB_TEST_JAVA_HOME": jdks.test.home].each { name, home -> + String configured = providers.environmentVariable(name).orNull + if (!configured || file(configured).canonicalPath != home) { + throw new GradleException("Use ./build.sh to resolve and export ${name} before starting Gradle") + } +} +if (System.getProperty("java.specification.version") != "25" + || System.getProperty("java.vendor") != "Oracle Corporation" + || file(System.getProperty("java.home")).canonicalPath != jdks.build.home) { + throw new GradleException("Gradle must run on validated Oracle JDK 25 at ${jdks.build.home}; use ./build.sh") +} +gradle.ext.oracleJdks = jdks diff --git a/scripts/java_toolchains.py b/scripts/java_toolchains.py new file mode 100644 index 0000000..17fc4cb --- /dev/null +++ b/scripts/java_toolchains.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Select installed Oracle JDKs and launch this project's installed Gradle.""" + +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] + + +def resolve_jdk(major: int, override: str | None = None) -> dict: + variable = {25: "ONE_MB_BUILD_JAVA_HOME", 27: "ONE_MB_TEST_JAVA_HOME"}[major] + selected = override if override is not None else os.environ.get(variable) + if not selected: + if sys.platform != "darwin": + raise RuntimeError(f"Set {variable} to an installed Oracle JDK {major}; automatic discovery requires macOS") + discovery = subprocess.run(["/usr/libexec/java_home", "-F", "-v", str(major)], + capture_output=True, text=True) + if discovery.returncode or not discovery.stdout.strip(): + raise RuntimeError(f"Installed Oracle JDK {major} unavailable; select it with {variable}. No JDK will be downloaded.") + selected = discovery.stdout.strip() + home = Path(selected).expanduser().resolve() + java, javac = home / "bin/java", home / "bin/javac" + if not all(path.is_file() and os.access(path, os.X_OK) for path in (java, javac)): + raise RuntimeError(f"{variable}: {home} is not an installed JDK with executable java and javac") + probe = subprocess.run([str(java), "-XshowSettings:properties", "-version"], + capture_output=True, text=True, timeout=30) + properties = dict(re.findall(r"^\s*(java\.[\w.]+)\s*=\s*(.*?)\s*$", + probe.stdout + probe.stderr, re.MULTILINE)) + if (probe.returncode or properties.get("java.specification.version") != str(major) + or properties.get("java.vendor") != "Oracle Corporation" + or Path(properties.get("java.home", "/missing")).resolve() != home): + raise RuntimeError(f"{variable}: expected Oracle JDK {major} at {home}; found " + f"version={properties.get('java.version')}, vendor={properties.get('java.vendor')}, " + f"home={properties.get('java.home')}. No fallback or download is allowed.") + compiler = subprocess.run([str(javac), "-version"], capture_output=True, text=True, timeout=30) + compiler_version = re.search(r"^javac (\S+)", compiler.stdout + compiler.stderr, re.MULTILINE) + if compiler.returncode or not compiler_version or compiler_version[1] != properties["java.version"]: + raise RuntimeError(f"{variable}: javac does not match validated Oracle JDK {major} at {home}") + return {"home": str(home), "java": str(java), "javac": str(javac), "major": major, + "version": properties["java.version"], "runtime_version": properties["java.runtime.version"], + "vendor": properties["java.vendor"]} + + +def validate_gradle_arguments(arguments): + # Reject these before Gradle can start a daemon with an unvalidated JDK or download one. + for argument in arguments: + if ("org.gradle.java." in argument or "org.gradle.project.org.gradle.java." in argument + or argument == "--configuration-cache" + or "org.gradle.configuration-cache" in argument): + raise RuntimeError("JDK selection and configuration caching are managed by build.sh; " + "use ONE_MB_BUILD_JAVA_HOME / ONE_MB_TEST_JAVA_HOME for installed JDK overrides") + + +def main(): + if sys.version_info < (3, 11): + raise RuntimeError("Python 3.11+ is required") + if sys.argv[1:] == ["--json"]: + print(json.dumps({"build": resolve_jdk(25), "test": resolve_jdk(27)})) + return + arguments = sys.argv[1:] + validate_gradle_arguments(arguments) + criteria = ROOT / "gradle/gradle-daemon-jvm.properties" + if criteria.exists(): + raise RuntimeError(f"Remove stale daemon JVM criteria from {criteria}; this project selects installed Oracle JDK 25 dynamically") + build, test = resolve_jdk(25), resolve_jdk(27) + gradle = shutil.which("gradle") + if not gradle: + raise RuntimeError("Install Gradle 9.8.0+ on PATH; build.sh never downloads build tools or JDKs") + env = dict(os.environ, JAVA_HOME=build["home"], ONE_MB_BUILD_JAVA_HOME=build["home"], + ONE_MB_TEST_JAVA_HOME=test["home"]) + env["PATH"] = str(Path(build["home"]) / "bin") + os.pathsep + env.get("PATH", "") + for role, jdk in (("Gradle/compiler", build), ("Tests", test)): + print(f"{role}: {jdk['vendor']} {jdk['runtime_version']} at {jdk['home']}", flush=True) + os.chdir(ROOT) + os.execve(gradle, [gradle, f"-Dorg.gradle.java.home={build['home']}", *arguments], env) + + +if __name__ == "__main__": + try: + main() + except (RuntimeError, OSError, subprocess.SubprocessError) as error: + sys.exit(f"Java toolchain error: {error}") diff --git a/scripts/smoke-test.py b/scripts/smoke-test.py index cefd16a..c902d71 100644 --- a/scripts/smoke-test.py +++ b/scripts/smoke-test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Exercise the release jar on Java 27 in an isolated local Paper 26.3 copy.""" +"""Exercise Paper in disposable localhost servers using installed Oracle JDK 27.""" import argparse from datetime import datetime, timezone @@ -15,10 +15,18 @@ import time import zipfile +from java_toolchains import resolve_jdk + ROOT = Path(__file__).resolve().parents[1] -JAVA_ROOT = Path("/Library/Java/JavaVirtualMachines") -RUNTIME_VERSION = "27" + + +def resolve_test_runtimes(java_homes=None): + """Validate every requested runtime before creating a disposable server.""" + if java_homes: + return [resolve_jdk(27, override=str(home)) for home in java_homes] + return [resolve_jdk(27)] + def properties(text): @@ -89,11 +97,15 @@ def prepare_server(source, destination, artifact, metadata): def run_server(directory, java_home, version, metadata): + jdk = resolve_jdk(27, override=str(java_home)) + java_home = Path(jdk["home"]) + if jdk["version"] != version: + raise RuntimeError(f"Java runtime changed after preflight: {version} -> {jdk['version']}") env = dict(os.environ, JAVA_HOME=str(java_home)) env["PATH"] = str(java_home / "bin") + os.pathsep + env.get("PATH", "") - runtime = subprocess.run(["java", "-version"], env=env, text=True, + runtime = subprocess.run([jdk["java"], "-version"], env=env, text=True, capture_output=True, check=True).stderr.strip() - if f'java version "{version}"' not in runtime: + if not re.search(rf'(?:java|openjdk) version "{re.escape(version)}"', runtime): raise RuntimeError(f"Expected Java {version}: {runtime}") (directory / "java-version.txt").write_text(runtime + "\n") print(f"Starting Paper {metadata['paperTarget']} build {metadata['paperBuild']} on {version}", flush=True) @@ -108,7 +120,7 @@ def run_server(directory, java_home, version, metadata): bluemap_plugin_version = re.search(r"(?m)^version:\s*['\"]?([^'\"\s]+)", manifest).group(1) with (directory / "console.log").open("w") as log: process = subprocess.Popen( - ["java", "-Xms512M", "-Xmx2G", "-XX:ActiveProcessorCount=4", + [jdk["java"], "-Xms512M", "-Xmx2G", "-XX:ActiveProcessorCount=4", "-Dfile.encoding=UTF-8", "-Dterminal.ansi=false", "-Dterminal.jline=false", "-Dapple.awt.UIElement=true", "-jar", "paper.jar", "--nogui"], cwd=directory, env=env, text=True, stdin=subprocess.PIPE, @@ -203,7 +215,7 @@ def command(value, *expected): r"(?:/|\s)(?:ERROR|SEVERE)\]|Exception|UnsupportedClassVersionError|NoClassDefFoundError|NoSuchMethodError", line)] if failures: raise RuntimeError("Errors in server log:\n" + "\n".join(failures)) - return {"runtime": runtime, "checks": checks, "exit_code": process.returncode, + return {"runtime": runtime, "java_toolchain": jdk, "checks": checks, "exit_code": process.returncode, "console_log": str(directory / "console.log")} @@ -211,9 +223,11 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--server", type=Path, default=ROOT / "servers/Paper-26.3") parser.add_argument("--output", type=Path) - parser.add_argument("--java27-home", type=Path, - default=JAVA_ROOT / "jdk-27.jdk/Contents/Home") + parser.add_argument("--java-home", type=Path, action="append", + help="Installed Oracle JDK 27 home; repeat for multiple Oracle 27 installations. " + "Defaults to ONE_MB_TEST_JAVA_HOME or macOS java_home -F -v 27.") args = parser.parse_args() + runtimes = resolve_test_runtimes(args.java_home) config = properties((ROOT / "gradle.properties").read_text()) build = (ROOT / "build-number.txt").read_text().strip() artifact = ROOT / "build/libs" / ( @@ -225,12 +239,18 @@ def main(): stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") output = (args.output or ROOT / f"servers/verification/build-{build}-{stamp}").resolve() output.mkdir(parents=True, exist_ok=False) - report = {"artifact": str(artifact), "sha256": sha256(artifact), "metadata": metadata, "runs": []} - directory = output / f"java-{RUNTIME_VERSION}" - report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) - report["runs"].append(run_server(directory, args.java27_home.resolve(), RUNTIME_VERSION, metadata)) - (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") - print(f"PASS: Java {RUNTIME_VERSION}. Results: {output / 'results.json'}", flush=True) + report = {"artifact": str(artifact), "sha256": sha256(artifact), "metadata": metadata, + "java_toolchains": runtimes, "runs": []} + try: + for index, jdk in enumerate(runtimes, start=1): + java_home = Path(jdk["home"]) + version = jdk["version"] + directory = output / f"java-{version}-{index}" + report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) + report["runs"].append(run_server(directory, java_home, version, metadata)) + finally: + (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") + print(f"PASS: {len(report['runs'])} server lifecycles. Results: {output / 'results.json'}", flush=True) if __name__ == "__main__": diff --git a/scripts/tests/test_java_toolchains.py b/scripts/tests/test_java_toolchains.py new file mode 100644 index 0000000..dd72968 --- /dev/null +++ b/scripts/tests/test_java_toolchains.py @@ -0,0 +1,125 @@ +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +import java_toolchains + + +class JavaToolchainTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.home = Path(self.directory.name).resolve() + (self.home / "bin").mkdir() + for executable in ("java", "javac"): + path = self.home / "bin" / executable + path.write_text("#!/bin/sh\nexit 1\n") + path.chmod(0o755) + self.environment = patch.dict(os.environ, {}, clear=True) + self.environment.start() + self.addCleanup(self.environment.stop) + + def java_probe(self, major=27, vendor="Oracle Corporation", home=None, version=None): + version = version or f"{major}.0.42" + properties = {"java.specification.version": str(major), "java.vendor": vendor, + "java.home": str(home or self.home), "java.version": version, + "java.runtime.version": f"{version}+7"} + return subprocess.CompletedProcess([], 0, "", "\n".join( + f" {key} = {value}" for key, value in properties.items())) + + @staticmethod + def javac_probe(version="27.0.42"): + return subprocess.CompletedProcess([], 0, f"javac {version}\n", "") + + def test_macos_discovers_both_major_versions_without_patch_pin(self): + for major in (25, 27): + with self.subTest(major=major), patch.object(sys, "platform", "darwin"), \ + patch.object(java_toolchains.subprocess, "run", side_effect=[ + subprocess.CompletedProcess([], 0, str(self.home) + "\n", ""), + self.java_probe(major), self.javac_probe(f"{major}.0.42")]) as run: + jdk = java_toolchains.resolve_jdk(major) + self.assertEqual(["/usr/libexec/java_home", "-F", "-v", str(major)], run.call_args_list[0].args[0]) + self.assertEqual(major, jdk["major"]) + self.assertEqual(f"{major}.0.42", jdk["version"]) + self.assertEqual("Oracle Corporation", jdk["vendor"]) + self.assertEqual(str(self.home / "bin/java"), jdk["java"]) + + def test_explicit_override_takes_precedence_over_environment(self): + with patch.dict(os.environ, {"ONE_MB_TEST_JAVA_HOME": "/stale/jdk"}), \ + patch.object(java_toolchains.subprocess, "run", side_effect=[self.java_probe(), self.javac_probe()]) as run: + jdk = java_toolchains.resolve_jdk(27, override=str(self.home)) + self.assertEqual(str(self.home), jdk["home"]) + self.assertEqual(2, run.call_count) + + def test_environment_override_works_without_macos_discovery(self): + with patch.object(sys, "platform", "linux"), \ + patch.dict(os.environ, {"ONE_MB_TEST_JAVA_HOME": str(self.home)}), \ + patch.object(java_toolchains.subprocess, "run", side_effect=[self.java_probe(), self.javac_probe()]) as run: + self.assertEqual(str(self.home), java_toolchains.resolve_jdk(27)["home"]) + self.assertEqual(2, run.call_count) + + def test_failed_discovery_never_falls_back_to_default_java(self): + with patch.object(sys, "platform", "darwin"), \ + patch.object(java_toolchains.subprocess, "run", return_value=subprocess.CompletedProcess([], 1, "", "missing")) as run: + with self.assertRaisesRegex(RuntimeError, "No JDK will be downloaded"): + java_toolchains.resolve_jdk(27) + self.assertEqual(1, run.call_count) + + def test_wrong_vendor_is_rejected_before_compiler_probe(self): + with patch.object(java_toolchains.subprocess, "run", return_value=self.java_probe(vendor="Eclipse Adoptium")) as run: + with self.assertRaisesRegex(RuntimeError, "expected Oracle JDK 27"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + self.assertEqual(1, run.call_count) + + def test_wrong_major_is_rejected_before_compiler_probe(self): + with patch.object(java_toolchains.subprocess, "run", return_value=self.java_probe(25)) as run: + with self.assertRaisesRegex(RuntimeError, "expected Oracle JDK 27"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + self.assertEqual(1, run.call_count) + + def test_reported_home_must_match_selected_installation(self): + with patch.object(java_toolchains.subprocess, "run", return_value=self.java_probe(home="/another/jdk")): + with self.assertRaisesRegex(RuntimeError, "expected Oracle JDK 27"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + + def test_missing_java_or_javac_is_rejected_before_launch(self): + for executable in ("java", "javac"): + with self.subTest(executable=executable): + path = self.home / "bin" / executable + path.chmod(0o644) + try: + with patch.object(java_toolchains.subprocess, "run") as run: + with self.assertRaisesRegex(RuntimeError, "executable java and javac"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + run.assert_not_called() + finally: + path.chmod(0o755) + + def test_mismatched_javac_is_rejected(self): + with patch.object(java_toolchains.subprocess, "run", side_effect=[self.java_probe(), self.javac_probe("25.0.42")]): + with self.assertRaisesRegex(RuntimeError, "javac does not match"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + + def test_failed_java_probe_is_rejected_even_with_valid_metadata(self): + probe = self.java_probe() + probe.returncode = 1 + with patch.object(java_toolchains.subprocess, "run", return_value=probe): + with self.assertRaisesRegex(RuntimeError, "expected Oracle JDK 27"): + java_toolchains.resolve_jdk(27, override=str(self.home)) + + def test_command_line_cannot_replace_jdks_or_enable_downloads(self): + for argument in ("-Dorg.gradle.java.home=/elsewhere", "-Porg.gradle.java.installations.auto-download=true", + "-Dorg.gradle.project.org.gradle.java.installations.auto-detect=true", + "--configuration-cache"): + with self.subTest(argument=argument), self.assertRaisesRegex(RuntimeError, "managed by build.sh"): + java_toolchains.validate_gradle_arguments([argument]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_smoke_java.py b/scripts/tests/test_smoke_java.py new file mode 100644 index 0000000..58073db --- /dev/null +++ b/scripts/tests/test_smoke_java.py @@ -0,0 +1,47 @@ +import importlib.util +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +spec = importlib.util.spec_from_file_location("smoke_java_test", SCRIPTS / "smoke-test.py") +smoke = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke) + + +class SmokeJavaTests(unittest.TestCase): + def test_default_uses_validated_test_jdk(self): + with patch.object(smoke, "resolve_jdk", return_value={"home": "/oracle27"}) as resolve: + self.assertEqual([{"home": "/oracle27"}], smoke.resolve_test_runtimes()) + resolve.assert_called_once_with(27) + + def test_all_explicit_homes_require_oracle_27(self): + with patch.object(smoke, "resolve_jdk", side_effect=[{"home": "/first"}, {"home": "/second"}]) as resolve: + runtimes = smoke.resolve_test_runtimes([Path("/first"), Path("/second")]) + self.assertEqual([{"home": "/first"}, {"home": "/second"}], runtimes) + self.assertEqual([((27,), {"override": "/first"}), ((27,), {"override": "/second"})], + resolve.call_args_list) + + def test_invalid_runtime_fails_before_reading_artifact_or_preparing_server(self): + with patch.object(sys, "argv", ["smoke-test.py", "--java-home", "/invalid"]), \ + patch.object(smoke, "resolve_jdk", side_effect=RuntimeError("Oracle JDK 27 required")), \ + patch.object(smoke.zipfile, "ZipFile") as artifact, \ + patch.object(smoke, "prepare_server") as prepare: + with self.assertRaisesRegex(RuntimeError, "Oracle JDK 27 required"): + smoke.main() + artifact.assert_not_called() + prepare.assert_not_called() + + def test_runtime_is_revalidated_before_every_server_launch(self): + with patch.object(smoke, "resolve_jdk", side_effect=RuntimeError("Oracle JDK 27 required")) as resolve, \ + patch.object(smoke.subprocess, "Popen") as start: + with self.assertRaisesRegex(RuntimeError, "Oracle JDK 27 required"): + smoke.run_server(Path("/server"), Path("/replaced-jdk"), "27", {}) + resolve.assert_called_once_with(27, override="/replaced-jdk") + start.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/settings.gradle b/settings.gradle index 091afcd..462d59c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,9 @@ pluginManagement { } } +// Validate before Java plugins can resolve or provision a toolchain. +apply from: file("gradle/oracle-jdks.settings.gradle") + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { diff --git a/src/test/java/com/onemb/toolchain/JavaToolchainTest.java b/src/test/java/com/onemb/toolchain/JavaToolchainTest.java new file mode 100644 index 0000000..e34a0fb --- /dev/null +++ b/src/test/java/com/onemb/toolchain/JavaToolchainTest.java @@ -0,0 +1,95 @@ +package com.onemb.toolchain; + +import org.junit.Test; + +import java.io.DataInputStream; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** Verifies the compiler/runtime split using the JVM and class files actually used by Gradle. */ +public final class JavaToolchainTest { + + @Test + public void testsRunOnTheSelectedOracle27Installation() throws Exception { + assertEquals("Test JVM major version", 27, Runtime.version().feature()); + assertEquals("Test JVM vendor", "Oracle Corporation", System.getProperty("java.vendor")); + assertEquals("Test JVM installation", expectedTestHome(), Path.of(System.getProperty("java.home")).toRealPath()); + System.out.printf("Test JVM: %s; vendor=%s; home=%s%n", + System.getProperty("java.runtime.version"), System.getProperty("java.vendor"), expectedTestHome()); + } + + @Test + public void pluginAndTestClassesTargetJava25WithoutPreviewFeatures() throws Exception { + assertJava25Class("/com/onemb/toolchain/JavaToolchainTest.class"); + assertJava25Class("/com/onemb/toolchain/JavaToolchainTest$ForkProbe.class"); + // Read the resource without loading the plugin class or requiring Paper on the test runtime. + assertJava25Class("/com/technicjelle/bluemapplayercontrol/BlueMapPlayerControl.class"); + } + + @Test + public void forkedJvmUsesTheSelectedOracle27Installation() throws Exception { + Path javaHome = expectedTestHome(); + Path testClasses = Path.of(ForkProbe.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + Path outputFile = Files.createTempFile("maphide-jdk-probe-", ".log"); + Process process = null; + try { + process = new ProcessBuilder( + javaHome.resolve("bin/java").toString(), "-cp", testClasses.toString(), ForkProbe.class.getName()) + .redirectErrorStream(true) + .redirectOutput(outputFile.toFile()) + .start(); + assertTrue("Forked JVM did not finish within 20 seconds", process.waitFor(20, TimeUnit.SECONDS)); + String output = Files.readString(outputFile); + assertEquals("Forked JVM failed:\n" + output, 0, process.exitValue()); + Properties metadata = new Properties(); + metadata.load(new StringReader(output)); + assertEquals("Forked JVM major version", "27", metadata.getProperty("major")); + assertEquals("Forked JVM vendor", "Oracle Corporation", metadata.getProperty("vendor")); + assertNotNull("Forked JVM did not report its installation:\n" + output, metadata.getProperty("home")); + assertEquals("Forked JVM installation", javaHome, Path.of(metadata.getProperty("home")).toRealPath()); + System.out.println("Forked JVM:\n" + output.strip()); + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + process.waitFor(5, TimeUnit.SECONDS); + } + Files.deleteIfExists(outputFile); + } + } + + private static Path expectedTestHome() throws Exception { + String configuredHome = System.getProperty("oneMb.testJavaHome"); + assertNotNull("Gradle must pass the validated test JDK as oneMb.testJavaHome", configuredHome); + return Path.of(configuredHome).toRealPath(); + } + + private static void assertJava25Class(String resource) throws Exception { + InputStream stream = JavaToolchainTest.class.getResourceAsStream(resource); + assertNotNull("Missing compiled class " + resource, stream); + try (DataInputStream input = new DataInputStream(stream)) { + assertEquals("Class file magic: " + resource, 0xCAFEBABE, input.readInt()); + assertEquals("No preview bytecode: " + resource, 0, input.readUnsignedShort()); + assertEquals("Java 25 class major: " + resource, 69, input.readUnsignedShort()); + } + } + + /** Runs without a test framework so the fork proves the selected Java executable works independently. */ + public static final class ForkProbe { + public static void main(String[] args) throws Exception { + Properties metadata = new Properties(); + metadata.setProperty("major", Integer.toString(Runtime.version().feature())); + metadata.setProperty("version", System.getProperty("java.runtime.version")); + metadata.setProperty("vendor", System.getProperty("java.vendor")); + metadata.setProperty("home", Path.of(System.getProperty("java.home")).toRealPath().toString()); + metadata.store(System.out, "Forked JVM metadata"); + } + } +} From d6318cf45f4021e180083df70e3e99e9b7a19eae Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 25 Oct 2026 11:16:18 +0100 Subject: [PATCH 6/8] Upgrade to Paper 26.3 build 222 STABLE Pin the latest verified Paper 26.3 STABLE server and exact API, lock dependencies, and prepare isolated fixtures without changing historical servers. Keep normal builds free of deployment. Align smoke fixture selection with release configuration, guard direct Java launches before deployment, and make smoke shutdown cleanup reliable when stdin closes. Record official API/default review and local verification. Validation: clean locked build, 3 Java tests, 48 Python tests, 93 checks across four Paper lifecycles on Oracle 27, and live/stopped deployment-guard checks. No deployment or remote CI run. --- .github/workflows/main.yml | 3 + .gitignore | 1 + AGENTS.md | 8 +- README.md | 30 ++- build-number.txt | 2 +- build.gradle | 108 ++++---- docs/installation.md | 38 +-- docs/integrations.md | 8 +- docs/plugin-docs.yml | 3 + .../2026-10-11-paper-26.3-stable.md | 82 ++++++ .../2026-10-25-paper-26.3-stable-222.md | 80 ++++++ gradle.lockfile | 59 +++++ gradle.properties | 9 +- scripts/deploy_guard.py | 71 ++++++ scripts/paper_fixture.py | 134 ++++++++++ scripts/smoke-test.py | 238 +++++++++++------- scripts/tests/test_deploy_guard.py | 92 +++++++ scripts/tests/test_paper_fixture.py | 102 ++++++++ scripts/tests/test_smoke_cleanup.py | 87 +++++++ scripts/tests/test_smoke_errors.py | 33 +++ scripts/tests/test_smoke_fixture.py | 54 ++++ 21 files changed, 1047 insertions(+), 195 deletions(-) create mode 100644 docs/verification/2026-10-11-paper-26.3-stable.md create mode 100644 docs/verification/2026-10-25-paper-26.3-stable-222.md create mode 100644 gradle.lockfile create mode 100644 scripts/deploy_guard.py create mode 100644 scripts/paper_fixture.py create mode 100644 scripts/tests/test_deploy_guard.py create mode 100644 scripts/tests/test_paper_fixture.py create mode 100644 scripts/tests/test_smoke_cleanup.py create mode 100644 scripts/tests/test_smoke_errors.py create mode 100644 scripts/tests/test_smoke_fixture.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index db32f21..edfe251 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,6 +38,9 @@ jobs: gradle-version: '9.8.2' cache-provider: basic + - name: Verify Python runtime for fixture tests + run: python3 -c 'import sys; assert sys.version_info >= (3, 11), "Python 3.11+ required"' + - name: Read release build number id: build-number shell: bash diff --git a/.gitignore b/.gitignore index fe4513b..46fd455 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,5 @@ run/ build/ servers/ build/libs/ +__pycache__/ .scratch/ diff --git a/AGENTS.md b/AGENTS.md index 1d3ba1f..56257bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,11 +10,11 @@ This repository uses the single-context domain-doc layout. See `docs/agents/doma ## Maintained Paper release -- Target Paper 26.3 build 41 ALPHA and `io.papermc.paper:paper-api:26.3.build.41-alpha`. This experimental channel is explicitly authorized; do not silently substitute a stable 26.2 build. +- Target Paper 26.3 build 222 STABLE and `io.papermc.paper:paper-api:26.3.build.222-stable`. Resolve future updates from official Fill and Maven metadata; keep Minecraft at 26.3 and the channel STABLE. - Use installed Gradle 9.8.0+ through `./build.sh`. It resolves `/usr/libexec/java_home -F -v 25` and `-v 27` on every invocation and validates actual versions, Oracle Corporation vendor, and matching javac. Gradle and all Java compilation use Oracle JDK 25 with `--release 25`; every Java test task and disposable Paper smoke server uses Oracle JDK 27. - Select another installed patch with `ONE_MB_BUILD_JAVA_HOME` / `ONE_MB_TEST_JAVA_HOME`; never pin patch-version folders in executable configuration. Missing, wrong-major, or non-Oracle selections fail. Automatic toolchain detection/downloads are disabled. Do not override the validated toolchain properties or add daemon JVM criteria. Use `./build.sh verifyBuildJava` to print the actual paths, versions, and vendors. Configuration caching is disabled to preserve validation on every invocation. -- Release 2.0.2 reserves build `030`. Run `./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer`; reuse that number on retries. The prepareBuildNumber task owns increments. -- Maintain `servers/Paper-26.3` on dynamically selected installed Oracle JDK 27; keep `servers/Paper-26.2` unchanged as rollback material. Stop the local launcher before building because build deploys locally. -- Run `python3 scripts/smoke-test.py` after building. It resolves and validates installed Oracle JDK 27 independently and uses disposable localhost servers. See `docs/installation.md` and `docs/verification/2026-10-25-oracle-toolchains.md` for the workflow and verification evidence. Historical records describe their original targets. +- Release 2.0.2 reserves build `031`. Run `./build.sh --no-daemon -PreleaseBuildNumber=031 clean build`; reuse that number on retries. The prepareBuildNumber task owns increments. Use Python 3.11+ on PATH for verification scripts. +- Prepare the isolated `servers/Paper-26.3-stable-222` fixture with `python3 scripts/paper_fixture.py prepare`, then run `./build.sh verifyLocalPaperServer`. Keep `servers/Paper-26.3-stable` (build 175), `servers/Paper-26.3`, and `servers/Paper-26.2` unchanged as historical fixtures. Builds do not deploy automatically; `deployServers` is an explicit, stopped-server operation. +- Run `python3 scripts/smoke-test.py` after building. It resolves and validates installed Oracle JDK 27 independently and uses disposable localhost servers. See `docs/installation.md`, `docs/verification/2026-10-25-oracle-toolchains.md`, and `docs/verification/2026-10-25-paper-26.3-stable-222.md` for the workflow and verification evidence. Historical records describe their original targets. - Read the live Paper LLM index, official docs, roadmap, and exact target Javadocs for future upgrades. Pin the server checksum and actual channel, and review compiler deprecations and exploit-protection defaults. - The shared test server must stay stopped during artifact replacement. Match this project's `1MB-MapHide` plugin identity and main class, replace only its JARs with backups and checksum verification, and never launch the shared server as part of deployment. diff --git a/README.md b/README.md index c66e27a..f7719f4 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,15 @@ https://docs.1moreblock.com/custom-server-plugins/maphide/ ## Changelog Summary -Version `2.0.2` targets Paper 26.3 build 41 on its experimental ALPHA channel, with the exact API `io.papermc.paper:paper-api:26.3.build.41-alpha`. Build `030` retains Java 25 bytecode and is tested on Java 27. The Paper 26.2 baseline is preserved at annotated tag `v2.0.1-paper-26.2` and in `servers/Paper-26.2`. Plugin identity, commands, permissions, configuration, player visibility, and PlaceholderAPI behavior are unchanged. +Version `2.0.2`, build `031`, migrates development and compatibility testing to Paper 26.3 build 222 stable, with the published API `io.papermc.paper:paper-api:26.3.build.222-stable`. Java 25 bytecode, plugin identity, commands, permissions, configuration, and visibility behavior are preserved. Builds now leave existing servers untouched. Stable fixture provisioning checks official metadata and SHA-256; regression checks cover release pins, restart persistence, and optional/missing dependencies. See the [stable verification record](docs/verification/2026-10-25-paper-26.3-stable-222.md). ## Supported Servers -This build targets Java 25 bytecode and the exact Paper API coordinate `io.papermc.paper:paper-api:26.3.build.41-alpha`. Paper 26.3 build 41 alpha is the maintained release-test server: +This build targets Java 25 bytecode and the exact Paper API coordinate `io.papermc.paper:paper-api:26.3.build.222-stable`. Paper 26.3 build 222 stable is the maintained release-test server: | Server | BlueMap plugin | | --- | --- | -| Paper 26.3 build 41 alpha | `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26) | +| Paper 26.3 build 222 stable | `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26) | The generated plugin metadata declares `api-version: 26.3`, so this release is not intended for older Paper versions. Use an older MapHide build for Paper 26.1.2 or 1.21.11. @@ -139,35 +139,43 @@ Requirements: - Installed Gradle 9.8.0 or newer - Python 3.11 or newer on PATH - Installed Oracle JDK 25 for Gradle and compilation, and Oracle JDK 27 for automated tests -- The exact Maven dependency `io.papermc.paper:paper-api:26.3.build.41-alpha` +- The exact Maven dependency `io.papermc.paper:paper-api:26.3.build.222-stable` - The Maven dependencies `de.bluecolored:bluemap-api:2.8.0` and `me.clip:placeholderapi:2.12.3` -A clean checkout can compile without a local Paper server. The local `servers/Paper-26.3` folder is only needed for `deployServers` and `verifyLocalPaperServer`. +A clean checkout builds without a local Paper server. The isolated `servers/Paper-26.3-stable-222` fixture is only needed for runtime verification and explicit development deployment. -Build release `030` and deploy it to the maintained Paper 26.3 test server: +Build release `031`: ```sh -./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer +./build.sh --no-daemon -PreleaseBuildNumber=031 clean build ``` Jars are named like: ```text -1MB-BlueMap-MapHide-v2.0.2-030-j25-26.3.jar +1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar ``` The plugin version comes from `gradle.properties`. The three-digit build number is stored in `build-number.txt` and advances for a new unpinned jar build. Pin `releaseBuildNumber` when repeating a release build so retries reuse the intended number instead of incrementing it again. The Java and Paper parts of the jar name are target identifiers, not the semantic plugin version. `build.sh` dynamically resolves installed Oracle JDK 25 and Oracle JDK 27 with macOS `java_home -F -v`, validates their major versions and Oracle vendor, and runs the installed Gradle on JDK 25. All plugin and test sources compile with JDK 25 and `--release 25`; all Java test tasks and their Java subprocesses use JDK 27. `verifyBuildJava` checks the actual toolchain paths. No JDKs are downloaded or silently substituted. Explicit installed-home overrides use `ONE_MB_BUILD_JAVA_HOME` and `ONE_MB_TEST_JAVA_HOME`; see the [build workflow](docs/installation.md#build). `verifyReleaseMetadata` checks source and documentation values. `verifyArtifactMetadata` checks the generated `plugin.yml`, build information, jar name, and every class for Java 25 bytecode without preview features. All run as part of `check`. -After a successful `build`, `deployServers` copies the jar into `servers/Paper-26.3/plugins/`. Before copying, it renames active older MapHide jars in that folder by appending `.disabled`. +`build` does not deploy. When a development server is stopped, `deployServers` explicitly copies the artifact to `servers/Paper-26.3-stable-222/plugins/` and retains older MapHide JARs as disabled files. Dependency resolution is pinned in `gradle.lockfile`; refresh locks intentionally when changing dependencies. -Run `python3 scripts/smoke-test.py` after building to verify Paper startup, plugin loading, console commands, configuration reloads, placeholders, and clean shutdown on JDK 27. It uses an isolated copy of `servers/Paper-26.3` and saves logs under `servers/verification/`. See [runtime verification](docs/installation.md#runtime-verification) for coverage and [build 030 evidence](docs/verification/2026-09-25-paper-26.3.md) for results. Compilation uses Oracle JDK 25; Java tests and smoke servers use dynamically selected Oracle JDK 27. Set `ONE_MB_TEST_JAVA_HOME` or pass `--java-home` to select another installed Oracle 27 patch. +Prepare and test the stable fixture (see [setup details](docs/installation.md#runtime-verification)): + +```sh +python3 scripts/paper_fixture.py prepare +./build.sh --no-daemon verifyLocalPaperServer +python3 scripts/smoke-test.py +``` + +The preparer downloads only the checksum-pinned stable build. It reads accepted EULA files, BlueMap/PlaceholderAPI JARs, and runtime caches from the historical local fixture without importing its worlds, player records, or credentials. Each smoke run creates fresh localhost worlds under `servers/verification/` and exercises startup, shutdown, restart, configuration persistence, commands, and dependency failure paths on Oracle JDK 27. Use `ONE_MB_TEST_JAVA_HOME` or `--java-home` to select another installed Oracle 27 patch. Existing `servers/Paper-26.3` and `servers/Paper-26.2` remain historical fixtures. ## Installation 1. Stop the Paper server. -2. Put `1MB-BlueMap-MapHide-v2.0.2-030-j25-26.3.jar` in `plugins/`. +2. Put `1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar` in `plugins/`. 3. Make sure BlueMap is also in `plugins/`. 4. Remove or disable old `BlueMapPlayerControl-*.jar` copies. 5. Start the server. diff --git a/build-number.txt b/build-number.txt index cadb715..513eb2f 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -030 +031 diff --git a/build.gradle b/build.gradle index 0e85ffc..aca2e28 100644 --- a/build.gradle +++ b/build.gradle @@ -23,9 +23,13 @@ def releaseBuildNumber = new java.util.concurrent.atomic.AtomicReference def pinnedBuildNumber = providers.gradleProperty("releaseBuildNumber") def maintainedServerDirs = [ - file("servers/Paper-26.3") + file(providers.gradleProperty("localPaperServerDir").get()) ] +dependencyLocking { + lockAllConfigurations() +} + java { toolchain { languageVersion = JavaLanguageVersion.of(javaTarget) @@ -199,8 +203,8 @@ tasks.register("verifyReleaseMetadata") { if (paperApiVersion != expectedPaperApi) { throw new GradleException("paperApiVersion ${paperApiVersion} does not match ${expectedPaperApi}") } - if (!(paperChannel in ["STABLE", "BETA", "ALPHA"])) { - throw new GradleException("Paper release channel must explicitly select STABLE, BETA, or ALPHA") + if (paperChannel != "STABLE" || paperTarget != "26.3") { + throw new GradleException("The maintained target must remain Paper 26.3 STABLE") } if (!(paperSha256 ==~ /[0-9a-f]{64}/)) { throw new GradleException("paperSha256 must pin the official server download checksum") @@ -213,11 +217,17 @@ tasks.register("verifyReleaseMetadata") { File workflow = file(".github/workflows/main.yml") File pluginMetadata = file("src/main/resources/plugin.yml") File buildInfoTemplate = file("src/main/resources/build-info.properties") + File agentGuidance = file("AGENTS.md") String documentation = ([readme, installation, integrations] + fileTree("docs") { include "*.md" }).unique().collect { it.text }.join(System.lineSeparator()) [ + (agentGuidance): [ + "Paper ${paperTarget} build ${paperBuild} ${paperChannel}", + "`io.papermc.paper:paper-api:${paperApiVersion}`", + "-PreleaseBuildNumber=${buildNumber}" + ], (readme): [ "Version `${pluginVersion}`", "JDK ${buildJavaVersion}", @@ -243,7 +253,10 @@ tasks.register("verifyReleaseMetadata") { ], (docsMetadata): [ "java_target: ${javaTarget}", - "paper_target: ${paperTarget}" + "paper_target: ${paperTarget}", + "paper_build: ${paperBuild}", + "paper_channel: ${paperChannel}", + "paper_api: ${paperApiVersion}" ], (workflow): [ "java-version: '${buildJavaVersion}'", @@ -282,6 +295,18 @@ tasks.register("verifyReleaseMetadata") { throw new GradleException("Stale documented semantic version: ${releaseMatcher.group(1)}") } } + def apiMatcher = documentation =~ /paper-api:(26\.[0-9.]+\.build\.\d+-[a-z]+)/ + while (apiMatcher.find()) { + if (apiMatcher.group(1) != paperApiVersion) { + throw new GradleException("Stale documented Paper API: ${apiMatcher.group(1)}") + } + } + def channelMatcher = documentation =~ /Paper 26\.3 build (\d+) (alpha|beta|stable)/ + while (channelMatcher.find()) { + if (channelMatcher.group(1) != paperBuild.toString() || channelMatcher.group(2) != "stable") { + throw new GradleException("Stale documented Paper release: ${channelMatcher.group(0)}") + } + } } } @@ -346,64 +371,11 @@ tasks.register("verifyArtifactMetadata") { } } -tasks.register("verifyLocalPaperServer") { +tasks.register("verifyLocalPaperServer", Exec) { group = "verification" - description = "Checks the maintained local PaperScript config, Paper build, and saved SHA-256." - - doLast { - File serverDir = file(providers.gradleProperty("localPaperServerDir").getOrElse("servers/Paper-26.3")) - File configFile = new File(serverDir, "paperscript/config.json") - File stateFile = new File(serverDir, "paperscript/state.json") - if (!configFile.isFile() || !stateFile.isFile()) { - logger.lifecycle("Skipping local Paper server verification: PaperScript state is not present") - return - } - - def parser = new groovy.json.JsonSlurper() - Map config = parser.parse(configFile) as Map - Map state = parser.parse(stateFile) as Map - if (config.default_channel?.toString()?.toUpperCase() != paperChannel - || config.check_latest_channel_only?.toString()?.toUpperCase() != paperChannel - || config.allow_same_version_build_upgrade != true - || (config.containsKey("download_filename_pattern") - && config.download_filename_pattern != "Paper-{version}.jar")) { - throw new GradleException("Maintained PaperScript config does not match the explicit ${paperChannel} release channel") - } - if (state.current_version != paperTarget || state.current_build.toString().toInteger() != paperBuild) { - throw new GradleException("Maintained Paper server is not ${paperTarget} build ${paperBuild}") - } - if (state.current_channel != paperChannel || file(state.server_dir.toString()).canonicalFile != serverDir.canonicalFile) { - throw new GradleException("Maintained Paper state has a stale channel or server directory") - } - - File paperJar = new File(serverDir, state.current_jar.toString()) - if (!paperJar.isFile()) { - throw new GradleException("Maintained Paper jar is missing: ${paperJar}") - } - String actualSha256 = java.security.MessageDigest.getInstance("SHA-256") - .digest(paperJar.bytes) - .collect { String.format("%02x", it & 0xff) } - .join() - if (actualSha256 != state.current_sha256 || actualSha256 != paperSha256) { - throw new GradleException("Maintained Paper jar SHA-256 does not match PaperScript state and the pinned official checksum") - } - // Newer PaperScript records expected_sha256 for a staged upgrade, not the current jar. - if (state.staged_jar) { - File stagedJar = new File(serverDir, state.staged_jar.toString()) - if (!stagedJar.isFile()) { - throw new GradleException("Staged Paper jar is missing: ${stagedJar}") - } - String stagedSha256 = java.security.MessageDigest.getInstance("SHA-256") - .digest(stagedJar.bytes).collect { String.format("%02x", it & 0xff) }.join() - if (stagedSha256 != state.staged_sha256 || stagedSha256 != state.expected_sha256) { - throw new GradleException("Staged Paper jar SHA-256 does not match PaperScript state") - } - logger.lifecycle("Verified separately staged ${stagedJar.name}; release tests still use ${paperJar.name}") - } else if (actualSha256 != state.expected_sha256) { - throw new GradleException("Maintained Paper jar SHA-256 does not match the expected download checksum") - } - logger.lifecycle("Verified Paper ${paperTarget} build ${paperBuild} ${paperChannel}: ${paperJar}") - } + description = "Checks the isolated development server against the stable build and checksum pins." + commandLine "python3", "scripts/paper_fixture.py", "verify", + "--server", providers.gradleProperty("localPaperServerDir").get() } tasks.register("testVerificationScripts", Exec) { @@ -420,9 +392,14 @@ tasks.register("deployServers") { doLast { File builtJar = tasks.jar.archiveFile.get().asFile maintainedServerDirs.each { File serverDir -> - if (new File(serverDir, "paperscript/locks/server-launch").exists()) { - throw new GradleException("Stop the maintained launcher before deploying: ${serverDir}") + def preflight = providers.exec { + commandLine "python3", "scripts/deploy_guard.py", serverDir.absolutePath + ignoreExitValue = true + } + if (preflight.result.get().exitValue != 0) { + throw new GradleException(preflight.standardError.asText.get().trim()) } + logger.lifecycle(preflight.standardOutput.asText.get().trim()) File pluginsDir = new File(serverDir, "plugins") if (!pluginsDir.isDirectory()) { logger.lifecycle("Skipping ${pluginsDir}: directory does not exist") @@ -469,6 +446,7 @@ tasks.named("deployServers") { mustRunAfter(tasks.named("check")) } -tasks.named("build") { - dependsOn(tasks.named("deployServers")) +// Deployment is explicit. A normal build must never modify an existing server. +tasks.named("deployServers") { + dependsOn(tasks.named("check"), tasks.named("verifyLocalPaperServer")) } diff --git a/docs/installation.md b/docs/installation.md index 3da6745..17fb4a8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,9 +5,9 @@ - Paper server with BlueMap installed. - Installed Oracle JDK 25 for Gradle and compilation, targeting Java 25 bytecode. - Installed Oracle JDK 27 for all automated Java tests and Paper runtime verification. -- Installed Gradle 9.8.0+ and Python 3.11+ on PATH. -- Paper 26.3 build 41 alpha is the maintained compile and release-test target. -- The exact compile coordinate is `io.papermc.paper:paper-api:26.3.build.41-alpha`. +- Installed Gradle 9.8.0+ and Python 3.11+ on PATH for build and fixture verification. +- Paper 26.3 build 222 stable is the maintained compile and release-test target. +- The exact compile coordinate is `io.papermc.paper:paper-api:26.3.build.222-stable`. - BlueMapAPI `de.bluecolored:bluemap-api:2.8.0` and PlaceholderAPI `me.clip:placeholderapi:2.12.3` are resolved from their official Maven repositories. - PlaceholderAPI is optional. @@ -51,7 +51,7 @@ Existing `config.yml` values are preserved. Missing defaults and managed comment The normal local build command is: ```sh -./build.sh --no-daemon -PreleaseBuildNumber=030 clean build verifyLocalPaperServer +./build.sh --no-daemon -PreleaseBuildNumber=031 clean build ``` The jar name follows this pattern: @@ -60,11 +60,11 @@ The jar name follows this pattern: 1MB-BlueMap-MapHide-v2.0.2--j25-26.3.jar ``` -`2.0.2` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=030` makes release retries reuse build `030`. The `j25` and `26.3` parts identify the Java bytecode and Paper target. +`2.0.2` is the plugin version. The three-digit build number is stored in `build-number.txt`. An unpinned artifact build advances it once; `-PreleaseBuildNumber=031` makes release retries reuse build `031`. The `j25` and `26.3` parts identify the Java bytecode and Paper target. -The Gradle deploy task copies the jar only into the maintained local `servers/Paper-26.3/plugins/` folder and appends `.disabled` to older active MapHide jars. +A normal build does not deploy or start any server. `deployServers` explicitly copies the jar into the stopped `servers/Paper-26.3-stable-222/plugins/` fixture and appends `.disabled` to older active MapHide JARs. The `localPaperServerDir` Gradle property selects the development fixture for both verification and explicit deployment. Never point it at production. -The local server folder is not required to compile or package the plugin. It is only used by `deployServers` and `verifyLocalPaperServer`. +Deployment preflight rejects a PaperScript launcher lock or a direct Java process using the fixture directory. It requires `lsof` and refuses deployment if process inspection fails. Keep the fixture stopped throughout deployment; the check does not coordinate with an external launcher starting concurrently. `build.sh` is a local launcher for the already installed Gradle, not a downloading Gradle wrapper. It resolves `/usr/libexec/java_home -F -v 25` and `/usr/libexec/java_home -F -v 27` on every invocation, probes each installation's actual major version, Oracle Corporation vendor and matching `javac`, then sets Gradle's `JAVA_HOME` and daemon home to Oracle JDK 25. The shell's default Java does not select the build runtime. @@ -75,36 +75,40 @@ To select a particular installed patch or use another machine, set `ONE_MB_BUILD ```sh ONE_MB_BUILD_JAVA_HOME="$(/usr/libexec/java_home -F -v 25)" \ ONE_MB_TEST_JAVA_HOME="$(/usr/libexec/java_home -F -v 27)" \ -./build.sh --no-daemon -PreleaseBuildNumber=030 clean build +./build.sh --no-daemon -PreleaseBuildNumber=031 clean build ``` Automatic Gradle toolchain discovery and downloads are disabled. Toolchains come only from the two validated environment variables. Settings reject conflicting project/system properties, alternate installation paths, and wrong bytecode targets before resolving toolchains. The launcher pins the Gradle daemon home and rejects JDK command-line overrides; use the two environment variables instead. Stale `gradle/gradle-daemon-jvm.properties` files are rejected because daemon criteria override `JAVA_HOME`. Configuration caching is disabled so validation runs each time. Raw `gradle` invocations must already have the same validated homes exported and run on Oracle 25; use `build.sh` for normal builds. -`./build.sh check` runs JDK selection, release/documentation drift checks, artifact/bytecode validation, Java toolchain regression tests, and Python resolver/smoke-selection regression tests. The Java tests execute a real Gradle test worker and a child JVM on Oracle 27 and inspect Java 25 class headers in plugin and test classes. Paper integration tests remain the separate smoke command below. The local GitHub Actions definition uses Oracle 25/27, Gradle 9.8.2, and the same launcher with explicit installed-home overrides from its setup steps. No remote CI run was triggered for this local change. +`./build.sh check` runs JDK selection, release/documentation drift checks, artifact/bytecode validation, Java toolchain regression tests, and Python fixture/resolver regression tests. The Java tests execute a real Gradle test worker and a child JVM on Oracle 27 and inspect Java 25 class headers in plugin and test classes. Paper integration tests remain the separate smoke command below. The local GitHub Actions definition uses Oracle 25/27, Gradle 9.8.2, and the same launcher with explicit installed-home overrides from its setup steps. No remote CI run was triggered for this local change. The selected Gradle must support Java 27 toolchains, which starts at 9.8.0 in the [official compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html). Toolchain tasks use the [official Gradle compiler and launcher APIs](https://docs.gradle.org/current/userguide/toolchains.html). See the [local verification record](verification/2026-10-25-oracle-toolchains.md) for the installed versions used. -`./build.sh verifyLocalPaperServer` validates PaperScript's explicitly selected ALPHA channel, Paper build, server directory, installed jar, and both saved and pinned official SHA-256. Newer PaperScript stores a staged upgrade's checksum separately from the current jar. Both are checked without promoting the staged build. `-PlocalPaperServerDir=/path/to/fixture` selects an alternate verification fixture without changing the deployment destination. +Paper API, BlueMap API, and PlaceholderAPI versions are pinned in `gradle.properties`; resolved transitive dependencies are recorded in `gradle.lockfile`. There is no paperweight/dev-bundle dependency or NMS use. Update the pins from official metadata and refresh the lock deliberately with `--write-locks` when upgrading. + +Check `python3 --version` before building; Python 3.11 or newer must be on PATH for checksum verification and the JDK resolver. ## Runtime Verification -After the full build, run: +Prepare the stable fixture once, then verify and test it: ```sh +python3 scripts/paper_fixture.py prepare +./build.sh --no-daemon verifyLocalPaperServer python3 scripts/smoke-test.py ``` -The script requires Python 3.11 or newer and the maintained `servers/Paper-26.3` fixture: Paper build 41 ALPHA, its existing accepted EULA, cached server files/world, `bluemap-5.27-paper.jar` (runtime 5.27, manifest 5.26), cached BlueMap assets, and PlaceholderAPI 2.12.3. It copies these into a fresh timestamped `servers/verification/` directory. The Minecraft listener binds to localhost on an available port; the copied BlueMap HTTP listener, RCON, and query listener are disabled. Only MapHide, BlueMap, and PlaceholderAPI are copied into this isolated smoke fixture, keeping external messaging integrations inactive. +The preparer and smoke script both default to `localPaperServerDir`, currently `servers/Paper-26.3-stable-222`. Preparation refuses existing destinations. It checks the exact Fill build's STABLE channel, build ID, download size, and pinned SHA-256. `paper-release.json` records the verified build and source plugin checksums. `verifyLocalPaperServer` fails if the fixture is absent, has stale release metadata, or its JAR checksum differs from the release pin. It never silently skips verification. -The runtime is installed Oracle JDK 27, discovered using `java_home -F -v 27` or selected with `ONE_MB_TEST_JAVA_HOME`. Repeat `--java-home` to select installed Oracle 27 patches, use `--server` for another fixture, and `--output` for a new results directory. Other vendors and majors are rejected. The script validates runtime, artifact, channel, and official Paper checksum, then checks loading, BlueMap readiness, version/status/help/debug commands, console-only guards, missing players, invalid input, PlaceholderAPI values, config edits and reloads, comment/custom-key preservation, and graceful shutdown with exit code 0. Logs and a JSON report are retained. A failed expectation, server error, or shutdown timeout fails the run. +Provisioning requires a source test fixture with an already accepted Minecraft EULA, already accepted BlueMap resource terms, one BlueMap runtime JAR and one PlaceholderAPI JAR. By default it reads `servers/Paper-26.3`; use `--fixtures /path/to/test-fixture` for another. It copies only binaries, runtime caches, the accepted EULA, and cached Minecraft client assets. Fresh configuration disables HTTP, RCON, query, telemetry, and external integrations. Worlds, player records, credentials, and other plugins' configuration are not imported. `--server /new/path` prepares a different new destination; use the matching `-PlocalPaperServerDir=/new/path` and smoke `--server /new/path` for verification. -The optional CMI, CMILib, and Vault JARs are disabled in the maintained fixture after a third-party startup deadlock. Their data remains intact; see the verification record before re-enabling them. DiscordSRV is absent and other external messaging integrations stay inactive. +The smoke script copies this stable fixture into a new timestamped `servers/verification/` directory and generates disposable worlds. Minecraft binds to localhost on an available port. It checks the release artifact and server checksum, plugin loading, BlueMap readiness, info/status/help/debug commands, player-only guards, missing players, invalid selectors/pages/config keys, PlaceholderAPI values, config edits/reloads, preserved comments/custom keys, and clean shutdown. It then restarts the same disposable world to verify persisted nondefault settings. Further lifecycles verify operation without PlaceholderAPI and the expected missing-BlueMap dependency rejection. Unexpected errors, linkage failures, shutdown timeouts, or nonzero exits fail the run; JSON results and separate console logs survive failures. -The maintained launcher `servers/Paper-26.3/1MB-minecraft.sh` selects Java 27 and Paper 26.3. Its PaperScript session name is `maphide-26-3-030`. Minecraft TCP uses `52227`; reserved query UDP, RCON TCP, and BlueMap HTTP ports are `52228`, `52229`, and `52230` respectively (those listeners are disabled for smoke testing). Check availability before subsequent manual launches. Start from this directory with `./1MB-minecraft.sh` and stop through the console with `stop`. +The default runtime is the installed Oracle JDK 27 discovered with `java_home -F -v 27`, or `ONE_MB_TEST_JAVA_HOME` when set. Repeat `--java-home /path/to/jdk` to choose other installed Oracle 27 patches (other vendors or majors are rejected), and use `--output /new/results/path` to select a new output directory. Tests stop every process they start. No existing launcher, shared test server, or production service is started or stopped. -`servers/Paper-26.2` remains untouched rollback material. Its annotated tag is `v2.0.1-paper-26.2`. The new ALPHA target is deliberate; neither the build nor the launcher falls back to Paper 26.2. Use PaperScript's explicit `download --version 26.3 --build 41 --channel ALPHA` command for this pin; its `update` shortcut selects stable builds and is not the release workflow. +`servers/Paper-26.3-stable` (build 175), `servers/Paper-26.3`, and `servers/Paper-26.2` remain historical fixtures. Their old launchers are not the current stable development workflow. The Paper 26.2 baseline is also preserved at `v2.0.1-paper-26.2`. Current verification is recorded in the [build 222 report](verification/2026-10-25-paper-26.3-stable-222.md). -These are local console smoke tests. Player joins, actual marker toggles, forced player permissions, and timer expiry require a connected player and are outside this automated check. +These are console integration tests. Actual player marker changes, permission denials, forced permission conflicts, timer expiry, reconnects, and the full third-party stack need the [manual checks](verification/2026-10-11-paper-26.3-stable.md#manual-verification). Inventories, item metadata, economy transactions, and LuckPerms command delivery are not MapHide features. ## Readiness Check diff --git a/docs/integrations.md b/docs/integrations.md index a3d11e1..12c14f8 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -34,12 +34,14 @@ The default `/map hide` alias is handled by MapHide itself and runs the same per ## Compatibility Notes -The project is compiled with dynamically selected installed Oracle JDK 25 for Java 25 and `paper-api:26.3.build.41-alpha`. The maintained test target is Paper 26.3 build 41 alpha on installed Oracle JDK 27. The generated metadata declares `api-version: 26.3`; earlier Paper versions require an older MapHide release. Java 25 bytecode is retained without preview features. Automated Java tests use installed Oracle JDK 27. +The project is compiled with dynamically selected installed Oracle JDK 25 for Java 25 and `paper-api:26.3.build.222-stable`. Automated Java tests run with installed Oracle JDK 27. The maintained test target is Paper 26.3 build 222 stable. The generated metadata declares `api-version: 26.3`; earlier Paper versions require an older MapHide release. Java 25 bytecode is retained without preview features. -The new local fixture contains BlueMap runtime 5.27 / manifest version 5.26 (file `bluemap-5.27-paper.jar`), PlaceholderAPI 2.12.3, LuckPerms 5.5.85, CMI 9.8.10.1, CMILib 1.6.0.0, and Vault 1.7.3-CMI (file `Vault-1.7.4.jar`). CMI, CMILib, and Vault are disabled in the final local smoke fixture after a startup class-initialization deadlock involving CMILib and BlueMap; their configurations and data remain preserved. The passing maintained-server check covers MapHide, BlueMap, PlaceholderAPI, and LuckPerms. Only JARs were copied from the shared test server; existing project-local configurations and data were retained. MapHide does not dispatch LuckPerms commands or perform external player identity lookups. +The isolated stable fixture uses BlueMap runtime 5.27 / manifest 5.26 (`bluemap-5.27-paper.jar`) and PlaceholderAPI 2.12.3, with fresh configuration and worlds. The smoke suite uses Oracle JDK 27, including operation without PlaceholderAPI and expected refusal to load without BlueMap. MapHide reads Bukkit permissions and targets BlueMap state by `Player#getUniqueId()`; it does not dispatch LuckPerms commands or perform external player identity lookups. -See the [build 030 verification record](verification/2026-09-25-paper-26.3.md) for exact checksums, runtime results, third-party notices, and manual checks. +Connected-player behavior, asynchronous placeholder consumers, and the complete LuckPerms/CMI/CMILib/Vault stack require manual integration checks. The earlier CMI/BlueMap startup deadlock remains historical evidence, not a stable-build test result. See the [stable verification record](verification/2026-10-25-paper-26.3-stable-222.md) for results and manual steps. MapHide uses the Bukkit scheduler and does not claim verified Folia compatibility. ### Historical verification Version 2.0.1 build 029 remains preserved at `v2.0.1-paper-26.2`. Its [Java 25/26 verification record](verification/2026-09-15-jdk-update.md) applies only to that baseline and its Paper 26.2 environment. + +The [2026-09-25 record](verification/2026-09-25-paper-26.3.md) describes the previous experimental 26.3 environment. diff --git a/docs/plugin-docs.yml b/docs/plugin-docs.yml index 437e53e..86363fc 100644 --- a/docs/plugin-docs.yml +++ b/docs/plugin-docs.yml @@ -8,5 +8,8 @@ player_guide: player-guide.md technical_readme: ../README.md java_target: 25 paper_target: 26.3 +paper_build: 222 +paper_channel: STABLE +paper_api: 26.3.build.222-stable official_project: true repository: mrfdev/1MB-MapHide diff --git a/docs/verification/2026-10-11-paper-26.3-stable.md b/docs/verification/2026-10-11-paper-26.3-stable.md new file mode 100644 index 0000000..eede0d7 --- /dev/null +++ b/docs/verification/2026-10-11-paper-26.3-stable.md @@ -0,0 +1,82 @@ +# Paper 26.3 STABLE verification, 2026-10-11 + +## Release pins and sources + +Version 2.0.2 build 031 keeps Minecraft at 26.3 and changes the maintained channel to STABLE. `prepareBuildNumber` advanced 030 to 031 once; retries reuse `-PreleaseBuildNumber=031`. + +| Component | Verified pin | +| --- | --- | +| Paper server | 26.3 build 175 STABLE, `26.3-175-bd9daa8` | +| Official build time | 2026-10-11T15:32:14Z | +| Paper API | `io.papermc.paper:paper-api:26.3.build.175-stable` | +| Paper server SHA-256 | `ece5afb6d0cdf0c930489618bf92341e33d2ebcd4f21e6d365daff6ed26dafb0` | +| BlueMap compile API | `de.bluecolored:bluemap-api:2.8.0` | +| PlaceholderAPI | `me.clip:placeholderapi:2.12.3` | +| Build JDK / bytecode | Oracle `25.0.4.1+1-LTS-5` / Java 25, class version 69.0 | +| Additional server runtime | Oracle Java 27, `27+35-2325` | +| Local Gradle / Python | 9.8.1 / 3.14.8 | + +Discovery began with the live [Paper LLM index](https://docs.papermc.io/llms.txt). The [version build list](https://fill.papermc.io/v3/projects/paper/versions/26.3/builds) identified 175 as the newest STABLE build at resolution time. The [exact Fill record](https://fill.papermc.io/v3/projects/paper/versions/26.3/builds/175) supplies its channel, commit, size, download URL, and checksum. The API coordinate was found in [Maven metadata](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/maven-metadata.xml) and verified against its [published POM](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.175-stable/paper-api-26.3.build.175-stable.pom). No dependency coordinate was inferred from the server filename. Neither documentation index nor `llms-full.txt` is vendored. + +The hosted [26.3 Javadocs](https://jd.papermc.io/paper/26.3/) still identified `26.3.build.157-beta` during this review, and a build-specific hosted URL returned 404. Exact contracts were therefore checked in the officially published [175-stable Javadoc artifact](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.175-stable/paper-api-26.3.build.175-stable-javadoc.jar); its HTML titles identify `26.3.build.175-stable`. This documentation publication lag did not require using beta dependencies. + +## Compatibility review and changes + +Reviewed official [project setup](https://docs.papermc.io/paper/dev/project-setup/), [Java requirements](https://docs.papermc.io/paper/getting-started/), [plugin.yml](https://docs.papermc.io/paper/dev/plugin-yml/), [plugin lifecycle](https://docs.papermc.io/paper/dev/how-do-plugins-work/), [scheduling](https://docs.papermc.io/paper/dev/scheduler/), [event priority/cancellation](https://docs.papermc.io/paper/dev/event-listeners/), and [roadmap](https://docs.papermc.io/paper/dev/roadmap/). Paper 26.3 requires Java 25 or newer, so the existing Java 25 toolchain and bytecode target remain appropriate. + +Exact-build Javadocs confirm the JavaPlugin enable/disable lifecycle, synchronous Runnable scheduler overloads, exact local online-player lookup, Bukkit permissions, `getPluginMeta`, and configuration comment APIs used here. Adventure remains 5.2.0. Compilation emitted no deprecation diagnostics; the runtime suite checks linkage on the actual stable server. No plugin Java-source changes were needed for stable compatibility. Commands, permission definitions, translations, configuration schema, stored visibility ownership, plugin identity (`1MB-MapHide`), and main class remain unchanged; the descriptor keeps `api-version: '26.3'`. + +MapHide uses Bukkit permissions and authoritative `Player#getUniqueId()` values for BlueMap visibility and timer state. It has no LuckPerms command/API integration or external identity lookup. There are no inventory menus, custom items/PDC, economic transactions, database operations, NMS, or paperweight/dev-bundle dependencies in this project. Gameplay timer work remains on the Bukkit main-thread scheduler. Configuration and translation reloads retain their existing synchronous file I/O; this migration does not establish slow-storage performance or asynchronous PlaceholderAPI consumer safety. Folia support is not verified by these Paper tests. + +Build and development changes: + +- Pin Paper API, STABLE channel, build, and server checksum in `gradle.properties`; record resolved dependencies in `gradle.lockfile`. +- Use `servers/Paper-26.3-stable` for future development fixtures. Provision from the exact Fill record and fail on mismatched channel, size, or SHA-256. Existing destinations are never overwritten. +- Remove automatic deployment from `build`; explicit `deployServers` targets the selected development fixture. No existing server needed to stop for this build. +- Run fixture regression tests in Gradle `check` and CI. Enforce stable release pins and detect stale API/channel declarations in maintained documentation and AGENTS.md. Clearly dated historical records remain unchanged. +- Extend the smoke harness with separate process restarts, persistence assertions, optional PlaceholderAPI absence, and expected missing-BlueMap rejection. Preserve separate logs and failure reports. + +The [global](https://docs.papermc.io/paper/reference/global-configuration/) and [world](https://docs.papermc.io/paper/reference/world-configuration/) defaults were reviewed. Generated stable-server configuration retains `false` for piston duplication, unsafe end portals, headless pistons, permanent-block exploits, disabled tripwire updates, and skipped tripwire-hook placement validation; oversized-component sanitizer exclusions remain `[]`. No permissive JVM flags or exploit-setting exceptions were added. + +## Build and runtime evidence + +The clean Gradle build passed using the pinned JDK, including 13 Python fixture/harness regression tests, release/documentation checks, generated artifact metadata, and all 12 Java classes at version 69.0 without preview features. Java `test` reports `NO-SOURCE`; there are no Java unit-test sources. CI configuration was updated but no remote CI run was launched from this task. + +The final suite passed all **186 assertions across eight server lifecycles**. Each process shut down with exit code 0. The two deliberate missing-BlueMap runs contain only the expected dependency rejection; the six normal runs contain no server errors or linkage exceptions. + +| Java runtime | Scenario | Passing assertions | Exit code | +| --- | --- | --- | --- | +| 25.0.4.1 | initial | 29 | 0 | +| 25.0.4.1 | restart | 33 | 0 | +| 25.0.4.1 | without-placeholderapi | 29 | 0 | +| 25.0.4.1 | without-bluemap | 2 | 0 | +| 27 | initial | 29 | 0 | +| 27 | restart | 33 | 0 | +| 27 | without-placeholderapi | 29 | 0 | +| 27 | without-bluemap | 2 | 0 | + +- Built JAR: `build/libs/1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar`. +- Artifact SHA-256: `b46e7edb3f2c67a8467cc504f635d51df4252cf993d8bf93c3548944cb65a87e`. Repeated clean builds produced identical bytes, and both runtime copies match the final artifact. +- Passing report: `servers/verification/paper-26.3-stable-031/passing-matrix/results.json`; each row links to its own console log in that report. +- Build evidence: `servers/verification/paper-26.3-stable-031/build-final.log`. +- Download metadata/POM snapshots: `servers/verification/paper-26.3-stable-031/metadata/`. +- Both runtimes' generated exploit-protection defaults were checked after shutdown and remain at the safe values described above. + +Runtime results are recorded in the local JSON reports under `servers/verification/paper-26.3-stable-031/`. The initial run exposed an invalid empty flat-world generator setting in the new test fixture; provisioning now lets Paper use its normal default generator. A later negative test exposed a harness mismatch with Paper's actual `Unknown/missing dependency plugins: [BlueMap]` wording. Unsuccessful attempts were retained and are not counted as passing full suites. The negative test permits only the expected MapHide load-failure header and BlueMap dependency exception; unrelated server errors remain fatal. + +## Isolation and limits + +The stable fixture reads only the previous local test fixture's accepted EULA, accepted BlueMap resource terms, plugin binaries, and runtime caches. It imports no worlds, player records, credentials, or existing plugin configurations. Every test process runs in a newly created disposable directory, with fresh worlds, localhost Minecraft binding, and disabled BlueMap HTTP/RCON/query listeners. Existing `servers/Paper-26.3`, `servers/Paper-26.2`, shared test-server files, production services, worlds, and databases are not changed or launched. + +The tested plugin stack is MapHide, BlueMap runtime 5.27 (manifest 5.26), and PlaceholderAPI 2.12.3, plus the explicit dependency-absence scenarios. The [September verification](2026-09-25-paper-26.3.md) documents an older CMI/CMILib/BlueMap startup deadlock; the wider plugin combination has not been validated on this stable build. OSHI's unrecognized macOS 27 warning and BlueMap's plugin-induced-save notice are nonfatal third-party notices. + +## Manual verification + +1. Use a separate stopped test instance of the verified stable fixture. Install the built MapHide JAR (or run the explicit `deployServers` task), select an unused Minecraft port in its `server.properties`, retain `server-ip=127.0.0.1`, and launch with the selected JDK's `java -Xms512M -Xmx2G -jar paper.jar --nogui`. Stop with `stop`. Do not use the historical ALPHA launcher. For map-browser checks, configure a test-only BlueMap web listener on an unused port, restricted to local access, using BlueMap's supported configuration. +2. Connect a Java client and, in a separately configured Floodgate test stack, a `.bedrock` client. Record each actual server UUID. Exercise `/bmpc`, `/bmpc toggle`, `/bmpc show`, `/bmpc hide`, `/map hide`, and admin target commands. Confirm actual marker visibility and `%maphide_state%` agree, and exact names preserve the Bedrock prefix. Reconnect and restart to check BlueMap's stored player choices. +3. On test LuckPerms only, address permission changes by the actual server UUID, including for Bedrock and renamed accounts. Run `lp user permission set maphide.player.toggle false`, test `/bmpc toggle` and `/map hide`, and confirm denial leaves visibility unchanged; restore the test override with `lp user permission unset maphide.player.toggle`. Deny admin target permissions and check that other players are unaffected. MapHide itself issues no LuckPerms commands. +4. Run `bmpc config set forced-permissions.enabled true`. Check hide-only, show-only, both-node conflicts under each priority, permission removal, and the optional repeating check. Commands must respect forced state. Set a short delay with `bmpc config set toggle-back-after-seconds 2`; rapidly repeat toggles, issue explicit show/hide, disconnect, reconnect, change worlds, and stop/restart while a timer is pending. Confirm no stale timer or unwanted later visibility change. +5. Check first joins versus returning players with both default-visibility settings and both first-join-only modes. Check language fallback, custom translations, config comments/unknown keys, and reload while players are active. Test PlaceholderAPI through the actual scoreboard/chat consumers, including any asynchronous consumer; console null-player parsing alone does not verify those thread interactions. +6. Add LuckPerms, CMI, CMILib, Vault, and other intended integrations one at a time using fresh test configuration, with external integrations inactive. Verify startup, actual marker behavior, and shutdown after each addition. Do not infer full-stack compatibility from this console suite. + +Inventory interactions, custom-item metadata, purchases, refunds, and transaction replay do not apply to MapHide's feature set. Player-connected permissions, live marker rendering, timer expiry, and asynchronous integration behavior were not claimed as automated passes. diff --git a/docs/verification/2026-10-25-paper-26.3-stable-222.md b/docs/verification/2026-10-25-paper-26.3-stable-222.md new file mode 100644 index 0000000..350305f --- /dev/null +++ b/docs/verification/2026-10-25-paper-26.3-stable-222.md @@ -0,0 +1,80 @@ +# Paper 26.3 build 222 STABLE verification, 2026-10-25 + +This upgrade is separate from Oracle toolchain commit `3c80f5c`. It replaces the uncommitted build-175 target with the latest verified STABLE Paper 26.3 build while retaining plugin version 2.0.2 and reserved release build 031. The existing build-175 fixture and dated verification records remain historical evidence. + +## Release selection + +The live [Paper documentation index](https://docs.papermc.io/llms.txt) was consulted first. The official [26.3 build list](https://fill.papermc.io/v3/projects/paper/versions/26.3/builds) identified **222** as both the newest STABLE build and the newest overall build; this was rechecked at `2026-10-25T10:14:34.205003+00:00`. Other Minecraft versions and experimental channels were excluded. + +| Pin | Verified value | +| --- | --- | +| Server | Paper 26.3 build 222 STABLE | +| Published | 2026-10-24T13:18:21Z | +| Commit | `e3ba7b1dc801e0c385051c34781ab30b226abee3` | +| Server size | 55,250,599 bytes | +| Server SHA-256 | `f649771a2190214306fb0cfba22c7b2cbac5df6308b1158309593603ed07d488` | +| API | `io.papermc.paper:paper-api:26.3.build.222-stable` | +| Isolated fixture | `servers/Paper-26.3-stable-222` | + +The [exact Fill record](https://fill.papermc.io/v3/projects/paper/versions/26.3/builds/222), [Maven metadata](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/maven-metadata.xml), and [published API POM](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.222-stable/paper-api-26.3.build.222-stable.pom) agree. The server was downloaded from the official Fill URL and verified against its size and checksum. `gradle.lockfile` records the resolved API and transitive dependencies. + +## Compatibility and review + +Reviewed official [project setup](https://docs.papermc.io/paper/dev/project-setup/), [Java requirements](https://docs.papermc.io/paper/getting-started/), [plugin metadata](https://docs.papermc.io/paper/dev/plugin-yml/), [lifecycle](https://docs.papermc.io/paper/dev/how-do-plugins-work/), [scheduler](https://docs.papermc.io/paper/dev/scheduler/), [events](https://docs.papermc.io/paper/dev/event-listeners/), and [roadmap](https://docs.papermc.io/paper/dev/roadmap/). + +The [hosted 26.3 Javadocs](https://jd.papermc.io/paper/26.3/) and exact [222-stable Javadoc artifact](https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.222-stable/paper-api-26.3.build.222-stable-javadoc.jar) both identify build 222. Exact signatures/contracts were checked for JavaPlugin enable/disable and metadata, synchronous Runnable scheduler overloads and cancellation, exact local player/UUID lookup, event priority/cancellation, and configuration comments. No plugin Java-source change was needed; the clean compiler emitted no deprecation warnings. Existing command behavior, permissions, plugin identity, main class, and `api-version: 26.3` are preserved. + +Builds 176–222 were reviewed from Fill commit metadata. Relevant fixes include Java 27 UnixSystem startup handling (193), shutdown deadlock handling (209), cancelled running scheduler state (221), and regeneration of missing worldgen settings (222). MapHide uses supported API scheduling and does not inspect scheduler internals. The roadmap's ItemStack/NMS changes do not affect its feature set. + +| Review axis | Findings and resolution | +| --- | --- | +| Standards | The deployment guard only covered PaperScript launches. It now checks direct Java processes before any JAR mutation and fails closed on inspection errors. No unresolved finding. | +| Spec | The smoke default could select the old fixture; it now follows `localPaperServerDir`. The deployment finding was also resolved. An inherited closed-stdin shutdown error could skip process cleanup; bounded wait/terminate/kill now runs even after a broken pipe, and reader cleanup always runs. No unresolved finding. | + +Smoke preparation also stops importing source worlds and Paper/Bukkit/Spigot configuration, ensuring disposable worlds and default server configuration even when a source fixture has been used manually. Regression tests cover this behavior. `build` no longer deploys automatically; `deployServers` remains an explicit stopped-server operation. + +## Final build and runtime results + +```sh +./build.sh --no-daemon --warning-mode all -PreleaseBuildNumber=031 clean build verifyLocalPaperServer +python3 scripts/smoke-test.py +``` + +- Final locked clean build: passed, all 12 tasks executed. No compiler or Gradle deprecation warnings. +- Gradle/compiler: Oracle `25.0.5.0.3+1-LTS-2` at `/Library/Java/JavaVirtualMachines/jdk-25.0.5.0.3.jdk/Contents/Home`. +- Java tests and all Paper processes: Oracle `27.0.1.0.3+1-2` at `/Library/Java/JavaVirtualMachines/jdk-27.0.1.0.3.jdk/Contents/Home`. +- Java tests: **3 passed**, including actual test-worker and child-JVM identity checks. +- Python suite: **48 passed**, including fixture/pin validation, deployment preflight, fixture selection/isolation, shutdown failures, error filtering, and JDK selection. +- All **12 plugin classes** and the test classes retain Java 25 bytecode (69.0, no preview). Every source compilation uses Oracle 25 and `--release 25`. +- Final Paper smoke suite: **93 checks across four lifecycles**, all exit codes 0. + +| Scenario | Passed checks | Exit code | +| --- | --- | --- | +| initial | 29 | 0 | +| restart | 33 | 0 | +| without-placeholderapi | 29 | 0 | +| without-bluemap | 2 | 0 | + +Artifact: `build/libs/1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar`. SHA-256: `3f3c7ebcf58f031f4129b3290c112c42f42ac52fa8f4c30af4947b4d38441785`. Generated build metadata records API 222-stable, server build 222, and channel STABLE. + +The deployment guard was also tested against a real running disposable Paper process: it refused the active JVM and accepted the same directory after shutdown. No deployment occurred. A separate harmless Oracle 27 probe independently exercised the same guard. + +## Safe defaults and isolation + +The official [global configuration](https://docs.papermc.io/paper/reference/global-configuration/) and [world configuration](https://docs.papermc.io/paper/reference/world-configuration/) references were reviewed and compared with the final generated server configuration. Piston duplication, unsafe end-portal teleportation, headless pistons, permanent-block exploits, disabled tripwire updates, and skipped tripwire-hook validation remain false. Oversized-component sanitizer exclusions remain empty. No exploit exceptions were introduced. + +The new fixture reads only binaries, accepted terms, and runtime caches from the prior fixture. All **217 source files remained byte-identical** after preparation and testing. Existing Paper 26.3 build-175, historical ALPHA, Paper 26.2, shared test, and production servers were not launched or deployed to. Test worlds were disposable, Minecraft listened only on localhost, and BlueMap HTTP/RCON/query were disabled. + +## Evidence and remaining limits + +- [Final build log](../../.scratch/paper-latest/build-final.log) +- [Final smoke results](../../servers/verification/build-031-20261025T101417Z/results.json) +- [Smoke command output](../../.scratch/paper-latest/smoke-final.log) +- [Latest-build recheck](../../.scratch/paper-latest/final-latest-check.json) +- [Generated safe-default checks](../../.scratch/paper-latest/config-default-checks-final.json) +- [Live/stopped deployment-guard evidence](../../.scratch/paper-latest/live-deployment-guard.json) +- [Java test results](../../build/reports/tests/test/index.html) + +Remote CI was not run and nothing was pushed. Console verification does not cover connected-player marker changes, permission conflicts, timer expiry, reconnects, asynchronous placeholder consumers, Folia, or the complete CMI/LuckPerms/Vault stack. The existing [manual checks](2026-10-11-paper-26.3-stable.md#manual-verification) still apply; use the current build-222 fixture and validated Oracle 27 runtime. Existing synchronous config/translation I/O was not redesigned by this compatibility upgrade. + +Deployment inspection cannot coordinate with an unrelated launcher starting after preflight; keep the fixture stopped throughout replacement. Historical records describe their original runtimes and pins. Official documentation and Javadocs were inspected in ignored analysis storage, not vendored into the repository. diff --git a/gradle.lockfile b/gradle.lockfile new file mode 100644 index 0000000..8335e84 --- /dev/null +++ b/gradle.lockfile @@ -0,0 +1,59 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :dependencies --write-locks +biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath +com.flowpowered:flow-math:1.0.3=compileClasspath +com.google.auto.service:auto-service-annotations:1.1.1=compileClasspath +com.google.code.gson:gson:2.14.0=compileClasspath +com.google.errorprone:error_prone_annotations:2.48.0=compileClasspath +com.google.guava:failureaccess:1.0.3=compileClasspath +com.google.guava:guava:33.6.0-jre=compileClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath +com.google.j2objc:j2objc-annotations:3.1=compileClasspath +com.mojang:brigadier:1.3.11=compileClasspath +de.bluecolored:bluemap-api:2.8.0=compileClasspath +io.papermc.paper:paper-api:26.3.build.222-stable=compileClasspath +it.unimi.dsi:fastutil:8.5.18=compileClasspath +javax.inject:javax.inject:1=compileClasspath +junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +me.clip:placeholderapi:2.12.3=compileClasspath +net.kyori:adventure-api:5.2.0=compileClasspath +net.kyori:adventure-bom:5.2.0=compileClasspath +net.kyori:adventure-key:5.2.0=compileClasspath +net.kyori:adventure-text-logger-slf4j:5.2.0=compileClasspath +net.kyori:adventure-text-minimessage:5.2.0=compileClasspath +net.kyori:adventure-text-serializer-gson:5.2.0=compileClasspath +net.kyori:adventure-text-serializer-json:5.2.0=compileClasspath +net.kyori:adventure-text-serializer-legacy:5.2.0=compileClasspath +net.kyori:adventure-text-serializer-plain:5.2.0=compileClasspath +net.kyori:option:1.1.0=compileClasspath +net.md-5:bungeecord-chat:1.21-R0.2-deprecated+build.21=compileClasspath +org.apache.commons:commons-lang3:3.12.0=compileClasspath +org.apache.logging.log4j:log4j-api:2.26.0=compileClasspath +org.apache.maven.resolver:maven-resolver-api:1.9.18=compileClasspath +org.apache.maven.resolver:maven-resolver-impl:1.9.18=compileClasspath +org.apache.maven.resolver:maven-resolver-named-locks:1.9.18=compileClasspath +org.apache.maven.resolver:maven-resolver-spi:1.9.18=compileClasspath +org.apache.maven.resolver:maven-resolver-util:1.9.18=compileClasspath +org.apache.maven:maven-artifact:3.9.6=compileClasspath +org.apache.maven:maven-builder-support:3.9.6=compileClasspath +org.apache.maven:maven-model-builder:3.9.6=compileClasspath +org.apache.maven:maven-model:3.9.6=compileClasspath +org.apache.maven:maven-repository-metadata:3.9.6=compileClasspath +org.apache.maven:maven-resolver-provider:3.9.6=compileClasspath +org.checkerframework:checker-qual:4.2.3=compileClasspath +org.codehaus.plexus:plexus-interpolation:1.26=compileClasspath +org.codehaus.plexus:plexus-utils:3.5.1=compileClasspath +org.eclipse.sisu:org.eclipse.sisu.inject:0.9.0.M2=compileClasspath +org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:26.1.0=compileClasspath +org.joml:joml:1.10.9=compileClasspath +org.jspecify:jspecify:1.0.0=compileClasspath +org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath +org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath +org.osgi:org.osgi.resource:1.0.0=compileClasspath +org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath +org.slf4j:slf4j-api:2.0.17=compileClasspath +org.yaml:snakeyaml:2.2=compileClasspath +empty=annotationProcessor,testAnnotationProcessor diff --git a/gradle.properties b/gradle.properties index c16b2d3..d1937f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,9 +8,10 @@ org.gradle.java.installations.auto-detect=false org.gradle.java.installations.auto-download=false org.gradle.configuration-cache=false paperTarget=26.3 -paperApiVersion=26.3.build.41-alpha -paperBuild=41 -paperChannel=ALPHA -paperSha256=2b77166ee61886a9bc9ab33dc9e4847fa3538b36d9ba6e5f2fa7ed90973aa748 +paperApiVersion=26.3.build.222-stable +paperBuild=222 +paperChannel=STABLE +paperSha256=f649771a2190214306fb0cfba22c7b2cbac5df6308b1158309593603ed07d488 +localPaperServerDir=servers/Paper-26.3-stable-222 blueMapApiVersion=2.8.0 placeholderApiVersion=2.12.3 diff --git a/scripts/deploy_guard.py b/scripts/deploy_guard.py new file mode 100644 index 0000000..4158737 --- /dev/null +++ b/scripts/deploy_guard.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Refuse local deployment while a launcher or direct Java server is active.""" + +import argparse +from pathlib import Path +import shutil +import subprocess +import sys + + +def java_process_ids(output): + """Parse lsof's process/command fields and its implicit descriptor fields.""" + processes = [] + current = None + for line in output.splitlines(): + if line.startswith("p") and line[1:].isdigit(): + current = {"pid": line[1:], "command": None, "cwd": False} + processes.append(current) + elif current is not None and line.startswith("c") and line[1:] and current["command"] is None: + current["command"] = line[1:] + elif current is not None and line == "fcwd": + current["cwd"] = True + else: + raise RuntimeError("Cannot verify stopped server: unexpected lsof output; deployment refused") + if not processes or any(not process["command"] or not process["cwd"] for process in processes): + raise RuntimeError("Cannot verify stopped server: incomplete lsof output; deployment refused") + return [process["pid"] for process in processes + if Path(process["command"]).name.lower() in ("java", "javaw")] + + +def verify_stopped_server(server): + server = Path(server).resolve(strict=True) + if not server.is_dir(): + raise RuntimeError(f"Server directory is not a directory: {server}") + launcher_lock = server / "paperscript/locks/server-launch" + if launcher_lock.exists() or launcher_lock.is_symlink(): + raise RuntimeError(f"Stop the maintained launcher before deploying: {server}") + lsof = shutil.which("lsof") + if not lsof: + raise RuntimeError("Cannot verify stopped server: lsof is unavailable; deployment refused") + try: + result = subprocess.run( + [lsof, "-nP", "+c", "0", "-a", "-d", "cwd", "-Fpc", "--", str(server)], + text=True, capture_output=True, timeout=15) + except (OSError, subprocess.SubprocessError) as error: + raise RuntimeError(f"Cannot verify stopped server: lsof inspection failed; deployment refused ({error})") from error + # lsof exits 1 with no output when no process has this directory as its cwd. + if result.returncode == 1 and not result.stdout and not result.stderr: + return server + if result.returncode != 0 or result.stderr: + detail = result.stderr.strip() or f"exit status {result.returncode}" + raise RuntimeError(f"Cannot verify stopped server: lsof inspection failed; deployment refused ({detail})") + running = java_process_ids(result.stdout) + if running: + raise RuntimeError(f"Stop Java process(es) {', '.join(running)} in {server} before deploying") + return server + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("server", type=Path) + args = parser.parse_args() + server = verify_stopped_server(args.server) + print(f"Verified stopped server for deployment: {server}") + + +if __name__ == "__main__": + try: + main() + except (OSError, RuntimeError) as error: + sys.exit(f"Deployment preflight failed: {error}") diff --git a/scripts/paper_fixture.py b/scripts/paper_fixture.py new file mode 100644 index 0000000..79d3ac6 --- /dev/null +++ b/scripts/paper_fixture.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Prepare or verify a pinned, isolated Paper STABLE fixture without copying player data.""" + +import argparse +import hashlib +import json +from pathlib import Path +import re +import shutil +import urllib.request + +ROOT = Path(__file__).resolve().parents[1] +USER_AGENT = "MapHideCompatibility/2.0.2 (https://github.com/mrfdev/1MB-MapHide)" + + +def properties(text): + return dict(line.split("=", 1) for line in text.splitlines() + if "=" in line and not line.startswith("#")) + + +def sha256(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def validate_pins(config): + if config["paperTarget"] != "26.3" or config["paperChannel"] != "STABLE": + raise RuntimeError("The maintained target must remain Paper 26.3 STABLE") + expected = f"{config['paperTarget']}.build.{config['paperBuild']}-stable" + if config["paperApiVersion"] != expected: + raise RuntimeError("Paper API does not match the stable server build") + if not re.fullmatch(r"[0-9a-f]{64}", config["paperSha256"]): + raise RuntimeError("Paper SHA-256 pin is invalid") + + +def verify_server(server, config): + validate_pins(config) + state = json.loads((server / "paper-release.json").read_text()) + for key in ("paperTarget", "paperBuild", "paperChannel", "paperApiVersion", "paperSha256"): + if state.get(key) != config[key]: + raise RuntimeError(f"Fixture {key} does not match the release pin") + name = state["jar"] + if Path(name).name != name or not name.endswith(".jar"): + raise RuntimeError("Paper JAR must be inside the fixture") + paper = server / name + if paper.is_symlink() or sha256(paper) != config["paperSha256"]: + raise RuntimeError("Paper JAR checksum mismatch") + return paper + + +def fetch(url): + return urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": USER_AGENT}), timeout=60) + + +def prepare_server(source, destination, config): + validate_pins(config) + # Refuse existing destinations before downloading or copying anything. + if destination.exists() or destination.is_symlink(): + raise RuntimeError(f"Refusing to replace an existing fixture: {destination}") + if not re.search(r"(?m)^eula=true\s*$", (source / "eula.txt").read_text()): + raise RuntimeError("Source fixture must already have an accepted Minecraft EULA") + if not re.search(r"(?m)^accept-download:\s*true\s*$", (source / "plugins/BlueMap/core.conf").read_text()): + raise RuntimeError("Source fixture must already accept BlueMap's resource download terms") + plugins = [] + for pattern in ("bluemap-*.jar", "PlaceholderAPI-*.jar"): + matches = list((source / "plugins").glob(pattern)) + if len(matches) != 1: + raise RuntimeError(f"Expected exactly one {pattern} in the source fixture") + plugins.append(matches[0]) + url = f"https://fill.papermc.io/v3/projects/paper/versions/{config['paperTarget']}/builds/{config['paperBuild']}" + with fetch(url) as response: + build = json.load(response) + download = build["downloads"]["server:default"] + if (str(build["id"]) != config["paperBuild"] or build["channel"] != "STABLE" + or download["checksums"]["sha256"] != config["paperSha256"]): + raise RuntimeError("Official Fill metadata does not match the stable release pins") + if not download["url"].startswith("https://fill-data.papermc.io/"): + raise RuntimeError("Unexpected Paper download host") + destination.mkdir(parents=True) + paper = destination / "paper.jar" + with fetch(download["url"]) as response, paper.open("xb") as output: + shutil.copyfileobj(response, output) + if paper.stat().st_size != download["size"] or sha256(paper) != config["paperSha256"]: + raise RuntimeError("Downloaded Paper JAR size/checksum mismatch") + shutil.copy2(source / "eula.txt", destination / "eula.txt") + # Only runtime caches and plugin binaries; never worlds, users, credentials or plugin settings. + for name in ("libraries", "versions", "cache"): + if (source / name).is_dir(): + shutil.copytree(source / name, destination / name) + (destination / "plugins/BlueMap").mkdir(parents=True) + for plugin in plugins: + shutil.copy2(plugin, destination / "plugins" / plugin.name) + (destination / "plugins/BlueMap/core.conf").write_text( + 'accept-download: true\ndata: "bluemap"\nrender-thread-count: 1\nmetrics: false\n') + (destination / "plugins/BlueMap/webserver.conf").write_text("enabled: false\n") + (destination / "plugins/PlaceholderAPI").mkdir() + (destination / "plugins/PlaceholderAPI/config.yml").write_text("check_updates: false\n") + (destination / "plugins/bStats").mkdir() + (destination / "plugins/bStats/config.yml").write_text("enabled: false\n") + (destination / "bluemap").mkdir() + client = source / f"bluemap/minecraft-client-{config['paperTarget']}.jar" + if client.is_file(): + shutil.copy2(client, destination / "bluemap" / client.name) + (destination / "server.properties").write_text( + "server-ip=127.0.0.1\nserver-port=0\nenable-rcon=false\nenable-query=false\n" + "online-mode=true\nlevel-name=world\n" + "generate-structures=false\nview-distance=2\nsimulation-distance=2\n" + "spawn-protection=0\npause-when-empty-seconds=-1\n") + state = {key: config[key] for key in ( + "paperTarget", "paperBuild", "paperChannel", "paperApiVersion", "paperSha256")} + state.update(jar="paper.jar", metadata_url=url, download=download, + plugins={p.name: sha256(p) for p in plugins}) + (destination / "paper-release.json").write_text(json.dumps(state, indent=2) + "\n") + verify_server(destination, config) + print(f"Prepared {destination}; no server has been started") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=("prepare", "verify")) + parser.add_argument("--server", type=Path) + parser.add_argument("--fixtures", type=Path, default=ROOT / "servers/Paper-26.3") + args = parser.parse_args() + config = properties((ROOT / "gradle.properties").read_text()) + server = (args.server or ROOT / config["localPaperServerDir"]).absolute() + if args.action == "prepare": + prepare_server(args.fixtures.resolve(), server, config) + else: + print(f"Verified Paper {config['paperTarget']} build {config['paperBuild']} STABLE: " + f"{verify_server(server, config)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke-test.py b/scripts/smoke-test.py index c902d71..b2c4e96 100644 --- a/scripts/smoke-test.py +++ b/scripts/smoke-test.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 -"""Exercise Paper in disposable localhost servers using installed Oracle JDK 27.""" +"""Exercise Paper 26.3 STABLE in disposable localhost servers on installed Oracle JDK 27.""" import argparse from datetime import datetime, timezone -import hashlib import json import os from pathlib import Path @@ -15,9 +14,9 @@ import time import zipfile +from paper_fixture import properties, sha256, verify_server from java_toolchains import resolve_jdk - ROOT = Path(__file__).resolve().parents[1] @@ -28,15 +27,8 @@ def resolve_test_runtimes(java_homes=None): return [resolve_jdk(27)] - -def properties(text): - return dict(line.split("=", 1) for line in text.splitlines() - if "=" in line and not line.startswith("#")) - - -def sha256(path): - with path.open("rb") as stream: - return hashlib.file_digest(stream, "sha256").hexdigest() +def resolve_server_path(server, config): + return (server or ROOT / config["localPaperServerDir"]).resolve() def free_port(): @@ -45,17 +37,23 @@ def free_port(): return sock.getsockname()[1] +def unexpected_errors(lines, metadata, missing_bluemap=False): + failures = [line.strip() for line in lines if re.search( + r"(?:/|\s)(?:ERROR|SEVERE)\]|Exception|UnsupportedClassVersionError|NoClassDefFoundError|NoSuchMethodError", line)] + if missing_bluemap: + artifact = (f"1MB-BlueMap-MapHide-v{metadata['pluginVersion']}-{metadata['buildNumber']}" + f"-j{metadata['javaTarget']}-{metadata['paperTarget']}.jar") + header = f"[ModernPluginLoadingStrategy] Could not load 'plugins/{artifact}' in 'plugins'" + exception = ("org.bukkit.plugin.UnknownDependencyException: Unknown/missing dependency plugins: " + "[BlueMap]. Please download and install these plugins to run '1MB-MapHide'.") + failures = [line for line in failures if not (line.endswith(header) or line == exception)] + return failures + + def prepare_server(source, destination, artifact, metadata): """Copy fixtures; never start or edit the maintained world's files.""" - state = json.loads((source / "paperscript/state.json").read_text()) - if (state["current_version"] != metadata["paperTarget"] - or str(state["current_build"]) != metadata["paperBuild"] - or state["current_channel"] != metadata["paperChannel"]): - raise RuntimeError("Maintained Paper version/build does not match the artifact") - paper = source / state["current_jar"] - pinned_sha256 = properties((ROOT / "gradle.properties").read_text())["paperSha256"] - if sha256(paper) != state["current_sha256"] or state["current_sha256"] != pinned_sha256: - raise RuntimeError("Maintained Paper checksum mismatch") + config = properties((ROOT / "gradle.properties").read_text()) + paper = verify_server(source, config) if not re.search(r"(?m)^eula=true$", (source / "eula.txt").read_text()): raise RuntimeError("The maintained test server must already have an accepted EULA") destination.mkdir(parents=True) @@ -64,12 +62,14 @@ def prepare_server(source, destination, artifact, metadata): world = server_properties.get("level-name", "world") if Path(world).name != world: raise RuntimeError("Smoke fixtures require a world folder inside the server directory") - for name in ("libraries", "versions", "cache", "config", world, "bluemap"): + # Generate new worlds/configuration even if the source fixture was used manually. + for name in ("libraries", "versions", "cache", "bluemap"): if (source / name).is_dir(): shutil.copytree(source / name, destination / name, ignore=shutil.ignore_patterns("session.lock", "logs", "*.log")) - for name in ("eula.txt", "bukkit.yml", "spigot.yml"): - shutil.copy2(source / name, destination / name) + for name in ("eula.txt",): + if (source / name).is_file(): + shutil.copy2(source / name, destination / name) server_properties.update({"server-ip": "127.0.0.1", "server-port": str(free_port()), "enable-rcon": "false", "enable-query": "false", "online-mode": "true", "pause-when-empty-seconds": "-1"}) @@ -86,17 +86,50 @@ def prepare_server(source, destination, artifact, metadata): shutil.copytree(source / "plugins/BlueMap", plugins / "BlueMap") # Rendering/API checks do not require a second HTTP listener. (plugins / "BlueMap/webserver.conf").write_text("enabled: false\n") + (plugins / "bStats").mkdir() + (plugins / "bStats/config.yml").write_text("enabled: false\n") (plugins / "PlaceholderAPI").mkdir() (plugins / "PlaceholderAPI/config.yml").write_text("check_updates: false\n") (plugins / "1MB-MapHide").mkdir() with zipfile.ZipFile(artifact) as jar: - config = jar.read("config.yml").decode() + plugin_config = jar.read("config.yml").decode() (plugins / "1MB-MapHide/config.yml").write_text( - "# Preserve this smoke-test comment.\n" + config + "\nsmoke-custom-key: preserved\n") - return state["current_sha256"] + "# Preserve this smoke-test comment.\n" + plugin_config + "\nsmoke-custom-key: preserved\n") + return config["paperSha256"] -def run_server(directory, java_home, version, metadata): +def stop_server(process, stop_timeout=60, terminate_timeout=10, kill_timeout=5): + """Reap the child even when it has closed stdin; forced shutdown still fails verification.""" + if process.poll() is not None: + return + try: + process.stdin.write("stop\n") + process.stdin.flush() + except OSError: + # The child can close stdin before exiting. Still wait and enforce the deadline. + pass + try: + process.wait(timeout=stop_timeout) + except subprocess.TimeoutExpired: + try: + process.terminate() + except ProcessLookupError: + pass + try: + process.wait(timeout=terminate_timeout) + except subprocess.TimeoutExpired: + try: + process.kill() + except ProcessLookupError: + pass + try: + process.wait(timeout=kill_timeout) + except subprocess.TimeoutExpired as error: + raise RuntimeError("Paper could not be reaped after forced shutdown") from error + raise RuntimeError(f"Paper did not shut down cleanly within {stop_timeout} seconds") + + +def run_server(directory, java_home, version, metadata, phase="initial", papi=True, missing_bluemap=False): jdk = resolve_jdk(27, override=str(java_home)) java_home = Path(jdk["home"]) if jdk["version"] != version: @@ -108,17 +141,19 @@ def run_server(directory, java_home, version, metadata): if not re.search(rf'(?:java|openjdk) version "{re.escape(version)}"', runtime): raise RuntimeError(f"Expected Java {version}: {runtime}") (directory / "java-version.txt").write_text(runtime + "\n") - print(f"Starting Paper {metadata['paperTarget']} build {metadata['paperBuild']} on {version}", flush=True) + print(f"Starting Paper {metadata['paperTarget']} build {metadata['paperBuild']} on {version} ({phase})", flush=True) lines = [] checks = [] bluemap = list((directory / "plugins").glob("bluemap-*.jar")) + if missing_bluemap: + bluemap = list((directory / "plugins").glob("bluemap-*.jar.disabled")) if len(bluemap) != 1: raise RuntimeError("Expected exactly one BlueMap runtime jar") with zipfile.ZipFile(bluemap[0]) as jar: manifest = jar.read("plugin.yml").decode() bluemap_version = json.loads(jar.read("de/bluecolored/bluemap/version.json"))["version"] bluemap_plugin_version = re.search(r"(?m)^version:\s*['\"]?([^'\"\s]+)", manifest).group(1) - with (directory / "console.log").open("w") as log: + with (directory / f"console-{phase}.log").open("w") as log: process = subprocess.Popen( [jdk["java"], "-Xms512M", "-Xmx2G", "-XX:ActiveProcessorCount=4", "-Dfile.encoding=UTF-8", "-Dterminal.ansi=false", "-Dterminal.jline=false", @@ -155,73 +190,79 @@ def command(value, *expected): expect(value, expected, start) try: - expect("startup and plugin loading", ["Done (", "[1MB-MapHide] 1MB-MapHide enabled", - "PlaceholderAPI expansion registered"], timeout=180) - expect("BlueMap ready", [f"[BlueMap] Enabling BlueMap v{bluemap_plugin_version}", - "[BlueMap] Loaded!"], timeout=180) - command("bmpc status", f"Build: {metadata['buildNumber']}", - f"API {metadata['paperApiVersion']}", f"Java: {version}", - f"BlueMap: {bluemap_version}, API:", f"{metadata['paperTarget']}-{metadata['paperBuild']}-") - command("bmpc info", f"build {metadata['buildNumber']}", - f"Target: Java {metadata['javaTarget']} / Paper {metadata['paperTarget']}") - command("bmpc version", f"build {metadata['buildNumber']}", "1MB-MapHide info") - for value, expected in (("bmpc help", "(page 1/"), - ("bmpc help 2", "(page 2/"), - ("bmpc debug status", "Force perms:"), - ("bmpc debug commands", "Command routes"), - ("bmpc debug permissions", "Permission debug"), - ("bmpc debug placeholders", "PlaceholderAPI enabled: true"), - ("bmpc config", "Config values")): - command(value, expected) - for value in ("bmpc", "bmpc toggle", "bmpc show", "bmpc hide", "map hide"): - command(value, "Only players can use this command.") - command("bmpc hide MapHideMissing", 'Player "MapHideMissing" was not found.') - command("bmpc help invalid", "Invalid page:") - command("bmpc config set unknown-key invalid", "Unknown config key:") - command("papi parse --null %maphide_state%|%maphide_default_visibility%|%maphide_toggle_back_seconds%|%maphide_language%", - "unknown|visible|0|EN") - command("bmpc config set default-visibility hide", "Updated default-visibility to hide.") - command("bmpc config set toggle-back-after-seconds 2", "Updated toggle-back-after-seconds to 2.") - command("bmpc reload", "Configuration and translations reloaded.") - command("papi parse --null %maphide_default_visibility%|%maphide_toggle_back_seconds%", - "hidden|2") - command("bmpc config set default-visibility show", "Updated default-visibility to show.") - command("bmpc config set toggle-back-after-seconds 0", "Updated toggle-back-after-seconds to 0.") - config = (directory / "plugins/1MB-MapHide/config.yml").read_text() - if "# Preserve this smoke-test comment." not in config or "smoke-custom-key: preserved" not in config: - raise RuntimeError("Config reload discarded a custom comment or unknown key") - checks.append("config comments and custom keys preserved") - command("bmpc status", f"Java: {version}", f"BlueMap: {bluemap_version}, API:") + if missing_bluemap: + expect("missing BlueMap rejected", ["Done (", "Unknown/missing dependency plugins: [BlueMap]"], timeout=180) + if "[1MB-MapHide] 1MB-MapHide enabled" in "".join(lines): + raise RuntimeError("MapHide enabled without its required BlueMap dependency") + else: + expected = ["Done (", "[1MB-MapHide] 1MB-MapHide enabled"] + if papi: + expected.append("PlaceholderAPI expansion registered") + expect("startup and plugin loading", expected, timeout=180) + expect("BlueMap ready", [f"[BlueMap] Enabling BlueMap v{bluemap_plugin_version}", + "[BlueMap] Loaded!"], timeout=180) + if phase == "restart": + command("papi parse --null %maphide_default_visibility%|%maphide_toggle_back_seconds%", + "hidden|2") + checks.append("configuration persisted across process restart") + if phase != "initial": + command("bmpc config set default-visibility show", "Updated default-visibility to show.") + command("bmpc config set toggle-back-after-seconds 0", "Updated toggle-back-after-seconds to 0.") + command("bmpc status", f"Build: {metadata['buildNumber']}", + f"API {metadata['paperApiVersion']}", f"Java: {version}", + f"BlueMap: {bluemap_version}, API:", f"{metadata['paperTarget']}-{metadata['paperBuild']}-") + command("bmpc info", f"build {metadata['buildNumber']}", + f"Target: Java {metadata['javaTarget']} / Paper {metadata['paperTarget']}") + command("bmpc version", f"build {metadata['buildNumber']}", "1MB-MapHide info") + for value, expected in (("bmpc help", "(page 1/"), + ("bmpc help 2", "(page 2/"), + ("bmpc debug status", "Force perms:"), + ("bmpc debug commands", "Command routes"), + ("bmpc debug permissions", "Permission debug"), + ("bmpc debug placeholders", f"PlaceholderAPI enabled: {str(papi).lower()}"), + ("bmpc config", "Config values")): + command(value, expected) + for value in ("bmpc", "bmpc toggle", "bmpc show", "bmpc hide", "map hide"): + command(value, "Only players can use this command.") + command("bmpc hide MapHideMissing", 'Player "MapHideMissing" was not found.') + command("bmpc hide @e[type=invalid]", 'Player "@e[type=invalid]" was not found.') + command("bmpc help invalid", "Invalid page:") + command("bmpc config set unknown-key invalid", "Unknown config key:") + if papi: + command("papi parse --null %maphide_state%|%maphide_default_visibility%|%maphide_toggle_back_seconds%|%maphide_language%", + "unknown|visible|0|EN") + command("bmpc config set default-visibility hide", "Updated default-visibility to hide.") + command("bmpc config set toggle-back-after-seconds 2", "Updated toggle-back-after-seconds to 2.") + command("bmpc reload", "Configuration and translations reloaded.") + if papi: + command("papi parse --null %maphide_default_visibility%|%maphide_toggle_back_seconds%", + "hidden|2") + config = (directory / "plugins/1MB-MapHide/config.yml").read_text() + if "# Preserve this smoke-test comment." not in config or "smoke-custom-key: preserved" not in config: + raise RuntimeError("Config reload discarded a custom comment or unknown key") + checks.append("config comments and custom keys preserved") + command("bmpc status", f"Java: {version}", f"BlueMap: {bluemap_version}, API:") finally: - if process.poll() is None: - process.stdin.write("stop\n") - process.stdin.flush() - try: - process.wait(timeout=60) - except subprocess.TimeoutExpired: - process.terminate() - try: - process.wait(timeout=10) - except subprocess.TimeoutExpired: - process.kill() - process.wait() - raise RuntimeError("Paper did not shut down cleanly within 60 seconds") - reader.join(timeout=5) - expect("clean shutdown", ["[1MB-MapHide] 1MB-MapHide disabled", "[BlueMap] Saved and stopped!", - "All RegionFile I/O tasks to complete"]) + try: + stop_server(process) + finally: + reader.join(timeout=5) + shutdown = ["All RegionFile I/O tasks to complete"] + if not missing_bluemap: + shutdown += ["[1MB-MapHide] 1MB-MapHide disabled", "[BlueMap] Saved and stopped!"] + expect("clean shutdown", shutdown) if process.returncode != 0: raise RuntimeError(f"Paper exited with {process.returncode}") - failures = [line.strip() for line in lines if re.search( - r"(?:/|\s)(?:ERROR|SEVERE)\]|Exception|UnsupportedClassVersionError|NoClassDefFoundError|NoSuchMethodError", line)] + failures = unexpected_errors(lines, metadata, missing_bluemap) if failures: raise RuntimeError("Errors in server log:\n" + "\n".join(failures)) - return {"runtime": runtime, "java_toolchain": jdk, "checks": checks, "exit_code": process.returncode, - "console_log": str(directory / "console.log")} + return {"runtime": runtime, "java_toolchain": jdk, "phase": phase, "checks": checks, "exit_code": process.returncode, + "console_log": str(directory / f"console-{phase}.log")} def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--server", type=Path, default=ROOT / "servers/Paper-26.3") + parser.add_argument("--server", type=Path, help="Defaults to localPaperServerDir in gradle.properties") parser.add_argument("--output", type=Path) parser.add_argument("--java-home", type=Path, action="append", help="Installed Oracle JDK 27 home; repeat for multiple Oracle 27 installations. " @@ -229,6 +270,7 @@ def main(): args = parser.parse_args() runtimes = resolve_test_runtimes(args.java_home) config = properties((ROOT / "gradle.properties").read_text()) + server = resolve_server_path(args.server, config) build = (ROOT / "build-number.txt").read_text().strip() artifact = ROOT / "build/libs" / ( f"1MB-BlueMap-MapHide-v{config['pluginVersion']}-{build}-j{config['javaTarget']}-{config['paperTarget']}.jar") @@ -246,12 +288,28 @@ def main(): java_home = Path(jdk["home"]) version = jdk["version"] directory = output / f"java-{version}-{index}" - report["paper_sha256"] = prepare_server(args.server.resolve(), directory, artifact, metadata) - report["runs"].append(run_server(directory, java_home, version, metadata)) + report["paper_sha256"] = prepare_server(server, directory, artifact, metadata) + for phase in ("initial", "restart", "without-placeholderapi", "without-bluemap"): + papi = phase != "without-placeholderapi" + missing = phase == "without-bluemap" + if not papi: + dependency = next((directory / "plugins").glob("PlaceholderAPI-*.jar")) + dependency.rename(dependency.with_suffix(".jar.disabled")) + if missing: + dependency = next((directory / "plugins").glob("PlaceholderAPI-*.jar.disabled")) + dependency.rename(dependency.with_suffix("")) + dependency = next((directory / "plugins").glob("bluemap-*.jar")) + dependency.rename(dependency.with_suffix(".jar.disabled")) + report["runs"].append(run_server(directory, java_home.resolve(), version, metadata, + phase, papi, missing)) + except Exception as error: + report["failure"] = str(error) + raise finally: (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") print(f"PASS: {len(report['runs'])} server lifecycles. Results: {output / 'results.json'}", flush=True) + if __name__ == "__main__": main() diff --git a/scripts/tests/test_deploy_guard.py b/scripts/tests/test_deploy_guard.py new file mode 100644 index 0000000..5545a8c --- /dev/null +++ b/scripts/tests/test_deploy_guard.py @@ -0,0 +1,92 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from deploy_guard import verify_stopped_server + + +class DeploymentGuardTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.server = Path(self.temp.name).resolve() + + def inspect(self, returncode=1, stdout="", stderr=""): + return patch("deploy_guard.subprocess.run", return_value=subprocess.CompletedProcess( + [], returncode, stdout, stderr)) + + def test_stopped_server_with_no_cwd_processes_passes(self): + with patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), self.inspect() as run: + self.assertEqual(self.server, verify_stopped_server(self.server)) + self.assertEqual(["/usr/sbin/lsof", "-nP", "+c", "0", "-a", "-d", "cwd", "-Fpc", "--", + str(self.server)], run.call_args.args[0]) + + def test_direct_java_launch_is_rejected_before_world_files_exist(self): + with patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + self.inspect(0, "p123\ncjava\nfcwd\n"): + with self.assertRaisesRegex(RuntimeError, "Stop Java process.*123"): + verify_stopped_server(self.server) + + def test_non_java_shell_in_server_directory_does_not_block(self): + with patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + self.inspect(0, "p123\nczsh\nfcwd\np456\nclsof\nfcwd\n"): + self.assertEqual(self.server, verify_stopped_server(self.server)) + + def test_java_is_detected_among_other_processes(self): + with patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + self.inspect(0, "p12\nczsh\nfcwd\np34\ncjava\nfcwd\np56\ncjava\nfcwd\n"): + with self.assertRaisesRegex(RuntimeError, "Stop Java process.*34, 56"): + verify_stopped_server(self.server) + + def test_existing_paperscript_lock_is_rejected_without_process_probe(self): + lock = self.server / "paperscript/locks/server-launch" + lock.mkdir(parents=True) + with patch("deploy_guard.subprocess.run") as run: + with self.assertRaisesRegex(RuntimeError, "Stop the maintained launcher"): + verify_stopped_server(self.server) + run.assert_not_called() + + def test_stale_world_session_file_does_not_block_stopped_server(self): + world = self.server / "world" + world.mkdir() + (world / "session.lock").write_bytes(b"stale persistent Minecraft lock file") + with patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), self.inspect(): + self.assertEqual(self.server, verify_stopped_server(self.server)) + + def test_missing_lsof_fails_closed(self): + with patch("deploy_guard.shutil.which", return_value=None), patch("deploy_guard.subprocess.run") as run: + with self.assertRaisesRegex(RuntimeError, "lsof is unavailable"): + verify_stopped_server(self.server) + run.assert_not_called() + + def test_probe_error_or_warning_fails_closed(self): + for returncode, stdout, stderr in ((2, "", "inspection failed"), + (1, "", "permission denied"), + (0, "p123\nczsh\nfcwd\n", "partial process information")): + with self.subTest(returncode=returncode), \ + patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + self.inspect(returncode, stdout, stderr): + with self.assertRaisesRegex(RuntimeError, "lsof inspection failed"): + verify_stopped_server(self.server) + + def test_timeout_or_launch_error_fails_closed(self): + for error in (subprocess.TimeoutExpired("lsof", 15), OSError("unavailable")): + with self.subTest(error=error), patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + patch("deploy_guard.subprocess.run", side_effect=error): + with self.assertRaisesRegex(RuntimeError, "lsof inspection failed"): + verify_stopped_server(self.server) + + def test_incomplete_or_unexpected_success_output_fails_closed(self): + for stdout in ("", "p123\n", "p123\ncjava\n", "p123\ncjava\nfcwd\nunknown\n"): + with self.subTest(stdout=stdout), patch("deploy_guard.shutil.which", return_value="/usr/sbin/lsof"), \ + self.inspect(0, stdout): + with self.assertRaisesRegex(RuntimeError, "lsof output"): + verify_stopped_server(self.server) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_paper_fixture.py b/scripts/tests/test_paper_fixture.py new file mode 100644 index 0000000..4ca02c7 --- /dev/null +++ b/scripts/tests/test_paper_fixture.py @@ -0,0 +1,102 @@ +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from paper_fixture import prepare_server, properties, sha256, validate_pins, verify_server + + +class StableFixtureTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.server = Path(self.temp.name) + self.paper = self.server / "paper.jar" + self.paper.write_bytes(b"test fixture, not a runnable server") + self.config = properties((Path(__file__).resolve().parents[2] / "gradle.properties").read_text()) + self.config["paperSha256"] = sha256(self.paper) + self.state = {key: self.config[key] for key in ( + "paperTarget", "paperBuild", "paperChannel", "paperApiVersion", "paperSha256")} + self.state["jar"] = "paper.jar" + self.write_state() + + def write_state(self): + (self.server / "paper-release.json").write_text(json.dumps(self.state)) + + def test_matching_stable_pin(self): + self.assertEqual(self.paper, verify_server(self.server, self.config)) + + def test_experimental_channel_is_rejected_even_with_matching_api(self): + for channel in ("ALPHA", "BETA"): + with self.subTest(channel=channel): + config = dict(self.config, paperChannel=channel, + paperApiVersion=f"26.3.build.{self.config['paperBuild']}-{channel.lower()}") + with self.assertRaisesRegex(RuntimeError, "26.3 STABLE"): + validate_pins(config) + + def test_other_minecraft_target_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "26.3 STABLE"): + validate_pins(dict(self.config, paperTarget="26.2")) + + def test_mismatched_api_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "API does not match"): + validate_pins(dict(self.config, paperApiVersion="26.3.build.41-alpha")) + + def test_invalid_checksum_pin_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "SHA-256 pin"): + validate_pins(dict(self.config, paperSha256="")) + + def test_stale_fixture_state_is_rejected(self): + for key, value in (("paperBuild", "41"), ("paperChannel", "ALPHA"), + ("paperTarget", "26.2"), ("paperSha256", "0" * 64)): + with self.subTest(key=key): + original = self.state[key] + self.state[key] = value + self.write_state() + with self.assertRaisesRegex(RuntimeError, "does not match"): + verify_server(self.server, self.config) + self.state[key] = original + + def test_modified_jar_is_rejected(self): + self.paper.write_bytes(b"corrupt download") + with self.assertRaisesRegex(RuntimeError, "checksum mismatch"): + verify_server(self.server, self.config) + + def test_path_escape_is_rejected(self): + for name in ("../paper.jar", "/tmp/paper.jar"): + with self.subTest(name=name): + self.state["jar"] = name + self.write_state() + with self.assertRaisesRegex(RuntimeError, "inside the fixture"): + verify_server(self.server, self.config) + + def test_symlinked_jar_is_rejected(self): + real = self.server / "elsewhere.jar" + self.paper.rename(real) + self.paper.symlink_to(real) + with self.assertRaisesRegex(RuntimeError, "checksum mismatch"): + verify_server(self.server, self.config) + + def test_existing_destination_is_never_replaced(self): + before = self.paper.read_bytes() + with patch("paper_fixture.fetch") as network: + with self.assertRaisesRegex(RuntimeError, "Refusing to replace"): + prepare_server(self.server, self.server, self.config) + network.assert_not_called() + self.assertEqual(before, self.paper.read_bytes()) + + def test_eula_is_not_accepted_implicitly(self): + (self.server / "eula.txt").write_text("eula=false\n") + destination = self.server / "new" + with patch("paper_fixture.fetch") as network: + with self.assertRaisesRegex(RuntimeError, "accepted Minecraft EULA"): + prepare_server(self.server, destination, self.config) + network.assert_not_called() + self.assertFalse(destination.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_smoke_cleanup.py b/scripts/tests/test_smoke_cleanup.py new file mode 100644 index 0000000..c506089 --- /dev/null +++ b/scripts/tests/test_smoke_cleanup.py @@ -0,0 +1,87 @@ +import importlib.util +from pathlib import Path +import subprocess +import sys +import unittest +from unittest.mock import Mock, call + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +spec = importlib.util.spec_from_file_location("smoke_cleanup_test", SCRIPTS / "smoke-test.py") +smoke = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke) + + +class SmokeCleanupTests(unittest.TestCase): + def process(self): + process = Mock() + process.poll.return_value = None + process.wait.return_value = 0 + return process + + def test_already_exited_process_needs_no_stop_request(self): + process = self.process() + process.poll.return_value = 0 + smoke.stop_server(process) + process.stdin.write.assert_not_called() + process.wait.assert_not_called() + process.terminate.assert_not_called() + process.kill.assert_not_called() + + def test_clean_shutdown_writes_stop_and_waits(self): + process = self.process() + smoke.stop_server(process) + process.stdin.write.assert_called_once_with("stop\n") + process.stdin.flush.assert_called_once_with() + process.wait.assert_called_once_with(timeout=60) + process.terminate.assert_not_called() + process.kill.assert_not_called() + + def test_closed_stdin_does_not_skip_waiting(self): + for operation, error in (("write", BrokenPipeError()), ("flush", BrokenPipeError()), + ("write", OSError("stdin unavailable"))): + with self.subTest(operation=operation, error=type(error).__name__): + process = self.process() + getattr(process.stdin, operation).side_effect = error + smoke.stop_server(process) + process.wait.assert_called_once_with(timeout=60) + process.terminate.assert_not_called() + + def test_timeout_terminates_and_still_fails_verification(self): + process = self.process() + process.wait.side_effect = [subprocess.TimeoutExpired("paper", 60), 0] + with self.assertRaisesRegex(RuntimeError, "did not shut down cleanly"): + smoke.stop_server(process) + self.assertEqual([call(timeout=60), call(timeout=10)], process.wait.call_args_list) + process.terminate.assert_called_once_with() + process.kill.assert_not_called() + + def test_broken_pipe_and_timeout_still_kills_and_reaps(self): + process = self.process() + process.stdin.flush.side_effect = BrokenPipeError() + process.wait.side_effect = [subprocess.TimeoutExpired("paper", 60), + subprocess.TimeoutExpired("paper", 10), -9] + with self.assertRaisesRegex(RuntimeError, "did not shut down cleanly"): + smoke.stop_server(process) + self.assertEqual([call(timeout=60), call(timeout=10), call(timeout=5)], process.wait.call_args_list) + process.terminate.assert_called_once_with() + process.kill.assert_called_once_with() + + def test_kill_wait_is_bounded_and_unreaped_child_fails(self): + process = self.process() + process.wait.side_effect = subprocess.TimeoutExpired("paper", 1) + with self.assertRaisesRegex(RuntimeError, "could not be reaped"): + smoke.stop_server(process, stop_timeout=1, terminate_timeout=2, kill_timeout=3) + self.assertEqual([call(timeout=1), call(timeout=2), call(timeout=3)], process.wait.call_args_list) + + def test_exit_race_during_termination_does_not_skip_reaping(self): + process = self.process() + process.wait.side_effect = [subprocess.TimeoutExpired("paper", 60), 0] + process.terminate.side_effect = ProcessLookupError() + with self.assertRaisesRegex(RuntimeError, "did not shut down cleanly"): + smoke.stop_server(process) + self.assertEqual([call(timeout=60), call(timeout=10)], process.wait.call_args_list) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_smoke_errors.py b/scripts/tests/test_smoke_errors.py new file mode 100644 index 0000000..6afab3c --- /dev/null +++ b/scripts/tests/test_smoke_errors.py @@ -0,0 +1,33 @@ +import importlib.util +from pathlib import Path +import sys +import unittest + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +spec = importlib.util.spec_from_file_location("smoke_test", SCRIPTS / "smoke-test.py") +smoke = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke) + + +class SmokeErrorTests(unittest.TestCase): + metadata = dict(pluginVersion="2.0.2", buildNumber="031", javaTarget="25", paperTarget="26.3") + expected = [ + "[18:12:48 ERROR]: [ModernPluginLoadingStrategy] Could not load 'plugins/1MB-BlueMap-MapHide-v2.0.2-031-j25-26.3.jar' in 'plugins'", + "org.bukkit.plugin.UnknownDependencyException: Unknown/missing dependency plugins: [BlueMap]. Please download and install these plugins to run '1MB-MapHide'.", + ] + + def test_missing_bluemap_only_expected_in_negative_scenario(self): + self.assertEqual(self.expected, smoke.unexpected_errors(self.expected, self.metadata)) + self.assertEqual([], smoke.unexpected_errors(self.expected, self.metadata, missing_bluemap=True)) + + def test_negative_scenario_still_rejects_unrelated_errors(self): + errors = ["[18:12:48 ERROR]: World save failed", + "java.lang.NoSuchMethodError: missing method", + "org.bukkit.plugin.UnknownDependencyException: another missing dependency", + self.expected[0].replace("1MB-BlueMap-MapHide", "AnotherPlugin")] + self.assertEqual(errors, smoke.unexpected_errors(self.expected + errors, self.metadata, missing_bluemap=True)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_smoke_fixture.py b/scripts/tests/test_smoke_fixture.py new file mode 100644 index 0000000..98bb222 --- /dev/null +++ b/scripts/tests/test_smoke_fixture.py @@ -0,0 +1,54 @@ +import importlib.util +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch +import zipfile + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +spec = importlib.util.spec_from_file_location("smoke_fixture_test", SCRIPTS / "smoke-test.py") +smoke = importlib.util.module_from_spec(spec) +spec.loader.exec_module(smoke) + + +class SmokeFixtureTests(unittest.TestCase): + def test_default_server_follows_release_configuration(self): + config = {"localPaperServerDir": "servers/a-new-stable-build"} + self.assertEqual((smoke.ROOT / config["localPaperServerDir"]).resolve(), + smoke.resolve_server_path(None, config)) + + def test_explicit_server_overrides_release_configuration(self): + selected = Path("/tmp/selected-paper-fixture") + self.assertEqual(selected.resolve(), smoke.resolve_server_path(selected, {})) + + def test_reused_fixture_does_not_import_worlds_or_server_configuration(self): + with tempfile.TemporaryDirectory() as temp: + source = Path(temp) / "source" + destination = Path(temp) / "smoke" + source.mkdir() + (source / "paper.jar").write_bytes(b"paper") + (source / "eula.txt").write_text("eula=true\n") + (source / "server.properties").write_text("level-name=world\n") + for name in ("world", "world_nether", "world_the_end", "config"): + (source / name).mkdir() + (source / name / "sensitive-data").write_text("must not be copied") + (source / "bukkit.yml").write_text("source settings") + (source / "spigot.yml").write_text("source settings") + plugins = source / "plugins" + (plugins / "BlueMap").mkdir(parents=True) + (plugins / "bluemap-test.jar").write_bytes(b"bluemap") + (plugins / "PlaceholderAPI-test.jar").write_bytes(b"papi") + artifact = Path(temp) / "maphide.jar" + with zipfile.ZipFile(artifact, "w") as jar: + jar.writestr("config.yml", "language: EN\n") + with patch.object(smoke, "verify_server", return_value=source / "paper.jar"): + smoke.prepare_server(source, destination, artifact, {}) + for name in ("world", "world_nether", "world_the_end", "config", "bukkit.yml", "spigot.yml"): + self.assertFalse((destination / name).exists(), name) + self.assertTrue((destination / "plugins" / artifact.name).is_file()) + + +if __name__ == "__main__": + unittest.main() From 09472b87ea3ccb9633548deafa87fd210dce2cc8 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 25 Oct 2026 11:56:45 +0100 Subject: [PATCH 7/8] Document automatic CI activation for the GitHub fork GitHub recorded pushes but suppressed inherited workflows behind the fork activation gate, despite enabled Actions permissions, an active workflow, and a successful manual run. Enable the fork's workflows in the Actions UI and document how to verify an event=push run for the exact commit. Keep the existing unrestricted push trigger, build-only job, and read-only workflow permissions. This documentation push exercises the repaired automatic trigger without deploying or starting Paper servers. --- AGENTS.md | 5 +++++ docs/installation.md | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 56257bf..7edb7a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,3 +18,8 @@ This repository uses the single-context domain-doc layout. See `docs/agents/doma - Run `python3 scripts/smoke-test.py` after building. It resolves and validates installed Oracle JDK 27 independently and uses disposable localhost servers. See `docs/installation.md`, `docs/verification/2026-10-25-oracle-toolchains.md`, and `docs/verification/2026-10-25-paper-26.3-stable-222.md` for the workflow and verification evidence. Historical records describe their original targets. - Read the live Paper LLM index, official docs, roadmap, and exact target Javadocs for future upgrades. Pin the server checksum and actual channel, and review compiler deprecations and exploit-protection defaults. - The shared test server must stay stopped during artifact replacement. Match this project's `1MB-MapHide` plugin identity and main class, replace only its JARs with backups and checksum verification, and never launch the shared server as part of deployment. + +## GitHub Actions + +- `.github/workflows/main.yml` runs the build and automated tests on pushes, pull requests, and manual dispatch; it does not deploy or start servers. Verify automatic CI by querying the exact pushed commit with `gh run list --workflow main.yml --commit "$(git rev-parse HEAD)" --event push` and checking its final conclusion. +- New GitHub forks require activation of inherited workflows in the signed-in Actions tab. This fork was activated on 2026-10-25. An `enabled` Actions permission, an `active` workflow, or a successful manual dispatch alone does not prove automatic triggers work. See `docs/installation.md#github-actions`. diff --git a/docs/installation.md b/docs/installation.md index 17fb4a8..6fa4018 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -80,7 +80,7 @@ ONE_MB_TEST_JAVA_HOME="$(/usr/libexec/java_home -F -v 27)" \ Automatic Gradle toolchain discovery and downloads are disabled. Toolchains come only from the two validated environment variables. Settings reject conflicting project/system properties, alternate installation paths, and wrong bytecode targets before resolving toolchains. The launcher pins the Gradle daemon home and rejects JDK command-line overrides; use the two environment variables instead. Stale `gradle/gradle-daemon-jvm.properties` files are rejected because daemon criteria override `JAVA_HOME`. Configuration caching is disabled so validation runs each time. Raw `gradle` invocations must already have the same validated homes exported and run on Oracle 25; use `build.sh` for normal builds. -`./build.sh check` runs JDK selection, release/documentation drift checks, artifact/bytecode validation, Java toolchain regression tests, and Python fixture/resolver regression tests. The Java tests execute a real Gradle test worker and a child JVM on Oracle 27 and inspect Java 25 class headers in plugin and test classes. Paper integration tests remain the separate smoke command below. The local GitHub Actions definition uses Oracle 25/27, Gradle 9.8.2, and the same launcher with explicit installed-home overrides from its setup steps. No remote CI run was triggered for this local change. +`./build.sh check` runs JDK selection, release/documentation drift checks, artifact/bytecode validation, Java toolchain regression tests, and Python fixture/resolver regression tests. The Java tests execute a real Gradle test worker and a child JVM on Oracle 27 and inspect Java 25 class headers in plugin and test classes. Paper integration tests remain the separate smoke command below. The selected Gradle must support Java 27 toolchains, which starts at 9.8.0 in the [official compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html). Toolchain tasks use the [official Gradle compiler and launcher APIs](https://docs.gradle.org/current/userguide/toolchains.html). See the [local verification record](verification/2026-10-25-oracle-toolchains.md) for the installed versions used. @@ -88,6 +88,21 @@ Paper API, BlueMap API, and PlaceholderAPI versions are pinned in `gradle.proper Check `python3 --version` before building; Python 3.11 or newer must be on PATH for checksum verification and the JDK resolver. +## GitHub Actions + +`.github/workflows/main.yml` runs on pushes, pull requests, and manual dispatch. It sets up Oracle 25/27 and Gradle 9.8.2, then uses the same launcher with explicit installed-home overrides and the checked-in release build number. CI runs `clean build` and uploads the plugin artifact. It does not deploy or start Paper servers. + +GitHub disables inherited workflows on new forks until a maintainer enables them from the fork's **Actions** tab. If the page shows **Workflows aren't being run on this forked repository**, review the workflows and select **I understand my workflows, go ahead and enable them**. This activation was completed for `mrfdev/1MB-MapHide` on 2026-10-25. Before activation, GitHub recorded the push but created no build run, even though the Actions permissions API reported `enabled: true`, the workflow reported `active`, and a manual dispatch succeeded. See [GitHub's fork workflow documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflows-in-forked-repositories). + +After pushing, verify an automatic run for the exact commit from the repository root: + +```sh +gh run list --workflow main.yml --commit "$(git rev-parse HEAD)" --event push \ + --json databaseId,headSha,event,status,conclusion,url +``` + +Wait for that run to complete successfully. An empty result means no push run was created; a successful `workflow_dispatch` run alone does not verify the push trigger. Identify the workflow by `main.yml` because GitHub may display its name from the default branch, which still calls it Java CI with Maven. + ## Runtime Verification Prepare the stable fixture once, then verify and test it: From 59e943a19d328725480bfeddb6c3d7926c1c8bda Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 25 Oct 2026 12:09:40 +0100 Subject: [PATCH 8/8] Update upload-artifact to v7.0.2 Replace the Node 20 action with the current Node 24 release to remove GitHub Actions' runtime deprecation warning. Pin the official release commit and retain the artifact name, path, failure policy, and retention. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index edfe251..b580a36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: clean build - name: Upload plugin jar - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: 1MB-MapHide-${{ github.sha }} path: build/libs/*.jar