package de.hoennig.gittally.config import java.time.Duration import java.time.Instant /** * A named build (job) over the branches — ADR 0007. In YAML these live in the * top-level `builds` section next to the reserved execution key `maxConcurrent` * (split apart by [ConfigLoader]); a build definition always comes from the repo * install/project config, never from a build worktree. * * The `default` build records its results under the plain branch name; every other * build records under `@` with its own history, retention pool, and * permanent latest-green link. */ data class BuildDefinition( /** Build every new commit of the selected branches. */ val onPush: Boolean = false, /** * Daily UTC times `HH:MM`; each slot rebuilds the selected branches' heads once per day. * `??:MM` is the hourly form — it stands for that minute of every hour, so each of its * 24 slots triggers separately. */ val atTimes: List = emptyList(), /** * Branch names or glob patterns (`*` matches any characters, also across `/`); * empty selects all origin branches. */ val branches: List = emptyList(), /** * Only branches whose origin head commit is younger than this (e.g. `24h`); * empty applies no age filter. Combines with [branches] as an intersection. */ val activeWithin: String = "", /** Overrides the branch's build command; null inherits it. */ val buildCommand: String? = null, /** Overrides the branch's clean command; null inherits it. */ val cleanCommand: String? = null, /** Overrides the branch's artifact directories; null inherits them. */ val artifactDirs: List? = null, /** Overrides the branch's stdout log file name; null inherits it. */ val stdoutLog: String? = null, /** Overrides the branch's stderr log file name; null inherits it. */ val stderrLog: String? = null, /** Overrides of the branch's docker image settings; the sandbox policy (`enabled`, `network`) is not overridable. */ val docker: DockerOverrides? = null, ) { /** True when [branch] matches the [branches] patterns (or none are configured). */ fun selectsByName(branch: String): Boolean = branches.isEmpty() || branches.any { globToRegex(it).matches(branch) } /** * True when [branch] passes both selector parts; [headCommittedAt] is the branch * head's committer time, only consulted while [activeWithin] is set (null then * deselects the branch). */ fun selects( branch: String, headCommittedAt: () -> Instant?, now: Instant, ): Boolean { if (!selectsByName(branch)) { return false } if (activeWithin.isBlank()) { return true } val committedAt = headCommittedAt() ?: return false return committedAt >= now.minus(maxAge()) } fun maxAge(): Duration = DurationParser.parse(activeWithin) /** The branch settings with this build's overrides applied; unset values fall through. */ fun applyTo(branchConfig: BranchConfig): BranchConfig = branchConfig.copy( buildCommand = buildCommand ?: branchConfig.buildCommand, cleanCommand = cleanCommand ?: branchConfig.cleanCommand, artifactDirs = artifactDirs ?: branchConfig.artifactDirs, stdoutLog = stdoutLog ?: branchConfig.stdoutLog, stderrLog = stderrLog ?: branchConfig.stderrLog, docker = branchConfig.docker.copy( image = docker?.image ?: branchConfig.docker.image, dockerfile = docker?.dockerfile ?: branchConfig.docker.dockerfile, context = docker?.context ?: branchConfig.docker.context, env = docker?.env ?: branchConfig.docker.env, ), ) companion object { /** Name of the implicit build that preserves the pre-ADR-0007 behavior: `onPush` over all branches. */ const val DEFAULT = "default" /** The result-pool name of [build] on [branch]: the plain branch for the default build. */ fun poolName( branch: String, build: String, ): String = if (build == DEFAULT) branch else "$branch@$build" private fun globToRegex(pattern: String): Regex = Regex( pattern .split('*') .joinToString(".*") { Regex.escape(it) }, ) } } /** Nullable docker image overrides of a [BuildDefinition]; null values inherit the branch's setting. */ data class DockerOverrides( val image: String? = null, val dockerfile: String? = null, val context: String? = null, val env: Map? = null, )