artifacts: archive non-report dirs at their own paths, list them on the artifact page
Every artifactDir landed below reports/ — the legacy archived_artefact_dir_path layout — which mislabeled non-report outputs (reports/werkdock/dist/werkdock) AND hid them: the artifact page's report index only scans reports/ for HTML pages, so a built binary was stored but never shown. Now build/reports keeps archiving as reports/ (the browsable anchor and every existing link), every other directory archives at its workspace-relative path, and the artifact page gains a plain-files list for everything outside reports/ (log files stay in their own section; capped at 200 entries). Existing stored artifacts keep their old layout and remain served; only new builds use the new one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
db06d805ec
commit
4cdd5a1986
@@ -201,7 +201,9 @@ builds:
|
|||||||
cleanCommand: rm -rf build
|
cleanCommand: rm -rf build
|
||||||
# shell command for each build
|
# shell command for each build
|
||||||
buildCommand: ./gradlew --console=plain --no-daemon test
|
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:
|
artifactDirs:
|
||||||
- build/reports
|
- build/reports
|
||||||
- build/doc
|
- build/doc
|
||||||
|
|||||||
@@ -151,12 +151,18 @@ class FileArtifactStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Legacy `archived_artefact_dir_path`: `build/reports` archives as `reports/`, everything else below `reports/<dir>`. */
|
/**
|
||||||
|
* `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 =
|
private fun archivedPath(artifactDir: String): String =
|
||||||
if (artifactDir == "build/reports") {
|
if (artifactDir == "build/reports") {
|
||||||
"reports"
|
"reports"
|
||||||
} else {
|
} else {
|
||||||
"reports/$artifactDir"
|
artifactDir
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -203,9 +203,27 @@ class UiController(
|
|||||||
?: emptyList<LogFileView>(),
|
?: emptyList<LogFileView>(),
|
||||||
)
|
)
|
||||||
model.addAttribute("reportIndexes", artifactDir?.let { reportIndexes(it) } ?: emptyList<String>())
|
model.addAttribute("reportIndexes", artifactDir?.let { reportIndexes(it) } ?: emptyList<String>())
|
||||||
|
model.addAttribute("fileArtifacts", artifactDir?.let { fileArtifacts(it) } ?: emptyList<String>())
|
||||||
return "artifact"
|
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<String> =
|
||||||
|
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. */
|
/** Adds the attributes every page needs and returns the Gitea link helper for row building. */
|
||||||
private fun baseModel(
|
private fun baseModel(
|
||||||
model: Model,
|
model: Model,
|
||||||
@@ -366,6 +384,8 @@ class UiController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val MAX_FILE_ARTIFACTS = 200
|
||||||
|
|
||||||
private val FAILURES_COUNTER = Regex("""id="failures">\s*<div class="counter">(\d+)""")
|
private val FAILURES_COUNTER = Regex("""id="failures">\s*<div class="counter">(\d+)""")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,7 +70,13 @@
|
|||||||
th:text="${report.failures} + ' failed'">2 failed</span>
|
th:text="${report.failures} + ' failed'">2 failed</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p th:if="${#lists.isEmpty(reportIndexes)}" class="muted">
|
<ul th:if="${!#lists.isEmpty(fileArtifacts)}">
|
||||||
|
<li th:each="file : ${fileArtifacts}">
|
||||||
|
<a th:href="${filesBase} + '/' + ${file}" target="_blank"
|
||||||
|
rel="noopener noreferrer" th:text="${file}">werkdock/dist/werkdock</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p th:if="${#lists.isEmpty(reportIndexes) and #lists.isEmpty(fileArtifacts)}" class="muted">
|
||||||
No artifact directories were produced by this build.
|
No artifact directories were produced by this build.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -90,9 +90,11 @@ class FileArtifactStoreTest : FunSpec() {
|
|||||||
Files.readString(artifactDir.resolve("build.stdout.log")) shouldBe "out"
|
Files.readString(artifactDir.resolve("build.stdout.log")) shouldBe "out"
|
||||||
Files.readString(artifactDir.resolve("build.stderr.log")) shouldBe "err"
|
Files.readString(artifactDir.resolve("build.stderr.log")) shouldBe "err"
|
||||||
Files.readString(artifactDir.resolve("build.log")) shouldBe "live"
|
Files.readString(artifactDir.resolve("build.log")) shouldBe "live"
|
||||||
// legacy layout: build/reports archives as reports/, other dirs below reports/<dir>
|
// 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/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
|
Files.exists(staging) shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +106,8 @@ class FileArtifactStoreTest : FunSpec() {
|
|||||||
h.store.persist(build, stagingDir(), workspace)
|
h.store.persist(build, stagingDir(), workspace)
|
||||||
|
|
||||||
val artifactDir = h.branchesDir().resolve(build.artifactKey)
|
val artifactDir = h.branchesDir().resolve(build.artifactKey)
|
||||||
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/tests")) shouldBe false
|
Files.exists(artifactDir.resolve("reports")) shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
test("persist without a workspace stores only the logs") {
|
test("persist without a workspace stores only the logs") {
|
||||||
|
|||||||
@@ -291,6 +291,30 @@ class UiControllerTest : FunSpec() {
|
|||||||
).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html"))))
|
).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"), "<html></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") {
|
test("the artifact page shows the command of the build's own definition, not the plain branch command") {
|
||||||
val pitestResult =
|
val pitestResult =
|
||||||
successResult.copy(
|
successResult.copy(
|
||||||
|
|||||||
Reference in New Issue
Block a user