Configuration files declare the GitTally they are written for

Until now a version that renames or drops a key did not fail — it silently
ignored what it no longer understood, and the effect surfaced as a build
doing the wrong thing. Both directions of that happened within two days:
a branch config using `??:00` on a GitTally that did not know it yet, and a
`builds.maxConcurrent` that had moved to another section.

    gitTally:
      version:
        since: "0.9.18"   # enforced
        below: "2.0"      # release marker; GitTally decides how strictly

There is deliberately no version of the file format (no `apiVersion`): no API
is involved — GitTally reads its own configuration — and only one generation
is ever supported. The declaration exists to make an incompatibility
nameable, never to run two parsers.

`since` is hard in both directions. Too new a requirement is refused, and so
is a file written before the version in which the configuration format last
broke (`ConfigVersions.FORMAT_BROKE_IN`, empty for now) — that check needs no
declared ceiling, because GitTally knows its own breaking changes.

`below` is the team's release marker and only warns: an unmaintained caution
value must never stop a CI. The routine it serves is the one known from IDE
plugins — new version, warning, try it, then raise the marker and commit.

The reach of a violation follows the layer: the machine and project configs
abort the start naming the file and the rollback, while an incompatible
branch config fails only that branch's builds. A branch cut before a
migration must not stop the server or hold up the branches that are fine.
A file that declares nothing keeps working, and the CLI prints one line
instead of a stack trace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-29 10:15:35 +02:00
co-authored by Claude Opus 5
parent fa183ef1db
commit 8608170f5b
10 changed files with 471 additions and 6 deletions
@@ -2,6 +2,8 @@ package de.hoennig.gittally.commands
import de.hoennig.gittally.SecretFiles
import de.hoennig.gittally.git.GitService
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.info.BuildProperties
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.Option
@@ -16,6 +18,8 @@ import java.nio.file.Paths
)
class InitCommand(
private val gitService: GitService,
/** The version written into the generated config as `gitTally.version.since`. */
private val buildProperties: ObjectProvider<BuildProperties>? = null,
) : Runnable {
var workingDir: Path = Paths.get(".")
@@ -51,6 +55,12 @@ class InitCommand(
}
}
/**
* The running version for `gitTally.version.since`; outside a built jar (IDE, tests)
* there is none, and `0.0.0` then declares no floor at all rather than a wrong one.
*/
private fun runningVersion(): String = buildProperties?.getIfAvailable()?.version ?: "0.0.0"
private fun detectFromUrl(url: String?): DetectedValues {
if (url == null) return DetectedValues()
@@ -120,6 +130,16 @@ class InitCommand(
}
val content =
"""
# The GitTally this file is written for.
# since: enforced — an older GitTally refuses to read this file instead of
# silently ignoring the keys it does not know yet.
# below: your release marker for a coming major; GitTally decides how strictly
# to take it, and warns rather than blocks unless the format really broke.
gitTally:
version:
since: "${runningVersion()}"
# below: "2.0"
server:
# Public base URL of this GitTally installation — used for all links posted to Gitea.
publicBaseUrl: ""