`atTimes: ["??:05"]` runs a build five past every hour. The pattern expands to its 24 concrete slots before the due-slot match, so each hour is its own slot in the trigger state and fires once — the existing per-slot semantics carry over unchanged, including that only the latest due slot of a day triggers and that a slot whose pool is still building is retried until it starts. Only the hour may be a wildcard; anything else is skipped with a warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.7 KiB
Kotlin
116 lines
4.7 KiB
Kotlin
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 `<branch>@<name>` 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<String> = emptyList(),
|
|
/**
|
|
* Branch names or glob patterns (`*` matches any characters, also across `/`);
|
|
* empty selects all origin branches.
|
|
*/
|
|
val branches: List<String> = 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<String>? = 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<String, String>? = null,
|
|
)
|