Build definitions with onPush/atTimes replace branch-owned schedules
ADR 0007: the YAML builds section (next to the reserved maxConcurrent key) defines named builds (jobs) with onPush/atTimes triggers, branch selectors (name globs, activeWithin age filter), and build-setting overrides applied last over the merged branch config. The implicit default build (onPush over all branches) preserves the previous behavior; the section is pinned against the worktree layer. Results record the job name; restart, retry, and startup recovery re-run by it, resolving settings from the current config. A non-default build records under the <branch>@<build> pool with its own row, retention count, and permanent latest-green link. branches.*.autoBuild stays as a deprecated alias (plain times only); the unreleased-in-practice v0.9.13 per-slot buildCommand/name syntax is removed again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5051c7bb99
commit
0e8db18e69
@@ -0,0 +1,111 @@
|
||||
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. */
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user