From 80a14a7467de36a3b075282a85abeed5387d6ed4 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Thu, 3 Sep 2026 13:53:51 +0200 Subject: [PATCH] =?UTF-8?q?feat(gitea):=20der=20Commit-Status=20verlinkt?= =?UTF-8?q?=20endlich=20die=20Artefaktseite=20=E2=80=94=20repo-bezogen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/skills/architecture/SKILL.md | 2 +- docs/plan/22-multi-repo.md | 4 +- .../hoennig/werkator/build/BuildExecutor.kt | 23 +++++++++++- .../de/hoennig/werkator/repo/RepoLinks.kt | 37 +++++++++++++++++++ .../werkator/server/BuildsApiController.kt | 4 +- .../hoennig/werkator/server/UiController.kt | 6 ++- .../BuildExecutorArtifactIntegrationTest.kt | 4 ++ .../werkator/build/BuildExecutorTest.kt | 32 ++++++++++++++++ .../server/BuildsApiControllerTest.kt | 3 ++ .../server/PermanentBranchRoutesTest.kt | 3 ++ .../werkator/server/UiControllerTest.kt | 3 ++ 11 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/de/hoennig/werkator/repo/RepoLinks.kt diff --git a/.claude/skills/architecture/SKILL.md b/.claude/skills/architecture/SKILL.md index 9cb94ab..dbfde90 100644 --- a/.claude/skills/architecture/SKILL.md +++ b/.claude/skills/architecture/SKILL.md @@ -67,7 +67,7 @@ Everything repository-scoped goes through a `RepoContext` (`repo` package, ADR 0 ## 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/` (`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, `@` 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/` (`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, `@` 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`). diff --git a/docs/plan/22-multi-repo.md b/docs/plan/22-multi-repo.md index da96d02..095265a 100644 --- a/docs/plan/22-multi-repo.md +++ b/docs/plan/22-multi-repo.md @@ -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//builds/…`, `/repos//builds/`); 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. -- 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. -- 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 diff --git a/src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt b/src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt index ed7e8cd..0de0403 100644 --- a/src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt +++ b/src/main/kotlin/de/hoennig/werkator/build/BuildExecutor.kt @@ -5,6 +5,7 @@ import de.hoennig.werkator.config.BuildDefinition import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.gitea.GiteaClient import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.context.event.ContextClosedEvent @@ -36,6 +37,7 @@ import kotlin.concurrent.thread @Service class BuildExecutor( private val configLoader: ConfigLoader, + private val repoLinks: RepoLinks, private val giteaClient: GiteaClient, private val buildRunner: BuildRunner, private val workspaces: BranchWorkspaces, @@ -397,7 +399,7 @@ class BuildExecutor( sha = build.runningBuild.commit, status = status, description = description(status, duration), - targetUrl = null, + targetUrl = targetUrlOf(build), workingDir = build.repo.workingDir, // from the primary config, not the worktree: statusContext is pinned, so a // 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. */ private fun statusContextOf(build: ActiveBuild): String = try { diff --git a/src/main/kotlin/de/hoennig/werkator/repo/RepoLinks.kt b/src/main/kotlin/de/hoennig/werkator/repo/RepoLinks.kt new file mode 100644 index 0000000..9a71f00 --- /dev/null +++ b/src/main/kotlin/de/hoennig/werkator/repo/RepoLinks.kt @@ -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" + } +} diff --git a/src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt b/src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt index 26030b7..1eec728 100644 --- a/src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/BuildsApiController.kt @@ -6,6 +6,7 @@ import de.hoennig.werkator.build.BuildStatus import de.hoennig.werkator.config.BuildDefinition import de.hoennig.werkator.git.GitService import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import de.hoennig.werkator.repo.RepoRegistry import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -37,6 +38,7 @@ class BuildsApiController( private val gitService: GitService, private val branchListing: BranchListing, private val registry: RepoRegistry, + private val repoLinks: RepoLinks, ) { /** * Every route exists twice: repository-scoped (`/api/repos//…`) and unscoped. @@ -52,7 +54,7 @@ class BuildsApiController( results.latestGreenFor(result.name)?.artifactKey == result.artifactKey /** 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`. */ @ExceptionHandler(UnknownRepositoryException::class) diff --git a/src/main/kotlin/de/hoennig/werkator/server/UiController.kt b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt index 0701b6c..d591238 100644 --- a/src/main/kotlin/de/hoennig/werkator/server/UiController.kt +++ b/src/main/kotlin/de/hoennig/werkator/server/UiController.kt @@ -8,6 +8,7 @@ import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.git.GitService import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import de.hoennig.werkator.repo.RepoRegistry import jakarta.servlet.http.HttpServletRequest import org.springframework.beans.factory.ObjectProvider @@ -43,6 +44,7 @@ class UiController( private val branchPermalinks: BranchPermalinks, private val buildProperties: ObjectProvider, private val registry: RepoRegistry, + private val repoLinks: RepoLinks, ) { /** * 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 * 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. */ private fun baseModel( diff --git a/src/test/kotlin/de/hoennig/werkator/artifacts/BuildExecutorArtifactIntegrationTest.kt b/src/test/kotlin/de/hoennig/werkator/artifacts/BuildExecutorArtifactIntegrationTest.kt index d39dc84..1fa7050 100644 --- a/src/test/kotlin/de/hoennig/werkator/artifacts/BuildExecutorArtifactIntegrationTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/artifacts/BuildExecutorArtifactIntegrationTest.kt @@ -7,11 +7,14 @@ import de.hoennig.werkator.build.ProcessBuildRunner import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.gitea.GiteaClient 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.core.spec.style.FunSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import io.mockk.every import io.mockk.mockk import org.springframework.context.ApplicationEventPublisher import java.nio.file.Files @@ -42,6 +45,7 @@ class BuildExecutorArtifactIntegrationTest : FunSpec() { val executor = BuildExecutor( configLoader = ConfigLoader(), + repoLinks = RepoLinks(mockk().also { every { it.all() } returns listOf(repo) }), giteaClient = mockk(relaxed = true), buildRunner = ProcessBuildRunner(), workspaces = BranchWorkspaces { _, _, _ -> workspace }, diff --git a/src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt b/src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt index fdc167a..1174a1f 100644 --- a/src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt @@ -3,6 +3,8 @@ package de.hoennig.werkator.build import de.hoennig.werkator.config.ConfigLoader import de.hoennig.werkator.gitea.GiteaClient 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.core.spec.style.FunSpec import io.kotest.matchers.booleans.shouldBeFalse @@ -53,6 +55,7 @@ class BuildExecutorTest : FunSpec() { val executor = BuildExecutor( configLoader = ConfigLoader(), + repoLinks = RepoLinks(mockk().also { every { it.all() } returns listOf(repo) }), giteaClient = giteaClient, buildRunner = buildRunner, workspaces = workspaces, @@ -138,6 +141,35 @@ class BuildExecutorTest : FunSpec() { 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") { val h = harness(buildCommand = "pwd", workspaceSubdir = "branch-workspace") diff --git a/src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt b/src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt index f034de4..3516ae2 100644 --- a/src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt @@ -9,6 +9,7 @@ import de.hoennig.werkator.build.BuildStatus import de.hoennig.werkator.build.RunningBuild import de.hoennig.werkator.git.GitService import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import de.hoennig.werkator.repo.RepoRegistry import io.kotest.core.spec.style.FunSpec import io.mockk.clearMocks @@ -17,6 +18,7 @@ import io.mockk.mockk import io.mockk.verify import org.springframework.beans.factory.annotation.Autowired 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.request.MockMvcRequestBuilders.delete import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -29,6 +31,7 @@ import java.time.Duration import java.time.Instant @WebMvcTest(BuildsApiController::class, properties = ["spring.main.web-application-type=servlet"]) +@Import(RepoLinks::class) class BuildsApiControllerTest : FunSpec() { private val tempDir: Path = Files.createTempDirectory("werkator-server-test") diff --git a/src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt b/src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt index d10b965..aa47ab8 100644 --- a/src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/server/PermanentBranchRoutesTest.kt @@ -11,6 +11,7 @@ import de.hoennig.werkator.config.WerkatorConfig import de.hoennig.werkator.git.GitService import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import de.hoennig.werkator.repo.RepoRegistry import io.kotest.core.spec.style.FunSpec import io.mockk.clearMocks @@ -18,6 +19,7 @@ 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.context.annotation.Import 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 @@ -37,6 +39,7 @@ import java.time.Instant controllers = [UiController::class, ArtifactFileController::class], properties = ["spring.main.web-application-type=servlet"], ) +@Import(RepoLinks::class) class PermanentBranchRoutesTest : FunSpec() { @Autowired lateinit var mockMvc: MockMvc diff --git a/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt b/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt index 7374aea..aa2b5f4 100644 --- a/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt @@ -18,6 +18,7 @@ import de.hoennig.werkator.metrics.MetricAggregate import de.hoennig.werkator.metrics.SystemMetrics import de.hoennig.werkator.metrics.SystemMetricsCollector import de.hoennig.werkator.repo.RepoContext +import de.hoennig.werkator.repo.RepoLinks import de.hoennig.werkator.repo.RepoRegistry import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -30,6 +31,7 @@ import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.not import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest +import org.springframework.context.annotation.Import import org.springframework.http.HttpStatus import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -44,6 +46,7 @@ import java.time.Duration import java.time.Instant @WebMvcTest(UiController::class, properties = ["spring.main.web-application-type=servlet"]) +@Import(RepoLinks::class) class UiControllerTest : FunSpec() { private val tempDir: Path = Files.createTempDirectory("werkator-ui-test")