From e93454a7ec7ff16d0f2ca3c644d686ceec1e9cc1 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Sat, 29 Aug 2026 18:56:35 +0100 Subject: [PATCH 1/2] refactor: take release notes as a separate file This drops the `changelog` field from ReleaseMetadata and adds a new `--release-notes` option to PublishCliCommand. Users should drop their usage of `changelog` in their JSON metadata and supply a separate `release-notes.md` file. --- .../freecam/publish/cli/PublishCliCommand.kt | 14 +++++++++++- .../publish/cli/PublishCliCommandTest.kt | 22 ++++++++++--------- .../net/xolt/freecam/publish/Publisher.kt | 5 +++-- .../freecam/publish/model/ReleaseArtifact.kt | 16 ++++++++------ .../publish/model/ResolveArtifactsTest.kt | 12 ++++++---- .../kotlin/test/ReleaseArtifactFixtures.kt | 4 ++-- .../testFixtures/kotlin/test/StringUtils.kt | 7 ++++++ .../publish/platforms/CurseForgeClient.kt | 2 +- .../publish/platforms/ModrinthClient.kt | 2 +- .../net/xolt/freecam/model/ReleaseMetadata.kt | 1 - .../kotlin/test/MetadataFixtures.kt | 2 -- .../xolt/freecam/publish/DefaultPublisher.kt | 10 ++++----- .../freecam/publish/DefaultPublisherTest.kt | 7 ++++-- 13 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 core/src/testFixtures/kotlin/test/StringUtils.kt diff --git a/cli/src/main/kotlin/net/xolt/freecam/publish/cli/PublishCliCommand.kt b/cli/src/main/kotlin/net/xolt/freecam/publish/cli/PublishCliCommand.kt index a9fce3d..206cfbb 100644 --- a/cli/src/main/kotlin/net/xolt/freecam/publish/cli/PublishCliCommand.kt +++ b/cli/src/main/kotlin/net/xolt/freecam/publish/cli/PublishCliCommand.kt @@ -18,6 +18,7 @@ import net.xolt.freecam.publish.model.CurseForgeConfig import net.xolt.freecam.publish.model.ModrinthConfig import java.nio.file.Path import kotlin.io.path.inputStream +import kotlin.io.path.readText internal class PublishCliCommand( version: String = "0.0.1", @@ -47,6 +48,10 @@ internal class PublishCliCommand( ) } + val releaseNotes: String by lazy { + releaseNotesPath.readText() + } + @OptIn(ExperimentalSerializationApi::class) val metadata: ReleaseMetadata by lazy { metadataPath.inputStream().use { input -> @@ -58,6 +63,13 @@ internal class PublishCliCommand( .path(mustExist = true, mustBeReadable = true, canBeFile = false, canBeDir = true) .help("Directory containing the release artifacts") + val releaseNotesPath: Path by option("--release-notes") + .path(mustExist = true, mustBeReadable = true, canBeFile = true, canBeDir = false) + .help("Release notes file") + .defaultLazy(defaultForHelp = "[artifacts-dir]/release-notes.md") { + artifactsDir.resolve("release-notes.md") + } + val metadataPath: Path by option("--metadata") .path(mustExist = true, mustBeReadable = true, canBeFile = true, canBeDir = false) .help("Release metadata JSON file") @@ -91,6 +103,6 @@ internal class PublishCliCommand( } } - publisher(metadata) + publisher(releaseNotes, metadata) } } diff --git a/cli/src/test/kotlin/net/xolt/freecam/publish/cli/PublishCliCommandTest.kt b/cli/src/test/kotlin/net/xolt/freecam/publish/cli/PublishCliCommandTest.kt index 639ccb0..b79d550 100644 --- a/cli/src/test/kotlin/net/xolt/freecam/publish/cli/PublishCliCommandTest.kt +++ b/cli/src/test/kotlin/net/xolt/freecam/publish/cli/PublishCliCommandTest.kt @@ -49,6 +49,7 @@ class PublishCliCommandTest { @Test fun `dry-run uses dry publisher`() = runTest { + val releaseNotes = "release notes" val metadata = testMetadata() val dir = createTestDir() var dryPublisher: Boolean? = null @@ -66,8 +67,8 @@ class PublishCliCommandTest { "--dry-run", "--curseforge-token", "token", "--modrinth-token", "token", - "--metadata", - metadata.toTestFile().absolutePathString(), + "--release-notes", releaseNotes.toTestFile().absolutePathString(), + "--metadata", metadata.toTestFile().absolutePathString(), dir.absolutePathString(), )) @@ -76,12 +77,13 @@ class PublishCliCommandTest { dryPublisher shouldBe true actualDir shouldBe dir.absolute() result.statusCode shouldBe 0 - coVerifySequence { publisher(metadata) } + coVerifySequence { publisher(releaseNotes, metadata) } confirmVerified(publisher) } @Test fun `non-dry-run uses 'real' publisher`() = runTest { + val releaseNotes = "release notes" val metadata = testMetadata() var dryPublisher: Boolean? = null var actualDir: Path? = null @@ -100,8 +102,8 @@ class PublishCliCommandTest { val result = cmd.test(listOf( "--curseforge-token", "token", "--modrinth-token", "token", - "--metadata", - metadata.toTestFile().absolutePathString(), + "--release-notes", releaseNotes.toTestFile().absolutePathString(), + "--metadata", metadata.toTestFile().absolutePathString(), dir.absolutePathString(), )) @@ -109,7 +111,7 @@ class PublishCliCommandTest { cmd.dryRun shouldBe false dryPublisher shouldBe false actualDir shouldBe dir.absolute() - coVerifySequence { publisher(metadata) } + coVerifySequence { publisher(releaseNotes, metadata) } confirmVerified(publisher) } @@ -121,8 +123,8 @@ class PublishCliCommandTest { val cmd = testCommand() val result = cmd.test(listOf( - "--metadata", - metadata.toTestFile().absolutePathString(), + "--release-notes", createTestFile().absolutePathString(), + "--metadata", metadata.toTestFile().absolutePathString(), dir.absolutePathString(), )) @@ -138,8 +140,8 @@ class PublishCliCommandTest { val cmd = testCommand() val result = cmd.test(listOf( - "--metadata", - metadata.toTestFile().absolutePathString(), + "--release-notes", createTestFile().absolutePathString(), + "--metadata", metadata.toTestFile().absolutePathString(), file.absolutePathString(), )) diff --git a/core/src/main/kotlin/net/xolt/freecam/publish/Publisher.kt b/core/src/main/kotlin/net/xolt/freecam/publish/Publisher.kt index e967e75..c65f26f 100644 --- a/core/src/main/kotlin/net/xolt/freecam/publish/Publisher.kt +++ b/core/src/main/kotlin/net/xolt/freecam/publish/Publisher.kt @@ -6,8 +6,9 @@ import net.xolt.freecam.publish.model.ModrinthConfig import java.nio.file.Path fun interface Publisher { - suspend fun publish(metadata: ReleaseMetadata) - suspend operator fun invoke(metadata: ReleaseMetadata) = publish(metadata) + suspend fun publish(releaseNotes: String, metadata: ReleaseMetadata) + suspend operator fun invoke(releaseNotes: String, metadata: ReleaseMetadata) + = publish(releaseNotes, metadata) } fun interface PublisherFactory { diff --git a/core/src/main/kotlin/net/xolt/freecam/publish/model/ReleaseArtifact.kt b/core/src/main/kotlin/net/xolt/freecam/publish/model/ReleaseArtifact.kt index 9f138b3..891fd9a 100644 --- a/core/src/main/kotlin/net/xolt/freecam/publish/model/ReleaseArtifact.kt +++ b/core/src/main/kotlin/net/xolt/freecam/publish/model/ReleaseArtifact.kt @@ -17,7 +17,7 @@ data class ReleaseArtifact( val displayName: String, val version: String, val versionType: ReleaseType, - val changelog: String, + val releaseNotes: String, val environments: Set, val loaders: Set, val gameVersion: String, @@ -62,23 +62,25 @@ data class ReleaseArtifact( } } -fun ReleaseMetadata.resolveArtifacts(artifactsDir: Path): List - = versions +fun Path.resolveReleaseArtifacts( + releaseNotes: String, + metadata: ReleaseMetadata, +) = metadata.versions .asSequence() .sorted() .map { ReleaseArtifact( displayName = it.displayName, - version = modVersion, - versionType = releaseType, - changelog = changelog, + version = metadata.modVersion, + versionType = metadata.releaseType, + releaseNotes = releaseNotes, environments = it.environments.toSet(), loaders = setOf(it.loader), gameVersion = it.minecraft, gameVersions = it.gameVersions.toSet(), javaVersions = it.javaVersions.toSet(), relationships = it.relationships.toSet(), - artifact = artifactsDir.resolve(it.filename), + artifact = resolve(it.filename), ) } .toList() diff --git a/core/src/test/kotlin/net/xolt/freecam/publish/model/ResolveArtifactsTest.kt b/core/src/test/kotlin/net/xolt/freecam/publish/model/ResolveArtifactsTest.kt index 78bd2d0..d595861 100644 --- a/core/src/test/kotlin/net/xolt/freecam/publish/model/ResolveArtifactsTest.kt +++ b/core/src/test/kotlin/net/xolt/freecam/publish/model/ResolveArtifactsTest.kt @@ -16,13 +16,17 @@ class ResolveArtifactsTest { fun `resolves to expected artifacts`() { val artifactsDir = createTestDir() val filenames = listOf("a", "b", "c") + val releaseNotes = "release notes" val metadata = testMetadata( versions = filenames.map { testProjectMetadata(filename = it) }, ) - val artifacts: List = metadata.resolveArtifacts(artifactsDir) + val artifacts: List = artifactsDir.resolveReleaseArtifacts(releaseNotes, metadata) artifacts.map { it.name } shouldContainExactly filenames + artifacts.map { it.releaseNotes } shouldContainExactly List(filenames.size) { + releaseNotes + } artifacts.map { it.artifact } shouldContainExactly filenames.map { artifactsDir.resolve(it) } @@ -31,21 +35,21 @@ class ResolveArtifactsTest { @Test fun `retains top-level metadata`() { val artifactsDir = Path("") + val releaseNotes = "release notes" val metadata = testMetadata( modVersion = "1.2.3", releaseType = ReleaseType.ALPHA, - changelog = "Changelog", versions = List(10) { testProjectMetadata() }, ) - val artifacts: List = metadata.resolveArtifacts(artifactsDir) + val artifacts: List = artifactsDir.resolveReleaseArtifacts(releaseNotes, metadata) assertSoftly(artifacts) { size shouldBe 10 forEach { it.version shouldBe "1.2.3" it.versionType shouldBe ReleaseType.ALPHA - it.changelog shouldBe "Changelog" + it.releaseNotes shouldBe releaseNotes } } } diff --git a/core/src/testFixtures/kotlin/test/ReleaseArtifactFixtures.kt b/core/src/testFixtures/kotlin/test/ReleaseArtifactFixtures.kt index 4011c8a..97b47b0 100644 --- a/core/src/testFixtures/kotlin/test/ReleaseArtifactFixtures.kt +++ b/core/src/testFixtures/kotlin/test/ReleaseArtifactFixtures.kt @@ -11,7 +11,7 @@ object ReleaseArtifactFixtures { displayName: String = "Fake Release", version: String = "1.2.3", versionType: ReleaseType = ReleaseType.RELEASE, - changelog: String = "", + releaseNotes: String = "", environments: Set = emptySet(), loaders: Set = setOf("loader"), gameVersion: String = "1.2.3", @@ -23,7 +23,7 @@ object ReleaseArtifactFixtures { displayName = displayName, version = version, versionType = versionType, - changelog = changelog, + releaseNotes = releaseNotes, environments = environments, loaders = loaders, gameVersion = gameVersion, diff --git a/core/src/testFixtures/kotlin/test/StringUtils.kt b/core/src/testFixtures/kotlin/test/StringUtils.kt new file mode 100644 index 0000000..1b6f61c --- /dev/null +++ b/core/src/testFixtures/kotlin/test/StringUtils.kt @@ -0,0 +1,7 @@ +package net.xolt.freecam.test + +import kotlin.io.path.writeText + +fun String.toTestFile() = createTestFile().also { file -> + file.writeText(this) +} diff --git a/curseforge/src/main/kotlin/net/xolt/freecam/publish/platforms/CurseForgeClient.kt b/curseforge/src/main/kotlin/net/xolt/freecam/publish/platforms/CurseForgeClient.kt index b1cb23a..a41672f 100644 --- a/curseforge/src/main/kotlin/net/xolt/freecam/publish/platforms/CurseForgeClient.kt +++ b/curseforge/src/main/kotlin/net/xolt/freecam/publish/platforms/CurseForgeClient.kt @@ -39,7 +39,7 @@ internal class CurseForgeClient( displayName(spec.displayName) releaseType(spec.versionType.toCurseForge()) - spec.changelog.takeIf { it.isNotBlank() }?.let { + spec.releaseNotes.takeIf { it.isNotBlank() }?.let { changelog(it) changelogType(CurseChangelogType.MARKDOWN) } diff --git a/modrinth/src/main/kotlin/net/xolt/freecam/publish/platforms/ModrinthClient.kt b/modrinth/src/main/kotlin/net/xolt/freecam/publish/platforms/ModrinthClient.kt index f944fa6..6c4617b 100644 --- a/modrinth/src/main/kotlin/net/xolt/freecam/publish/platforms/ModrinthClient.kt +++ b/modrinth/src/main/kotlin/net/xolt/freecam/publish/platforms/ModrinthClient.kt @@ -44,7 +44,7 @@ internal class ModrinthClient( suspend fun uploadVersion(spec: ReleaseArtifact) = uploadVersion { projectId(projectId) name(spec.displayName) - changelog(spec.changelog) + changelog(spec.releaseNotes) versionNumber(spec.version) versionType(spec.versionType.toModrinth()) loaders(spec.loaders.toList()) diff --git a/schema/src/main/kotlin/net/xolt/freecam/model/ReleaseMetadata.kt b/schema/src/main/kotlin/net/xolt/freecam/model/ReleaseMetadata.kt index 55aec1a..adc421c 100644 --- a/schema/src/main/kotlin/net/xolt/freecam/model/ReleaseMetadata.kt +++ b/schema/src/main/kotlin/net/xolt/freecam/model/ReleaseMetadata.kt @@ -10,7 +10,6 @@ data class ReleaseMetadata( @SerialName("mod_version") val modVersion: String, @SerialName("display_name") val displayName: String, @SerialName("release_type") val releaseType: ReleaseType, - val changelog: String, val platforms: Platforms, val versions: List, ) diff --git a/schema/src/testFixtures/kotlin/test/MetadataFixtures.kt b/schema/src/testFixtures/kotlin/test/MetadataFixtures.kt index 3671770..8c0d0ee 100644 --- a/schema/src/testFixtures/kotlin/test/MetadataFixtures.kt +++ b/schema/src/testFixtures/kotlin/test/MetadataFixtures.kt @@ -13,7 +13,6 @@ object MetadataFixtures { modVersion: String = "0.1.0", releaseType: ReleaseType = ReleaseType.RELEASE, displayName: String = "Fake Release", - changelog: String = "Changelog", githubTag: String = "v0.1.0", modrinthId: String = "id", curseforgeId: ULong = 1234UL, @@ -22,7 +21,6 @@ object MetadataFixtures { modVersion = modVersion, releaseType = releaseType, displayName = displayName, - changelog = changelog, platforms = Platforms( github = Platforms.Github(githubTag), modrinth = Platforms.Modrinth(modrinthId), diff --git a/src/main/kotlin/net/xolt/freecam/publish/DefaultPublisher.kt b/src/main/kotlin/net/xolt/freecam/publish/DefaultPublisher.kt index 7784113..a91dfb7 100644 --- a/src/main/kotlin/net/xolt/freecam/publish/DefaultPublisher.kt +++ b/src/main/kotlin/net/xolt/freecam/publish/DefaultPublisher.kt @@ -2,9 +2,8 @@ package net.xolt.freecam.publish import net.xolt.freecam.model.ReleaseMetadata import net.xolt.freecam.publish.model.ReleaseArtifact -import net.xolt.freecam.publish.model.resolveArtifacts +import net.xolt.freecam.publish.model.resolveReleaseArtifacts import net.xolt.freecam.publish.platforms.CurseForgePlatform -import net.xolt.freecam.publish.platforms.create import net.xolt.freecam.publish.platforms.ModrinthPlatform import net.xolt.freecam.publish.platforms.create import java.nio.file.Path @@ -25,10 +24,9 @@ data class DefaultPublisher( val modrinth: ModrinthPlatform, ) : Publisher { - override suspend fun publish(metadata: ReleaseMetadata) { - val artifacts = metadata.resolveArtifacts(artifactsDir).apply { - verifyExists() - } + override suspend fun publish(releaseNotes: String, metadata: ReleaseMetadata) { + val artifacts = artifactsDir.resolveReleaseArtifacts(releaseNotes, metadata) + artifacts.verifyExists() curseforge.publishRelease(metadata, artifacts) modrinth.publishRelease(metadata, artifacts) } diff --git a/src/test/kotlin/net/xolt/freecam/publish/DefaultPublisherTest.kt b/src/test/kotlin/net/xolt/freecam/publish/DefaultPublisherTest.kt index 9574479..f9e6eb1 100644 --- a/src/test/kotlin/net/xolt/freecam/publish/DefaultPublisherTest.kt +++ b/src/test/kotlin/net/xolt/freecam/publish/DefaultPublisherTest.kt @@ -30,15 +30,17 @@ class DefaultPublisherTest { val modrinth = mockk(relaxUnitFun = true) val dir = createTestDir() + val releaseNotes = "release notes" val metadata = testMetadata(versions = testVersions) val metadataArtifacts = metadata.versions.map { dir.resolve(it.filename).apply(Path::createFile) } - DefaultPublisher(dir, curseforge, modrinth).publish(metadata) + DefaultPublisher(dir, curseforge, modrinth).publish(releaseNotes, metadata) fun MockKMatcherScope.verifyArtifacts() = match> { artifacts -> artifacts.map { it.artifact } == metadataArtifacts + && artifacts.map { it.releaseNotes } == List(metadata.versions.size) { releaseNotes } } coVerify { curseforge.publishRelease(metadata, verifyArtifacts()) } @@ -51,11 +53,12 @@ class DefaultPublisherTest { val modrinth = mockk(relaxUnitFun = true) val dir = createTestDir() + val releaseNotes = "release notes" val metadata = testMetadata(versions = testVersions) val publisher = DefaultPublisher(dir, curseforge, modrinth) val ex = shouldThrowExactly { - publisher.publish(metadata) + publisher.publish(releaseNotes, metadata) } ex.message shouldContainOnlyOnce "artifacts were not found" From 755310484ee201b54534c75c95e3fc112b9b010b Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Sat, 29 Aug 2026 19:20:34 +0100 Subject: [PATCH 2/2] chore: release 0.0.6 & schema 0.1.0 Breaking changes: - schema: `changelog` field dropped from `ReleaseMetadata` - cli: `--release-notes` option was added --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2e3e615..f3d00dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -schema_version=0.0.3 -version=0.0.5 +schema_version=0.1.0 +version=0.0.6 group=net.xolt.freecam