added configuration.md
This commit is contained in:
@@ -25,6 +25,8 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter")
|
implementation("org.springframework.boot:spring-boot-starter")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
implementation("info.picocli:picocli-spring-boot-starter:4.7.6")
|
implementation("info.picocli:picocli-spring-boot-starter:4.7.6")
|
||||||
|
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# GitTally Configuration Reference
|
||||||
|
|
||||||
|
GitTally is configured via YAML files. Settings are merged from two sources in order — later layers override earlier ones.
|
||||||
|
|
||||||
|
## Config File Locations
|
||||||
|
|
||||||
|
| Layer | Path | Committed to Git | Purpose |
|
||||||
|
|--------------------------|----------------------------|------------------|----------------------------------------------|
|
||||||
|
| Project config | `.gittally.yml` | Yes | Shared team settings |
|
||||||
|
| Repo installation config | `.git/gittally/config.yml` | No | Machine- or user-specific overrides, secrets |
|
||||||
|
|
||||||
|
The repo install config (`.git/gittally/config.yml`) wins on any key present in both files. Typically used to set `gitea.token` without committing it.
|
||||||
|
|
||||||
|
## Inspect the Effective Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar gittally.jar config:print # only explicitly set values
|
||||||
|
java -jar gittally.jar config:print --full # all values including defaults
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.gittally.yml`
|
||||||
|
|
||||||
|
Values shown are the defaults.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
# Public base URL of this GitTally installation — used for all links posted to Gitea.
|
||||||
|
publicBaseUrl: https://ci.example.org/
|
||||||
|
|
||||||
|
# Gitea integration for fetching commits and posting build statuses.
|
||||||
|
gitea:
|
||||||
|
baseUrl: https://git.example.org # base URL of the Gitea instance
|
||||||
|
owner: my-org # repository owner (user or organisation)
|
||||||
|
repo: my-repo # repository name
|
||||||
|
statusContext: GitTally # label shown on Gitea commit status checks (default: GitTally)
|
||||||
|
|
||||||
|
# Build artifact retention.
|
||||||
|
artifacts:
|
||||||
|
# number of builds to keep per branch
|
||||||
|
retentionPerBranch: 3
|
||||||
|
|
||||||
|
# Controls the branch-polling loop.
|
||||||
|
watcher:
|
||||||
|
# max commit age for new origin branches to be pulled automatically
|
||||||
|
newBranchMaxAge: 5d
|
||||||
|
|
||||||
|
# Per-branch build configuration.
|
||||||
|
# Use "default" as the fallback for all branches not listed explicitly.
|
||||||
|
# Each entry merges build settings and auto-build scheduling.
|
||||||
|
branches:
|
||||||
|
default:
|
||||||
|
# run before each build
|
||||||
|
cleanCommand: rm -rf build
|
||||||
|
# shell command for each build
|
||||||
|
buildCommand: ./gradlew --console=plain --no-daemon test
|
||||||
|
# directories copied as build artifacts
|
||||||
|
artifactDirs:
|
||||||
|
- build/reports
|
||||||
|
- build/doc
|
||||||
|
stdoutLog: build.stdout.log # filename for captured stdout
|
||||||
|
stderrLog: build.stderr.log # filename for captured stderr
|
||||||
|
autoBuild:
|
||||||
|
enabled: false # whether to rebuild on schedule
|
||||||
|
times: ["01:00"] # UTC times HH:MM for scheduled builds
|
||||||
|
|
||||||
|
main:
|
||||||
|
autoBuild:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
master:
|
||||||
|
autoBuild:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
release:
|
||||||
|
buildCommand: ./gradlew --console=plain --no-daemon --no-build-cache test jacocoReport
|
||||||
|
autoBuild:
|
||||||
|
enabled: true
|
||||||
|
times:
|
||||||
|
- "04:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.git/gittally/config.yml` (not committed)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Machine- or user-specific overrides. Keys here win over .gittally.yml.
|
||||||
|
gitea:
|
||||||
|
gitUsername: my-user # git username for HTTPS authentication
|
||||||
|
token: glpat-xxxxxxxxxxxxxxxxxxxx # Gitea API token — never commit this
|
||||||
|
```
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.hoennig.gittally.commands
|
package de.hoennig.gittally.commands
|
||||||
|
|
||||||
|
import de.hoennig.gittally.config.ConfigLoader
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import picocli.CommandLine.Command
|
import picocli.CommandLine.Command
|
||||||
import picocli.CommandLine.Option
|
import picocli.CommandLine.Option
|
||||||
@@ -10,11 +11,22 @@ import picocli.CommandLine.Option
|
|||||||
description = ["Print the effective configuration"],
|
description = ["Print the effective configuration"],
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
)
|
)
|
||||||
class ConfigPrintCommand : Runnable {
|
class ConfigPrintCommand(
|
||||||
|
private val configLoader: ConfigLoader,
|
||||||
|
) : Runnable {
|
||||||
@Option(names = ["--full"], description = ["Include all defaults"])
|
@Option(names = ["--full"], description = ["Include all defaults"])
|
||||||
var full: Boolean = false
|
var full: Boolean = false
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
println("config:print${if (full) " --full" else ""} – not yet implemented")
|
if (full) {
|
||||||
|
print(configLoader.toYaml(configLoader.load()))
|
||||||
|
} else {
|
||||||
|
val raw = configLoader.loadRaw()
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
println("(no configuration files found)")
|
||||||
|
} else {
|
||||||
|
print(configLoader.toYaml(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package de.hoennig.gittally.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
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.springframework.stereotype.Service
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ConfigLoader {
|
||||||
|
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)
|
||||||
|
|
||||||
|
fun load(workingDir: Path = Paths.get(".")): GitTallyConfig {
|
||||||
|
val raw = loadRaw(workingDir)
|
||||||
|
return if (raw.isEmpty()) {
|
||||||
|
GitTallyConfig()
|
||||||
|
} else {
|
||||||
|
yaml.convertValue(mergeBranchDefaults(raw), GitTallyConfig::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadRaw(workingDir: Path = Paths.get(".")): Map<String, Any?> {
|
||||||
|
val repoInstall = loadFile(workingDir.resolve(".git/gittally/config.yml").toFile())
|
||||||
|
val project = loadFile(workingDir.resolve(".gittally.yml").toFile())
|
||||||
|
return deepMerge(project, repoInstall)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toYaml(value: Any): String = yaml.writeValueAsString(value)
|
||||||
|
|
||||||
|
private fun loadFile(file: File): Map<String, Any?> {
|
||||||
|
if (!file.exists()) return emptyMap()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return yaml.readValue(file, Map::class.java) as Map<String, Any?>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun mergeBranchDefaults(raw: Map<String, Any?>): Map<String, Any?> {
|
||||||
|
val branches = raw["branches"] as? Map<String, Any?> ?: return raw
|
||||||
|
val default = branches["default"] as? Map<String, Any?> ?: return raw
|
||||||
|
if (default.isEmpty()) return raw
|
||||||
|
val merged =
|
||||||
|
branches.mapValues { (name, value) ->
|
||||||
|
if (name == "default") {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
deepMerge(default, value as? Map<String, Any?> ?: emptyMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw + ("branches" to merged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun deepMerge(
|
||||||
|
base: Map<String, Any?>,
|
||||||
|
overlay: Map<String, Any?>,
|
||||||
|
): Map<String, Any?> {
|
||||||
|
val result = base.toMutableMap()
|
||||||
|
for ((key, value) in overlay) {
|
||||||
|
val existing = result[key]
|
||||||
|
result[key] =
|
||||||
|
if (existing is Map<*, *> && value is Map<*, *>) {
|
||||||
|
deepMerge(existing as Map<String, Any?>, value as Map<String, Any?>)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.hoennig.gittally.config
|
||||||
|
|
||||||
|
data class GitTallyConfig(
|
||||||
|
val server: ServerConfig = ServerConfig(),
|
||||||
|
val gitea: GiteaConfig = GiteaConfig(),
|
||||||
|
val artifacts: ArtifactsConfig = ArtifactsConfig(),
|
||||||
|
val watcher: WatcherConfig = WatcherConfig(),
|
||||||
|
val branches: Map<String, BranchConfig> = mapOf("default" to BranchConfig()),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ServerConfig(
|
||||||
|
val publicBaseUrl: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiteaConfig(
|
||||||
|
val baseUrl: String = "",
|
||||||
|
val owner: String = "",
|
||||||
|
val repo: String = "",
|
||||||
|
val gitUsername: String = "",
|
||||||
|
val token: String = "",
|
||||||
|
val statusContext: String = "GitTally",
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ArtifactsConfig(
|
||||||
|
val retentionPerBranch: Int = 3,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class WatcherConfig(
|
||||||
|
val newBranchMaxAge: String = "5d",
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
val autoBuild: AutoBuildConfig = AutoBuildConfig(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AutoBuildConfig(
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val times: List<String> = listOf("01:00"),
|
||||||
|
)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package de.hoennig.gittally.config
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.maps.shouldBeEmpty
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.kotest.matchers.string.shouldContain
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
class ConfigLoaderTest : FunSpec() {
|
||||||
|
private val loader = ConfigLoader()
|
||||||
|
|
||||||
|
init {
|
||||||
|
test("returns defaults when no config files exist") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
loader.load(dir) shouldBe GitTallyConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("loadRaw returns empty map when no config files exist") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
loader.loadRaw(dir).shouldBeEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("reads gitea config from .gittally.yml") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
dir.resolve(".gittally.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
gitea:
|
||||||
|
owner: my-org
|
||||||
|
repo: my-repo
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val config = loader.load(dir)
|
||||||
|
config.gitea.owner shouldBe "my-org"
|
||||||
|
config.gitea.repo shouldBe "my-repo"
|
||||||
|
config.branches["default"]!!.buildCommand shouldBe BranchConfig().buildCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
test("repo install config overrides project config for same keys") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
dir.resolve(".gittally.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
gitea:
|
||||||
|
owner: my-org
|
||||||
|
token: from-project
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
dir.resolve(".git/gittally").toFile().mkdirs()
|
||||||
|
dir.resolve(".git/gittally/config.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
gitea:
|
||||||
|
token: from-repo-install
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val config = loader.load(dir)
|
||||||
|
config.gitea.owner shouldBe "my-org"
|
||||||
|
config.gitea.token shouldBe "from-repo-install"
|
||||||
|
}
|
||||||
|
|
||||||
|
test("loadRaw only returns explicitly set values") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
dir.resolve(".gittally.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
branches:
|
||||||
|
default:
|
||||||
|
buildCommand: ./mvnw test
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val raw = loader.loadRaw(dir)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val default = (raw["branches"] as Map<String, Any?>)["default"] as Map<String, Any?>
|
||||||
|
default["buildCommand"] shouldBe "./mvnw test"
|
||||||
|
default.containsKey("cleanCommand") shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
test("branch-specific config inherits from branches.default") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
dir.resolve(".gittally.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
branches:
|
||||||
|
default:
|
||||||
|
buildCommand: ./mvnw test
|
||||||
|
main:
|
||||||
|
autoBuild:
|
||||||
|
enabled: true
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val config = loader.load(dir)
|
||||||
|
config.branches["main"]!!.buildCommand shouldBe "./mvnw test"
|
||||||
|
config.branches["main"]!!.autoBuild.enabled shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
test("branch-specific buildCommand overrides branches.default") {
|
||||||
|
val dir = Files.createTempDirectory("gittally-test")
|
||||||
|
dir.resolve(".gittally.yml").toFile().writeText(
|
||||||
|
"""
|
||||||
|
branches:
|
||||||
|
default:
|
||||||
|
buildCommand: ./mvnw test
|
||||||
|
release:
|
||||||
|
buildCommand: ./mvnw -P release test
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val config = loader.load(dir)
|
||||||
|
config.branches["release"]!!.buildCommand shouldBe "./mvnw -P release test"
|
||||||
|
}
|
||||||
|
|
||||||
|
test("toYaml serializes GitTallyConfig with all sections") {
|
||||||
|
val yaml = loader.toYaml(GitTallyConfig())
|
||||||
|
yaml shouldContain "server:"
|
||||||
|
yaml shouldContain "gitea:"
|
||||||
|
yaml shouldContain "artifacts:"
|
||||||
|
yaml shouldContain "watcher:"
|
||||||
|
yaml shouldContain "branches:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user