7.1 KiB
GitTally — Agent Instructions
This file is read by Claude Code (CLAUDE.md) and other AI coding agents (AGENTS.md → CLAUDE.md).
Build and Test Commands
./gradlew build # compile + ktlintCheck + test
./gradlew ktlintFormat # auto-format before committing
./gradlew test # run all tests (can be slow, prefer single test)
./gradlew test --tests "de.hoennig.gittally.ApplicationContextTest" # example for running a single test class
Run the JAR directly:
java -jar build/libs/gittally-0.1.0-SNAPSHOT.jar --help
java -jar build/libs/gittally-0.1.0-SNAPSHOT.jar init
ktlintFormat must be run before build passes — the formatter is enforced as part of the check lifecycle.
Architecture
GitTally is a lightweight, declarative CI/CD build system. It is a dual-mode application: CLI (interactive, status, config) and Server (HTTP, persistent). It is currently CLI-only; the server mode is not yet implemented.
Entry Point and CLI Wiring
Spring Boot starts via GitTallyApplication. A separate CliRunner component (in the same file) implements both CommandLineRunner (runs picocli) and ExitCodeGenerator (returns the exit code). exitProcess is called only from main() via SpringApplication.exit() — never inside run(). This keeps the Spring context alive during tests.
Picocli commands are Spring @Component beans. The root command (GitTallyCommand) declares subcommands as class references in @Command(subcommands = [...]). Picocli resolves them from the Spring context via the auto-configured IFactory bean.
GitTallyApplication ← @SpringBootApplication
CliRunner ← CommandLineRunner + ExitCodeGenerator
GitTallyCommand ← root @Command, delegates to subcommands
commands/
InitCommand ← "init"
ServerCommand ← "server"
ConfigPrintCommand ← "config:print [--full]"
The web application type is set to none in application.yml. The server subcommand will need to restart the context with a web type when implemented.
Configuration System
GitTally is configured by two YAML files, deep-merged by ConfigLoader (later wins):
.gittally.ymlat the repo root — committed, shared team settings..git/gittally/.gittally.yml— not committed; machine-specific overrides and secrets (git.account,git.token).
After merging, branches.default is merged into every other named branch entry as its fallback, then the result is bound to the GitTallyConfig data classes (config/GitTallyConfig.kt), which define the schema and all defaults.
Three places must stay in sync when config keys change: the GitTallyConfig data classes, the commented templates generated by InitCommand, and the reference in docs/configuration.md.
Git Access
GitService shells out to the git CLI via GitCommandRunner (a thin ProcessBuilder wrapper; no JGit). Commands that need repo information take it as a constructor dependency so tests can mock it. HTTPS fetches authenticate via a temporary, secret-free GIT_ASKPASS script (GitAskPass) with credentials from config passed through environment variables.
Build Execution
BuildExecutor runs builds asynchronously: up to builds.maxConcurrent branches concurrently (default 1), but never more than one build per branch at a time. Each branch builds in its own reusable git worktree at .git/gittally/worktrees/<branchKey> (BranchWorkspaces), checked out detached at the requested commit — the primary checkout is never used for builds. Status transitions are persisted via BuildResultRepository (JSON file under .git/gittally/), published to Gitea non-fatally, and emitted as BuildStatusChangedEvents. Cancellation addresses a build by artifact key and terminates the whole process tree. Future code (watcher, server, UI) must not assume a single running build.
Watcher
Watcher replaces the legacy blocking main loop with a non-blocking fixed-delay poll cycle: fetch origin, enqueue due branches (changed local, recent new origin, due auto-build slots) via BuildExecutor, then prune results, artifacts, and stale worktrees. Nothing is scheduled until Watcher.start() is called explicitly (server/watch mode) — CLI commands and tests never start the loop. "Already built" is tracked via the result repository, not by moving local branch refs. Auto-build slot state lives in .git/gittally/auto-builds.json; watcher health is exposed via Watcher.state().
Package Structure
All production code lives under de.hoennig.gittally, with sub-packages commands (picocli subcommands), config (YAML config loading and schema), git (git CLI access), gitea (Gitea commit-status API client), build (build execution, results, workspaces), artifacts (filesystem artifact store), and watcher (branch polling, auto-builds, startup recovery). Tests mirror this structure under src/test/kotlin.
Testing Conventions
Tests use Kotest FunSpec style. SpringExtension is registered globally in io.kotest.provided.ProjectConfig — do not add it per-spec.
class MyTest : FunSpec() {
init {
test("description") { ... }
beforeEach { ... }
}
}
Use shouldBe, shouldNotBe, shouldThrow etc. from io.kotest.matchers.
Mocking in Spring Slice Tests
Use @MockkBean from springmockk to inject MockK mocks into the Spring context:
@WebMvcTest(SomeController::class)
class SomeControllerTest : FunSpec() {
@MockkBean
lateinit var someService: SomeService
init {
beforeEach { clearMocks(someService) }
// full MockK syntax: every { } / verify { }
}
}
Alternatively, register mocks via @TestConfiguration without the springmockk dependency:
@WebMvcTest(SomeController::class)
@Import(SomeControllerTest.Mocks::class)
class SomeControllerTest : FunSpec() {
@TestConfiguration
class Mocks {
@Bean fun someService(): SomeService = mockk()
}
@Autowired lateinit var someService: SomeService
init {
beforeEach { clearMocks(someService) }
}
}
Pure unit tests (no Spring context) use MockK directly without any Spring wiring.
File-Formatting
Markdown
Write documentation in English in Markdown files. In Markdown, use a single line per sentence. Keep sentences short.
Documentation
docs/GitTally-Konzept.md— product concept and target architecture (in German): git-centric CI, builds in Docker, one instance per repository, status reported back to Gitea.docs/configuration.md— configuration reference; keep in sync withGitTallyConfigand theinittemplates.docs/bootstrapping.md— howinitprepares a repository.docs/plan/— the step-by-step rewrite plan;docs/plan/README.mdexplains how to execute a step,docs/plan/00-legacy-analysis.mdsummarizes the legacy bash script.
Key Architectural Decisions
All major decisions are in docs/adrs/. Run adr-status (after source .envrc) for a one-line summary of each. Decisions in force:
- Test framework: Kotest + MockK + WireMock + Testcontainers (ADR 0001)
- Gradle: 8.14.5 (ADR 0002)
- Spring Boot: 4.0.6 (ADR 0003)