diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..30d6e07 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,36 @@ +name: PR title + +# release-please builds the CHANGELOG from the commits that land on main. With +# squash merges the PR title becomes that commit message, so it has to follow +# Conventional Commits. +on: + # `synchronize` is here so the check re-reports on every push, which matters + # if this is made a required check. + pull_request: + types: [opened, reopened, edited, synchronize] + +permissions: + pull-requests: read + +jobs: + lint: + name: Validate the PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Mirrors the types listed in release-please-config.json. + types: | + feat + fix + perf + refactor + docs + build + ci + chore + style + test + revert diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 02940bf..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Publish to GitHub Packages - -on: - release: - types: [published] - workflow_dispatch: - inputs: - version: - description: "Optional version override (defaults to build.gradle)" - required: false - type: string - -jobs: - publish: - name: Publish shadow JAR to GitHub Packages - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v6 - - - name: Set up JDK 21 - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: "21" - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Resolve publish version - id: version - run: | - if [ -n "${{ inputs.version }}" ]; then - echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - elif [ "${{ github.event_name }}" = "release" ]; then - # Strip an optional leading "v" from the tag (v1.2.3 -> 1.2.3). - tag="${{ github.event.release.tag_name }}" - echo "value=${tag#v}" >> "$GITHUB_OUTPUT" - else - echo "value=" >> "$GITHUB_OUTPUT" - fi - - - name: Publish - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - args="publish --no-daemon --stacktrace" - if [ -n "${{ steps.version.outputs.value }}" ]; then - args="$args -Pversion=${{ steps.version.outputs.value }}" - fi - ./gradlew $args diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d79a05d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to (re)publish, e.g. v0.1.0" + required: true + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-please: + name: Maintain the release PR + # Manual runs skip straight to publishing an already-tagged version. + if: github.event_name == 'push' + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v5 + id: release + + publish: + name: Publish to GitHub Packages and Modrinth + needs: [release-please] + # `!cancelled()` keeps this job reachable when release-please is skipped + # (workflow_dispatch) instead of inheriting the skip. + if: >- + !cancelled() + && (needs.release-please.outputs.release_created == 'true' + || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + permissions: + # `write` is needed to attach the JAR to the GitHub release. + contents: write + packages: write + env: + TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }} + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + # SQLite-only here; the MySQL matrix already ran on the release PR. + - name: Build + run: ./gradlew build --no-daemon --stacktrace + + - name: Extract the release notes for Modrinth + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p build + gh release view "$TAG" --json body -q .body > build/modrinth-changelog.md + + - name: Publish to GitHub Packages + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew publish --no-daemon --stacktrace + + - name: Publish to Modrinth + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + run: ./gradlew modrinth modrinthSyncBody --no-daemon --stacktrace + + - name: Attach the shaded JAR to the GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$TAG" build/libs/ModernVillagerShop-*.jar --clobber diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/AGENTS.md b/AGENTS.md index 6133f6b..d00fc36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,8 +39,8 @@ ## Commit & Pull Request Guidelines -- Existing history is minimal (`initial commit`), so use clear imperative commits going forward. -- Suggested commit format: `type(scope): short summary` (example: `feat(ui): add listing pagination controls`). +- Commit format is [Conventional Commits](https://www.conventionalcommits.org/): `type(scope): short summary` (example: `feat(ui): add listing pagination controls`). This is not cosmetic — release-please derives the version bump and the CHANGELOG from it, so `feat` means a minor bump, `fix` a patch, and `!` or a `BREAKING CHANGE:` footer a major. +- Because merges are squashed, **the PR title becomes the commit message on `main`** and must follow the same convention. `.github/workflows/pr-title.yml` enforces it. - Keep commits focused; avoid mixing refactors with feature work. - PRs should include: - What changed and why @@ -48,6 +48,23 @@ - Validation steps (`./gradlew build`, local `runServer` checks) - Screenshots/GIFs for UI or dialog flow changes +## Releasing + +Releases are fully automated; nothing is bumped or tagged by hand. + +1. Merge PRs into `main` as usual. `.github/workflows/release.yml` runs release-please, which opens (or updates) a **"chore: release x.y.z"** PR containing the CHANGELOG entries and the new version in `gradle.properties`. +2. Review that PR — the CHANGELOG is a normal file, so hand-edit it there if the generated wording needs help. +3. Merge it. release-please then creates the `vx.y.z` tag and the GitHub release, and the same workflow builds and publishes to GitHub Packages and to [Modrinth](https://modrinth.com/plugin/modernvillagershop), attaches the shaded JAR to the GitHub release, and syncs `README.md` to the Modrinth project description. + +Notes: + +- The version lives only in `gradle.properties`, inside the `x-release-please-start-version` block. `processResources` expands it into `paper-plugin.yml` and Minotaur uses it as the Modrinth version number. +- To force a specific version (e.g. going 0.x → 1.0.0), land an empty commit with a `Release-As: 1.0.0` footer: `git commit --allow-empty -m "chore: release 1.0.0" -m "Release-As: 1.0.0"`. +- The Minecraft versions advertised on Modrinth come from `modrinth.gameVersions` in `gradle.properties`. Keep them aligned with what BedrockDialog supports, since it is a hard dependency. +- Because `README.md` is pushed as the Modrinth description, every link in it must be an absolute URL. +- Re-publishing an existing tag (e.g. after a transient Modrinth failure): run the **Release** workflow manually with the tag name as input. +- Required repository setup: the `MODRINTH_TOKEN` secret, and *Settings → Actions → General → Allow GitHub Actions to create and approve pull requests*. + ## Security & Configuration Tips - Never commit real database credentials or server secrets. diff --git a/CLAUDE.md b/CLAUDE.md index 3b71c30..d9dd263 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,6 +65,16 @@ CI runs both backends — see [.github/workflows/ci.yml](.github/workflows/ci.ym - Tests using Bukkit types should extend or use `testsupport/BukkitTestSupport` to manage MockBukkit lifecycle. - Test naming: `Test`; method names describe behavior (`createsShopWhenVillagerIsValid`). +## Release automation + +Versioning, CHANGELOG and publishing are automated — never bump a version or write a CHANGELOG entry by hand. + +- **Commits drive everything.** [release-please](https://github.com/googleapis/release-please) parses Conventional Commits on `main` and maintains a "chore: release x.y.z" PR that carries the CHANGELOG diff and the version bump. Merging that PR creates the tag, the GitHub release, and triggers publishing. Sections and hidden types are configured in [release-please-config.json](release-please-config.json); the last released version is in [.release-please-manifest.json](.release-please-manifest.json). +- **The version lives in [gradle.properties](gradle.properties)** inside the `x-release-please-start-version` / `x-release-please-end` block. Gradle reads it automatically (there is no `version =` in [build.gradle](build.gradle)), `processResources` expands it into `paper-plugin.yml`, and Minotaur reuses it as the Modrinth version number. The block-comment form matters: a trailing `# x-release-please-version` would be swallowed into the properties value. +- **[.github/workflows/release.yml](.github/workflows/release.yml)** holds both the release-please job and the publish job. They are one workflow on purpose — a release created with `GITHUB_TOKEN` does not fire `release: published`, so a separate publish workflow would never run without a PAT. +- **Modrinth** upload is the `modrinth` Gradle task ([com.modrinth.minotaur](https://github.com/modrinth/minotaur)); `modrinthSyncBody` pushes `README.md` as the project description, so links in it must stay absolute. Declared Minecraft versions come from `modrinth.gameVersions` in `gradle.properties` and track BedrockDialog's supported list. Modrinth dependencies are `bedrockdialog` (required) and `placeholderapi` (optional) — Vault has no Modrinth project and can only be mentioned in the body. +- **PR titles are linted** ([.github/workflows/pr-title.yml](.github/workflows/pr-title.yml)) because squash merges turn the title into the commit message release-please reads. + ## Style - Java 21, UTF-8, 4-space indent, no tabs. diff --git a/README.md b/README.md index 765bfd8..19a4baf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # ModernVillagerShop +[![Modrinth](https://img.shields.io/modrinth/dt/modernvillagershop?logo=modrinth&label=Modrinth&color=00AF5C)](https://modrinth.com/plugin/modernvillagershop) +[![GitHub release](https://img.shields.io/github/v/release/f0reachARR/ModernVillagerShop?logo=github&label=Release)](https://github.com/f0reachARR/ModernVillagerShop/releases/latest) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/f0reachARR/ModernVillagerShop/blob/main/LICENSE) + Turn Villagers into real shops — player-owned or admin-run — with selling, buying (order slots), co-ownership and revenue sharing, on Paper 1.21.8+. ModernVillagerShop replaces the vanilla trade window with a **chest UI + Dialog** hybrid: a chest inventory to browse and manage listings, and native Paper Dialogs for every amount, price and confirmation prompt. Because dialogs are driven through [BedrockDialog](https://modrinth.com/plugin/bedrockdialog), Bedrock players (Geyser + Floodgate) get the same flows rendered as Bedrock forms. @@ -31,7 +35,7 @@ ModernVillagerShop replaces the vanilla trade window with a **chest UI + Dialog* ## Installation -1. Drop `ModernVillagerShop-*.jar`, `Vault` and `BedrockDialog` into `plugins/`. +1. Download `ModernVillagerShop-*.jar` from [Modrinth](https://modrinth.com/plugin/modernvillagershop) (or the [GitHub releases](https://github.com/f0reachARR/ModernVillagerShop/releases/latest)) and drop it, together with `Vault` and `BedrockDialog`, into `plugins/`. 2. Make sure a Vault-compatible economy plugin is installed and running. 3. Start the server. `plugins/ModernVillagerShop/config.yml` and `lang/messages_{en,ja}.yml` are generated on first boot. 4. Set `locale` in `config.yml` (`ja_JP` by default, `en_US` also ships) and run `/vshop reload`. diff --git a/build.gradle b/build.gradle index 5524792..522926e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,11 @@ plugins { id 'maven-publish' id("xyz.jpenilla.run-paper") version "2.3.1" id("com.gradleup.shadow") version "9.0.0-beta13" + id("com.modrinth.minotaur") version "2.9.0" } group = 'me.f0reach.vshop' -version = '1.0.0' +// `version` comes from gradle.properties, which release-please keeps up to date. repositories { mavenCentral() @@ -98,6 +99,37 @@ processResources { } } +// Written by the release workflow from the GitHub Release body. When it is +// absent (a hand-run publish), fall back to a link to the release page. +def modrinthChangelogFile = layout.buildDirectory.file("modrinth-changelog.md") + +modrinth { + token = providers.environmentVariable("MODRINTH_TOKEN") + projectId = "modernvillagershop" + versionNumber = project.version.toString() + versionName = "ModernVillagerShop ${project.version}" + versionType = project.version.toString().contains("-") ? "beta" : "release" + uploadFile = tasks.shadowJar + gameVersions = providers.gradleProperty("modrinth.gameVersions") + .map { spec -> spec.split(",").collect { it.trim() }.findAll { !it.isEmpty() } } + loaders = ["paper"] + detectLoaders = false + changelog = providers.provider { + def file = modrinthChangelogFile.get().asFile + file.exists() && !file.text.trim().isEmpty() + ? file.text + : "See https://github.com/f0reachARR/ModernVillagerShop/releases/tag/v${project.version}".toString() + } + // README.md doubles as the Modrinth project description; `modrinthSyncBody` + // pushes it. Every link in it must stay absolute for that reason. + syncBodyFrom = providers.fileContents(layout.projectDirectory.file("README.md")).asText + dependencies { + required.project "bedrockdialog" + optional.project "placeholderapi" + // Vault has no Modrinth project, so it can only be documented in the body. + } +} + publishing { publications { shadow(MavenPublication) { diff --git a/gradle.properties b/gradle.properties index e69de29..7ed9efa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -0,0 +1,13 @@ +# The plugin version. This is the single source of truth: Gradle picks it up +# automatically, processResources expands it into paper-plugin.yml, and Minotaur +# uses it as the Modrinth version number. +# +# release-please rewrites the value inside the block below when a release PR is +# merged -- do not bump it by hand. +# x-release-please-start-version +version=0.0.0 +# x-release-please-end + +# Minecraft versions declared on the Modrinth release page. Kept in sync with +# BedrockDialog's supported versions, since it is a hard dependency. +modrinth.gameVersions=1.21.8,1.21.9,1.21.10,1.21.11,26.1,26.1.1,26.1.2,26.2 diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..1bb3651 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "simple", + "package-name": "ModernVillagerShop", + "include-component-in-tag": false, + "extra-files": ["gradle.properties"], + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "refactor", "section": "Refactoring" }, + { "type": "docs", "section": "Documentation" }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true }, + { "type": "style", "section": "Styling", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true } + ] + } + } +}