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
@@ -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 {
|
||||
|
||||
@@ -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.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/<name>/…`) 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)
|
||||
|
||||
@@ -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<BuildProperties>,
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user