feat(gitea): der Commit-Status verlinkt endlich die Artefaktseite — repo-bezogen

Der letzte offene Punkt der Sitzung D („Gitea status links use the repo-scoped
URLs"). Beim Nachsehen war es kein Scoping-Problem, sondern ein fehlendes
Feature: `server.publicBaseUrl` ist als „used for all links posted to Gitea"
dokumentiert, der Executor postete aber `targetUrl = null` — es gab überhaupt
keinen Link. Jetzt zeigt der Status auf die Artefaktseite des Builds, also auf
die Logs, um die es geht. Ohne `publicBaseUrl` bleibt er null: ein relativer
Link in einem Gitea-Status wäre schlimmer als keiner, er löste gegen die Forge
auf.

Die Präfix-Regel wohnt jetzt an einer Stelle (`RepoLinks`), weil drei
dasselbe damit tun: die Seiten, die API-Antworten und diese Ziel-URLs — und
eine dreimal ausgeschriebene Regel driftet. Die Slice-Tests binden die echte
Komponente per @Import ein, statt die Regel im Test nachzubauen.

Ein neuer Test (mit publicBaseUrl trägt der Status die Artefaktseite; die
vorhandenen Tests halten fest, dass ohne sie weiterhin null gepostet wird).
499 Tests grün, ktlint sauber. Plan und Architektur-Skill nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-03 13:53:51 +02:00
co-authored by Claude Opus 5
parent 91e1dd47a3
commit 80a14a7467
11 changed files with 114 additions and 7 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ Everything repository-scoped goes through a `RepoContext` (`repo` package, ADR 0
## Build Execution ## Build Execution
`BuildExecutor` runs builds asynchronously: `startBuild(repo, branch, commit, build)` takes the `RepoContext` first; up to `executor.maxConcurrent` builds run concurrently across all repositories (default 1, sized once from the first build's config — an instance-level setting), but never more than one build per (repository, branch) at a time. Each branch builds in its own reusable git worktree at `.git/werkator/worktrees/<branchKey>` (`BranchWorkspaces`), checked out detached at the requested commit — the primary checkout is never used for builds. Status transitions are persisted in the build's own `RepoContext.results` (JSON file under that repository's `.git/werkator/`), published to Gitea non-fatally, and emitted as `BuildStatusChangedEvent`s. Every run belongs to a named build definition (job, ADR 0007): the YAML `builds` section defines triggers (`onPush`, `atTimes`), branch selectors (`branches` globs, `activeWithin`), and build-setting overrides applied last over the merged branch config; the implicit `default` build (`onPush`, all branches) preserves the job-less behavior. Definitions are part of the branch layer — a branch may add and override its own, and they apply to that branch alone (its selectors are evaluated for it only) — while `executor.maxConcurrent` stays pinned. `BuildResult.build` records the job; restart, retry, and startup recovery re-run by that name, resolving settings from the *current* config. `BuildResult.name` — the pool, `<branch>@<build>` for non-default builds — keys everything display- and retention-side (repository grouping via `latestPerName`, retention pools, branches-view rows, permanent latest-green links), while `BuildResult.branch` keys everything git-side: origin lookups, gone-from-origin pruning, worktrees (every build runs in its branch's worktree, serialized per branch), and Gitea links/statuses. `branches.*.autoBuild` survives as a deprecated alias for a scheduled default-pool rebuild. Cancellation addresses a build by artifact key and terminates the whole process tree. Future code (watcher, server, UI) must not assume a single running build. `BuildExecutor` runs builds asynchronously: `startBuild(repo, branch, commit, build)` takes the `RepoContext` first; up to `executor.maxConcurrent` builds run concurrently across all repositories (default 1, sized once from the first build's config — an instance-level setting), but never more than one build per (repository, branch) at a time. Each branch builds in its own reusable git worktree at `.git/werkator/worktrees/<branchKey>` (`BranchWorkspaces`), checked out detached at the requested commit — the primary checkout is never used for builds. Status transitions are persisted in the build's own `RepoContext.results` (JSON file under that repository's `.git/werkator/`), published to Gitea non-fatally — with the build's artifact page as `target_url` when `server.publicBaseUrl` is set, repository-scoped through `RepoLinks` and emitted as `BuildStatusChangedEvent`s. Every run belongs to a named build definition (job, ADR 0007): the YAML `builds` section defines triggers (`onPush`, `atTimes`), branch selectors (`branches` globs, `activeWithin`), and build-setting overrides applied last over the merged branch config; the implicit `default` build (`onPush`, all branches) preserves the job-less behavior. Definitions are part of the branch layer — a branch may add and override its own, and they apply to that branch alone (its selectors are evaluated for it only) — while `executor.maxConcurrent` stays pinned. `BuildResult.build` records the job; restart, retry, and startup recovery re-run by that name, resolving settings from the *current* config. `BuildResult.name` — the pool, `<branch>@<build>` for non-default builds — keys everything display- and retention-side (repository grouping via `latestPerName`, retention pools, branches-view rows, permanent latest-green links), while `BuildResult.branch` keys everything git-side: origin lookups, gone-from-origin pruning, worktrees (every build runs in its branch's worktree, serialized per branch), and Gitea links/statuses. `branches.*.autoBuild` survives as a deprecated alias for a scheduled default-pool rebuild. Cancellation addresses a build by artifact key and terminates the whole process tree. Future code (watcher, server, UI) must not assume a single running build.
On context close (e.g. systemd SIGTERM), a `ContextClosedEvent` listener in `BuildExecutor` terminates the process trees of all executing builds and waits (bounded) until their results are persisted as INTERRUPTED — a shutdown is never recorded as FAILED. Builds still queued stay PENDING and start no process. Both are re-enqueued by the watcher's startup recovery; INTERRUPTED therefore publishes as Gitea state `pending`, not `failure` (`GiteaStateMapping`). On context close (e.g. systemd SIGTERM), a `ContextClosedEvent` listener in `BuildExecutor` terminates the process trees of all executing builds and waits (bounded) until their results are persisted as INTERRUPTED — a shutdown is never recorded as FAILED. Builds still queued stay PENDING and start no process. Both are re-enqueued by the watcher's startup recovery; INTERRUPTED therefore publishes as Gitea state `pending`, not `failure` (`GiteaStateMapping`).
+2 -2
View File
@@ -66,9 +66,9 @@ The pinning model is untouched: pinned keys still come from each repo's machine
- ~~Routes gain the repo segment (`/api/repos/<name>/builds/…`, `/repos/<name>/builds/<key>`); with exactly one registered repo the today-routes keep working (redirect or alias) so bookmarks and posted Gitea links survive.~~ — done 2026-09-03 (PR #13): every route of the builds API, the pages, and the artifact files is mapped twice; the unscoped form is not an alias with an expiry date but the permanent way to say "the served repository", and an unknown name is a 404 in each controller's own shape. - ~~Routes gain the repo segment (`/api/repos/<name>/builds/…`, `/repos/<name>/builds/<key>`); with exactly one registered repo the today-routes keep working (redirect or alias) so bookmarks and posted Gitea links survive.~~ — done 2026-09-03 (PR #13): every route of the builds API, the pages, and the artifact files is mapped twice; the unscoped form is not an alias with an expiry date but the permanent way to say "the served repository", and an unknown name is a 404 in each controller's own shape.
- ~~Latest/branches/history views group by repo or gain a repo column; one instance-wide metrics page; one control token.~~ — done 2026-09-03, decided against the column: the pages stay per repository and the navigation gains a **repository switcher** (a row's actions need the repository anyway, branches come from one origin, artifacts from one store — and with one repository a column is noise). Metrics page and control token stay instance-wide as planned. - ~~Latest/branches/history views group by repo or gain a repo column; one instance-wide metrics page; one control token.~~ — done 2026-09-03, decided against the column: the pages stay per repository and the navigation gains a **repository switcher** (a row's actions need the repository anyway, branches come from one origin, artifacts from one store — and with one repository a column is noise). Metrics page and control token stay instance-wide as planned.
- Gitea status links use the repo-scoped URLs — the permanent artifact links do (`BranchPermalinks.permanentUrl` takes the prefix, because the key is a hash of the build name alone and two repositories both having `main` would otherwise share one URL); the commit-status target URLs posted by `GiteaStatusPublisher` are carried over to session E, where the instance actually serves two repositories. - ~~Gitea status links use the repo-scoped URLs~~ done 2026-09-03: the permanent artifact links take the prefix (`BranchPermalinks.permanentUrl`; the key is a hash of the build name alone, so two repositories both having `main` would share one URL), and the **commit status now carries a target URL at all**`server.publicBaseUrl` was documented as "used for all links posted to Gitea" while the executor posted `targetUrl = null`. It is the build's artifact page, repository-scoped.
- Also done: `RunningBuild` carries its `RepoContext` (the carry-over from session C), so the current-builds views and the watcher's worktree pruning tell repositories apart, and `cancel` refuses a key that is not recorded in the named repository. - Also done: `RunningBuild` carries its `RepoContext` (the carry-over from session C), so the current-builds views and the watcher's worktree pruning tell repositories apart, and `cancel` refuses a key that is not recorded in the named repository.
- Carried over to session E: the commit-status URLs. ~~`docs/deployment.md` gets the registry setup~~ — done 2026-09-03: section "Serving Several Repositories" (clone, prepare, register, restart) with the name rules, the per-repository guard, and what the URLs look like with one repository and with several. - ~~`docs/deployment.md` gets the registry setup~~ — done 2026-09-03: section "Serving Several Repositories" (clone, prepare, register, restart) with the name rules, the per-repository guard, and what the URLs look like with one repository and with several.
### E — Rollout on mih34: Werkbaum joins ### E — Rollout on mih34: Werkbaum joins
@@ -5,6 +5,7 @@ import de.hoennig.werkator.config.BuildDefinition
import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient import de.hoennig.werkator.gitea.GiteaClient
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.event.ContextClosedEvent import org.springframework.context.event.ContextClosedEvent
@@ -36,6 +37,7 @@ import kotlin.concurrent.thread
@Service @Service
class BuildExecutor( class BuildExecutor(
private val configLoader: ConfigLoader, private val configLoader: ConfigLoader,
private val repoLinks: RepoLinks,
private val giteaClient: GiteaClient, private val giteaClient: GiteaClient,
private val buildRunner: BuildRunner, private val buildRunner: BuildRunner,
private val workspaces: BranchWorkspaces, private val workspaces: BranchWorkspaces,
@@ -397,7 +399,7 @@ class BuildExecutor(
sha = build.runningBuild.commit, sha = build.runningBuild.commit,
status = status, status = status,
description = description(status, duration), description = description(status, duration),
targetUrl = null, targetUrl = targetUrlOf(build),
workingDir = build.repo.workingDir, workingDir = build.repo.workingDir,
// from the primary config, not the worktree: statusContext is pinned, so a // from the primary config, not the worktree: statusContext is pinned, so a
// branch cannot report under a check name it was not given // branch cannot report under a check name it was not given
@@ -408,6 +410,25 @@ class BuildExecutor(
} }
} }
/**
* The artifact page of this build, so a commit status in the forge leads to the
* logs it is about — that is what `server.publicBaseUrl` is documented for, and
* until now nothing posted a link at all. Repository-scoped (ADR 0009): with
* several served repositories the unscoped path would resolve against whichever
* one the instance serves by default, which is the wrong build's page.
*/
private fun targetUrlOf(build: ActiveBuild): String? =
try {
repoLinks.buildUrl(
repo = build.repo,
publicBaseUrl = configLoader.load(build.repo.workingDir).server.publicBaseUrl,
artifactKey = build.runningBuild.artifactKey,
)
} catch (e: Exception) {
log.warn("could not build the status target URL of {}: {}", build.runningBuild.branch, e.message)
null
}
/** The build's own Gitea status context, empty when it uses the repository-wide one. */ /** The build's own Gitea status context, empty when it uses the repository-wide one. */
private fun statusContextOf(build: ActiveBuild): String = private fun statusContextOf(build: ActiveBuild): String =
try { try {
@@ -0,0 +1,37 @@
package de.hoennig.werkator.repo
import org.springframework.stereotype.Component
/**
* The path prefix a link to a repository carries (ADR 0009). One place knows the
* rule, because three do the same thing with it: the pages, the API answers, and
* the target URLs posted to Gitea — and a rule spelled out three times is a rule
* that drifts.
*
* It follows the NUMBER of served repositories, not the route a request arrived
* through: with one repository an installation keeps the URLs it always had, with
* several every link names its repository.
*/
@Component
class RepoLinks(
private val registry: RepoRegistry,
) {
fun base(repo: RepoContext): String = if (registry.all().size > 1) "/repos/${repo.name}" else ""
fun apiBase(repo: RepoContext): String = if (registry.all().size > 1) "/api/repos/${repo.name}" else "/api"
/**
* The absolute artifact-page URL of a build, for links Werkator posts elsewhere
* (`server.publicBaseUrl`). Null without a public base URL: a relative link in a
* Gitea status is worse than none — it would resolve against the forge.
*/
fun buildUrl(
repo: RepoContext,
publicBaseUrl: String,
artifactKey: String,
): String? {
val root = publicBaseUrl.trim().trimEnd('/')
if (root.isEmpty()) return null
return "$root${base(repo)}/builds/$artifactKey"
}
}
@@ -6,6 +6,7 @@ import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.config.BuildDefinition import de.hoennig.werkator.config.BuildDefinition
import de.hoennig.werkator.git.GitService import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry import de.hoennig.werkator.repo.RepoRegistry
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -37,6 +38,7 @@ class BuildsApiController(
private val gitService: GitService, private val gitService: GitService,
private val branchListing: BranchListing, private val branchListing: BranchListing,
private val registry: RepoRegistry, private val registry: RepoRegistry,
private val repoLinks: RepoLinks,
) { ) {
/** /**
* Every route exists twice: repository-scoped (`/api/repos/<name>/…`) and unscoped. * Every route exists twice: repository-scoped (`/api/repos/<name>/…`) and unscoped.
@@ -52,7 +54,7 @@ class BuildsApiController(
results.latestGreenFor(result.name)?.artifactKey == result.artifactKey results.latestGreenFor(result.name)?.artifactKey == result.artifactKey
/** The prefix the permanent links in the answers carry; empty with one served repository. */ /** The prefix the permanent links in the answers carry; empty with one served repository. */
private fun uiBase(repo: RepoContext): String = if (registry.all().size > 1) "/repos/${repo.name}" else "" private fun uiBase(repo: RepoContext): String = repoLinks.base(repo)
/** An unknown repository name answers like every other miss of this API: 404 with `error`. */ /** An unknown repository name answers like every other miss of this API: 404 with `error`. */
@ExceptionHandler(UnknownRepositoryException::class) @ExceptionHandler(UnknownRepositoryException::class)
@@ -8,6 +8,7 @@ import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.git.GitService import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.metrics.SystemMetricsCollector
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry import de.hoennig.werkator.repo.RepoRegistry
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.ObjectProvider
@@ -43,6 +44,7 @@ class UiController(
private val branchPermalinks: BranchPermalinks, private val branchPermalinks: BranchPermalinks,
private val buildProperties: ObjectProvider<BuildProperties>, private val buildProperties: ObjectProvider<BuildProperties>,
private val registry: RepoRegistry, private val registry: RepoRegistry,
private val repoLinks: RepoLinks,
) { ) {
/** /**
* Every page exists twice, like the API (ADR 0009): repository-scoped under * Every page exists twice, like the API (ADR 0009): repository-scoped under
@@ -272,9 +274,9 @@ class UiController(
* repository the installation keeps its existing URLs (the session-D acceptance * repository the installation keeps its existing URLs (the session-D acceptance
* criterion), with several every link names its repository. * criterion), with several every link names its repository.
*/ */
private fun uiBase(repo: RepoContext): String = if (registry.all().size > 1) "/repos/${repo.name}" else "" private fun uiBase(repo: RepoContext): String = repoLinks.base(repo)
private fun apiBase(repo: RepoContext): String = if (registry.all().size > 1) "/api/repos/${repo.name}" else "/api" private fun apiBase(repo: RepoContext): String = repoLinks.apiBase(repo)
/** Adds the attributes every page needs and returns the Gitea link helper for row building. */ /** Adds the attributes every page needs and returns the Gitea link helper for row building. */
private fun baseModel( private fun baseModel(
@@ -7,11 +7,14 @@ import de.hoennig.werkator.build.ProcessBuildRunner
import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient import de.hoennig.werkator.gitea.GiteaClient
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import java.nio.file.Files import java.nio.file.Files
@@ -42,6 +45,7 @@ class BuildExecutorArtifactIntegrationTest : FunSpec() {
val executor = val executor =
BuildExecutor( BuildExecutor(
configLoader = ConfigLoader(), configLoader = ConfigLoader(),
repoLinks = RepoLinks(mockk<RepoRegistry>().also { every { it.all() } returns listOf(repo) }),
giteaClient = mockk<GiteaClient>(relaxed = true), giteaClient = mockk<GiteaClient>(relaxed = true),
buildRunner = ProcessBuildRunner(), buildRunner = ProcessBuildRunner(),
workspaces = BranchWorkspaces { _, _, _ -> workspace }, workspaces = BranchWorkspaces { _, _, _ -> workspace },
@@ -3,6 +3,8 @@ package de.hoennig.werkator.build
import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient import de.hoennig.werkator.gitea.GiteaClient
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeFalse
@@ -53,6 +55,7 @@ class BuildExecutorTest : FunSpec() {
val executor = val executor =
BuildExecutor( BuildExecutor(
configLoader = ConfigLoader(), configLoader = ConfigLoader(),
repoLinks = RepoLinks(mockk<RepoRegistry>().also { every { it.all() } returns listOf(repo) }),
giteaClient = giteaClient, giteaClient = giteaClient,
buildRunner = buildRunner, buildRunner = buildRunner,
workspaces = workspaces, workspaces = workspaces,
@@ -138,6 +141,35 @@ class BuildExecutorTest : FunSpec() {
verify { h.artifactStore.persist(match { it.status == BuildStatus.SUCCESS }, build.stagingDir, h.workingDir) } verify { h.artifactStore.persist(match { it.status == BuildStatus.SUCCESS }, build.stagingDir, h.workingDir) }
} }
test("the commit status carries the artifact page when a public base URL is configured") {
val h =
Harness(
"""
server:
publicBaseUrl: https://ci.example.org/
branches:
default:
buildCommand: "true"
""".trimIndent(),
)
val build = h.executor.startBuild(h.repo, "main", "abc123")
awaitStatus(h, "main", BuildStatus.SUCCESS)
awaitIdle(h)
// one served repository: the installation's existing URLs, no repository segment
verify {
h.giteaClient.publishStatus(
"abc123",
BuildStatus.SUCCESS,
any(),
"https://ci.example.org/builds/" + build.artifactKey,
h.workingDir,
any(),
)
}
}
test("build commands run in the workspace prepared for the branch") { test("build commands run in the workspace prepared for the branch") {
val h = harness(buildCommand = "pwd", workspaceSubdir = "branch-workspace") val h = harness(buildCommand = "pwd", workspaceSubdir = "branch-workspace")
@@ -9,6 +9,7 @@ import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.build.RunningBuild import de.hoennig.werkator.build.RunningBuild
import de.hoennig.werkator.git.GitService import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.mockk.clearMocks import io.mockk.clearMocks
@@ -17,6 +18,7 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.context.annotation.Import
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@@ -29,6 +31,7 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
@WebMvcTest(BuildsApiController::class, properties = ["spring.main.web-application-type=servlet"]) @WebMvcTest(BuildsApiController::class, properties = ["spring.main.web-application-type=servlet"])
@Import(RepoLinks::class)
class BuildsApiControllerTest : FunSpec() { class BuildsApiControllerTest : FunSpec() {
private val tempDir: Path = Files.createTempDirectory("werkator-server-test") private val tempDir: Path = Files.createTempDirectory("werkator-server-test")
@@ -11,6 +11,7 @@ import de.hoennig.werkator.config.WerkatorConfig
import de.hoennig.werkator.git.GitService import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.metrics.SystemMetricsCollector
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.mockk.clearMocks import io.mockk.clearMocks
@@ -18,6 +19,7 @@ import io.mockk.every
import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.containsString
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.context.annotation.Import
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 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.content
@@ -37,6 +39,7 @@ import java.time.Instant
controllers = [UiController::class, ArtifactFileController::class], controllers = [UiController::class, ArtifactFileController::class],
properties = ["spring.main.web-application-type=servlet"], properties = ["spring.main.web-application-type=servlet"],
) )
@Import(RepoLinks::class)
class PermanentBranchRoutesTest : FunSpec() { class PermanentBranchRoutesTest : FunSpec() {
@Autowired @Autowired
lateinit var mockMvc: MockMvc lateinit var mockMvc: MockMvc
@@ -18,6 +18,7 @@ import de.hoennig.werkator.metrics.MetricAggregate
import de.hoennig.werkator.metrics.SystemMetrics import de.hoennig.werkator.metrics.SystemMetrics
import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.metrics.SystemMetricsCollector
import de.hoennig.werkator.repo.RepoContext import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoLinks
import de.hoennig.werkator.repo.RepoRegistry import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
@@ -30,6 +31,7 @@ import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.not import org.hamcrest.Matchers.not
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.context.annotation.Import
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@@ -44,6 +46,7 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
@WebMvcTest(UiController::class, properties = ["spring.main.web-application-type=servlet"]) @WebMvcTest(UiController::class, properties = ["spring.main.web-application-type=servlet"])
@Import(RepoLinks::class)
class UiControllerTest : FunSpec() { class UiControllerTest : FunSpec() {
private val tempDir: Path = Files.createTempDirectory("werkator-ui-test") private val tempDir: Path = Files.createTempDirectory("werkator-ui-test")