`onPush`, `atTimes`, `branches`, and `activeWithin` move into a nested `trigger`. The split is structural on purpose: the inheritance from `builds.default` now subtracts one key instead of a list of four, so a selector added to `TriggerConfig` later is non-inheritable by construction rather than because someone remembered to extend the list. A definition still writing those keys flat is refused by name, per file and scoped like the version check — the machine and project config abort the start, a branch's committed config fails only that branch. Ignoring them would leave the build with no trigger at all, which is a job that quietly stops running: the failure this refusal exists to prevent. Two more things a definition can now say: - A `!` prefix in `trigger.branches` excludes, and an exclusion wins whatever the order. `["*", "!master"]` gives one branch a build of its own without the default build running over it as well — until now the only way out of that double build was to drop the second definition's push trigger. - `statusContext` overrides the Gitea check this build reports as, empty keeping the repository-wide one. Two builds of a commit shared a context and overwrote each other's result, so a quick check beside a long build was not readable in Gitea. Pinned like `requirePullRequest`: a branch that could pick its context could take over the check a branch protection rule depends on. Fixed on the way: a branch whose builds all belong to named definitions rendered an empty row in the branches view, reading as "never built" directly beside its real builds. That row was unreachable before the exclusion patterns made such a branch possible.
193 lines
8.3 KiB
Kotlin
193 lines
8.3 KiB
Kotlin
package de.hoennig.gittally.config
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
|
|
data class GitTallyConfig(
|
|
/** What this file declares about the GitTally that reads it; see [VersionRequirement]. */
|
|
val gitTally: GitTallyMeta = GitTallyMeta(),
|
|
val server: ServerConfig = ServerConfig(),
|
|
val git: GitConfig = GitConfig(),
|
|
val gitea: GiteaConfig = GiteaConfig(),
|
|
val executor: ExecutorConfig = ExecutorConfig(),
|
|
val artifacts: ArtifactsConfig = ArtifactsConfig(),
|
|
val watcher: WatcherConfig = WatcherConfig(),
|
|
val branches: Map<String, BranchConfig> = mapOf("default" to BranchConfig()),
|
|
/**
|
|
* Named build definitions (jobs) over the branches (ADR 0007), the YAML `builds`
|
|
* section. The implicit [BuildDefinition.DEFAULT] build (`onPush` over all
|
|
* branches) applies unless this map overrides it; see [effectiveBuildDefinitions].
|
|
*/
|
|
@JsonProperty("builds")
|
|
val buildDefinitions: Map<String, BuildDefinition> = emptyMap(),
|
|
) {
|
|
/** The configured [buildDefinitions] plus the implicit `default` build unless overridden. */
|
|
fun effectiveBuildDefinitions(): Map<String, BuildDefinition> =
|
|
mapOf(BuildDefinition.DEFAULT to BuildDefinition(trigger = TriggerConfig(onPush = true))) + buildDefinitions
|
|
|
|
/**
|
|
* The settings one build of [build] on [branch] runs with: the branch entry (falling
|
|
* back to `branches.default`) with the build definition's overrides applied last.
|
|
* A build name without a definition — a job removed from the config since the run —
|
|
* falls back to the plain branch settings.
|
|
* This must stay the single answer to "what does this build run", used by the
|
|
* executor and by everything that displays it.
|
|
*/
|
|
fun buildSettings(
|
|
branch: String,
|
|
build: String,
|
|
): BranchConfig {
|
|
val branchConfig = branches[branch] ?: branches["default"] ?: BranchConfig()
|
|
return effectiveBuildDefinitions()[build]?.applyTo(branchConfig) ?: branchConfig
|
|
}
|
|
}
|
|
|
|
data class ServerConfig(
|
|
/**
|
|
* Public base URL of this installation; empty defaults to `https://<nginx.serverName>/`
|
|
* when [NginxConfig.serverName] is set (applied by [ConfigLoader]).
|
|
*/
|
|
val publicBaseUrl: String = "",
|
|
/** HTTP port of the `server` subcommand; 18080 like the legacy artifact server. */
|
|
val port: Int = 18080,
|
|
/**
|
|
* Loopback by default: the UI and the API are unauthenticated, so reaching them should
|
|
* require the host's reverse proxy. Set `0.0.0.0` explicitly to expose all interfaces —
|
|
* which the managed nginx container needs (see `docs/deployment.md`).
|
|
*/
|
|
val bindAddress: String = "127.0.0.1",
|
|
/** Optional Impressum (legal disclosure) link shown in the web UI footer; empty hides the link. */
|
|
val impressumUrl: String = "",
|
|
val nginx: NginxConfig = NginxConfig(),
|
|
)
|
|
|
|
/**
|
|
* Opt-in managed nginx+certbot Docker container serving GitTally over HTTPS,
|
|
* for hosts without a usable reverse proxy (ADR 0005). Off by default; the
|
|
* reverse-proxy deployment from `docs/deployment.md` stays the recommended setup.
|
|
*/
|
|
data class NginxConfig(
|
|
/** Manage an nginx Docker container with Let's Encrypt certificates. */
|
|
val enabled: Boolean = false,
|
|
/** Public DNS name served by nginx and used for the certificate; required when [enabled]. */
|
|
val serverName: String = "",
|
|
/** Host port published as nginx port 80 (ACME challenge + HTTPS redirect). */
|
|
val httpPort: Int = 8080,
|
|
/** Host port published as nginx port 443. */
|
|
val httpsPort: Int = 8443,
|
|
/** Host nginx proxies to; empty uses [serverName] (the container cannot reach `localhost`). */
|
|
val upstreamHost: String = "",
|
|
/** Name of the managed container; empty means `gittally-nginx-<repo-name>`. */
|
|
val containerName: String = "",
|
|
/**
|
|
* Directory for nginx config, certificates, and logs; empty means the platform
|
|
* default `XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/nginx/<repo-key>`.
|
|
*/
|
|
val stateDir: String = "",
|
|
/** E-mail for the Let's Encrypt account; empty registers without one. */
|
|
val letsencryptEmail: String = "",
|
|
)
|
|
|
|
data class GitConfig(
|
|
val account: String = "",
|
|
val token: String = "",
|
|
)
|
|
|
|
data class GiteaConfig(
|
|
val baseUrl: String = "",
|
|
val owner: String = "",
|
|
val repo: String = "",
|
|
val statusContext: String = "GitTally",
|
|
)
|
|
|
|
data class ArtifactsConfig(
|
|
val retentionPerBranch: Int = 3,
|
|
/**
|
|
* Additionally drop builds older than this age (e.g. `30d` or `12h`); empty means no
|
|
* age limit. Combines with [retentionPerBranch] — a build is kept only while it
|
|
* satisfies both limits. A branch's newest build is never age-pruned, so dormant
|
|
* branches keep their last status.
|
|
*/
|
|
val retentionMaxAge: String = "",
|
|
/**
|
|
* Keep each branch's latest green (SUCCESS) build beyond [retentionPerBranch] and
|
|
* [retentionMaxAge], so the permanent `/branches/<branch-key>/…` artifact URLs stay
|
|
* valid while newer builds fail; the build is still dropped once its branch is gone
|
|
* from origin.
|
|
*/
|
|
val keepLatestGreen: Boolean = true,
|
|
/**
|
|
* Root directory for stored build artifacts; empty means the platform default
|
|
* `XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/artifacts/<repo-key>`.
|
|
*/
|
|
val rootDir: String = "",
|
|
)
|
|
|
|
data class WatcherConfig(
|
|
/** Delay between poll cycles, e.g. `10s` or `1m`. */
|
|
val pollInterval: String = "10s",
|
|
val newBranchMaxAge: String = "5d",
|
|
/**
|
|
* Honor the `branches.<name>.requirePullRequest` gates. Set false for a plain git
|
|
* origin without pull-request refs (no Gitea/GitHub) — gated branches then build
|
|
* on new commits like any other branch. Typically overridden per machine in
|
|
* `.git/gittally/.gittally.yml` when the committed config enables the gates.
|
|
*/
|
|
val pullRequestGate: Boolean = true,
|
|
/**
|
|
* At the end of each poll cycle, fast-forward the primary checkout's local branch
|
|
* refs to their origin counterparts, so build tools reading the shared `.git` see
|
|
* the same refs as origin. Fast-forward only: diverged or ahead local branches are
|
|
* never touched. Set false to leave the local branch refs alone entirely.
|
|
*/
|
|
val fastForwardLocalRefs: Boolean = true,
|
|
)
|
|
|
|
data class BranchConfig(
|
|
val buildCommand: String = "./gradlew --console=plain --no-daemon test",
|
|
val cleanCommand: String = "rm -rf build",
|
|
val artifactDirs: List<String> = listOf("build/reports"),
|
|
val stdoutLog: String = "build.stdout.log",
|
|
val stderrLog: String = "build.stderr.log",
|
|
/**
|
|
* The watcher builds this branch only while its head commit matches a pull-request
|
|
* head (`refs/pull/<n>/head` on origin); manual `build` commands are not affected.
|
|
*/
|
|
val requirePullRequest: Boolean = false,
|
|
/** Gitea commit status context of this build; empty uses `gitea.statusContext`. */
|
|
val statusContext: String = "",
|
|
val autoBuild: AutoBuildConfig = AutoBuildConfig(),
|
|
val docker: DockerConfig = DockerConfig(),
|
|
)
|
|
|
|
data class DockerConfig(
|
|
/** Run the clean and build commands in a Docker container instead of natively. */
|
|
val enabled: Boolean = false,
|
|
/** Image for the build container; required when [enabled]. */
|
|
val image: String = "",
|
|
/** Dockerfile to build [image] from when it is missing or stale; empty uses [image] as-is (pulled on demand). */
|
|
val dockerfile: String = "",
|
|
/** Docker build context used with [dockerfile]. */
|
|
val context: String = ".",
|
|
/** Docker network mode for the build container; empty uses Docker's default network. */
|
|
val network: String = "",
|
|
/** Additional environment variables set inside the build container. */
|
|
val env: Map<String, String> = emptyMap(),
|
|
)
|
|
|
|
/** Build execution settings, enforced by the executor for all builds regardless of their trigger. */
|
|
data class ExecutorConfig(
|
|
/** How many builds may run at the same time; at most one build per branch runs regardless. */
|
|
val maxConcurrent: Int = 1,
|
|
)
|
|
|
|
/**
|
|
* Deprecated per-branch schedule (pre-ADR-0007), kept for compatibility: mapped to a
|
|
* daily rebuild of the branch's own pool with its regular command. New configurations
|
|
* define a build with `atTimes` in the top-level `builds` section instead.
|
|
*/
|
|
data class AutoBuildConfig(
|
|
val enabled: Boolean = false,
|
|
/** Daily UTC times `HH:MM`. */
|
|
val times: List<String> = listOf("01:00"),
|
|
)
|