diff --git a/build.gradle.kts b/build.gradle.kts index c28b715..2c52711 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,11 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + // Tests need Paper on both compile + runtime classpaths because + // Mockito loads the target class to generate the proxy. testCompileOnly + // alone passes compilation but throws ClassNotFoundException when + // mock(Server::class) runs. + testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") // Compact JSON parser — sufficient for the small whitelist payloads diff --git a/src/main/kotlin/gg/grounds/platform/GroundsPlatformPlugin.kt b/src/main/kotlin/gg/grounds/platform/GroundsPlatformPlugin.kt index 6cc5844..44e7b6a 100644 --- a/src/main/kotlin/gg/grounds/platform/GroundsPlatformPlugin.kt +++ b/src/main/kotlin/gg/grounds/platform/GroundsPlatformPlugin.kt @@ -36,9 +36,10 @@ class GroundsPlatformPlugin : JavaPlugin() { ) } - MotdSetter(server).apply(env.projectName) + MotdSetter(server).apply(env.projectName, env.pushId) logger.info( - "MOTD set from platform context (projectId=${env.projectId}, projectName=${env.projectName})" + "MOTD set from platform context (projectId=${env.projectId}, " + + "projectName=${env.projectName}, pushId=${env.pushId ?: "n/a"})" ) val sync = WhitelistSync(server, logger) diff --git a/src/main/kotlin/gg/grounds/platform/PlatformEnv.kt b/src/main/kotlin/gg/grounds/platform/PlatformEnv.kt index cf7ca8d..f802cf9 100644 --- a/src/main/kotlin/gg/grounds/platform/PlatformEnv.kt +++ b/src/main/kotlin/gg/grounds/platform/PlatformEnv.kt @@ -9,7 +9,17 @@ package gg.grounds.platform * Token is read separately from a secret-mounted env var rather than from this struct, so we never * accidentally include it in a `toString` or log line. */ -data class PlatformEnv(val projectId: String, val projectName: String, val forgeUrl: String) +data class PlatformEnv( + val projectId: String, + val projectName: String, + val forgeUrl: String, + /** + * Push ID of the deployment that produced this pod (the renderer's `GROUNDS_PUSH_ID` env). + * Optional — older deployments may not have it. Surfaced in the MOTD as a "version" tag for + * operator orientation; whitelist sync doesn't depend on it. + */ + val pushId: String? = null, +) interface EnvReader { operator fun get(name: String): String? @@ -35,6 +45,7 @@ fun readPlatformEnv(reader: EnvReader = SystemEnvReader): PlatformEnv? { projectId = projectId, projectName = projectName, forgeUrl = forgeUrl.trimEnd('/'), + pushId = reader["GROUNDS_PUSH_ID"]?.trim()?.takeIf { it.isNotEmpty() }, ) } diff --git a/src/main/kotlin/gg/grounds/platform/motd/MotdSetter.kt b/src/main/kotlin/gg/grounds/platform/motd/MotdSetter.kt index 3d180d3..d53cc85 100644 --- a/src/main/kotlin/gg/grounds/platform/motd/MotdSetter.kt +++ b/src/main/kotlin/gg/grounds/platform/motd/MotdSetter.kt @@ -3,18 +3,33 @@ package gg.grounds.platform.motd import org.bukkit.Server /** - * Sets the server MOTD to a project-aware string at startup. Format: + * Sets the server MOTD to a project-aware string at startup. Two lines, joined by a single newline + * (the only separator Paper's MOTD field accepts): + * - Line one: `§f §8` + * - Line two: `§8powered by Grounds Developer Platform` * - * §8via Grounds - * - * Two lines because Paper's MOTD field accepts a single newline; the second line uses §8 (dark - * grey) to keep the platform attribution subtle. Operators can override post-startup via `/motd` if - * Paper supports it on their server, or a future per-project MOTD setting. + * Project name in bright white, push-id tail in dim grey so operators can tell at a glance which + * deployment is running. Color codes use the §-prefix legacy form which Paper renders consistently + * across vanilla + modded clients. */ class MotdSetter(private val server: Server) { - fun apply(projectName: String) { - val motd = "$projectName\n§8via Grounds" + fun apply(projectName: String, pushId: String? = null) { + val versionSuffix = + pushId + ?.let { it.replace("-", "").take(SHORT_PUSH_ID_LEN) } + ?.takeIf { it.isNotEmpty() } + ?.let { " §8$it" } ?: "" + val motd = "§f$projectName$versionSuffix\n§8powered by Grounds Developer Platform" server.setMotd(motd) } + + companion object { + /** + * Push IDs are UUIDs. We strip dashes and keep the first 8 chars to mirror the portal's + * push- row convention (`p.id.slice(0, 8)` in `pushes-table.tsx`). Long enough to be + * near-unique within a project, short enough not to dominate the MOTD line. + */ + private const val SHORT_PUSH_ID_LEN = 8 + } } diff --git a/src/test/kotlin/gg/grounds/platform/PlatformEnvTest.kt b/src/test/kotlin/gg/grounds/platform/PlatformEnvTest.kt index ab4b2f5..3e101da 100644 --- a/src/test/kotlin/gg/grounds/platform/PlatformEnvTest.kt +++ b/src/test/kotlin/gg/grounds/platform/PlatformEnvTest.kt @@ -23,7 +23,23 @@ class PlatformEnvTest { ) ) ) - assertEquals(PlatformEnv("p-1", "Demo Project", "http://forge:8080"), env) + assertEquals(PlatformEnv("p-1", "Demo Project", "http://forge:8080", pushId = null), env) + } + + @Test + fun `pushId surfaces when GROUNDS_PUSH_ID is set`() { + val env = + readPlatformEnv( + reader( + mapOf( + "GROUNDS_PROJECT_ID" to "p-1", + "GROUNDS_PROJECT_NAME" to "P", + "GROUNDS_FORGE_URL" to "http://forge", + "GROUNDS_PUSH_ID" to "abc12345-6789-0abc-def0-123456789abc", + ) + ) + ) + assertEquals("abc12345-6789-0abc-def0-123456789abc", env?.pushId) } @Test diff --git a/src/test/kotlin/gg/grounds/platform/motd/MotdSetterTest.kt b/src/test/kotlin/gg/grounds/platform/motd/MotdSetterTest.kt new file mode 100644 index 0000000..5729946 --- /dev/null +++ b/src/test/kotlin/gg/grounds/platform/motd/MotdSetterTest.kt @@ -0,0 +1,43 @@ +package gg.grounds.platform.motd + +import org.bukkit.Server +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class MotdSetterTest { + + private fun captureMotd(projectName: String, pushId: String? = null): String { + val server: Server = mock() + MotdSetter(server).apply(projectName, pushId) + val captor = argumentCaptor() + verify(server).setMotd(captor.capture()) + return captor.firstValue + } + + @Test + fun `formats project name and short push id on line one with attribution on line two`() { + val motd = captureMotd("Demo Project", "abc12345-6789-0abc-def0-123456789abc") + assertEquals("§fDemo Project §8abc12345\n§8powered by Grounds Developer Platform", motd) + } + + @Test + fun `omits push-id suffix when pushId is null`() { + val motd = captureMotd("Demo Project", pushId = null) + assertEquals("§fDemo Project\n§8powered by Grounds Developer Platform", motd) + } + + @Test + fun `omits push-id suffix when pushId is blank after dash-strip`() { + val motd = captureMotd("Demo Project", pushId = "----") + assertEquals("§fDemo Project\n§8powered by Grounds Developer Platform", motd) + } + + @Test + fun `truncates short pushId without underflow`() { + val motd = captureMotd("P", pushId = "abc") + assertEquals("§fP §8abc\n§8powered by Grounds Developer Platform", motd) + } +}