diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index cd56ba3..0d2a5be 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1 @@
-# Default ownership for repository review requests.
-* @HauntedMC
+* @remdui
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 54df9ac..1f8f6ba 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -63,7 +63,7 @@ body:
id: paper_version
attributes:
label: Paper Version and Build
- placeholder: 26.2 build 65
+ placeholder: 26.2 build 129
validations:
required: true
- type: input
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 353ee2c..4795e08 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,19 +1,16 @@
version: 2
+# Platform-owned Maven versions are updated by HauntedPlatform's reviewed reconciler.
updates:
- - package-ecosystem: "gradle"
- directory: "/"
- schedule:
- interval: "weekly"
- open-pull-requests-limit: 10
- labels:
- - "dependencies"
- - "java"
-
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+ day: "monday"
+ time: "06:15"
+ timezone: "Europe/Amsterdam"
open-pull-requests-limit: 5
- labels:
- - "dependencies"
- - "ci"
+ groups:
+ routine-action-updates:
+ patterns: ["*"]
+ update-types: ["minor", "patch"]
+ labels: ["dependencies", "ci"]
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 9443373..d71c223 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,23 +1,13 @@
-## What Changed
+## Change
-Describe the change in a few sentences.
-
-## Why
-
-Why is this needed?
+Describe the behavior or tooling change, why it is needed, and any operator impact.
## Validation
-- [ ] `./gradlew test` (or explain why skipped)
-- [ ] `./gradlew acceptanceTest` for lifecycle, command, or watcher changes
-- [ ] Documentation updated when operator behavior changed
-
-## Related Issues
-
-List related issues (example: `Closes #123`).
+- [ ] `./mvnw -B -ntp verify`
+- [ ] `./mvnw -B -ntp -Pplatform-acceptance verify` for runtime, packaging, or dependency changes
+- [ ] `bash scripts/verify-artifact.sh` for packaging changes
-## Notes
+## Compatibility
-- Breaking changes:
-- Config/API changes:
-- Anything reviewers should pay extra attention to:
+Document configuration, command, API, and packaging changes, or state that there are none.
diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml
deleted file mode 100644
index cbfd3d7..0000000
--- a/.github/workflows/ci-lint.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: CI Lint
-
-on:
- pull_request:
- types: [opened, synchronize, reopened]
- push:
- branches: [main]
-
-jobs:
- lint:
- name: Build Hygiene
- runs-on: ubuntu-24.04
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - name: Set up JDK 25
- uses: actions/setup-java@v6
- with:
- distribution: temurin
- java-version: "25"
- cache: gradle
-
- - name: Set up Gradle
- uses: gradle/actions/setup-gradle@v6.3.0
-
- - name: Verify build hygiene
- run: |
- ./gradlew --no-daemon check
- shellcheck src/acceptance/run-acceptance.sh scripts/bump-version.sh
diff --git a/.github/workflows/ci-tests-and-coverage.yml b/.github/workflows/ci-tests-and-coverage.yml
deleted file mode 100644
index c3cb516..0000000
--- a/.github/workflows/ci-tests-and-coverage.yml
+++ /dev/null
@@ -1,82 +0,0 @@
-name: CI Tests and Coverage
-
-on:
- pull_request:
- types: [opened, synchronize, reopened]
- push:
- branches: [main]
-
-jobs:
- tests:
- name: Tests and Coverage
- runs-on: ubuntu-24.04
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - name: Set up JDK 25
- uses: actions/setup-java@v6
- with:
- distribution: temurin
- java-version: "25"
- cache: gradle
-
- - name: Set up Gradle
- uses: gradle/actions/setup-gradle@v6.3.0
-
- - name: Run tests and generate coverage report
- run: ./gradlew --no-daemon test jacocoTestReport
-
- - name: Upload test report
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: test-report
- if-no-files-found: warn
- path: build/reports/tests/test
-
- - name: Upload JaCoCo report
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: jacoco-report
- if-no-files-found: warn
- path: build/reports/jacoco/test
-
- platform-acceptance:
- name: Paper Platform Acceptance
- runs-on: ubuntu-24.04
- timeout-minutes: 15
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - name: Set up JDK 25
- uses: actions/setup-java@v6
- with:
- distribution: temurin
- java-version: "25"
- cache: gradle
-
- - name: Set up Gradle
- uses: gradle/actions/setup-gradle@v6.3.0
-
- - name: Run Paper acceptance
- env:
- PHR_ACCEPTANCE_WORK_DIRECTORY: ${{ runner.temp }}/paperhotreloader-platform-acceptance
- PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY: "true"
- run: ./gradlew --no-daemon acceptanceTest
-
- - name: Upload Paper acceptance logs
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: paperhotreloader-platform-acceptance-logs
- if-no-files-found: warn
- path: ${{ runner.temp }}/paperhotreloader-platform-acceptance/paper/paper.log
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1b67d9a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,127 @@
+name: CI
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ push:
+ branches: [main]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ packages: read
+
+jobs:
+ tests:
+ name: Maven tests and artifact audit
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ env:
+ PACKAGES_USER: ${{ secrets.HAUNTEDMC_PACKAGES_USERNAME || github.actor }}
+ PACKAGES_TOKEN: ${{ secrets.HAUNTEDMC_PACKAGES_TOKEN || github.token }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
+ with:
+ distribution: temurin
+ java-version: '25'
+ cache: maven
+ server-id: github
+ server-username: PACKAGES_USER
+ server-password: PACKAGES_TOKEN
+ - run: ./mvnw -U -B -ntp verify
+ - run: bash scripts/verify-artifact.sh
+ - name: Upload test and coverage reports
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: test-and-coverage-reports
+ if-no-files-found: warn
+ path: |
+ target/surefire-reports
+ target/site/jacoco
+
+ platform-acceptance:
+ name: Paper platform acceptance
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ env:
+ PACKAGES_USER: ${{ secrets.HAUNTEDMC_PACKAGES_USERNAME || github.actor }}
+ PACKAGES_TOKEN: ${{ secrets.HAUNTEDMC_PACKAGES_TOKEN || github.token }}
+ PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY: 'true'
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
+ with:
+ distribution: temurin
+ java-version: '25'
+ cache: maven
+ server-id: github
+ server-username: PACKAGES_USER
+ server-password: PACKAGES_TOKEN
+ - name: Run platform acceptance
+ env:
+ PHR_ACCEPTANCE_WORK_DIRECTORY: ${{ runner.temp }}/paperhotreloader-acceptance
+ run: ./mvnw -U -B -ntp -Pplatform-acceptance verify
+ - name: Upload acceptance logs
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: platform-acceptance-logs
+ if-no-files-found: warn
+ path: ${{ runner.temp }}/paperhotreloader-acceptance/paper/paper.log
+
+ maven-policy:
+ name: Shared Maven policy
+ uses: HauntedMC/HauntedPlatform/.github/workflows/maven-ci.yml@10dfbacc8515208bf3b0ee236a8c7688fda49c9e # release hardening
+ with:
+ maven-command: ./mvnw -U -B -ntp validate
+ secrets:
+ PACKAGES_USER: ${{ secrets.HAUNTEDMC_PACKAGES_USERNAME || github.actor }}
+ PACKAGES_TOKEN: ${{ secrets.HAUNTEDMC_PACKAGES_TOKEN || github.token }}
+
+ tooling:
+ name: ShellCheck and version tooling
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ env:
+ GH_TOKEN: ${{ github.token }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - run: |
+ mapfile -d '' scripts < <(git ls-files -z '*.sh')
+ shellcheck tools/release/update-version "${scripts[@]}"
+ shell: bash
+ - run: gh extension install HauntedMC/gh-haunted-release --pin de43c102d81797001eef36fe45725744fdd22031
+ - run: ./tools/release/update-version --dry-run patch
+
+ ci-required:
+ name: ci-required
+ if: ${{ always() }}
+ needs: [tests, platform-acceptance, maven-policy, tooling]
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ env:
+ CHECK_RESULTS: ${{ toJSON(needs) }}
+ steps:
+ - name: Require every validation job
+ run: |
+ python3 - <<'PYTHON'
+ import json
+ import os
+ results = {name: job['result'] for name, job in json.loads(os.environ['CHECK_RESULTS']).items()}
+ failed = {name: result for name, result in results.items() if result != 'success'}
+ if failed:
+ raise SystemExit(f'Validation did not pass: {failed}')
+ PYTHON
diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml
deleted file mode 100644
index c67bc6b..0000000
--- a/.github/workflows/release-package.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-name: Release Package
-
-on:
- push:
- tags: ["v*"]
-
-jobs:
- release:
- name: Build, Verify, and Release
- runs-on: ubuntu-24.04
- permissions:
- contents: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
- with:
- fetch-depth: 0
-
- - name: Set up JDK 25
- uses: actions/setup-java@v6
- with:
- distribution: temurin
- java-version: "25"
- cache: gradle
-
- - name: Set up Gradle
- uses: gradle/actions/setup-gradle@v6.3.0
-
- - name: Run release verification
- env:
- PHR_ACCEPTANCE_WORK_DIRECTORY: ${{ runner.temp }}/paperhotreloader-platform-acceptance
- PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY: "true"
- run: ./gradlew --no-daemon clean test acceptanceTest
-
- - name: Resolve distribution artifact
- run: |
- version="$(./gradlew --quiet properties | sed -n 's/^version: //p')"
- test -n "$version" || { echo "Could not determine project version." >&2; exit 1; }
- test "${GITHUB_REF_NAME}" = "v$version" || {
- echo "Tag ${GITHUB_REF_NAME} does not match project version $version." >&2
- exit 1
- }
- echo "DIST_JAR=build/libs/PaperHotReloader-$version.jar" >> "$GITHUB_ENV"
-
- - name: Create checksum
- run: sha256sum "${{ env.DIST_JAR }}" > "${{ env.DIST_JAR }}.sha256"
-
- - name: Upload failing Paper acceptance logs
- if: failure()
- uses: actions/upload-artifact@v7
- with:
- name: paperhotreloader-platform-acceptance-logs
- if-no-files-found: warn
- path: ${{ runner.temp }}/paperhotreloader-platform-acceptance/paper/paper.log
-
- - name: Upload distribution
- uses: actions/upload-artifact@v7
- with:
- name: dist
- path: |
- ${{ env.DIST_JAR }}
- ${{ env.DIST_JAR }}.sha256
-
- - name: Create GitHub release
- uses: softprops/action-gh-release@v3
- with:
- name: PaperHotReloader ${{ github.ref_name }}
- generate_release_notes: true
- files: |
- ${{ env.DIST_JAR }}
- ${{ env.DIST_JAR }}.sha256
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..b8de9b3
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,95 @@
+name: Release Package
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+concurrency:
+ group: release-main
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ gate:
+ runs-on: ubuntu-24.04
+ outputs:
+ publish: ${{ steps.version.outputs.publish }}
+ version: ${{ steps.version.outputs.version }}
+ tag: ${{ steps.version.outputs.tag }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install pinned release CLI
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh extension install HauntedMC/gh-haunted-release --pin de43c102d81797001eef36fe45725744fdd22031
+ - id: version
+ name: Detect a reviewed version change
+ env:
+ GITHUB_EVENT_BEFORE: ${{ github.event.before }}
+ GITHUB_EVENT_NAME: ${{ github.event_name }}
+ run: gh haunted-release gate
+
+ publish:
+ needs: gate
+ if: needs.gate.outputs.publish == 'true'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ permissions:
+ contents: write
+ packages: write
+ env:
+ PACKAGES_USER: ${{ secrets.HAUNTEDMC_PACKAGES_USERNAME }}
+ PACKAGES_TOKEN: ${{ secrets.HAUNTEDMC_PACKAGES_TOKEN }}
+ GH_TOKEN: ${{ github.token }}
+ PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY: 'true'
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install pinned release CLI
+ run: gh extension install HauntedMC/gh-haunted-release --pin de43c102d81797001eef36fe45725744fdd22031
+ - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
+ with:
+ distribution: temurin
+ java-version: '25'
+ cache: maven
+ server-id: github
+ server-username: PACKAGES_USER
+ server-password: PACKAGES_TOKEN
+ - name: Require Maven package credentials
+ run: test -n "$PACKAGES_USER" && test -n "$PACKAGES_TOKEN"
+ - id: preflight
+ name: Inspect published Maven coordinates
+ run: gh haunted-release published-state "${{ needs.gate.outputs.version }}"
+ - name: Verify release candidate with platform acceptance
+ env:
+ PHR_ACCEPTANCE_WORK_DIRECTORY: ${{ runner.temp }}/paperhotreloader-acceptance
+ run: ./mvnw -U -B -ntp -Prelease,platform-acceptance verify
+ - name: Audit distributable jar before publication
+ run: bash scripts/verify-artifact.sh
+ - name: Publish verified Maven package
+ if: steps.preflight.outputs.state != 'complete'
+ run: ./mvnw -B -ntp -DskipTests -Prelease deploy
+ - name: Resolve published Maven coordinates from a fresh repository
+ run: gh haunted-release verify-published "${{ needs.gate.outputs.version }}"
+ - name: Checksum distributable
+ run: sha256sum "target/PaperHotReloader-${{ needs.gate.outputs.version }}.jar" > "target/PaperHotReloader-${{ needs.gate.outputs.version }}.jar.sha256"
+ - name: Create release after package verification
+ run: |
+ gh release create "${{ needs.gate.outputs.tag }}" --target "$GITHUB_SHA" --title "PaperHotReloader ${{ needs.gate.outputs.tag }}" --generate-notes \
+ "target/PaperHotReloader-${{ needs.gate.outputs.version }}.jar" \
+ "target/PaperHotReloader-${{ needs.gate.outputs.version }}.jar.sha256"
+ - name: Upload acceptance logs on failure
+ if: failure()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: release-platform-acceptance-logs
+ if-no-files-found: warn
+ path: ${{ runner.temp }}/paperhotreloader-acceptance/paper/paper.log
diff --git a/.gitignore b/.gitignore
index aa3034a..e66b6e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,10 @@ jars/
*.class
*.jar
-!gradle/wrapper/gradle-wrapper.jar
.DS_Store
+
+# Maven and release tooling
+target/
+.flattened-pom.xml
+.mvn/timing.properties
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000..72fce39
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,2 @@
+--settings
+.mvn/settings.xml
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
new file mode 100644
index 0000000..cb9d6e9
--- /dev/null
+++ b/.mvn/settings.xml
@@ -0,0 +1,9 @@
+
+
+ github${env.PACKAGES_USER}${env.PACKAGES_TOKEN}
+ hauntedmc-github-packages
+ centralhttps://repo.maven.apache.org/maven2githubhttps://maven.pkg.github.com/hauntedmc/*true
+ centralhttps://repo.maven.apache.org/maven2githubhttps://maven.pkg.github.com/hauntedmc/*true
+
+ hauntedmc-github-packages
+
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..222cba3
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,4 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b45c7cf..f0b0cf0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@ Thanks for taking the time to contribute to PaperHotReloader.
## Before You Start
-- Use Java 25 and Gradle.
+- Use Java 25 and Maven.
- Run the unit suite before opening a pull request.
- Run the Paper acceptance suite for lifecycle, command, or watcher changes.
@@ -13,7 +13,7 @@ Thanks for taking the time to contribute to PaperHotReloader.
```bash
git clone https://github.com/HauntedMC/PaperHotReloader.git
cd PaperHotReloader
-./gradlew compileJava
+./mvnw -B -ntp -DskipTests compile
```
## Local Validation
@@ -21,14 +21,14 @@ cd PaperHotReloader
Minimum checks:
```bash
-./gradlew test
-shellcheck src/acceptance/run-acceptance.sh scripts/bump-version.sh
+./mvnw -B -ntp test
+shellcheck src/acceptance/run-acceptance.sh scripts/verify-artifact.sh tools/release/prepare-version.sh
```
Run the full Paper acceptance suite:
```bash
-./gradlew acceptanceTest
+./mvnw -B -ntp -Pplatform-acceptance verify
```
The acceptance task downloads the pinned Paper runtime, starts a temporary server, and builds temporary
@@ -40,7 +40,7 @@ plugins. It requires `curl`, `java`, `javac`, `jar`, and `sha256sum`.
2. Include tests with behavior changes.
3. Update documentation for operator-visible changes.
4. Describe lifecycle, configuration, or compatibility impact in the pull request.
-5. Do not include generated `build/` output.
+5. Do not include generated `target/` output.
## Security
diff --git a/README.md b/README.md
index e398884..725b6a3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# PaperHotReloader
-[](https://github.com/HauntedMC/PaperHotReloader/actions/workflows/ci-tests-and-coverage.yml)
-[](https://github.com/HauntedMC/PaperHotReloader/actions/workflows/ci-tests-and-coverage.yml)
+[](https://github.com/HauntedMC/PaperHotReloader/actions/workflows/ci.yml)
[](https://github.com/HauntedMC/PaperHotReloader/releases/latest)
[](https://adoptium.net/)
[](LICENSE)
@@ -31,32 +30,17 @@ Hot-load, unload, reload, and watch Bukkit plugins without restarting a Paper se
## Build From Source
-Build with Java 25 and the Paper 26.2 development bundle:
+Use Java 25. HauntedPlatform is resolved from GitHub Packages; set `PACKAGES_USER` and `PACKAGES_TOKEN` (with `read:packages`) for a fresh local Maven cache. The committed `.mvn/settings.xml` reads these variables.
```bash
-./gradlew clean build
+./mvnw -B -ntp verify
```
-Output jar: `build/libs/PaperHotReloader-.jar`
+Output jar: `target/PaperHotReloader-.jar`
-## Release Workflow
+## Release workflow
-Create a semantic-version bump, commit, and release tag locally:
-
-```bash
-scripts/bump-version.sh patch
-```
-
-Add `--push` to publish the branch and tag after the repository is reviewed:
-
-```bash
-scripts/bump-version.sh minor --push
-```
-
-Run the fast unit suite with `./gradlew test`. `./gradlew acceptanceTest` follows the ServerFeatures
-acceptance pattern: it downloads the pinned Paper 26.2 build, starts a disposable server, builds temporary
-sample plugins, and verifies every PHR command plus dependency protection, dynamic reload, file watching,
-and PHR's own restart.
+From clean `main`, run `./tools/release/update-version patch --pr` to open a reviewed version PR. CI tests the PR; after merge, GitHub Actions publishes the Maven package, verifies that it resolves, and creates the tag and downloadable release jar with a SHA-256 checksum. See [release tooling](tools/release/README.md).
## Commands
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index 925fbdd..0000000
--- a/build.gradle.kts
+++ /dev/null
@@ -1,84 +0,0 @@
-import org.gradle.testing.jacoco.tasks.JacocoReport
-import org.gradle.api.tasks.Exec
-
-plugins {
- `java-library`
- jacoco
-}
-
-group = "nl.hauntedmc.paperhotreloader"
-version = "1.0.4"
-
-val javaVersion = 25
-val paperApiVersion = "26.2.build.124-stable"
-val acceptanceCompileClasspath by configurations.creating {
- isCanBeConsumed = false
- isCanBeResolved = true
-}
-
-java {
- toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersion))
- withSourcesJar()
-}
-
-repositories {
- mavenCentral()
- maven("https://repo.papermc.io/repository/maven-public/")
-}
-
-dependencies {
- // This plugin uses only the public Bukkit/Paper API. Unlike AIlex, it has no NMS classes to remap.
- compileOnly("io.papermc.paper:paper-api:$paperApiVersion")
- testImplementation("io.papermc.paper:paper-api:$paperApiVersion")
- acceptanceCompileClasspath("io.papermc.paper:paper-api:$paperApiVersion")
-
- testImplementation(platform("org.junit:junit-bom:6.1.3"))
- testImplementation("org.junit.jupiter:junit-jupiter")
- testRuntimeOnly("org.junit.platform:junit-platform-launcher")
-}
-
-tasks {
- compileJava {
- options.release.set(javaVersion)
- options.encoding = Charsets.UTF_8.name()
- }
-
- processResources {
- val properties = mapOf("version" to project.version)
- inputs.properties(properties)
- filteringCharset = Charsets.UTF_8.name()
- filesMatching("plugin.yml") {
- expand(properties)
- }
- }
-
- test {
- useJUnitPlatform()
- testLogging {
- events("passed", "skipped", "failed")
- }
- finalizedBy(jacocoTestReport)
- }
-
- check {
- dependsOn(jacocoTestReport)
- }
-}
-
-tasks.withType().configureEach {
- reports {
- xml.required.set(true)
- html.required.set(true)
- }
-}
-
-val acceptanceTest = tasks.register("acceptanceTest") {
- group = LifecycleBasePlugin.VERIFICATION_GROUP
- description = "Boots Paper and verifies PHR against temporary sample plugins."
- dependsOn(tasks.jar)
- commandLine("bash", "src/acceptance/run-acceptance.sh")
- doFirst {
- environment("PHR_ARTIFACT", tasks.jar.get().archiveFile.get().asFile.absolutePath)
- environment("PHR_PAPER_API_CLASSPATH", acceptanceCompileClasspath.asPath)
- }
-}
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 1d0c9cb..24bafe6 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -3,15 +3,15 @@
## Local Workflow
```bash
-./gradlew compileJava
-./gradlew test
-./gradlew check
+./mvnw -B -ntp -DskipTests compile
+./mvnw -B -ntp test
+./mvnw -B -ntp verify
```
Use the platform acceptance task for changes to plugin loading, unloading, commands, or file watching:
```bash
-./gradlew acceptanceTest
+./mvnw -B -ntp -Pplatform-acceptance verify
```
The acceptance task is intentionally isolated: it builds temporary sample plugins and boots a disposable
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 66621f6..37b26d2 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -5,26 +5,26 @@
Run deterministic tests with:
```bash
-./gradlew test
+./mvnw -B -ntp test
```
The suite covers plugin metadata parsing, dependency ordering, and command completion helpers. JaCoCo reports
-are written to `build/reports/jacoco/test/html/index.html`.
+are written to `target/site/jacoco/index.html`.
## Paper Platform Acceptance
Run the real-server test with:
```bash
-./gradlew acceptanceTest
+./mvnw -B -ntp -Pplatform-acceptance verify
```
-It downloads pinned Paper 26.2 build 65 and verifies PHR against temporary sample plugins. The test exercises
+It downloads pinned the HauntedPlatform-pinned Paper runtime and verifies PHR against temporary sample plugins. The test exercises
`help`, configuration reload, listing and metadata commands, load, dependency protection, reload, unload,
watch/unwatch, file-triggered reload, and PHR self-restart.
For troubleshooting, preserve the temporary server directory:
```bash
-PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY=true ./gradlew acceptanceTest
+PHR_ACCEPTANCE_KEEP_WORK_DIRECTORY=true ./mvnw -B -ntp -Pplatform-acceptance verify
```
diff --git a/gradle.properties b/gradle.properties
deleted file mode 100644
index 7c1e909..0000000
--- a/gradle.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-org.gradle.caching=true
-org.gradle.parallel=true
-org.gradle.jvmargs=-Dfile.encoding=UTF-8
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index eddabd2..0000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index ad7845b..0000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
-networkTimeout=10000
-retries=0
-retryBackOffMs=500
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
deleted file mode 100755
index 249efbb..0000000
--- a/gradlew
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-
-##############################################################################
-#
-# gradlew start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh gradlew
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-# and any embedded shellness will be escaped.
-# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-# treated as '${Hostname}' itself on the command line.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index a51ec4f..0000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,82 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem gradlew startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables, and ensure extensions are enabled
-setlocal EnableExtensions
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-"%COMSPEC%" /c exit 1
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-"%COMSPEC%" /c exit 1
-
-:execute
-@rem Setup the command line
-
-
-
-@rem Execute gradlew
-@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
-@rem which allows us to clear the local environment before executing the java command
-endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-
-:exitWithErrorLevel
-@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
-"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..bd8896b
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,295 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.4
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..92450f9
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..89d323e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,161 @@
+
+
+ 4.0.0
+
+ nl.hauntedmc.platform
+ haunted-application-parent
+ 2.0.0
+
+
+ nl.hauntedmc.paperhotreloader
+ paperhotreloader
+ ${revision}
+ PaperHotReloader
+ Hot-load, unload, reload, and watch Paper plugins.
+ https://github.com/HauntedMC/PaperHotReloader
+ 2026
+
+ HauntedMC
+ https://www.hauntedmc.nl
+
+
+
+ GNU Affero General Public License v3.0
+ https://www.gnu.org/licenses/agpl-3.0.txt
+ repo
+
+
+
+
+ HauntedMC
+ HauntedMC
+ https://www.hauntedmc.nl
+
+
+
+ scm:git:https://github.com/HauntedMC/PaperHotReloader.git
+ scm:git:ssh://git@github.com/HauntedMC/PaperHotReloader.git
+ https://github.com/HauntedMC/PaperHotReloader
+ v1.0.5
+
+
+ GitHub
+ https://github.com/HauntedMC/PaperHotReloader/issues
+
+
+ GitHub Actions
+ https://github.com/HauntedMC/PaperHotReloader/actions
+
+
+
+ github
+ GitHub Packages - PaperHotReloader
+ https://maven.pkg.github.com/HauntedMC/PaperHotReloader
+
+
+
+ 1.0.5
+ UTF-8
+ 2026-09-25T00:00:00Z
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+
+
+ io.papermc.paper
+ paper-api
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+ PaperHotReloader-${project.version}
+
+
+ src/main/resources
+ true
+
+
+
+
+
+ release
+
+
+
+ maven-source-plugin
+
+
+ sources
+
+ jar-no-fork
+
+
+
+
+
+
+
+
+ platform-acceptance
+
+
+
+ maven-dependency-plugin
+
+
+ acceptance-classpath
+ generate-test-resources
+
+ build-classpath
+
+
+ test
+ ${project.build.directory}/acceptance-classpath.txt
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ run-platform-acceptance
+ verify
+
+ exec
+
+
+ bash
+
+ src/acceptance/run-acceptance.sh
+
+
+ ${project.build.directory}/${project.build.finalName}.jar
+ ${project.build.directory}/acceptance-classpath.txt
+ ${haunted.paper.runtime.version}
+ ${haunted.paper.runtime.build}
+ ${haunted.paper.runtime.sha256}
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
deleted file mode 100755
index 0ed80fb..0000000
--- a/scripts/bump-version.sh
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-BUILD_FILE="build.gradle.kts"
-PUSH_REMOTE="origin"
-DO_PUSH=false
-
-usage() {
- cat >&2 <<'EOF'
-Usage: scripts/bump-version.sh [--push] [--remote ]
-
-Examples:
- scripts/bump-version.sh patch
- scripts/bump-version.sh minor --push
- scripts/bump-version.sh major --push --remote origin
-EOF
-}
-
-if [[ $# -lt 1 ]]; then
- usage
- exit 1
-fi
-
-bump_type="$1"
-shift
-
-if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then
- usage
- exit 1
-fi
-
-while [[ $# -gt 0 ]]; do
- case "$1" in
- --push)
- DO_PUSH=true
- shift
- ;;
- --remote)
- if [[ $# -lt 2 ]]; then
- echo "--remote requires a value." >&2
- exit 1
- fi
- PUSH_REMOTE="$2"
- shift 2
- ;;
- *)
- echo "Unknown option: $1" >&2
- usage
- exit 1
- ;;
- esac
-done
-
-if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
- echo "This script must be run inside a git repository." >&2
- exit 1
-fi
-
-if [[ ! -f "$BUILD_FILE" ]]; then
- echo "$BUILD_FILE not found." >&2
- exit 1
-fi
-
-if [[ -n "$(git status --porcelain)" ]]; then
- echo "Working tree is not clean. Commit or stash changes before bumping version." >&2
- exit 1
-fi
-
-current_version="$(
- sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' "$BUILD_FILE" | head -n 1
-)"
-
-if [[ -z "$current_version" ]]; then
- echo "Could not parse version from $BUILD_FILE." >&2
- exit 1
-fi
-
-if [[ ! "$current_version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
- echo "Project version must be a semantic version like 1.2.3." >&2
- exit 1
-fi
-
-major="${BASH_REMATCH[1]}"
-minor="${BASH_REMATCH[2]}"
-patch="${BASH_REMATCH[3]}"
-
-case "$bump_type" in
- major)
- major=$((major + 1))
- minor=0
- patch=0
- ;;
- minor)
- minor=$((minor + 1))
- patch=0
- ;;
- patch)
- patch=$((patch + 1))
- ;;
-esac
-
-new_version="${major}.${minor}.${patch}"
-new_tag="v${new_version}"
-
-if git rev-parse -q --verify "refs/tags/${new_tag}" >/dev/null 2>&1; then
- echo "Tag ${new_tag} already exists locally." >&2
- exit 1
-fi
-
-if git ls-remote --exit-code --tags "$PUSH_REMOTE" "refs/tags/${new_tag}" >/dev/null 2>&1; then
- echo "Tag ${new_tag} already exists on remote '${PUSH_REMOTE}'." >&2
- exit 1
-fi
-
-echo "Current version: ${current_version}"
-echo "New version: ${new_version}"
-
-sed -i -E "s/^([[:space:]]*version[[:space:]]*=[[:space:]]*\")[0-9]+\.[0-9]+\.[0-9]+(\".*)$/\1${new_version}\2/" "$BUILD_FILE"
-
-git add "$BUILD_FILE"
-git commit -m "Bump version to ${new_tag} for release"
-git tag "$new_tag"
-
-if [[ "$DO_PUSH" == "true" ]]; then
- current_branch="$(git rev-parse --abbrev-ref HEAD)"
- git push "$PUSH_REMOTE" "$current_branch"
- git push "$PUSH_REMOTE" "$new_tag"
- echo "Version bump committed, tagged, and pushed to ${PUSH_REMOTE}."
-else
- echo "Version bump committed and tagged locally."
- echo "Push when ready:"
- echo " git push ${PUSH_REMOTE} && git push ${PUSH_REMOTE} ${new_tag}"
-fi
diff --git a/scripts/verify-artifact.sh b/scripts/verify-artifact.sh
new file mode 100755
index 0000000..c4675c2
--- /dev/null
+++ b/scripts/verify-artifact.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(git rev-parse --show-toplevel)"
+version="$(sed -n 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)<\/revision>.*/\1/p' pom.xml | head -n 1)"
+[[ -n "$version" ]] || { echo 'Missing semantic Maven revision' >&2; exit 1; }
+artifact="target/PaperHotReloader-$version.jar"
+[[ -f "$artifact" ]] || { echo "Missing distributable $artifact" >&2; exit 1; }
+entries="$(jar tf "$artifact")"
+descriptor="$(unzip -p "$artifact" plugin.yml)"
+grep -Fxq "version: '$version'" <<<"$descriptor"
+grep -Fq 'nl/hauntedmc/paperhotreloader/PaperHotReloader.class' <<<"$entries"
+if grep -Eq '^org/bukkit/|^io/papermc/paper/' <<<"$entries"; then
+ echo 'Plugin jar contains provided Paper classes' >&2
+ exit 1
+fi
+echo "Artifact audit passed: $artifact"
diff --git a/settings.gradle.kts b/settings.gradle.kts
deleted file mode 100644
index 4fc824c..0000000
--- a/settings.gradle.kts
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = "PaperHotReloader"
diff --git a/src/acceptance/run-acceptance.sh b/src/acceptance/run-acceptance.sh
index b0e90f2..c8a1bf5 100644
--- a/src/acceptance/run-acceptance.sh
+++ b/src/acceptance/run-acceptance.sh
@@ -2,10 +2,12 @@
set -euo pipefail
root_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-artifact="${PHR_ARTIFACT:-$root_directory/build/libs/PaperHotReloader-1.0.0.jar}"
-paper_api_classpath="${PHR_PAPER_API_CLASSPATH:-}"
-paper_url="https://fill-data.papermc.io/v1/objects/d290ee2a3ae92fea10afb0c74f797023d74cfd72cbcee44314e0c50d5551f2c6/paper-26.2-65.jar"
-paper_sha256="d290ee2a3ae92fea10afb0c74f797023d74cfd72cbcee44314e0c50d5551f2c6"
+artifact="${PHR_ARTIFACT:-}"
+paper_api_classpath="$(cat "${PHR_PAPER_API_CLASSPATH:?Missing classpath file}")"
+paper_version="${PHR_RUNTIME_VERSION:?Missing Paper runtime version}"
+paper_build="${PHR_RUNTIME_BUILD:?Missing Paper runtime build}"
+paper_sha256="${PHR_RUNTIME_SHA256:?Missing Paper runtime checksum}"
+paper_url="https://fill-data.papermc.io/v1/objects/$paper_sha256/paper-$paper_version-$paper_build.jar"
work_directory="${PHR_ACCEPTANCE_WORK_DIRECTORY:-$(mktemp -d)}"
created_work_directory="${PHR_ACCEPTANCE_WORK_DIRECTORY:+false}"
created_work_directory="${created_work_directory:-true}"
@@ -89,11 +91,11 @@ mkdir -p "$work_directory/paper/plugins" "$work_directory/sample/classes" "$work
curl --fail --silent --show-error --location --output "$work_directory/paper/paper.jar" "$paper_url"
[[ "$(sha256sum "$work_directory/paper/paper.jar" | awk '{print $1}')" == "$paper_sha256" ]] \
- || fail "Downloaded Paper 26.2 build 65 checksum mismatch"
+ || fail "Downloaded Paper runtime checksum mismatch"
cp "$artifact" "$work_directory/paper/plugins/PaperHotReloader.jar"
sample_source="$root_directory/src/acceptance/sample-plugin/AcceptancePlugin.java"
-javac --release 25 -cp "$paper_api_classpath" -d "$work_directory/sample/classes" "$sample_source"
+"${JAVA_HOME:?Set JAVA_HOME to JDK 25}/bin/javac" --release 25 -cp "$paper_api_classpath" -d "$work_directory/sample/classes" "$sample_source"
build_sample_plugin() {
local destination=$1 descriptor=$2 marker=$3
@@ -116,7 +118,7 @@ build_sample_plugin \
printf '%s\n' 'eula=true' >"$work_directory/paper/eula.txt"
printf '%s\n' 'server-port=0' 'level-type=minecraft:flat' >"$work_directory/paper/server.properties"
mkfifo "$work_directory/paper/console.in"
-(cd "$work_directory/paper" && exec java -Xms512M -Xmx1G -jar paper.jar --nogui paper.log 2>&1) &
+(cd "$work_directory/paper" && exec "${JAVA_HOME:?Set JAVA_HOME to JDK 25}/bin/java" -Xms512M -Xmx1G -jar paper.jar --nogui paper.log 2>&1) &
paper_pid=$!
exec {paper_input_fd}>"$work_directory/paper/console.in"
paper_log="$work_directory/paper/paper.log"
diff --git a/tools/release/README.md b/tools/release/README.md
new file mode 100644
index 0000000..b2733bc
--- /dev/null
+++ b/tools/release/README.md
@@ -0,0 +1,5 @@
+# Version updates
+
+From clean, current `main`, run `./tools/release/update-version patch --pr` to prepare, push, and open a reviewed version PR in HauntedMC/PaperHotReloader. The pinned `gh-haunted-release` extension manages the worktree. Use `--dry-run` to inspect the next version. CI validates the PR; merging a version change publishes Maven packages and creates the GitHub release after verification. No local tag or direct push to `main` is needed.
+
+Install once: `gh extension install HauntedMC/gh-haunted-release --pin v1.0.3`.
diff --git a/tools/release/prepare-version.sh b/tools/release/prepare-version.sh
new file mode 100755
index 0000000..80c2ad6
--- /dev/null
+++ b/tools/release/prepare-version.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(git rev-parse --show-toplevel)"
+[[ $# -eq 1 && "$1" =~ ^(major|minor|patch)$ ]] || { echo 'Usage: prepare-version.sh major|minor|patch' >&2; exit 2; }
+python3 - "$1" <<'PYVERSION'
+from pathlib import Path
+from datetime import datetime, timezone
+import re
+import sys
+kind = sys.argv[1]
+pom = Path('pom.xml')
+content = pom.read_text()
+match = re.search(r'(\d+)\.(\d+)\.(\d+)', content)
+if not match:
+ raise SystemExit('Missing semantic revision in pom.xml')
+old = match.group(0).removeprefix('').removesuffix('')
+parts = list(map(int, match.groups()))
+index = {'major': 0, 'minor': 1, 'patch': 2}[kind]
+parts[index] += 1
+for i in range(index + 1, 3):
+ parts[i] = 0
+new = '.'.join(map(str, parts))
+def replace(path, before, after):
+ value = path.read_text()
+ if value.count(before) != 1:
+ raise SystemExit(f'{path}: expected exactly one {before!r}')
+ path.write_text(value.replace(before, after))
+replace(pom, f'{old}', f'{new}')
+replace(pom, f'v{old}', f'v{new}')
+timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT00:00:00Z')
+content = pom.read_text()
+match = re.search(r'([^<]+)', content)
+if not match:
+ raise SystemExit('Missing reproducible-build timestamp')
+replace(pom, match.group(0), f'{timestamp}')
+print(f'Prepared {old} -> {new}')
+PYVERSION
+git diff --check
diff --git a/tools/release/project.toml b/tools/release/project.toml
new file mode 100644
index 0000000..852d459
--- /dev/null
+++ b/tools/release/project.toml
@@ -0,0 +1,12 @@
+[project]
+name = "PaperHotReloader"
+repository = "HauntedMC/PaperHotReloader"
+tool_version = "1.0.3"
+mode = "bump"
+prepare = "tools/release/prepare-version.sh"
+verify = ["./mvnw", "-B", "-ntp", "verify"]
+verify_before_pr = false
+
+[components.default]
+pom = "pom.xml"
+tag_prefix = "v"
diff --git a/tools/release/update-version b/tools/release/update-version
new file mode 100755
index 0000000..7ba2eb3
--- /dev/null
+++ b/tools/release/update-version
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(git rev-parse --show-toplevel)"
+exec gh haunted-release version "$@"