Let auto-build slots run their own build command under their own name

A branches.<name>.autoBuild.times entry is now either a plain HH:MM
string or an object with time, its own buildCommand, and a name, so a
nightly slot can run a fuller check than the on-commit builds. The
watcher passes the slot's command and name to the executor, persisted in
the build result — UI restarts, gittally retry, and the startup recovery
repeat a build with the command and name it originally ran under.

A named slot (e.g. master@nightly) gets its own pool: repository
grouping, retention count, branches-view row (sorted after its branch),
latest status, and permanent latest-green artifact link are keyed by the
build name, while origin lookups, gone-branch pruning, worktrees, and
Gitea links/statuses stay keyed by the real branch. Without a name,
slot builds share the branch's pool as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-28 19:11:35 +02:00
co-authored by Claude Fable 5
parent f292badac1
commit 8aee3190ea
35 changed files with 702 additions and 158 deletions
@@ -44,7 +44,7 @@ class RetryCommandTest : FunSpec() {
}
test("retries every branch whose latest build failed, but no others") {
every { repository.latestPerBranch() } returns
every { repository.latestPerName() } returns
listOf(
result("main", BuildStatus.FAILED),
result("feature/ok", BuildStatus.SUCCESS),
@@ -52,19 +52,19 @@ class RetryCommandTest : FunSpec() {
)
every { gitService.originHeadCommit("main", dir) } returns "head-main"
every { gitService.originHeadCommit("feature/y", dir) } returns "head-y"
every { consoleBuildRunner.buildAndStream(any(), any(), dir) } returns BuildStatus.SUCCESS
every { consoleBuildRunner.buildAndStream(any(), any(), dir, anyNullable(), any()) } returns BuildStatus.SUCCESS
var exitCode = -1
captureConsole { exitCode = command().call() }
exitCode shouldBe 0
verify { consoleBuildRunner.buildAndStream("main", "head-main", dir) }
verify { consoleBuildRunner.buildAndStream("feature/y", "head-y", dir) }
verify(exactly = 0) { consoleBuildRunner.buildAndStream("feature/ok", any(), dir) }
verify { consoleBuildRunner.buildAndStream("main", "head-main", dir, null, "main") }
verify { consoleBuildRunner.buildAndStream("feature/y", "head-y", dir, null, "feature/y") }
verify(exactly = 0) { consoleBuildRunner.buildAndStream("feature/ok", any(), dir, anyNullable(), any()) }
}
test("exits with code 1 when a retried build fails again") {
every { repository.latestPerBranch() } returns listOf(result("main", BuildStatus.FAILED))
every { repository.latestPerName() } returns listOf(result("main", BuildStatus.FAILED))
every { gitService.originHeadCommit("main", dir) } returns "head-main"
every { consoleBuildRunner.buildAndStream("main", "head-main", dir) } returns BuildStatus.FAILED
@@ -75,7 +75,7 @@ class RetryCommandTest : FunSpec() {
}
test("skips failed branches that are gone from origin") {
every { repository.latestPerBranch() } returns listOf(result("gone", BuildStatus.FAILED))
every { repository.latestPerName() } returns listOf(result("gone", BuildStatus.FAILED))
every { gitService.originHeadCommit("gone", dir) } returns null
var exitCode = -1
@@ -87,7 +87,7 @@ class RetryCommandTest : FunSpec() {
}
test("prints a hint when there is nothing to retry") {
every { repository.latestPerBranch() } returns
every { repository.latestPerName() } returns
listOf(
result("main", BuildStatus.SUCCESS),
result("feature/x", BuildStatus.INTERRUPTED),
@@ -36,7 +36,7 @@ class StatusCommandTest : FunSpec() {
}
test("prints the latest build per branch as a table with short commits and legacy duration format") {
every { repository.latestPerBranch() } returns
every { repository.latestPerName() } returns
listOf(
result("main", BuildStatus.SUCCESS),
result("feature/x", BuildStatus.FAILED, duration = null),
@@ -75,7 +75,7 @@ class StatusCommandTest : FunSpec() {
}
test("prints a hint when no builds are recorded yet") {
every { repository.latestPerBranch() } returns emptyList()
every { repository.latestPerName() } returns emptyList()
var exitCode = -1
val console = captureConsole { exitCode = StatusCommand(repository).call() }