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:
co-authored by
Claude Opus 5
parent
91e1dd47a3
commit
80a14a7467
@@ -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`).
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user