diff --git a/docs/configuration.md b/docs/configuration.md index 7cb34b0..ee34b71 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -201,7 +201,9 @@ builds: cleanCommand: rm -rf build # shell command for each build buildCommand: ./gradlew --console=plain --no-daemon test - # directories copied as build artifacts + # directories copied as build artifacts; each is archived at its own + # workspace-relative path, except build/reports, which archives as reports/ + # and is browsed by the artifact page's report index artifactDirs: - build/reports - build/doc diff --git a/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt b/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt index 1ac2129..a08f50e 100644 --- a/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt +++ b/src/main/kotlin/de/hoennig/werkator/artifacts/FileArtifactStore.kt @@ -151,12 +151,18 @@ class FileArtifactStore( } } - /** Legacy `archived_artefact_dir_path`: `build/reports` archives as `reports/`, everything else below `reports/`. */ + /** + * `build/reports` archives as `reports/` — the browsable-reports anchor of the + * artifact page and the legacy `archived_artefact_dir_path` layout. Every other + * directory archives at its own workspace-relative path: it is not a report, + * and hiding e.g. a built binary below `reports/` made it both mislabeled and + * invisible (the report index only scans for HTML pages). + */ private fun archivedPath(artifactDir: String): String = if (artifactDir == "build/reports") { "reports" } else { - "reports/$artifactDir" + artifactDir } /** diff --git a/src/main/kotlin/de/hoennig/werkator/server/UiController.kt b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt index 925ed1d..4121bf9 100644 --- a/src/main/kotlin/de/hoennig/werkator/server/UiController.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt @@ -203,9 +203,27 @@ class UiController( ?: emptyList(), ) model.addAttribute("reportIndexes", artifactDir?.let { reportIndexes(it) } ?: emptyList()) + model.addAttribute("fileArtifacts", artifactDir?.let { fileArtifacts(it) } ?: emptyList()) return "artifact" } + /** + * Plain artifact files outside `reports/` — build outputs like binaries or + * jars, archived at their workspace-relative paths. The top-level log files + * have their own section. Capped so a huge output tree cannot flood the page. + */ + private fun fileArtifacts(artifactDir: Path): List = + Files.walk(artifactDir).use { paths -> + paths + .asSequence() + .filter { Files.isRegularFile(it) } + .map { artifactDir.relativize(it).toString() } + .filterNot { it.startsWith("reports/") || (!it.contains('/') && it.endsWith(".log")) } + .sorted() + .take(MAX_FILE_ARTIFACTS) + .toList() + } + /** Adds the attributes every page needs and returns the Gitea link helper for row building. */ private fun baseModel( model: Model, @@ -366,6 +384,8 @@ class UiController( } companion object { + private const val MAX_FILE_ARTIFACTS = 200 + private val FAILURES_COUNTER = Regex("""id="failures">\s*
(\d+)""") /** diff --git a/src/main/resources/templates/artifact.html b/src/main/resources/templates/artifact.html index ac8ad76..1cd3eb7 100644 --- a/src/main/resources/templates/artifact.html +++ b/src/main/resources/templates/artifact.html @@ -70,7 +70,13 @@ th:text="${report.failures} + ' failed'">2 failed -

+

+

No artifact directories were produced by this build.

diff --git a/src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreTest.kt b/src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreTest.kt index 7f72fc0..ca11788 100644 --- a/src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreTest.kt @@ -90,9 +90,11 @@ class FileArtifactStoreTest : FunSpec() { Files.readString(artifactDir.resolve("build.stdout.log")) shouldBe "out" Files.readString(artifactDir.resolve("build.stderr.log")) shouldBe "err" Files.readString(artifactDir.resolve("build.log")) shouldBe "live" - // legacy layout: build/reports archives as reports/, other dirs below reports/ + // build/reports archives as reports/ (the artifact page's browsable + // anchor), every other dir at its own workspace-relative path Files.exists(artifactDir.resolve("reports/tests/index.html")) shouldBe true - Files.exists(artifactDir.resolve("reports/build/doc/readme.txt")) shouldBe true + Files.exists(artifactDir.resolve("build/doc/readme.txt")) shouldBe true + Files.exists(artifactDir.resolve("reports/build")) shouldBe false Files.exists(staging) shouldBe false } @@ -104,8 +106,8 @@ class FileArtifactStoreTest : FunSpec() { h.store.persist(build, stagingDir(), workspace) val artifactDir = h.branchesDir().resolve(build.artifactKey) - Files.exists(artifactDir.resolve("reports/build/doc/readme.txt")) shouldBe true - Files.exists(artifactDir.resolve("reports/tests")) shouldBe false + Files.exists(artifactDir.resolve("build/doc/readme.txt")) shouldBe true + Files.exists(artifactDir.resolve("reports")) shouldBe false } test("persist without a workspace stores only the logs") { diff --git a/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt b/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt index f35baa6..83bcff8 100644 --- a/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt @@ -291,6 +291,30 @@ class UiControllerTest : FunSpec() { ).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html")))) } + test("artifact index lists plain files outside reports/ and keeps logs and report files out of that list") { + val artifactDir = Files.createDirectories(tempDir.resolve("files-view-key")) + Files.writeString(artifactDir.resolve("build.stdout.log"), "out") + Files.createDirectories(artifactDir.resolve("werkdock/dist")) + Files.writeString(artifactDir.resolve("werkdock/dist/werkdock"), "elf") + Files.createDirectories(artifactDir.resolve("reports/tests")) + Files.writeString(artifactDir.resolve("reports/tests/index.html"), "") + every { repository.history() } returns listOf(successResult) + every { artifactStore.artifactDir("files-view-key") } returns artifactDir + + val page = + mockMvc + .perform(get("/builds/files-view-key")) + .andExpect(status().isOk) + .andExpect( + content().string(containsString("""/artifacts/files-view-key/werkdock/dist/werkdock" target="_blank"""")), + ).andReturn() + .response.contentAsString + // stored at its own path, not below reports/; the log stays in the + // logs section and is not repeated in the files list + page shouldNotContain "reports/werkdock" + (page.split("/artifacts/files-view-key/build.stdout.log").size - 1) shouldBe 1 + } + test("the artifact page shows the command of the build's own definition, not the plain branch command") { val pitestResult = successResult.copy(