From 9027d9f04fb833f428d6ef528cb19542cef8bd49 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sat, 4 Apr 2026 17:22:27 +0900 Subject: ci: switch nightly release trigger from push to daily schedule Add check job to skip build when no new commits since last nightly. Remove concurrency group (unnecessary with cron), add --target flag to record exact commit SHA for skip detection. Co-Authored-By: Claude --- .github/workflows/nightly.yaml | 51 +++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 33de763..e6d69f9 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -1,21 +1,41 @@ name: Nightly Release on: - push: - branches: - - main - paths-ignore: - - 'docs/**' - - '*.md' - - 'LICENSE' - -concurrency: - group: nightly-release - cancel-in-progress: true + schedule: + - cron: '0 0 * * *' # Daily at UTC 00:00 (JST 09:00) + workflow_dispatch: jobs: + check: + runs-on: ubuntu-24.04 + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for new commits since last nightly + id: check + env: + GH_TOKEN: ${{ github.token }} + run: | + LAST_NIGHTLY_SHA=$(gh release view nightly --json targetCommitish --jq '.targetCommitish' 2>/dev/null || echo "") + HEAD_SHA=$(git rev-parse HEAD) + + if [ "$LAST_NIGHTLY_SHA" = "$HEAD_SHA" ]; then + echo "No new commits since last nightly release" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "New commits found (last: ${LAST_NIGHTLY_SHA:-none}, current: $HEAD_SHA)" + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + build: runs-on: ubuntu-24.04 + needs: check + if: needs.check.outputs.has_changes == 'true' outputs: version: ${{ steps.version.outputs.version }} paper_jar_name: ${{ steps.version.outputs.paper_jar_name }} @@ -95,15 +115,14 @@ jobs: "platform-paper/build/libs/$PAPER_JAR_NAME" \ "platform-velocity/build/libs/$VELOCITY_JAR_NAME" \ --title "Nightly Build ($VERSION)" \ + --target ${{ github.sha }} \ --prerelease \ - --notes "$(cat <<'EOF' - ## ⚠️ Nightly Build - - This is an automatically generated nightly build from the latest `main` branch. + --notes "$(cat < Date: Sat, 4 Apr 2026 20:26:37 +0900 Subject: feat: Improving Velocity's Cycling Compatibility --- .claude/commands/release.md | 33 +++++ .github/workflows/nightly.yaml | 130 ------------------- .github/workflows/release-nightly.yaml | 134 ++++++++++++++++++++ .github/workflows/release-paper.yaml | 140 +++++++++++++++++++++ .github/workflows/release-velocity.yaml | 139 ++++++++++++++++++++ .github/workflows/release.yaml | 62 +++++---- CHANGELOG.md | 1 + CLAUDE.md | 37 ++++++ build.gradle.kts | 1 - .../lunaticChat/engine/protocol/PluginMessage.kt | 3 + .../lunaticChat/engine/protocol/ProtocolVersion.kt | 43 ++++++- .../engine/protocol/PluginMessageCodecTest.kt | 22 ++++ .../protocol/ProtocolBackwardCompatibilityTest.kt | 140 +++++++++++++++++++++ .../engine/protocol/ProtocolVersionTest.kt | 20 +++ gradle.properties | 4 + platform-paper/build.gradle.kts | 2 + .../command/impl/lcv/VelocityStatusCommand.kt | 9 -- .../paper/velocity/VelocityConnectionManager.kt | 5 +- platform-velocity/build.gradle.kts | 2 + .../velocity/messaging/PluginMessageHandler.kt | 14 +-- .../velocity/messaging/PluginMessageHandlerTest.kt | 3 +- 21 files changed, 755 insertions(+), 189 deletions(-) create mode 100644 .claude/commands/release.md delete mode 100644 .github/workflows/nightly.yaml create mode 100644 .github/workflows/release-nightly.yaml create mode 100644 .github/workflows/release-paper.yaml create mode 100644 .github/workflows/release-velocity.yaml create mode 100644 engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000..ff3edc1 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,33 @@ +--- +description: リリースの準備とタグ作成を行う。引数に paper, velocity, both のいずれかを指定。 +--- + +# Release Command + +引数: `$ARGUMENTS` + +## 手順 + +1. `gradle.properties` から `paperVersion` と `velocityVersion` を読み取る +2. 引数に応じてリリース対象を決定: + - `paper` → Paper のみリリース + - `velocity` → Velocity のみリリース + - `both` → 両方同時リリース + - 引数なし → ユーザーに確認する +3. リリース前チェック: + - `./gradlew ktlintCheck` が通ること + - `./gradlew test` が通ること + - 対象プラットフォームの `shadowJar` がビルドできること + - git working tree がクリーンであること + - 該当タグがまだ存在しないこと +4. チェックがすべて通ったら、タグの作成をユーザーに提案する: + - `paper` → `git tag paper/v{paperVersion}` + - `velocity` → `git tag velocity/v{velocityVersion}` + - `both` → `git tag v{paperVersion}` (Paper/Velocity バージョンが異なる場合は注意を促す) +5. タグを push するかユーザーに確認する (`git push origin {tag}`) + +## 注意事項 + +- タグの作成と push は必ずユーザーの確認を得てから行うこと +- `both` の場合、Paper と Velocity のバージョンが異なる場合はその旨を明示すること +- プロトコルバージョン (`ProtocolVersion.kt`) の変更がある場合は、後方互換テスト (`ProtocolBackwardCompatibilityTest`) が通っていることを確認すること diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml deleted file mode 100644 index e6d69f9..0000000 --- a/.github/workflows/nightly.yaml +++ /dev/null @@ -1,130 +0,0 @@ -name: Nightly Release - -on: - schedule: - - cron: '0 0 * * *' # Daily at UTC 00:00 (JST 09:00) - workflow_dispatch: - -jobs: - check: - runs-on: ubuntu-24.04 - outputs: - has_changes: ${{ steps.check.outputs.has_changes }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check for new commits since last nightly - id: check - env: - GH_TOKEN: ${{ github.token }} - run: | - LAST_NIGHTLY_SHA=$(gh release view nightly --json targetCommitish --jq '.targetCommitish' 2>/dev/null || echo "") - HEAD_SHA=$(git rev-parse HEAD) - - if [ "$LAST_NIGHTLY_SHA" = "$HEAD_SHA" ]; then - echo "No new commits since last nightly release" - echo "has_changes=false" >> $GITHUB_OUTPUT - else - echo "New commits found (last: ${LAST_NIGHTLY_SHA:-none}, current: $HEAD_SHA)" - echo "has_changes=true" >> $GITHUB_OUTPUT - fi - - build: - runs-on: ubuntu-24.04 - needs: check - if: needs.check.outputs.has_changes == 'true' - outputs: - version: ${{ steps.version.outputs.version }} - paper_jar_name: ${{ steps.version.outputs.paper_jar_name }} - velocity_jar_name: ${{ steps.version.outputs.velocity_jar_name }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Compute nightly version - id: version - run: | - BASE_VERSION=$(grep '?: "' build.gradle.kts | head -1 | sed 's/.*?: "\(.*\)"/\1/') - if [ -z "$BASE_VERSION" ]; then - BASE_VERSION="1.0.0" - fi - SHORT_HASH=$(git rev-parse --short HEAD) - NIGHTLY_VERSION="${BASE_VERSION}-nightly.${SHORT_HASH}" - echo "version=$NIGHTLY_VERSION" >> $GITHUB_OUTPUT - echo "paper_jar_name=LunaticChat-${NIGHTLY_VERSION}.jar" >> $GITHUB_OUTPUT - echo "velocity_jar_name=LunaticChat-${NIGHTLY_VERSION}-velocity.jar" >> $GITHUB_OUTPUT - echo "Nightly version: $NIGHTLY_VERSION" - - - name: Setup Development Environment - uses: jdx/mise-action@v4 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with shadowJar - run: | - ./gradlew :platform-paper:shadowJar :platform-velocity:shadowJar \ - -Pversion="${{ steps.version.outputs.version }}" \ - -PisNightly=true \ - --parallel --no-daemon - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: LunaticChat-nightly-${{ steps.version.outputs.version }} - path: | - platform-paper/build/libs/${{ steps.version.outputs.paper_jar_name }} - platform-velocity/build/libs/${{ steps.version.outputs.velocity_jar_name }} - retention-days: 7 - - release: - runs-on: ubuntu-24.04 - needs: build - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Download build artifact - uses: actions/download-artifact@v8 - with: - name: LunaticChat-nightly-${{ needs.build.outputs.version }} - - - name: Delete existing nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release delete nightly --yes --cleanup-tag 2>/dev/null || true - - - name: Create nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION=${{ needs.build.outputs.version }} - PAPER_JAR_NAME=${{ needs.build.outputs.paper_jar_name }} - VELOCITY_JAR_NAME=${{ needs.build.outputs.velocity_jar_name }} - - gh release create nightly \ - "platform-paper/build/libs/$PAPER_JAR_NAME" \ - "platform-velocity/build/libs/$VELOCITY_JAR_NAME" \ - --title "Nightly Build ($VERSION)" \ - --target ${{ github.sha }} \ - --prerelease \ - --notes "$(cat </dev/null || echo "") + HEAD_SHA=$(git rev-parse HEAD) + + if [ "$LAST_NIGHTLY_SHA" = "$HEAD_SHA" ]; then + echo "No new commits since last nightly release" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "New commits found (last: ${LAST_NIGHTLY_SHA:-none}, current: $HEAD_SHA)" + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + build: + runs-on: ubuntu-24.04 + needs: check + if: needs.check.outputs.has_changes == 'true' + outputs: + version: ${{ steps.version.outputs.version }} + paper_jar_name: ${{ steps.version.outputs.paper_jar_name }} + velocity_jar_name: ${{ steps.version.outputs.velocity_jar_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Compute nightly version + id: version + run: | + PAPER_BASE=$(grep '^paperVersion=' gradle.properties | cut -d'=' -f2) + VELOCITY_BASE=$(grep '^velocityVersion=' gradle.properties | cut -d'=' -f2) + SHORT_HASH=$(git rev-parse --short HEAD) + + PAPER_NIGHTLY="${PAPER_BASE}-nightly.${SHORT_HASH}" + VELOCITY_NIGHTLY="${VELOCITY_BASE}-nightly.${SHORT_HASH}" + + echo "version=${PAPER_NIGHTLY}" >> $GITHUB_OUTPUT + echo "paper_version=${PAPER_NIGHTLY}" >> $GITHUB_OUTPUT + echo "velocity_version=${VELOCITY_NIGHTLY}" >> $GITHUB_OUTPUT + echo "paper_jar_name=LunaticChat-${PAPER_NIGHTLY}.jar" >> $GITHUB_OUTPUT + echo "velocity_jar_name=LunaticChat-${VELOCITY_NIGHTLY}-velocity.jar" >> $GITHUB_OUTPUT + echo "Paper nightly: $PAPER_NIGHTLY, Velocity nightly: $VELOCITY_NIGHTLY" + + - name: Setup Development Environment + uses: jdx/mise-action@v4 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with shadowJar + run: | + ./gradlew :platform-paper:shadowJar :platform-velocity:shadowJar \ + -PpaperVersion="${{ steps.version.outputs.paper_version }}" \ + -PvelocityVersion="${{ steps.version.outputs.velocity_version }}" \ + -PisNightly=true \ + --parallel --no-daemon + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: LunaticChat-nightly-${{ steps.version.outputs.version }} + path: | + platform-paper/build/libs/${{ steps.version.outputs.paper_jar_name }} + platform-velocity/build/libs/${{ steps.version.outputs.velocity_jar_name }} + retention-days: 7 + + release: + runs-on: ubuntu-24.04 + needs: build + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: LunaticChat-nightly-${{ needs.build.outputs.version }} + + - name: Delete existing nightly release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release delete nightly --yes --cleanup-tag 2>/dev/null || true + + - name: Create nightly release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION=${{ needs.build.outputs.version }} + PAPER_JAR_NAME=${{ needs.build.outputs.paper_jar_name }} + VELOCITY_JAR_NAME=${{ needs.build.outputs.velocity_jar_name }} + + gh release create nightly \ + "platform-paper/build/libs/$PAPER_JAR_NAME" \ + "platform-velocity/build/libs/$VELOCITY_JAR_NAME" \ + --title "Nightly Build ($VERSION)" \ + --target ${{ github.sha }} \ + --prerelease \ + --notes "$(cat <> $GITHUB_OUTPUT + echo "jar_name=LunaticChat-$TAG_VERSION.jar" >> $GITHUB_OUTPUT + echo "Version validation passed: $TAG_VERSION" + + - name: Check if release already exists + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + if gh release view "$TAG_NAME" &> /dev/null; then + echo "Error: Release $TAG_NAME already exists" + exit 1 + fi + echo "Release validation passed: $TAG_NAME does not exist yet" + + build: + runs-on: ubuntu-24.04 + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Development Environment + uses: jdx/mise-action@v4 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Paper shadowJar + run: ./gradlew :platform-paper:shadowJar --no-daemon + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: LunaticChat-paper-v${{ needs.validate.outputs.version }} + path: platform-paper/build/libs/${{ needs.validate.outputs.jar_name }} + retention-days: 90 + + release: + runs-on: ubuntu-24.04 + needs: [validate, build] + permissions: + contents: write + env: + MODRINTH_CHANNEL: release + MODRINTH_PAPER_GAME_VERSIONS: "26.1.x" + MODRINTH_MC_VERSION: "26.1" + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: LunaticChat-paper-v${{ needs.validate.outputs.version }} + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${{ needs.validate.outputs.version }} + JAR_NAME=${{ needs.validate.outputs.jar_name }} + + cat > /tmp/release-notes.md <> $GITHUB_OUTPUT + echo "jar_name=LunaticChat-$TAG_VERSION-velocity.jar" >> $GITHUB_OUTPUT + echo "Version validation passed: $TAG_VERSION" + + - name: Check if release already exists + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + if gh release view "$TAG_NAME" &> /dev/null; then + echo "Error: Release $TAG_NAME already exists" + exit 1 + fi + echo "Release validation passed: $TAG_NAME does not exist yet" + + build: + runs-on: ubuntu-24.04 + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Development Environment + uses: jdx/mise-action@v4 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Velocity shadowJar + run: ./gradlew :platform-velocity:shadowJar --no-daemon + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: LunaticChat-velocity-v${{ needs.validate.outputs.version }} + path: platform-velocity/build/libs/${{ needs.validate.outputs.jar_name }} + retention-days: 90 + + release: + runs-on: ubuntu-24.04 + needs: [validate, build] + permissions: + contents: write + env: + MODRINTH_CHANNEL: release + MODRINTH_VELOCITY_GAME_VERSIONS: |- + 1.21.x + 26.1.x + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: LunaticChat-velocity-v${{ needs.validate.outputs.version }} + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${{ needs.validate.outputs.version }} + JAR_NAME=${{ needs.validate.outputs.jar_name }} + + cat > /tmp/release-notes.md <> $GITHUB_OUTPUT - echo "paper_jar_name=LunaticChat-$VERSION.jar" >> $GITHUB_OUTPUT - echo "velocity_jar_name=LunaticChat-$VERSION-velocity.jar" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" + PAPER_VERSION=$(grep '^paperVersion=' gradle.properties | cut -d'=' -f2) + VELOCITY_VERSION=$(grep '^velocityVersion=' gradle.properties | cut -d'=' -f2) - - name: Validate tag matches version - run: | - TAG_VERSION=${GITHUB_REF#refs/tags/v} - GRADLE_VERSION="${{ steps.get_version.outputs.version }}" - - if [ "$TAG_VERSION" != "$GRADLE_VERSION" ]; then - echo "Error: Tag version ($TAG_VERSION) does not match build.gradle.kts version ($GRADLE_VERSION)" - exit 1 - fi - echo "Version validation passed: $TAG_VERSION" + echo "paper_version=$PAPER_VERSION" >> $GITHUB_OUTPUT + echo "velocity_version=$VELOCITY_VERSION" >> $GITHUB_OUTPUT + echo "paper_jar_name=LunaticChat-$PAPER_VERSION.jar" >> $GITHUB_OUTPUT + echo "velocity_jar_name=LunaticChat-$VELOCITY_VERSION-velocity.jar" >> $GITHUB_OUTPUT + echo "Paper version: $PAPER_VERSION, Velocity version: $VELOCITY_VERSION" - name: Check if release already exists env: @@ -69,7 +62,7 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v7 with: - name: LunaticChat-v${{ needs.validate.outputs.version }} + name: LunaticChat-${{ github.ref_name }} path: | platform-paper/build/libs/${{ needs.validate.outputs.paper_jar_name }} platform-velocity/build/libs/${{ needs.validate.outputs.velocity_jar_name }} @@ -94,28 +87,30 @@ jobs: - name: Download build artifact uses: actions/download-artifact@v8 with: - name: LunaticChat-v${{ needs.validate.outputs.version }} - - - name: Get current date - id: get_date - run: echo "current_date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + name: LunaticChat-${{ github.ref_name }} - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} run: | TAG_NAME=${GITHUB_REF#refs/tags/} - VERSION=${{ needs.validate.outputs.version }} + PAPER_VERSION=${{ needs.validate.outputs.paper_version }} + VELOCITY_VERSION=${{ needs.validate.outputs.velocity_version }} PAPER_JAR_NAME=${{ needs.validate.outputs.paper_jar_name }} VELOCITY_JAR_NAME=${{ needs.validate.outputs.velocity_jar_name }} cat > /tmp/release-notes.md < 1.0.1): + * - Add optional fields with default values to existing message types. + * - Add new sub-channels that peers can safely ignore. + * - No deployment coordination required. + * + * **MINOR** (e.g., 1.0.x -> 1.1.0): + * - Add required fields to existing messages. + * - Add sub-channels whose absence degrades functionality. + * - Deployment order: update Velocity first, then Paper servers. + * - Set [MIN_SUPPORTED_MINOR] to control the deprecation window. + * + * **MAJOR** (e.g., 1.x.x -> 2.0.0): + * - Remove or rename existing sub-channels or fields. + * - Change wire format or encoding. + * - Requires simultaneous deployment of all components. + * + * ## Adding a New Message Type (sub-channel) + * 1. Add the data class to [PluginMessage]. + * 2. Add a sub-channel constant to [PluginMessageCodec.SubChannel]. + * 3. Add encode/decode branches in [PluginMessageCodec]. + * 4. Add a backward compatibility snapshot to ProtocolBackwardCompatibilityTest. + * 5. Bump PATCH if the new sub-channel is optional, MINOR if it is required. */ object ProtocolVersion { const val MAJOR = 1 const val MINOR = 0 const val PATCH = 0 + /** + * Minimum MINOR version this build can interoperate with (same MAJOR). + * + * When bumping MINOR, set this to the oldest MINOR version that should still + * be accepted. This allows a controlled deprecation window for rolling updates. + */ + const val MIN_SUPPORTED_MINOR = 0 + val version: String = "$MAJOR.$MINOR.$PATCH" /** * Checks if specified protocol version is compatible * + * Compatibility rules: + * - MAJOR must match exactly. + * - Remote MINOR must be >= [MIN_SUPPORTED_MINOR] and <= [MINOR]. + * - PATCH is always ignored. + * * @param major Major version * @param minor Minor version - * @return true if MAJOR and MINOR match + * @return true if compatible */ fun isCompatible( major: Int, minor: Int, - ): Boolean = MAJOR == major && MINOR == minor + ): Boolean = MAJOR == major && minor in MIN_SUPPORTED_MINOR..MINOR /** * Checks compatibility from version string diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt index c792aad..e848780 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt @@ -44,6 +44,28 @@ class PluginMessageCodecTest { assertEquals(original.error, decoded.error) } + @Test + fun `encode and decode HandshakeResponse with protocol version fields`() { + val original = + PluginMessage.HandshakeResponse( + compatible = true, + velocityVersion = "0.11.0", + protocolMajor = 1, + protocolMinor = 1, + protocolPatch = 0, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs(decoded) + assertEquals(original.compatible, decoded.compatible) + assertEquals(original.velocityVersion, decoded.velocityVersion) + assertEquals(1, decoded.protocolMajor) + assertEquals(1, decoded.protocolMinor) + assertEquals(0, decoded.protocolPatch) + } + @Test fun `encode and decode HandshakeResponse incompatible round-trip`() { val original = diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt new file mode 100644 index 0000000..217baef --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt @@ -0,0 +1,140 @@ +package dev.m1sk9.lunaticChat.engine.protocol + +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Backward compatibility tests using fixed JSON snapshots. + * + * These snapshots represent the wire format of protocol version 1.0.0. + * DO NOT MODIFY these constants after commit — they are the compatibility contract. + * If a test fails after modifying a @Serializable class, it means backward compatibility is broken. + */ +class ProtocolBackwardCompatibilityTest { + companion object { + // Protocol 1.0.0 snapshots — NEVER MODIFY after commit + const val HANDSHAKE_V1_0_0 = + """{"pluginVersion":"0.10.0","protocolMajor":1,"protocolMinor":0,"protocolPatch":0}""" + + const val HANDSHAKE_RESPONSE_V1_0_0 = + """{"compatible":true,"velocityVersion":"0.10.0"}""" + + const val HANDSHAKE_RESPONSE_INCOMPATIBLE_V1_0_0 = + """{"compatible":false,"velocityVersion":"0.10.0","error":"Version mismatch"}""" + + const val STATUS_RESPONSE_V1_0_0 = + """{"velocityVersion":"0.10.0","protocolVersion":"1.0.0","online":true}""" + + const val GLOBAL_CHAT_V1_0_0 = + """{"messageId":"abc-123","serverName":"lobby","playerId":"00000001-0000-0000-0000-000000000000","playerName":"TestPlayer","message":"Hello, world!","timestamp":1000}""" + } + + private fun buildRawMessage( + subChannel: String, + json: String, + ): ByteArray { + val out = ByteArrayOutputStream() + val dataOut = DataOutputStream(out) + dataOut.writeUTF(subChannel) + dataOut.writeUTF(json) + return out.toByteArray() + } + + @Test + fun `current codec can decode protocol 1_0_0 Handshake`() { + val data = buildRawMessage("handshake", HANDSHAKE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals("0.10.0", decoded.pluginVersion) + assertEquals(1, decoded.protocolMajor) + assertEquals(0, decoded.protocolMinor) + assertEquals(0, decoded.protocolPatch) + } + + @Test + fun `current codec can decode HandshakeResponse without protocol version fields`() { + val data = buildRawMessage("handshake_response", HANDSHAKE_RESPONSE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertTrue(decoded.compatible) + assertEquals("0.10.0", decoded.velocityVersion) + assertNull(decoded.error) + // Protocol fields should get defaults when missing from old format + assertEquals(ProtocolVersion.MAJOR, decoded.protocolMajor) + assertEquals(ProtocolVersion.MINOR, decoded.protocolMinor) + assertEquals(ProtocolVersion.PATCH, decoded.protocolPatch) + } + + @Test + fun `current codec can decode HandshakeResponse incompatible without protocol version fields`() { + val data = buildRawMessage("handshake_response", HANDSHAKE_RESPONSE_INCOMPATIBLE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals(false, decoded.compatible) + assertEquals("Version mismatch", decoded.error) + } + + @Test + fun `current codec can decode protocol 1_0_0 StatusResponse`() { + val data = buildRawMessage("status_response", STATUS_RESPONSE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals("0.10.0", decoded.velocityVersion) + assertEquals("1.0.0", decoded.protocolVersion) + assertTrue(decoded.online) + } + + @Test + fun `current codec can decode protocol 1_0_0 GlobalChatMessage`() { + val data = buildRawMessage("global_chat", GLOBAL_CHAT_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals("abc-123", decoded.messageId) + assertEquals("lobby", decoded.serverName) + assertEquals("00000001-0000-0000-0000-000000000000", decoded.playerId) + assertEquals("TestPlayer", decoded.playerName) + assertEquals("Hello, world!", decoded.message) + assertEquals(1000L, decoded.timestamp) + } + + @Test + fun `current codec ignores unknown fields in Handshake`() { + val json = + """{"pluginVersion":"0.10.0","protocolMajor":1,"protocolMinor":0,"protocolPatch":0,"futureField":"value"}""" + val data = buildRawMessage("handshake", json) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals("0.10.0", decoded.pluginVersion) + } + + @Test + fun `current codec ignores unknown fields in GlobalChatMessage`() { + val json = + """{"messageId":"abc","serverName":"lobby","playerId":"pid","playerName":"Test","message":"Hello","timestamp":1000,"futureField":"value","anotherField":42}""" + val data = buildRawMessage("global_chat", json) + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + assertEquals("abc", decoded.messageId) + assertEquals("Hello", decoded.message) + } + + @Test + fun `StatusRequest sub-channel decodes correctly with empty JSON`() { + val data = buildRawMessage("status_request", "{}") + val decoded = PluginMessageCodec.decode(data) + + assertIs(decoded) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt index 7f05039..ad68beb 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt @@ -54,4 +54,24 @@ class ProtocolVersionTest { fun `isCompatible string with two parts should return false`() { assertFalse(ProtocolVersion.isCompatible("1.0")) } + + @Test + fun `MIN_SUPPORTED_MINOR should be less than or equal to MINOR`() { + assertTrue(ProtocolVersion.MIN_SUPPORTED_MINOR <= ProtocolVersion.MINOR) + } + + @Test + fun `isCompatible with minor equal to MINOR should return true`() { + assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR)) + } + + @Test + fun `isCompatible with minor equal to MIN_SUPPORTED_MINOR should return true`() { + assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MIN_SUPPORTED_MINOR)) + } + + @Test + fun `isCompatible with minor above MINOR should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR + 1)) + } } diff --git a/gradle.properties b/gradle.properties index 7750c23..9ab6267 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + +# Platform versions (can be released independently) +paperVersion=1.0.0 +velocityVersion=1.0.0 diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index e1042c5..1de4572 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -6,6 +6,8 @@ plugins { id("org.jetbrains.dokka") } +version = findProperty("paperVersion")?.toString() ?: "0.0.0" + repositories { maven("https://repo.papermc.io/repository/maven-public/") { name = "papermc-repo" diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt index 439a29a..4e48bfc 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt @@ -140,15 +140,6 @@ class VelocityStatusCommand( ), ) - // Nightly warning - if (BuildInfo.isNightly) { - sendMessage( - MessageFormatter - .format(languageManager.getMessage("general.nightlyWarning")) - .color(NamedTextColor.YELLOW), - ) - } - // Connection status details connectionStatus.forEach { line -> line?.let { sendMessage(it) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt index 1a7e667..a93cb9f 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt @@ -197,7 +197,10 @@ class VelocityConnectionManager( if (response.compatible) { state = ConnectionState.CONNECTED velocityVersion = response.velocityVersion - logger.info("Successfully connected to Velocity (version: ${response.velocityVersion})") + logger.info( + "Successfully connected to Velocity (version: ${response.velocityVersion}, " + + "protocol: ${response.protocolMajor}.${response.protocolMinor}.${response.protocolPatch})", + ) future.complete(HandshakeResult.Success(response.velocityVersion)) } else { state = ConnectionState.FAILED diff --git a/platform-velocity/build.gradle.kts b/platform-velocity/build.gradle.kts index f6a4e57..dcd9f9a 100644 --- a/platform-velocity/build.gradle.kts +++ b/platform-velocity/build.gradle.kts @@ -4,6 +4,8 @@ plugins { id("com.gradleup.shadow") } +version = findProperty("velocityVersion")?.toString() ?: "0.0.0" + repositories { maven("https://repo.papermc.io/repository/maven-public/") { name = "papermc-repo" diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt index 74da9f2..382eba4 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt @@ -84,16 +84,7 @@ class PluginMessageHandler( "Plugin=${handshake.pluginVersion}, Protocol=${handshake.protocolMajor}.${handshake.protocolMinor}.${handshake.protocolPatch}", ) - // Plugin version check (exact match required) - val versionMatch = handshake.pluginVersion == pluginVersion - if (!versionMatch) { - val error = "Plugin version mismatch: Paper=${handshake.pluginVersion}, Velocity=$pluginVersion" - logger.error(error) - sendHandshakeResponse(connection, false, error) - return - } - - // Protocol version check (MAJOR.MINOR must match) + // Protocol version check (MAJOR must match, MINOR within supported range) val protocolCompatible = ProtocolVersion.isCompatible( handshake.protocolMajor, @@ -126,6 +117,9 @@ class PluginMessageHandler( compatible = compatible, velocityVersion = pluginVersion, error = error, + protocolMajor = ProtocolVersion.MAJOR, + protocolMinor = ProtocolVersion.MINOR, + protocolPatch = ProtocolVersion.PATCH, ) val data = PluginMessageCodec.encode(response) diff --git a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt index 876eec6..3342331 100644 --- a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt +++ b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt @@ -69,7 +69,7 @@ class PluginMessageHandlerTest { } @Test - fun `onPluginMessage should reject version mismatch handshake`() { + fun `onPluginMessage should accept different plugin version with matching protocol`() { val (handler, _, _) = createHandler(pluginVersion = "0.10.0") val connection = createServerConnection() @@ -85,6 +85,7 @@ class PluginMessageHandlerTest { handler.onPluginMessage(event) + // Should still send a response (successful handshake) verify { connection.sendPluginMessage(any(), any()) } } -- cgit v1.2.1 From 178f5e3d538d1b54aa5f0c6637d8b0b7981ed802 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sat, 4 Apr 2026 20:57:03 +0900 Subject: ci: Remove nightly release --- .github/workflows/release-nightly.yaml | 134 --------------------- CHANGELOG.md | 4 - .../command/impl/lcv/VelocityStatusCommand.kt | 1 - 3 files changed, 139 deletions(-) delete mode 100644 .github/workflows/release-nightly.yaml diff --git a/.github/workflows/release-nightly.yaml b/.github/workflows/release-nightly.yaml deleted file mode 100644 index daa31fe..0000000 --- a/.github/workflows/release-nightly.yaml +++ /dev/null @@ -1,134 +0,0 @@ -name: Nightly Release - -on: - schedule: - - cron: '0 0 * * *' # Daily at UTC 00:00 (JST 09:00) - workflow_dispatch: - -jobs: - check: - runs-on: ubuntu-24.04 - outputs: - has_changes: ${{ steps.check.outputs.has_changes }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check for new commits since last nightly - id: check - env: - GH_TOKEN: ${{ github.token }} - run: | - LAST_NIGHTLY_SHA=$(gh release view nightly --json targetCommitish --jq '.targetCommitish' 2>/dev/null || echo "") - HEAD_SHA=$(git rev-parse HEAD) - - if [ "$LAST_NIGHTLY_SHA" = "$HEAD_SHA" ]; then - echo "No new commits since last nightly release" - echo "has_changes=false" >> $GITHUB_OUTPUT - else - echo "New commits found (last: ${LAST_NIGHTLY_SHA:-none}, current: $HEAD_SHA)" - echo "has_changes=true" >> $GITHUB_OUTPUT - fi - - build: - runs-on: ubuntu-24.04 - needs: check - if: needs.check.outputs.has_changes == 'true' - outputs: - version: ${{ steps.version.outputs.version }} - paper_jar_name: ${{ steps.version.outputs.paper_jar_name }} - velocity_jar_name: ${{ steps.version.outputs.velocity_jar_name }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Compute nightly version - id: version - run: | - PAPER_BASE=$(grep '^paperVersion=' gradle.properties | cut -d'=' -f2) - VELOCITY_BASE=$(grep '^velocityVersion=' gradle.properties | cut -d'=' -f2) - SHORT_HASH=$(git rev-parse --short HEAD) - - PAPER_NIGHTLY="${PAPER_BASE}-nightly.${SHORT_HASH}" - VELOCITY_NIGHTLY="${VELOCITY_BASE}-nightly.${SHORT_HASH}" - - echo "version=${PAPER_NIGHTLY}" >> $GITHUB_OUTPUT - echo "paper_version=${PAPER_NIGHTLY}" >> $GITHUB_OUTPUT - echo "velocity_version=${VELOCITY_NIGHTLY}" >> $GITHUB_OUTPUT - echo "paper_jar_name=LunaticChat-${PAPER_NIGHTLY}.jar" >> $GITHUB_OUTPUT - echo "velocity_jar_name=LunaticChat-${VELOCITY_NIGHTLY}-velocity.jar" >> $GITHUB_OUTPUT - echo "Paper nightly: $PAPER_NIGHTLY, Velocity nightly: $VELOCITY_NIGHTLY" - - - name: Setup Development Environment - uses: jdx/mise-action@v4 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with shadowJar - run: | - ./gradlew :platform-paper:shadowJar :platform-velocity:shadowJar \ - -PpaperVersion="${{ steps.version.outputs.paper_version }}" \ - -PvelocityVersion="${{ steps.version.outputs.velocity_version }}" \ - -PisNightly=true \ - --parallel --no-daemon - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: LunaticChat-nightly-${{ steps.version.outputs.version }} - path: | - platform-paper/build/libs/${{ steps.version.outputs.paper_jar_name }} - platform-velocity/build/libs/${{ steps.version.outputs.velocity_jar_name }} - retention-days: 7 - - release: - runs-on: ubuntu-24.04 - needs: build - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Download build artifact - uses: actions/download-artifact@v8 - with: - name: LunaticChat-nightly-${{ needs.build.outputs.version }} - - - name: Delete existing nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release delete nightly --yes --cleanup-tag 2>/dev/null || true - - - name: Create nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION=${{ needs.build.outputs.version }} - PAPER_JAR_NAME=${{ needs.build.outputs.paper_jar_name }} - VELOCITY_JAR_NAME=${{ needs.build.outputs.velocity_jar_name }} - - gh release create nightly \ - "platform-paper/build/libs/$PAPER_JAR_NAME" \ - "platform-velocity/build/libs/$VELOCITY_JAR_NAME" \ - --title "Nightly Build ($VERSION)" \ - --target ${{ github.sha }} \ - --prerelease \ - --notes "$(cat < Date: Sat, 4 Apr 2026 21:53:56 +0900 Subject: test: Add protocol version test-case --- codecov.yml | 3 +++ .../lunaticChat/engine/protocol/ProtocolVersionTest.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/codecov.yml b/codecov.yml index e23de4b..453e954 100644 --- a/codecov.yml +++ b/codecov.yml @@ -21,6 +21,9 @@ ignore: # VelocityStatusCommand is tightly coupled to VelocityConnectionManager and has no unit-test # infrastructure; it is exercised via end-to-end multi-server tests. - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt" + # VelocityConnectionManager is tightly coupled to the Paper/Bukkit runtime (Plugin, Player, + # PluginMessageListener) and is exercised via end-to-end multi-server tests. + - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt" # The Velocity platform has no unit-test infrastructure; it is exercised via end-to-end # multi-server tests (docker compose). - "platform-velocity/src/main/kotlin/**" diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt index ad68beb..e8b9df8 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt @@ -74,4 +74,18 @@ class ProtocolVersionTest { fun `isCompatible with minor above MINOR should return false`() { assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR + 1)) } + + @Test + fun `isCompatible with minor below MIN_SUPPORTED_MINOR should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MIN_SUPPORTED_MINOR - 1)) + } + + @Test + fun `isCompatible string with minor below MIN_SUPPORTED_MINOR should return false`() { + assertFalse( + ProtocolVersion.isCompatible( + "${ProtocolVersion.MAJOR}.${ProtocolVersion.MIN_SUPPORTED_MINOR - 1}.0", + ), + ) + } } -- cgit v1.2.1