added .claude/skills: introduced on-demand skill guides for architecture, PR documentation, and testing conventions; updated AGENTS.md to reference new skills
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
This file holds the shared, tool-agnostic instructions for all AI coding agents.
|
||||
Claude Code imports it from `CLAUDE.md` via `@AGENTS.md`; Claude-Code-specific instructions belong in `CLAUDE.md`, everything else here.
|
||||
Detailed guides live as Agent Skills under `.claude/skills/` ([SKILL.md format](https://agentskills.io)); they load on demand — see [Skills](#skills).
|
||||
|
||||
## Build and Test Commands
|
||||
|
||||
@@ -21,122 +22,29 @@ 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
|
||||
## Architecture Overview
|
||||
|
||||
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 [--systemd]"
|
||||
ServerCommand ← "server"
|
||||
StatusCommand ← "status [--history]"
|
||||
BuildCommand ← "build [<branch>]"
|
||||
RetryCommand ← "retry"
|
||||
ConfigPrintCommand ← "config:print [--full]"
|
||||
```
|
||||
|
||||
`status`, `build`, and `retry` implement `Callable<Int>` for their exit codes (0 success, 1 build failure, 2 usage/config errors).
|
||||
`build` and `retry` run builds through the async `BuildExecutor` but block until completion via `ConsoleBuildRunner`, which streams the live log to stdout and waits for the artifact persist before the JVM exits.
|
||||
Branch arguments resolve legacy-style name fragments (`BranchNameResolution`); the CLI reuses `UiFormats` so console and web UI display the same formats.
|
||||
|
||||
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, artifact serving, and the web UI live in the `server` package; mutating endpoints are guarded by a generated control token under `.git/gittally/control-token`.
|
||||
|
||||
### Web UI
|
||||
|
||||
The UI is server-rendered Thymeleaf (`UiController`, templates under `src/main/resources/templates/`) plus one hand-written JavaScript file (`static/gittally.js`) — no SPA framework, no frontend build pipeline. Pages render the full state server-side; the script then polls the JSON API and re-renders table bodies from data. Every fetch has a timeout and failures flip an explicit error badge — never re-fetch and diff whole HTML pages, and never leave a spinner without an error path (the legacy defect). Polling pauses while the tab is hidden. `UiFormats`/`gittally.js` must produce the same display formats (timestamps, durations).
|
||||
|
||||
### 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/<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 `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.
|
||||
|
||||
The runtime is selected per branch behind the `BuildRunner` interface: `DispatchingBuildRunner` (`@Primary`) routes to native `ProcessBuildRunner` (the default) or to `DockerBuildRunner` when `branches.<name>.docker.enabled`. The Docker runner shells out to the `docker` CLI (no SDK): it (re)builds the configured image when the Dockerfile inputs changed (tracked via the `org.gittally.build-inputs-sha256` image label), maintains a per-repo Gradle cache volume, mounts the worktree and the Docker socket into a labelled (`org.hoennig.gittally`) `--rm --init` container, and repairs workspace ownership in-container after each command. The returned `Process` is the attached `docker run` client, so log streaming and termination work exactly like native builds.
|
||||
|
||||
### 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. Branches with `branches.<name>.requirePullRequest` are enqueued only while their head commit matches a pull-request head, detected without an API token by listing `refs/pull/*/head` via `git ls-remote` (lazily, at most once per poll cycle); manual `build` commands bypass this gate. 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()`.
|
||||
|
||||
### System Metrics
|
||||
|
||||
`SystemMetricsCollector` samples CPU (`/proc/stat` deltas), RAM (`/proc/meminfo`), disk, and repository size every 60s, but only after `ServerMetricsLifecycle` (server profile) calls `start()` — like the watcher, nothing is scheduled in CLI runs or tests. Min/max/avg aggregation state persists as JSON in the artifact root (`ArtifactStore.rootDir()`), so restarts continue the series. Unavailable sources (e.g. no `/proc` outside Linux) yield null metrics served as HTTP 200 by `GET /api/system` — the `/system` page shows `n/a`, never an error.
|
||||
GitTally is a lightweight, declarative CI/CD build system: git-centric, one instance per repository, builds native or in Docker, statuses reported to Gitea.
|
||||
It is a dual-mode application: **CLI** (interactive, status, config) and **Server** (HTTP, persistent web UI + JSON API).
|
||||
IMPORTANT: Before designing or modifying code in any production package, load the [architecture skill](.claude/skills/architecture/SKILL.md) — it holds the subsystem details (CLI wiring, server mode, web UI, config system, git access, build execution, watcher, metrics).
|
||||
|
||||
### 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), `metrics` (system resource sampling and aggregation), and `server` (JSON API controllers, Thymeleaf UI, artifact serving, control token, watcher and metrics lifecycles). Tests mirror this structure under `src/test/kotlin`.
|
||||
|
||||
## Testing Conventions
|
||||
### Hard Invariants
|
||||
|
||||
Tests use **Kotest `FunSpec`** style. `SpringExtension` is registered globally in `io.kotest.provided.ProjectConfig` — do not add it per-spec.
|
||||
- `exitProcess` is called only from `main()` — never inside `CliRunner.run()`; this keeps the Spring context alive during tests.
|
||||
- Nothing is scheduled during CLI runs or tests: the watcher poll loop and metrics sampling start only via an explicit `start()` in the `server` profile.
|
||||
- Builds run detached in worktrees under `.git/gittally/worktrees/<branchKey>`; the primary checkout is never used for builds; never assume a single running build.
|
||||
- When config keys change, three places must stay in sync: the `GitTallyConfig` data classes, the `InitCommand` templates, and `docs/configuration.md`.
|
||||
- Web UI: server-rendered Thymeleaf plus one hand-written `static/gittally.js` — no SPA framework, no frontend build pipeline; every fetch has a timeout and an explicit error badge; `UiFormats` and `gittally.js` must produce identical display formats.
|
||||
- Git and Docker access shells out to the CLIs (`GitCommandRunner`, `docker`) — no JGit, no Docker SDK.
|
||||
|
||||
```kotlin
|
||||
class MyTest : FunSpec() {
|
||||
init {
|
||||
test("description") { ... }
|
||||
beforeEach { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
## Testing
|
||||
|
||||
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.
|
||||
Tests use Kotest `FunSpec` with MockK; `SpringExtension` is registered globally in `io.kotest.provided.ProjectConfig` — do not add it per-spec.
|
||||
IMPORTANT: Before writing or changing tests, load the [writing-tests skill](.claude/skills/writing-tests/SKILL.md) — it holds the spec structure and the Spring-slice mocking patterns.
|
||||
|
||||
## File-Formatting
|
||||
|
||||
@@ -154,7 +62,7 @@ Keep sentences short.
|
||||
- `docs/deployment.md` — running GitTally as a systemd user service behind an existing reverse proxy (`init --systemd` generates the unit).
|
||||
- `docs/migration-from-legacy.md` — legacy env vars → YAML keys mapping and the manual migration steps; `legacy/gitTally` is deprecated.
|
||||
- `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.
|
||||
- `docs/prs/` — one document per pull request describing the change of that PR. Every PR needs one: follow `docs/prs/README.md` for the filename convention (date + Gitea PR number) and the mandatory section order. Historic PR-docs are not maintained after merge.
|
||||
- `docs/prs/` — one document per pull request; every PR needs one. IMPORTANT: Before opening or finishing a pull request, load the [pr-doc skill](.claude/skills/pr-doc/SKILL.md) and write the PR-doc.
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
@@ -165,3 +73,11 @@ All major decisions are in `docs/adrs/`. Run `adr-status` (after `source .envrc`
|
||||
- **Spring Boot**: 4.0.6 (ADR 0003)
|
||||
- **Rewrite architecture**: JSON-file persistence behind a repository interface, server-rendered UI with JSON polling, no managed nginx — systemd unit behind the host's reverse proxy (ADR 0004)
|
||||
- **Managed nginx/TLS**: revises ADR 0004 — an opt-in nginx+certbot container for hosts without a reverse proxy (e.g. Hostsharing), planned as `docs/plan/13-nginx-tls.md` (ADR 0005)
|
||||
|
||||
## Skills
|
||||
|
||||
On-demand guides in the cross-tool [SKILL.md format](https://agentskills.io); agents with skill support load them automatically by description, all others should read the linked files when the topic comes up:
|
||||
|
||||
- [architecture](.claude/skills/architecture/SKILL.md) — subsystem details: CLI wiring, server mode, web UI, config, git access, build execution, watcher, metrics.
|
||||
- [writing-tests](.claude/skills/writing-tests/SKILL.md) — Kotest/MockK conventions and Spring slice-test mocking patterns.
|
||||
- [pr-doc](.claude/skills/pr-doc/SKILL.md) — how to write the mandatory per-PR documentation in `docs/prs/`.
|
||||
|
||||
Reference in New Issue
Block a user