feat(build): every running build knows its repository — die laufenden Builds und das Worktree-Aufräumen unterscheiden Repositories
Der letzte Übertrag aus Sitzung C: `RunningBuild` trug kein Repository, der Executor ist aber instanzweit. Zwei Folgen, beide gemessen und jetzt behoben: - Das Worktree-Aufräumen schützte die Worktrees ALLER Repositories. Ein laufender Build eines anderen Repositories auf einem gleichnamigen Branch hielt hier einen Worktree fest, dessen Branch längst von origin weg war. - Die Current-Builds-Ansicht und `/api/builds/current` zeigten die Builds aller Repositories, schlugen ihren Status aber in den Ergebnissen NUR dieses Repositories nach — ein fremder Build fiel auf RUNNING zurück und zeigte einen Zustand, den niemand aufgezeichnet hat. Dasselbe beim Live-Log: der Schlüssel eines fremden Builds wurde beantwortet. `RunningBuild.repo` ist der `RepoContext` selbst, verglichen wird per Referenz — der Kontext ist die Identität (ADR 0009), und der Executor hat ihn in `startBuild` ohnehin zur Hand. Watcher, API und UI filtern damit auf ihr eigenes Repository. Drei neue Tests, Gegenprobe per Mutation gezogen: Nimmt man die drei Filter wieder heraus, fallen genau diese drei und sonst keiner. 493 Tests grün, ktlint sauber. Architektur-Skill und Plan (docs/plan/22-multi-repo.md) nachgezogen — die Aussage „RunningBuild carries no repository" stimmte nicht mehr. Was von Sitzung D bleibt: die repo-bezogenen Routen und die Oberfläche, die weiterhin nur `registry.current()` bedienen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6fba43c627
commit
6ca67a6a37
@@ -34,6 +34,7 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
|
||||
private fun runningBuild(stagingDir: Path) =
|
||||
RunningBuild(
|
||||
repo = repo,
|
||||
branch = "main",
|
||||
commit = "0123456789abcdef",
|
||||
artifactKey = "main-key",
|
||||
|
||||
@@ -12,6 +12,7 @@ import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
|
||||
@@ -68,6 +69,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
|
||||
private fun runningBuild(liveLogFile: Path) =
|
||||
RunningBuild(
|
||||
repo = repo,
|
||||
branch = "main",
|
||||
commit = successResult.commit,
|
||||
artifactKey = "main-abc123-running",
|
||||
@@ -133,6 +135,35 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.andExpect(jsonPath("$[0].logSize").value(5))
|
||||
}
|
||||
|
||||
test("current answers only the served repository's builds") {
|
||||
val liveLogFile = Files.writeString(tempDir.resolve("mine.log"), "12345")
|
||||
val mine = runningBuild(liveLogFile)
|
||||
val foreign =
|
||||
runningBuild(liveLogFile).copy(
|
||||
repo = mockk<RepoContext>(),
|
||||
artifactKey = "other-repo-running",
|
||||
)
|
||||
every { buildExecutor.currentBuilds() } returns listOf(mine, foreign)
|
||||
every { repository.history() } returns
|
||||
listOf(successResult.copy(status = BuildStatus.RUNNING, artifactKey = mine.artifactKey))
|
||||
|
||||
mockMvc
|
||||
.perform(get("/api/builds/current"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.length()").value(1))
|
||||
.andExpect(jsonPath("$[0].artifactKey").value(mine.artifactKey))
|
||||
}
|
||||
|
||||
test("current log of a build in another repository answers 404") {
|
||||
val liveLogFile = Files.writeString(tempDir.resolve("foreign.log"), "hello world")
|
||||
val foreign = runningBuild(liveLogFile).copy(repo = mockk<RepoContext>())
|
||||
every { buildExecutor.currentBuilds() } returns listOf(foreign)
|
||||
|
||||
mockMvc
|
||||
.perform(get("/api/builds/current/${foreign.artifactKey}/log"))
|
||||
.andExpect(status().isNotFound)
|
||||
}
|
||||
|
||||
test("current log answers the tail from the requested offset") {
|
||||
val liveLogFile = Files.writeString(tempDir.resolve("tail.log"), "hello world")
|
||||
val build = runningBuild(liveLogFile)
|
||||
|
||||
@@ -246,6 +246,7 @@ class UiControllerTest : FunSpec() {
|
||||
test("current view renders a card per running build with cancel button and started-at attribute") {
|
||||
val build =
|
||||
RunningBuild(
|
||||
repo = repo,
|
||||
branch = "main",
|
||||
commit = successResult.commit,
|
||||
artifactKey = "main-abc123-running",
|
||||
|
||||
@@ -94,7 +94,7 @@ class WatcherTest : FunSpec() {
|
||||
val branch = secondArg<String>()
|
||||
val commit = thirdArg<String>()
|
||||
startedBuilds += branch to commit
|
||||
runningBuild(branch, commit)
|
||||
runningBuild(repo, branch, commit)
|
||||
}
|
||||
every { artifactStore.prune(any()) } returns emptyList()
|
||||
}
|
||||
@@ -131,11 +131,13 @@ class WatcherTest : FunSpec() {
|
||||
}
|
||||
|
||||
private fun runningBuild(
|
||||
repo: RepoContext,
|
||||
branch: String,
|
||||
commit: String,
|
||||
): RunningBuild {
|
||||
val stagingDir = Files.createTempDirectory("werkator-watcher-staging")
|
||||
return RunningBuild(
|
||||
repo = repo,
|
||||
branch = branch,
|
||||
commit = commit,
|
||||
artifactKey = ArtifactKeys.buildKey(branch, Instant.now()),
|
||||
@@ -272,7 +274,7 @@ class WatcherTest : FunSpec() {
|
||||
test("a poll cycle completes while a build is running and still enqueues other branches") {
|
||||
val harness = Harness()
|
||||
harness.seed("main", BuildStatus.RUNNING, commit = "commit-1")
|
||||
every { harness.buildExecutor.currentBuilds() } returns listOf(runningBuild("main", "commit-1"))
|
||||
every { harness.buildExecutor.currentBuilds() } returns listOf(runningBuild(harness.repo, "main", "commit-1"))
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main", "feature/other")
|
||||
every { harness.gitService.localBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
@@ -767,6 +769,28 @@ class WatcherTest : FunSpec() {
|
||||
Files.exists(busyWorktree).shouldBeTrue()
|
||||
}
|
||||
|
||||
test("a running build of another repository does not keep this repository's worktree") {
|
||||
val harness = Harness()
|
||||
harness.seed("gone", BuildStatus.SUCCESS, commit = "commit-1")
|
||||
val goneWorktree = harness.worktreeDir("gone")
|
||||
val otherDir = Files.createTempDirectory("werkator-watcher-other-running")
|
||||
val other =
|
||||
RepoContext(
|
||||
"other",
|
||||
otherDir,
|
||||
FileBuildResultRepository(otherDir.resolve(".git/werkator/build-results.json")),
|
||||
harness.artifactStore,
|
||||
)
|
||||
// the other repository builds a branch of the same name — its build must not
|
||||
// protect this repository's worktree, whose branch is gone from origin
|
||||
every { harness.buildExecutor.currentBuilds() } returns listOf(runningBuild(other, "gone", "commit-other"))
|
||||
every { harness.gitService.originBranches(any()) } returns emptyList()
|
||||
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
Files.exists(goneWorktree).shouldBeFalse()
|
||||
}
|
||||
|
||||
test("one repository's unreachable origin neither stops nor silences the other") {
|
||||
val harness = Harness()
|
||||
val otherDir = Files.createTempDirectory("werkator-watcher-other")
|
||||
|
||||
Reference in New Issue
Block a user