Show the command a build actually runs on its artifact page
The artifact page read `branches.<branch>.buildCommand` from the primary config only, so for a named build on a branch with its own config it showed a command that build never ran — on vm4006 it showed the host config's command for a build that ran the branch definition's `pitestFull`. Resolving "what does this build run" now has one implementation, `GitTallyConfig.buildSettings(branch, build)`: the branch entry with the build definition's overrides applied last. The executor uses it, and the page resolves it against the branch layer committed at the build's own commit — the same inputs the executor had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
939d8eeb9c
commit
1a84b1fc1b
@@ -474,12 +474,10 @@ class BuildExecutor(
|
||||
runningBuild: RunningBuild,
|
||||
workingDir: Path,
|
||||
worktree: Path,
|
||||
): BranchConfig {
|
||||
val config = configLoader.loadForWorktree(workingDir, worktree)
|
||||
val branchConfig = config.branches[runningBuild.branch] ?: config.branches["default"] ?: BranchConfig()
|
||||
val definition = config.effectiveBuildDefinitions()[runningBuild.build] ?: return branchConfig
|
||||
return definition.applyTo(branchConfig)
|
||||
}
|
||||
): BranchConfig =
|
||||
configLoader
|
||||
.loadForWorktree(workingDir, worktree)
|
||||
.buildSettings(runningBuild.branch, runningBuild.build)
|
||||
|
||||
private class ActiveBuild(
|
||||
val runningBuild: RunningBuild,
|
||||
|
||||
@@ -21,6 +21,22 @@ data class GitTallyConfig(
|
||||
/** The configured [buildDefinitions] plus the implicit `default` build unless overridden. */
|
||||
fun effectiveBuildDefinitions(): Map<String, BuildDefinition> =
|
||||
mapOf(BuildDefinition.DEFAULT to BuildDefinition(onPush = true)) + buildDefinitions
|
||||
|
||||
/**
|
||||
* The settings one build of [build] on [branch] runs with: the branch entry (falling
|
||||
* back to `branches.default`) with the build definition's overrides applied last.
|
||||
* A build name without a definition — a job removed from the config since the run —
|
||||
* falls back to the plain branch settings.
|
||||
* This must stay the single answer to "what does this build run", used by the
|
||||
* executor and by everything that displays it.
|
||||
*/
|
||||
fun buildSettings(
|
||||
branch: String,
|
||||
build: String,
|
||||
): BranchConfig {
|
||||
val branchConfig = branches[branch] ?: branches["default"] ?: BranchConfig()
|
||||
return effectiveBuildDefinitions()[build]?.applyTo(branchConfig) ?: branchConfig
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerConfig(
|
||||
|
||||
@@ -6,7 +6,9 @@ import de.hoennig.gittally.build.BuildResult
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.config.ConfigLoader
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
import de.hoennig.gittally.watcher.Watcher
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
@@ -36,6 +38,7 @@ class UiController(
|
||||
private val buildExecutor: BuildExecutor,
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val configLoader: ConfigLoader,
|
||||
private val gitService: GitService,
|
||||
private val metricsCollector: SystemMetricsCollector,
|
||||
private val branchListing: BranchListing,
|
||||
private val branchPermalinks: BranchPermalinks,
|
||||
@@ -189,7 +192,7 @@ class UiController(
|
||||
model.addAttribute("filesBase", filesBase)
|
||||
model.addAttribute("result", result?.let { BuildRowView.from(it, links) })
|
||||
model.addAttribute("hasArtifacts", artifactDir != null)
|
||||
model.addAttribute("buildCommand", result?.let { branchBuildCommand(it.branch) })
|
||||
model.addAttribute("buildCommand", result?.let { buildCommandOf(it) })
|
||||
model.addAttribute(
|
||||
"logs",
|
||||
artifactDir?.let { logFiles(it, scanForFailure = result != null && result.status != BuildStatus.SUCCESS) }
|
||||
@@ -220,10 +223,24 @@ class UiController(
|
||||
return links
|
||||
}
|
||||
|
||||
/** The currently configured build command — the command actually used at build time is not persisted. */
|
||||
private fun branchBuildCommand(branch: String): String {
|
||||
val branches = configLoader.load(workingDir).branches
|
||||
return (branches[branch] ?: branches["default"])?.buildCommand ?: ""
|
||||
/**
|
||||
* The command this build runs, resolved exactly like the executor resolves it: the
|
||||
* branch layer committed at the build's own commit, plus the overrides of the build
|
||||
* definition it belongs to. Reading only the primary config would show a command no
|
||||
* build of this pool ever ran — the branch and its job usually override it.
|
||||
* The command used by a past run is not persisted, so this is the current answer.
|
||||
*/
|
||||
private fun buildCommandOf(result: BuildResult): String {
|
||||
val config =
|
||||
try {
|
||||
configLoader.loadWithBranchLayer(
|
||||
workingDir,
|
||||
gitService.showFileAtCommit(result.commit, Watcher.CONFIG_FILE, workingDir),
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
configLoader.load(workingDir)
|
||||
}
|
||||
return config.buildSettings(result.branch, result.build).buildCommand
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<li>Started: <span th:text="${result.startedAt}">2026-07-07 12:00</span>
|
||||
<th:block th:if="${result.duration != ''}">— Duration: <span th:text="${result.duration}">1:23</span></th:block>
|
||||
</li>
|
||||
<li th:if="${buildCommand != null}">Build command (as currently configured):<br>
|
||||
<li th:if="${buildCommand != null}">Build command (as configured for this build):<br>
|
||||
<code th:text="${buildCommand}">./gradlew test</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -8,6 +8,7 @@ import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.config.ConfigLoader
|
||||
import de.hoennig.gittally.config.GitTallyConfig
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.mockk.clearMocks
|
||||
@@ -52,6 +53,9 @@ class PermanentBranchRoutesTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var configLoader: ConfigLoader
|
||||
|
||||
@MockkBean
|
||||
lateinit var gitService: GitService
|
||||
|
||||
@MockkBean
|
||||
lateinit var metricsCollector: SystemMetricsCollector
|
||||
|
||||
@@ -81,11 +85,14 @@ class PermanentBranchRoutesTest : FunSpec() {
|
||||
artifactStore,
|
||||
controlTokens,
|
||||
configLoader,
|
||||
gitService,
|
||||
metricsCollector,
|
||||
branchListing,
|
||||
branchPermalinks,
|
||||
)
|
||||
every { configLoader.load(any()) } returns GitTallyConfig()
|
||||
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns GitTallyConfig()
|
||||
every { gitService.showFileAtCommit(any(), any(), any()) } returns null
|
||||
every { controlTokens.token() } returns "test-token"
|
||||
every { branchListing.branches(any()) } returns emptyList()
|
||||
every { branchPermalinks.latestGreenBuild("main") } returns greenBuild
|
||||
|
||||
@@ -7,10 +7,13 @@ import de.hoennig.gittally.build.BuildResult
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.build.RunningBuild
|
||||
import de.hoennig.gittally.config.BranchConfig
|
||||
import de.hoennig.gittally.config.BuildDefinition
|
||||
import de.hoennig.gittally.config.ConfigLoader
|
||||
import de.hoennig.gittally.config.GitTallyConfig
|
||||
import de.hoennig.gittally.config.GiteaConfig
|
||||
import de.hoennig.gittally.config.ServerConfig
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import de.hoennig.gittally.metrics.MetricAggregate
|
||||
import de.hoennig.gittally.metrics.SystemMetrics
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
@@ -58,6 +61,9 @@ class UiControllerTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var configLoader: ConfigLoader
|
||||
|
||||
@MockkBean
|
||||
lateinit var gitService: GitService
|
||||
|
||||
@MockkBean
|
||||
lateinit var metricsCollector: SystemMetricsCollector
|
||||
|
||||
@@ -103,6 +109,7 @@ class UiControllerTest : FunSpec() {
|
||||
artifactStore,
|
||||
controlTokens,
|
||||
configLoader,
|
||||
gitService,
|
||||
metricsCollector,
|
||||
branchListing,
|
||||
branchPermalinks,
|
||||
@@ -112,6 +119,8 @@ class UiControllerTest : FunSpec() {
|
||||
server = ServerConfig(impressumUrl = "https://example.org/imprint"),
|
||||
gitea = GiteaConfig(baseUrl = "https://git.example.org", owner = "acme", repo = "widget"),
|
||||
)
|
||||
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns GitTallyConfig()
|
||||
every { gitService.showFileAtCommit(any(), any(), any()) } returns null
|
||||
every { controlTokens.token() } returns "test-token"
|
||||
every { repository.latestGreenFor(any()) } returns null
|
||||
}
|
||||
@@ -264,6 +273,28 @@ class UiControllerTest : FunSpec() {
|
||||
).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html"))))
|
||||
}
|
||||
|
||||
test("the artifact page shows the command of the build's own definition, not the plain branch command") {
|
||||
val pitestResult =
|
||||
successResult.copy(
|
||||
build = "pitest",
|
||||
name = "main@pitest",
|
||||
artifactKey = "main-pitest-key",
|
||||
)
|
||||
every { repository.history() } returns listOf(pitestResult)
|
||||
every { artifactStore.artifactDir("main-pitest-key") } returns null
|
||||
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns
|
||||
GitTallyConfig(
|
||||
branches = mapOf("default" to BranchConfig(buildCommand = "./gradlew quick-check")),
|
||||
buildDefinitions = mapOf("pitest" to BuildDefinition(buildCommand = "./gradlew pitestFull")),
|
||||
)
|
||||
|
||||
mockMvc
|
||||
.perform(get("/builds/main-pitest-key"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(content().string(containsString("./gradlew pitestFull")))
|
||||
.andExpect(content().string(not(containsString("./gradlew quick-check"))))
|
||||
}
|
||||
|
||||
test("artifact index links a single index-less report page as a directory, keeping the URL stable") {
|
||||
val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key"))
|
||||
Files.createDirectories(artifactDir.resolve("reports/profile"))
|
||||
|
||||
Reference in New Issue
Block a user