diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index ca8eceaeb..5dad15ffd 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -31,6 +31,9 @@ jobs:
with:
fetch-depth: 0
+ - name: Create stub changelog.md file
+ run: echo "# Changelog" > docs/pages/kotlinx-rpc/topics/changelog.md
+
- name: Build docs using Writerside Docker builder
uses: JetBrains/writerside-github-action@v4
with:
@@ -99,6 +102,13 @@ jobs:
- name: Update sitemap.xml
run: chmod +x updateSitemap.sh && ./updateSitemap.sh __docs_publication_dir/sitemap.xml __docs_publication_dir/api
+ - name: Run Changelog Generator
+ run: ./gradlew updateDocsChangelog
+
+ - name: Move Changelog.md to the publication directory
+ run:
+ mv docs/pages/kotlinx-rpc/topics/changelog.md __docs_publication_dir/changelog.md
+
- name: Setup Pages
uses: actions/configure-pages@v5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 762a96098..ab546d692 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,6 @@
This release enforces ERROR as a default reporting level for APIs that are forbidden by the strict mode.
You can still change the level manually, but in `0.8.0` strict mode will be enforced irreversibly.
-## What's Changed
-
### Breaking Changes 🔴
* Change strict mode to level ERROR by default by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/338
diff --git a/build.gradle.kts b/build.gradle.kts
index c82f21ac3..aa587897e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,6 +8,7 @@ import util.configureApiValidation
import util.configureNpm
import util.configureProjectReport
import util.registerDumpPlatformTableTask
+import util.registerChangelogTask
import util.libs
import util.registerVerifyPlatformTableTask
import java.time.Year
@@ -83,6 +84,7 @@ configureApiValidation()
registerDumpPlatformTableTask()
registerVerifyPlatformTableTask()
+registerChangelogTask()
val kotlinVersion = rootProject.libs.versions.kotlin.lang.get()
val kotlinCompiler = rootProject.libs.versions.kotlin.compiler.get()
diff --git a/docs/pages/.gitignore b/docs/pages/.gitignore
index d455b18d1..af4afceb2 100644
--- a/docs/pages/.gitignore
+++ b/docs/pages/.gitignore
@@ -1 +1,2 @@
api/**
+kotlinx-rpc/topics/changelog.md
diff --git a/docs/pages/kotlinx-rpc/rpc.tree b/docs/pages/kotlinx-rpc/rpc.tree
index 409749758..5e0c85269 100644
--- a/docs/pages/kotlinx-rpc/rpc.tree
+++ b/docs/pages/kotlinx-rpc/rpc.tree
@@ -49,5 +49,6 @@
+
diff --git a/gradle-conventions/common/src/main/kotlin/util/changelog.kt b/gradle-conventions/common/src/main/kotlin/util/changelog.kt
new file mode 100644
index 000000000..8ef63c472
--- /dev/null
+++ b/gradle-conventions/common/src/main/kotlin/util/changelog.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package util
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.kotlin.dsl.register
+import java.io.File
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
+import kotlin.io.path.exists
+import kotlin.io.path.readLines
+import kotlin.io.path.writeLines
+
+private val ROOT_CHANGELOG_PATH = File("CHANGELOG.md")
+private val DOCS_CHANGELOG_PATH = File("docs/pages/kotlinx-rpc/topics/changelog.md")
+
+private val PULL_REGEX = "https://github.com/Kotlin/kotlinx-rpc/pull/(\\d+)".toRegex()
+private val COMPARE_REGEX = "https://github.com/Kotlin/kotlinx-rpc/compare/([+.\\w_-]+)".toRegex()
+private val USERNAME_REGEX = "@([\\w_-]+)".toRegex()
+private val WHITESPACE = "\\s+".toRegex()
+
+abstract class UpdateDocsChangelog : DefaultTask() {
+ @get:InputFile
+ abstract val input: Property
+
+ @get:OutputFile
+ abstract val output: Property
+
+ @TaskAction
+ fun update() {
+ val inputPath = input.get().toPath()
+ val outputPath = output.get().toPath()
+
+ if (!inputPath.exists()) {
+ throw GradleException("fatal error: input file $inputPath does not exist")
+ }
+
+ var currentRelease = ""
+ val fullChangelogLines = mutableListOf()
+ val lines = inputPath.readLines(Charsets.UTF_8).flatMap { line ->
+ val updated = line
+ .replace(PULL_REGEX) {
+ "[#${it.groupValues[1]}](${it.groupValues[0]})"
+ }
+ .replace(COMPARE_REGEX) {
+ "[${it.groupValues[1]}](${it.groupValues[0]})"
+ }
+ .replace(USERNAME_REGEX) {
+ "[${it.groupValues[0]}](https://github.com/${it.groupValues[1]})"
+ }.let {
+ if (it.startsWith("#")) {
+ "#$it"
+ } else {
+ it
+ }
+ }
+
+ if (updated.startsWith("## ")) {
+ currentRelease = updated
+ .drop(3)
+ .replace(".", "_")
+ }
+
+ when {
+ updated.startsWith("###") -> {
+ val name = updated
+ .dropWhile { it == '#' }
+ .dropLastWhile { !it.isDigit() && !it.isLetter() }
+ .trim()
+ .replace(WHITESPACE, "_")
+
+ listOf("$updated {id=${name}_$currentRelease}")
+ }
+
+ updated.startsWith("**Full Changelog**:") -> {
+ fullChangelogLines.add(updated)
+ emptyList()
+ }
+
+ else -> listOf(updated)
+ }
+ }
+
+ val result = mutableListOf()
+
+ var i = 0
+ var fci = 0
+ while (i < lines.size) {
+ val line = lines[i]
+ result.add(line)
+
+ if (line.startsWith("## ")) {
+ result.add(lines[i + 1])
+ result.add("")
+ result.add(fullChangelogLines[fci++])
+ i++
+ }
+
+ i++
+ }
+
+ if (!outputPath.exists()) {
+ outputPath.parent.createDirectories()
+ outputPath.createFile()
+ }
+
+ val header = listOf(
+ "# Changelog",
+ "",
+ "This page contains all changes throughout releases of the library.",
+ "",
+ )
+
+ outputPath.writeLines(header + result)
+ }
+}
+
+fun Project.registerChangelogTask() {
+ tasks.register("updateDocsChangelog") {
+ input.set(ROOT_CHANGELOG_PATH)
+ output.set(DOCS_CHANGELOG_PATH)
+ }
+}