added permanent artifact links for latest green builds: /branches/<branch-key>/... serves the latest green build's artifacts (green-only, resolved per request), keepLatestGreen retention protection, permanent artifact-index page, and latestGreenUrl links in the branches view

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Michael Hoennig
2026-07-08 15:35:16 +02:00
co-authored by Claude Fable 5
parent 83e70e73c3
commit bb06c8b731
25 changed files with 820 additions and 18 deletions
@@ -0,0 +1,118 @@
package de.hoennig.gittally.server
import com.ninjasquad.springmockk.MockkBean
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildExecutor
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.config.GitTallyConfig
import de.hoennig.gittally.metrics.SystemMetricsCollector
import io.kotest.core.spec.style.FunSpec
import io.mockk.clearMocks
import io.mockk.every
import org.hamcrest.Matchers.containsString
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
/**
* The three `/branches…` routes live in two controllers; this slice registers both
* and proves the mappings coexist: the exact list page, the permanent index page,
* and the catch-all permanent file route.
*/
@WebMvcTest(
controllers = [UiController::class, ArtifactFileController::class],
properties = ["spring.main.web-application-type=servlet"],
)
class PermanentBranchRoutesTest : FunSpec() {
@Autowired
lateinit var mockMvc: MockMvc
@MockkBean
lateinit var repository: BuildResultRepository
@MockkBean
lateinit var buildExecutor: BuildExecutor
@MockkBean
lateinit var artifactStore: ArtifactStore
@MockkBean
lateinit var controlTokens: ControlTokenService
@MockkBean
lateinit var configLoader: ConfigLoader
@MockkBean
lateinit var metricsCollector: SystemMetricsCollector
@MockkBean
lateinit var branchListing: BranchListing
@MockkBean
lateinit var branchPermalinks: BranchPermalinks
private val artifactDir: Path = Files.createTempDirectory("gittally-permanent-routes-test")
private val greenBuild =
BuildResult(
branch = "main",
commit = "0123456789abcdef",
status = BuildStatus.SUCCESS,
startedAt = Instant.parse("2026-07-07T10:00:00Z"),
duration = Duration.ofSeconds(83),
artifactKey = "main-key",
)
init {
beforeEach {
clearMocks(
repository,
buildExecutor,
artifactStore,
controlTokens,
configLoader,
metricsCollector,
branchListing,
branchPermalinks,
)
every { configLoader.load(any()) } returns GitTallyConfig()
every { controlTokens.token() } returns "test-token"
every { branchListing.branches(any()) } returns emptyList()
every { branchPermalinks.latestGreenBuild("main") } returns greenBuild
every { artifactStore.artifactDir("main-key") } returns artifactDir
}
test("/branches still renders the branch list page") {
mockMvc
.perform(get("/branches"))
.andExpect(status().isOk)
.andExpect(content().string(containsString("""data-api="/api/branches"""")))
}
test("/branches/<key> renders the permanent artifact index page") {
mockMvc
.perform(get("/branches/main"))
.andExpect(status().isOk)
.andExpect(content().string(containsString("latest green build of branch")))
}
test("/branches/<key>/<path> serves the artifact file") {
Files.writeString(artifactDir.resolve("build.log"), "line one")
mockMvc
.perform(get("/branches/main/build.log"))
.andExpect(status().isOk)
.andExpect(content().string("line one"))
}
}
}