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
@@ -20,6 +20,11 @@ class ArtifactKeysTest : FunSpec() {
ArtifactKeys.branchKey("feature/x") shouldNotBe ArtifactKeys.branchKey("feature_x")
}
test("permanentBranchKey is the hash-free sanitized branch name") {
ArtifactKeys.permanentBranchKey("feature/x") shouldBe "feature_x"
ArtifactKeys.branchKey("feature/x") shouldContain ArtifactKeys.permanentBranchKey("feature/x")
}
test("buildKey is stable for the same input") {
ArtifactKeys.buildKey("main", startedAt) shouldBe ArtifactKeys.buildKey("main", startedAt)
}
@@ -80,6 +80,25 @@ class FileBuildResultRepositoryTest : FunSpec() {
repository.latestFor("main") shouldBe result(branch = "main", startedOffsetSeconds = 60)
}
test("latestGreenFor returns the newest SUCCESS entry even when newer builds failed") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "main", status = BuildStatus.SUCCESS, startedOffsetSeconds = 0))
repository.append(result(branch = "main", status = BuildStatus.SUCCESS, startedOffsetSeconds = 60))
repository.append(result(branch = "main", status = BuildStatus.FAILED, startedOffsetSeconds = 120))
repository.append(result(branch = "other", status = BuildStatus.SUCCESS, startedOffsetSeconds = 180))
repository.latestGreenFor("main") shouldBe
result(branch = "main", status = BuildStatus.SUCCESS, startedOffsetSeconds = 60)
}
test("latestGreenFor returns null for a branch without a successful build") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "main", status = BuildStatus.FAILED))
repository.latestGreenFor("main").shouldBeNull()
repository.latestGreenFor("unknown").shouldBeNull()
}
test("latestPerBranch returns one entry per branch, newest first") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "main", startedOffsetSeconds = 0))
@@ -201,6 +220,35 @@ class FileBuildResultRepositoryTest : FunSpec() {
)
}
test("prune with keepLatestGreen keeps the newest green build beyond the retention count") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "main", status = BuildStatus.SUCCESS, startedOffsetSeconds = 0))
repository.append(result(branch = "main", status = BuildStatus.FAILED, startedOffsetSeconds = 60))
repository.append(result(branch = "main", status = BuildStatus.FAILED, startedOffsetSeconds = 120))
val removed =
repository.prune(originBranches = listOf("main"), retentionPerBranch = 2, keepLatestGreen = true)
removed.shouldBeEmpty()
repository.history() shouldContainExactly
listOf(
result(branch = "main", status = BuildStatus.FAILED, startedOffsetSeconds = 120),
result(branch = "main", status = BuildStatus.FAILED, startedOffsetSeconds = 60),
result(branch = "main", status = BuildStatus.SUCCESS, startedOffsetSeconds = 0),
)
}
test("prune with keepLatestGreen still drops green builds of branches missing from origin") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "gone", status = BuildStatus.SUCCESS))
val removed =
repository.prune(originBranches = listOf("main"), retentionPerBranch = 3, keepLatestGreen = true)
removed shouldContainExactly listOf(result(branch = "gone", status = BuildStatus.SUCCESS))
repository.history().shouldBeEmpty()
}
test("prune drops entries of branches missing from origin") {
val repository = FileBuildResultRepository(newFile())
repository.append(result(branch = "main", startedOffsetSeconds = 0))