diff --git a/docs/configuration.md b/docs/configuration.md index 094223f..ccfbf17 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -287,7 +287,8 @@ The builds still run in their branch's worktree, one build per branch at a time, `branches..autoBuild` (`enabled` + `times`) is the deprecated pre-ADR-0007 schedule, kept for compatibility: it rebuilds the branch's own pool with its regular command and logs a deprecation warning. `autoBuild.times` entries carrying their own `buildCommand`/`name` (a short-lived v0.9.13 syntax) are no longer supported — use a build definition. -The concurrency limit that used to live in this section moved to `executor.maxConcurrent` without an alias — a leftover `builds.maxConcurrent` key is rejected as an invalid build definition. +The concurrency limit that used to live in this section moved to `executor.maxConcurrent` without an alias. +A leftover `builds.maxConcurrent` key (or any other scalar where a definition belongs) is ignored with a warning, not a startup failure — a committed config cannot always be changed right away. ### Notes on `watcher.fastForwardLocalRefs` diff --git a/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt b/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt index 93ea1d1..72f4984 100644 --- a/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt +++ b/src/main/kotlin/de/hoennig/gittally/config/ConfigLoader.kt @@ -6,19 +6,26 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.io.File import java.nio.file.Path import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap @Service class ConfigLoader { + private val log = LoggerFactory.getLogger(ConfigLoader::class.java) + private val yaml = ObjectMapper(YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)) .registerKotlinModule() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + /** Keys already reported by [dropNonDefinitionBuilds]; the config is loaded on every poll cycle. */ + private val warnedBuildKeys = ConcurrentHashMap.newKeySet() + fun load(workingDir: Path = Paths.get(".")): GitTallyConfig = toConfig(loadRaw(workingDir)) /** @@ -59,11 +66,38 @@ class ConfigLoader { if (raw.isEmpty()) { GitTallyConfig() } else { - yaml.convertValue(mergeBranchDefaults(raw), GitTallyConfig::class.java) + yaml.convertValue(mergeBranchDefaults(dropNonDefinitionBuilds(raw)), GitTallyConfig::class.java) } return defaultPublicBaseUrl(config) } + /** + * Ignores `builds` entries that are not a build definition — a scalar where a + * definition belongs, most likely the `builds.maxConcurrent` key that moved to + * `executor.maxConcurrent`. Such a leftover is a warning, not a startup failure: + * the config lives in a repository whose `master` may not be changeable right now, + * and the rest of it is perfectly usable. + */ + @Suppress("UNCHECKED_CAST") + private fun dropNonDefinitionBuilds(raw: Map): Map { + val builds = raw["builds"] as? Map ?: return raw + val definitions = builds.filterValues { it is Map<*, *> } + if (definitions.size == builds.size) { + return raw + } + for (key in builds.keys - definitions.keys) { + if (!warnedBuildKeys.add(key)) { + continue + } + if (key == "maxConcurrent") { + log.warn("ignoring builds.maxConcurrent; the concurrency limit is executor.maxConcurrent since v0.9.15") + } else { + log.warn("ignoring builds.{}: a build definition must be a mapping of keys", key) + } + } + return raw + ("builds" to definitions) + } + /** * Removes the keys a branch must never override: the secret and host-side top-level * sections, the per-branch trust gate, and the docker sandbox policy. diff --git a/src/main/resources/templates/releases.html b/src/main/resources/templates/releases.html index 504ce21..29dc999 100644 --- a/src/main/resources/templates/releases.html +++ b/src/main/resources/templates/releases.html @@ -23,8 +23,9 @@
  • Changed: the build concurrency limit moved from builds.maxConcurrent to executor.maxConcurrent (default 1, no compatibility alias) — the builds section now holds build definitions - only. A leftover builds.maxConcurrent key is rejected as an invalid - build definition.
  • + only. A leftover builds.maxConcurrent key is ignored with a warning + instead of failing the configuration, so an installation keeps running until its + committed config can be updated.

    v0.9.14 — 2026-08-28

    diff --git a/src/test/kotlin/de/hoennig/gittally/config/ConfigLoaderTest.kt b/src/test/kotlin/de/hoennig/gittally/config/ConfigLoaderTest.kt index f27cebc..2c40364 100644 --- a/src/test/kotlin/de/hoennig/gittally/config/ConfigLoaderTest.kt +++ b/src/test/kotlin/de/hoennig/gittally/config/ConfigLoaderTest.kt @@ -92,6 +92,23 @@ class ConfigLoaderTest : FunSpec() { loader.load(dir).effectiveBuildDefinitions()["default"] shouldBe BuildDefinition(onPush = false) } + test("a leftover builds.maxConcurrent is ignored instead of failing the config") { + val dir = Files.createTempDirectory("gittally-test") + dir.resolve(".gittally.yml").toFile().writeText( + """ + builds: + maxConcurrent: 1 + pitest: + buildCommand: ./gradlew piTestFull + """.trimIndent(), + ) + + val config = loader.load(dir) + + config.executor.maxConcurrent shouldBe 1 + config.buildDefinitions.keys shouldBe setOf("pitest") + } + test("a branch may redefine the builds section for its own builds") { val dir = Files.createTempDirectory("gittally-test") dir.resolve(".gittally.yml").toFile().writeText(