# 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 ```bash ./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: ```bash 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). ### 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`, so plain CLI runs never start a web server. The `server` subcommand launches a **second** `SpringApplication` with `WebApplicationType.SERVLET` and the `server` profile, then blocks until shutdown. `application-server.yml` switches the web type (`spring.main.*` properties beat programmatic builder settings), `CliRunner` is `@Profile("!server")` so the second context does not run picocli again, and the watcher poll loop starts only in the `server` profile (`ServerWatcherLifecycle`). The JSON API and artifact serving live in the `server` package; mutating endpoints are guarded by a generated control token under `.git/gittally/control-token`. ### Configuration System GitTally is configured by two YAML files, deep-merged by `ConfigLoader` (later wins): 1. `.gittally.yml` at the repo root — committed, shared team settings. 2. `.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/` (`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 `BuildStatusChangedEvent`s. 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), `watcher` (branch polling, auto-builds, startup recovery), and `server` (JSON API controllers, artifact serving, control token, watcher lifecycle). 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. ```kotlin 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: ```kotlin @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: ```kotlin @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 with `GitTallyConfig` and the `init` templates. - `docs/bootstrapping.md` — how `init` prepares a repository. - `docs/plan/` — the step-by-step rewrite plan; `docs/plan/README.md` explains how to execute a step, `docs/plan/00-legacy-analysis.md` summarizes 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)