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:
mhoennig
2026-09-01 08:51:09 +02:00
co-authored by Claude Fable 5
parent db06d805ec
commit 4cdd5a1986
6 changed files with 68 additions and 8 deletions
@@ -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 =
if (artifactDir == "build/reports") {
"reports"
} else {
"reports/$artifactDir"
artifactDir
}
/**
@@ -203,9 +203,27 @@ class UiController(
?: emptyList<LogFileView>(),
)
model.addAttribute("reportIndexes", artifactDir?.let { reportIndexes(it) } ?: emptyList<String>())
model.addAttribute("fileArtifacts", artifactDir?.let { fileArtifacts(it) } ?: emptyList<String>())
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. */
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*<div class="counter">(\d+)""")
/**
+7 -1
View File
@@ -70,7 +70,13 @@
th:text="${report.failures} + ' failed'">2 failed</span>
</li>
</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.
</p>
</article>