From 76deaa6b726871a18e54078b18ea83f6961537fd Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 21 Oct 2026 08:22:20 -0700 Subject: [PATCH 1/2] Fix Dokka generator exit when all sources are suppressed --- README.md | 6 +++--- dokka/private/DokkaGeneratorRunner.kt | 5 +++++ tests/fixtures/generated_docs/BUILD.bazel | 8 ++++++++ tests/generated_docs_tests.bzl | 7 +++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c98193..c4f2161 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ dokka( ) ``` -The target produces one directory artifact named after the target. HTML is the -default output format. Set `format` to `gfm` or `javadoc` for the other bundled -formats. +The target produces one directory artifact named after the target. If all sources +are suppressed, the directory is empty. HTML is the default output format. Set +`format` to `gfm` or `javadoc` for the other bundled formats. Without a target-level `config`, `dokka` uses the default `dokka_config` provided by its selected toolchain. The bundled default sets diff --git a/dokka/private/DokkaGeneratorRunner.kt b/dokka/private/DokkaGeneratorRunner.kt index 9d9eb7d..5ffc3e6 100644 --- a/dokka/private/DokkaGeneratorRunner.kt +++ b/dokka/private/DokkaGeneratorRunner.kt @@ -14,8 +14,10 @@ import org.jetbrains.dokka.apply import org.jetbrains.dokka.defaultLinks import org.jetbrains.dokka.utilities.DokkaConsoleLogger import java.io.File +import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import kotlin.system.exitProcess /** * Runs Dokka in-process with paths resolved inside the Bazel action's execution root. @@ -39,7 +41,10 @@ object DokkaGeneratorRunner { } else { standardConfiguration(root) } + Files.createDirectories(configuration.outputDir.toPath()) DokkaGenerator(configuration, DokkaConsoleLogger()).generate() + // Dokka skips analysis cleanup when there is nothing to document. + exitProcess(0) } private fun standardConfiguration(root: ObjectNode): DokkaConfiguration { diff --git a/tests/fixtures/generated_docs/BUILD.bazel b/tests/fixtures/generated_docs/BUILD.bazel index 6f50f4b..e38e2cc 100644 --- a/tests/fixtures/generated_docs/BUILD.bazel +++ b/tests/fixtures/generated_docs/BUILD.bazel @@ -57,6 +57,14 @@ dokka( tags = FIXTURE_TAGS, ) +dokka( + name = "all_sources_suppressed_docs", + srcs = ["Main.kt"], + config = ":config", + suppressed_files = ["Main.kt"], + tags = FIXTURE_TAGS, +) + dokka( name = "api_docs", srcs = ["Main.kt"], diff --git a/tests/generated_docs_tests.bzl b/tests/generated_docs_tests.bzl index a21bb29..7880a53 100644 --- a/tests/generated_docs_tests.bzl +++ b/tests/generated_docs_tests.bzl @@ -2,6 +2,7 @@ load("@bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") load("@bazel_lib//lib:diff_test.bzl", "diff_test") +load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_shell//shell:sh_test.bzl", "sh_test") def generated_docs_test_suite(name): @@ -43,6 +44,12 @@ def generated_docs_test_suite(name): tags = [name], ) + build_test( + name = name + "_all_sources_suppressed", + targets = ["//tests/fixtures/generated_docs:all_sources_suppressed_docs"], + tags = [name], + ) + sh_test( name = name + "_formats", srcs = ["generated_docs_test.sh"], From 6fef13c396d380fa59182a904934f3ab62bb3c65 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 21 Oct 2026 08:37:20 -0700 Subject: [PATCH 2/2] Fail Dokka actions on logged errors and thrown failures --- README.md | 3 + dokka/private/DokkaGeneratorRunner.kt | 13 +++- dokka/private/config.bzl | 2 +- tests/BUILD.bazel | 21 ++++++ tests/DokkaGeneratorRunnerTest.kt | 85 +++++++++++++++++++++++ tests/fixtures/generated_docs/BUILD.bazel | 8 --- tests/generated_docs_tests.bzl | 7 -- 7 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 tests/DokkaGeneratorRunnerTest.kt diff --git a/README.md b/README.md index c4f2161..566d6d8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ The target produces one directory artifact named after the target. If all source are suppressed, the directory is empty. HTML is the default output format. Set `format` to `gfm` or `javadoc` for the other bundled formats. +Logged Dokka errors always fail the action. Set `fail_on_warning = True` on a +`dokka_config` to fail on warnings too. + Without a target-level `config`, `dokka` uses the default `dokka_config` provided by its selected toolchain. The bundled default sets `offline_mode = True`, preventing network access during the Dokka action. diff --git a/dokka/private/DokkaGeneratorRunner.kt b/dokka/private/DokkaGeneratorRunner.kt index 5ffc3e6..c870d90 100644 --- a/dokka/private/DokkaGeneratorRunner.kt +++ b/dokka/private/DokkaGeneratorRunner.kt @@ -42,9 +42,16 @@ object DokkaGeneratorRunner { standardConfiguration(root) } Files.createDirectories(configuration.outputDir.toPath()) - DokkaGenerator(configuration, DokkaConsoleLogger()).generate() - // Dokka skips analysis cleanup when there is nothing to document. - exitProcess(0) + val logger = DokkaConsoleLogger() + val exitCode = try { + DokkaGenerator(configuration, logger).generate() + if (logger.errorsCount > 0 || (configuration.failOnWarning && logger.warningsCount > 0)) 1 else 0 + } catch (failure: Throwable) { + failure.printStackTrace() + 1 + } + // Dokka can return without completing analysis cleanup. + exitProcess(exitCode) } private fun standardConfiguration(root: ObjectNode): DokkaConfiguration { diff --git a/dokka/private/config.bzl b/dokka/private/config.bzl index cdecf9b..44280fa 100644 --- a/dokka/private/config.bzl +++ b/dokka/private/config.bzl @@ -27,7 +27,7 @@ def _dokka_config_attrs(): doc = "Declaration visibilities to document.", ), "fail_on_warning": attr.bool( - doc = "Fail when Dokka emits a warning or error.", + doc = "Treat Dokka warnings as build failures. Errors always fail.", ), "jdk_version": attr.int( default = 8, diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 52d3283..6784c06 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") load(":dokka_tests.bzl", "dokka_test_suite") load(":generated_docs_tests.bzl", "generated_docs_test_suite") load(":multi_module_tests.bzl", "multi_module_test_suite") @@ -8,4 +9,24 @@ generated_docs_test_suite(name = "generated_docs") multi_module_test_suite(name = "multi_module") +kt_jvm_test( + name = "generator_exit_test", + size = "medium", + srcs = [ + "DokkaGeneratorRunnerTest.kt", + "//dokka/private:DokkaGeneratorRunner.kt", + ], + main_class = "rules.dokka.DokkaGeneratorRunnerTest", + runtime_deps = [ + "@rules_dokka_dependencies//:org_freemarker_freemarker", + "@rules_dokka_dependencies//:org_jetbrains_dokka_analysis_kotlin_symbols", + "@rules_dokka_dependencies//:org_jetbrains_dokka_dokka_base", + "@rules_dokka_dependencies//:org_jetbrains_kotlinx_kotlinx_html_jvm", + ], + deps = [ + "@rules_dokka_dependencies//:com_fasterxml_jackson_core_jackson_databind", + "@rules_dokka_dependencies//:org_jetbrains_dokka_dokka_cli", + ], +) + test_suite(name = "all_tests") diff --git a/tests/DokkaGeneratorRunnerTest.kt b/tests/DokkaGeneratorRunnerTest.kt new file mode 100644 index 0000000..b0a44f4 --- /dev/null +++ b/tests/DokkaGeneratorRunnerTest.kt @@ -0,0 +1,85 @@ +package rules.dokka + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +/** Exercises the standalone runner's exit status in a separate JVM. */ +object DokkaGeneratorRunnerTest { + @JvmStatic + fun main(args: Array) { + val root = Files.createTempDirectory(Paths.get(requireNotNull(System.getenv("TEST_TMPDIR"))), "dokka-exit-") + val source = root.resolve("Api.kt") + Files.writeString(source, "package fixture\nclass Api\n") + val classpath = System.getProperty("java.class.path").split(File.pathSeparator) + .map { Paths.get(it).toAbsolutePath().toString() } + val plugins = listOf( + "org.jetbrains.dokka.base.DokkaBase", + "org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin", + ).map { Paths.get(Class.forName(it).protectionDomain.codeSource.location.toURI()).toString() } + val mapper = ObjectMapper() + val base = mapper.createObjectNode().apply { + put("moduleName", "Exit status fixture") + put("offlineMode", true) + putArray("pluginsClasspath").also { jars -> plugins.forEach { jars.add(it) } } + putArray("sourceSets").addObject().apply { + putObject("sourceSetID").put("scopeId", "fixture").put("sourceSetName", "main") + putArray("sourceRoots").add(source.toString()) + put("noJdkLink", true) + put("noStdlibLink", true) + } + } + + fun run(name: String, expectedExit: Int, configure: (ObjectNode, Path) -> Unit): Path { + val output = root.resolve(name) + val config = base.deepCopy().apply { put("outputDir", output.toString()) } + configure(config, output) + val manifest = root.resolve("$name.json") + mapper.writeValue(manifest.toFile(), config) + val log = root.resolve("$name.log") + val java = if (System.getProperty("os.name").startsWith("Windows")) "java.exe" else "java" + val process = ProcessBuilder( + Paths.get(System.getProperty("java.home"), "bin", java).toString(), + "-cp", classpath.joinToString(File.pathSeparator), + "rules.dokka.DokkaGeneratorRunner", manifest.toString(), + ).redirectErrorStream(true).redirectOutput(log.toFile()).start() + try { + check(process.waitFor(60, TimeUnit.SECONDS)) { "$name timed out: ${Files.readString(log)}" } + check(process.exitValue() == expectedExit) { + "$name exited ${process.exitValue()}, expected $expectedExit: ${Files.readString(log)}" + } + } finally { + process.destroyForcibly().waitFor() + } + return output + } + + run("write-error", 1) { config, output -> + config.put("failOnWarning", false) + Files.createDirectories(output.resolve("index.html")) + } + val writeErrorLog = Files.readString(root.resolve("write-error.log")) + check(writeErrorLog.contains("ERROR: Failed to write")) { writeErrorLog } + + val empty = run("suppressed", 0) { config, _ -> + (config["sourceSets"][0] as ObjectNode).putArray("suppressedFiles").add(source.toString()) + } + check(Files.isDirectory(empty)) + Files.list(empty).use { check(!it.findAny().isPresent) } + + run("warning", 0) { config, _ -> + (config["sourceSets"][0] as ObjectNode).put("reportUndocumented", true) + } + check(Files.readString(root.resolve("warning.log")).contains("WARN:")) + + run("strict-warning", 1) { config, _ -> + config.put("failOnWarning", true) + (config["sourceSets"][0] as ObjectNode).put("reportUndocumented", true) + } + check(Files.readString(root.resolve("strict-warning.log")).contains("DokkaException")) + } +} diff --git a/tests/fixtures/generated_docs/BUILD.bazel b/tests/fixtures/generated_docs/BUILD.bazel index e38e2cc..6f50f4b 100644 --- a/tests/fixtures/generated_docs/BUILD.bazel +++ b/tests/fixtures/generated_docs/BUILD.bazel @@ -57,14 +57,6 @@ dokka( tags = FIXTURE_TAGS, ) -dokka( - name = "all_sources_suppressed_docs", - srcs = ["Main.kt"], - config = ":config", - suppressed_files = ["Main.kt"], - tags = FIXTURE_TAGS, -) - dokka( name = "api_docs", srcs = ["Main.kt"], diff --git a/tests/generated_docs_tests.bzl b/tests/generated_docs_tests.bzl index 7880a53..a21bb29 100644 --- a/tests/generated_docs_tests.bzl +++ b/tests/generated_docs_tests.bzl @@ -2,7 +2,6 @@ load("@bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") load("@bazel_lib//lib:diff_test.bzl", "diff_test") -load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_shell//shell:sh_test.bzl", "sh_test") def generated_docs_test_suite(name): @@ -44,12 +43,6 @@ def generated_docs_test_suite(name): tags = [name], ) - build_test( - name = name + "_all_sources_suppressed", - targets = ["//tests/fixtures/generated_docs:all_sources_suppressed_docs"], - tags = [name], - ) - sh_test( name = name + "_formats", srcs = ["generated_docs_test.sh"],