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>
378 lines
23 KiB
Markdown
378 lines
23 KiB
Markdown
# GitTally Configuration Reference
|
|
|
|
GitTally is configured via YAML files. Settings are merged from several 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/.gittally.yml` | No | Machine- or user-specific overrides, secrets |
|
|
| Branch config | `.gittally.yml` committed on a branch | Yes | That branch's build settings and build definitions |
|
|
|
|
The repo install config (`.git/gittally/.gittally.yml`) wins on any key present in both files. Typically used to set `git.token` and `git.account` without committing them.
|
|
|
|
### Which GitTally a file is written for
|
|
|
|
Every configuration file may declare the GitTally it was written for. Without it, a
|
|
version that renames or drops a key does not fail — it silently ignores what it no longer
|
|
understands, and the effect shows up as a build that does the wrong thing.
|
|
|
|
```yaml
|
|
gitTally:
|
|
version:
|
|
since: "0.9.16" # enforced: an older GitTally refuses to read this file
|
|
below: "2.0" # your release marker; GitTally decides how strictly to take it
|
|
```
|
|
|
|
There is deliberately **no version of the file format** (no `apiVersion`): no API is
|
|
involved — GitTally reads its own configuration — and only one configuration generation is
|
|
ever supported. The declaration exists to make an incompatibility nameable, never to run
|
|
two parsers.
|
|
|
|
`since` is a hard floor and covers both directions:
|
|
|
|
- a newer file on an older GitTally is refused instead of being half-understood;
|
|
- a file written *before* a breaking change and read *after* it is refused as well —
|
|
GitTally knows in which version its configuration format last broke, so the message can
|
|
name the change: *"is written for GitTally 1.4.0, but the configuration format changed
|
|
incompatibly in 2.0.0: `builds:` is now `buildSpec:`"*.
|
|
|
|
`below` is optional and names the first version this file was **not** released for. The
|
|
bound is exclusive, so `below: "2.0"` means everything up to 2.0.0. On its own it only
|
|
warns — a caution marker nobody maintained must never stop a CI. The refusal above comes
|
|
from GitTally's own knowledge of its breaking changes, not from this value. The intended
|
|
routine is the one known from IDE plugins: a new version appears, the warning shows up, you
|
|
try it (on a test host, or in production with a rollback ready), and then raise `below` and
|
|
commit that.
|
|
|
|
A file that declares nothing is read as before, with a hint in the log — a missing line
|
|
must never stop a server either. `gittally init` writes the running version into the
|
|
generated config.
|
|
|
|
How far a violation reaches depends on the file, following the same rule as everything
|
|
else here: the machine and project configs abort the start (the message names the file and
|
|
the rollback), while an incompatible **branch** config fails only the builds of that
|
|
branch. A branch that was cut before a migration must never stop the server or hold up the
|
|
branches that are fine.
|
|
|
|
### The branch layer: a branch describes its own CI
|
|
|
|
The `.gittally.yml` committed on a branch is applied as a third layer on top of the two
|
|
above, giving the precedence **branch > repo install > project**. It takes precedence for
|
|
everything that describes how this branch is built: `buildCommand`, `cleanCommand`,
|
|
`artifactDirs`, log file names, `docker.image`/`dockerfile`/`context`/`env`, and the whole
|
|
`builds` section — its own definitions and its overrides of the definitions from the
|
|
project config. That is how a new configuration is tried out: change it on a branch, and
|
|
no other branch's builds are affected.
|
|
|
|
The branch layer is used in both places where it matters: the watcher reads the committed
|
|
config of each origin branch (via `git show`, only when the branch moved) to decide which
|
|
of *its* builds are due, and the build itself resolves its settings from the worktree of
|
|
the commit being built.
|
|
|
|
A branch's definitions apply to that branch alone. Their selectors are evaluated for it
|
|
only, so a definition committed on one branch can never trigger builds of another — even
|
|
when its `branches` selector names one.
|
|
|
|
A pinned set is always taken from the repo install/project config, because none of it
|
|
describes this branch's build:
|
|
|
|
- secrets: the whole `git` section;
|
|
- host- and repository-side settings: the whole `server`, `gitea`, `executor`, and `watcher` sections;
|
|
- the container sandbox policy: `docker.enabled` and `docker.network`;
|
|
- the trust gate: `requirePullRequest`.
|
|
|
|
This keeps a branch from reaching credentials, reporting statuses to another repository,
|
|
raising the global concurrency, disabling its own build container, changing its network
|
|
mode, or bypassing its own pull-request gate. Everything else is the branch's to decide —
|
|
it can already run any command through `buildCommand`.
|
|
The deprecated `autoBuild` schedules are read from the repo install/project config only.
|
|
|
|
## Inspect the Effective Config
|
|
|
|
```bash
|
|
java -jar build/libs/gittally.jar config:print # only explicitly set values
|
|
java -jar build/libs/gittally.jar config:print --full # all values including defaults
|
|
```
|
|
|
|
`git.token` is masked as `***` by default, so the output can safely be shared or pasted.
|
|
Add `--show-secrets` to print it in clear text.
|
|
|
|
## `.gittally.yml`
|
|
|
|
Values shown are the defaults.
|
|
|
|
```yaml
|
|
# The GitTally this file is written for (see the section above).
|
|
gitTally:
|
|
version:
|
|
since: "0.9.18" # enforced: older GitTally refuses this file
|
|
below: "2.0" # optional release marker; warns, does not block
|
|
|
|
server:
|
|
# Public base URL of this GitTally installation — used for all links posted to Gitea.
|
|
publicBaseUrl: https://ci.example.org/
|
|
# HTTP port of the `server` subcommand (default 18080, like legacy)
|
|
port: 18080
|
|
# bind address of the `server` subcommand; loopback only, because the UI and the API
|
|
# are unauthenticated — set 0.0.0.0 only deliberately (see the note below)
|
|
bindAddress: 127.0.0.1
|
|
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
|
impressumUrl: ""
|
|
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
|
|
# a usable reverse proxy (ADR 0005; see notes below and deployment.md).
|
|
nginx:
|
|
# manage an nginx Docker container with Let's Encrypt certificates
|
|
enabled: false
|
|
# public DNS name served by nginx and used for the certificate; required when enabled
|
|
serverName: ""
|
|
# host port published as nginx port 80 (ACME challenge + HTTPS redirect)
|
|
httpPort: 8080
|
|
# host port published as nginx port 443
|
|
httpsPort: 8443
|
|
# host nginx proxies to; empty = serverName (the container cannot reach localhost)
|
|
upstreamHost: ""
|
|
# name of the managed container; empty = gittally-nginx-<repo-name>
|
|
containerName: ""
|
|
# directory for nginx config, certificates, and logs;
|
|
# empty = $XDG_STATE_HOME (or ~/.local/state) plus /gittally/nginx/<repo-key>
|
|
stateDir: ""
|
|
# e-mail for the Let's Encrypt account; empty registers without one
|
|
letsencryptEmail: ""
|
|
|
|
# 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) for Gitea API (e.g. status checks)
|
|
repo: my-repo # repository name
|
|
statusContext: GitTally # label shown on Gitea commit status checks (default: GitTally)
|
|
|
|
# Build execution settings, enforced for all builds regardless of their trigger
|
|
# (watcher, UI restart, CLI build/retry).
|
|
executor:
|
|
# How many builds may run at the same time.
|
|
# At most one build per branch runs regardless; each branch builds in its own
|
|
# git worktree under .git/gittally/worktrees/, never in the primary checkout.
|
|
# Changing this value requires a restart.
|
|
maxConcurrent: 1
|
|
|
|
# Named build definitions (jobs, see notes below); every key names a build.
|
|
builds:
|
|
# Implicit unless overridden: the default build runs on push over all branches
|
|
# with the branch's regular settings — exactly the behavior without any
|
|
# build definitions. Set onPush: false here to disable on-push builds.
|
|
# default:
|
|
# onPush: true
|
|
#
|
|
# Example of a named build definition; all keys except its name are optional:
|
|
# pitest:
|
|
# onPush: false # trigger: build every new commit (default: false)
|
|
# atTimes: ["01:00"] # trigger: daily UTC times HH:MM, "??:05" = hourly at :05
|
|
# branches: ["master", "release/*"] # selector: names or glob patterns (default: all)
|
|
# activeWithin: 24h # selector: only branches with commits in the last 24h
|
|
# buildCommand: ./gradlew piTestFull # overrides; unset keys fall back to the
|
|
# cleanCommand: rm -rf build # merged branch settings (also available:
|
|
# artifactDirs: [build/reports] # stdoutLog, stderrLog, and docker
|
|
# # image/dockerfile/context/env)
|
|
|
|
# Build artifact storage and retention.
|
|
artifacts:
|
|
# Root directory for stored build artifacts.
|
|
# Empty means the platform default: $XDG_STATE_HOME (or ~/.local/state) plus /gittally/artifacts/<repo-key>,
|
|
# where <repo-key> is the sanitized absolute repository path.
|
|
# A leading ~/ expands to the home directory; a relative path is resolved against the repository.
|
|
rootDir: ""
|
|
# number of builds to keep per branch
|
|
retentionPerBranch: 3
|
|
# Additionally drop builds older than this age; suffixes s (seconds), m (minutes), h (hours), d (days).
|
|
# Empty means no age limit.
|
|
# Combines with retentionPerBranch: a build is kept only while it satisfies both limits.
|
|
# A branch's newest build is never age-pruned, so dormant branches keep their last status.
|
|
retentionMaxAge: ""
|
|
# Keep each branch's latest green (successful) build even beyond retentionPerBranch and retentionMaxAge.
|
|
# This backs the permanent artifact URLs /branches/<branch-key>/... — they always serve
|
|
# the latest green build of a branch and stay valid while newer builds fail.
|
|
# The kept build is still dropped once its branch is deleted from origin.
|
|
keepLatestGreen: true
|
|
|
|
# Controls the branch-polling loop.
|
|
watcher:
|
|
# delay between poll cycles; suffixes s (seconds), m (minutes), h (hours), d (days)
|
|
pollInterval: 10s
|
|
# max commit age for new origin branches to be pulled automatically
|
|
newBranchMaxAge: 5d
|
|
# Honor the branches.<name>.requirePullRequest gates (see notes below).
|
|
# Set false for a plain git origin without pull-request refs (no Gitea/GitHub);
|
|
# gated branches then build on new commits like any other branch.
|
|
pullRequestGate: true
|
|
# At the end of each poll cycle, fast-forward the primary checkout's local branch refs
|
|
# to their origin counterparts (see notes below). Fast-forward only — a diverged or
|
|
# ahead local branch is never touched. Set false to leave refs/heads/* alone entirely.
|
|
fastForwardLocalRefs: true
|
|
|
|
# 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
|
|
# Build this branch only while its head commit matches a pull-request head on origin
|
|
# (refs/pull/*/head — read via plain git, no API token needed; see notes below).
|
|
requirePullRequest: false
|
|
# DEPRECATED: define a build with atTimes in the builds section instead.
|
|
# Kept for compatibility: rebuilds this branch on schedule with its regular command.
|
|
autoBuild:
|
|
enabled: false # whether to rebuild on schedule
|
|
times: ["01:00"] # UTC times HH:MM for scheduled builds
|
|
# Optional Docker build runtime; when enabled, the clean and build commands
|
|
# run inside a container instead of natively (see notes below).
|
|
docker:
|
|
# run clean/build commands in a Docker container
|
|
enabled: false
|
|
# image for the build container; required when enabled
|
|
image: ""
|
|
# Dockerfile to (re)build the image from when it is missing or stale; empty pulls the image as-is
|
|
dockerfile: ""
|
|
# Docker build context used with dockerfile
|
|
context: "."
|
|
# Docker network mode for the build container; empty = Docker default
|
|
network: ""
|
|
# additional environment variables set inside the build container
|
|
env: {}
|
|
|
|
master:
|
|
buildCommand: ./gradlew --console=plain --no-daemon quickCheck
|
|
|
|
release:
|
|
buildCommand: ./gradlew --console=plain --no-daemon --no-build-cache test jacocoReport
|
|
|
|
builds:
|
|
# the nightly rebuild runs the full check instead of the quick on-commit one,
|
|
# recorded separately as master@pitest
|
|
pitest:
|
|
atTimes: ["01:00"]
|
|
branches: ["master"]
|
|
buildCommand: ./gradlew -PfullPitTest --console=plain --no-daemon piTestFull
|
|
```
|
|
|
|
### Notes on `server.bindAddress`
|
|
|
|
The default is `127.0.0.1`.
|
|
Neither the web UI nor the JSON API authenticates read access — which is intended, so build states and artifacts can be linked from anywhere — so GitTally is meant to sit behind the host's reverse proxy rather than on a public interface.
|
|
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches GitTally over the Docker bridge, not over loopback), or when the proxy runs on another host.
|
|
Installations created before v0.9.9 have `bindAddress: 0.0.0.0` written into their `.gittally.yml` and keep it; the new default only applies where the key is absent or `init` writes a fresh file.
|
|
|
|
### Notes on `server.nginx`
|
|
|
|
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves GitTally over HTTPS (ADR 0005).
|
|
This is meant for hosts that provide Docker but no usable reverse proxy (e.g. Hostsharing managed containers); otherwise prefer the reverse-proxy setup in [deployment.md](deployment.md).
|
|
Certificates are obtained and renewed via Let's Encrypt (certbot Docker container, webroot mode), so `serverName` must be a public DNS name pointing at the host and `httpPort` must be reachable from the internet as port 80 (or via a port forward).
|
|
When `server.publicBaseUrl` is empty and `serverName` is set, it defaults to `https://<serverName>/`.
|
|
All nginx/certificate failures are non-fatal warnings; the plain HTTP server keeps running without the proxy.
|
|
The container is labelled `org.hoennig.gittally`; stale nginx containers of the repository are removed before each start, and the container is removed on shutdown.
|
|
`server.port` must differ from `httpPort` and `httpsPort`.
|
|
|
|
### Notes on `branches.<name>.requirePullRequest`
|
|
|
|
The gate applies to all watcher-triggered builds (push-triggered and scheduled auto builds).
|
|
A manual `gittally build <branch>` always builds, regardless of this setting.
|
|
|
|
Detection works without a Gitea API token:
|
|
the watcher lists `refs/pull/*/head` on origin via `git ls-remote` and builds a branch only when its head commit equals one of those pull-request head commits.
|
|
This ls-remote call is made at most once per poll cycle, and only when a branch requiring a pull request is otherwise due.
|
|
|
|
Because matching is by commit id, a closed pull request whose head ref still equals the branch head also counts.
|
|
Distinguishing open from closed pull requests would require the Gitea API.
|
|
|
|
To build pull-request branches only, set the key under `branches.default` and override it for permanent branches:
|
|
|
|
```yaml
|
|
branches:
|
|
default:
|
|
requirePullRequest: true
|
|
main:
|
|
requirePullRequest: false
|
|
```
|
|
|
|
Without the `main` override, direct pushes and merges to `main` would never build — merge commits do not match any pull-request head.
|
|
|
|
A plain git origin (no Gitea/GitHub) serves no `refs/pull/*/head` at all, so gated branches would never build there.
|
|
For such origins, disable all gates globally with `watcher.pullRequestGate: false` — typically in the machine-specific `.git/gittally/.gittally.yml`, so the committed configuration keeps the gates for forge-backed environments.
|
|
|
|
### Notes on `builds` (build definitions)
|
|
|
|
Every key of the `builds` section names a build definition (a job) over the branches — ADR 0007.
|
|
A build definition has triggers, a branch selector, and build-setting overrides.
|
|
|
|
Triggers: `onPush: true` builds every new commit of the selected branches; `atTimes: ["HH:MM", …]` rebuilds their heads once per day and slot (UTC).
|
|
A slot may also be written as `??:MM` — that minute of every hour, expanded to its 24 slots, so the build runs hourly.
|
|
Only the latest due slot of a day triggers, so slots missed while the server was down are skipped instead of piling up, and a slot whose pool is still building is retried on the next poll cycle until it succeeds.
|
|
A definition may have both; one with neither never triggers automatically.
|
|
|
|
Selector: `branches` lists branch names or glob patterns (`*` matches any characters, also across `/`); empty selects all origin branches.
|
|
`activeWithin` (e.g. `24h`) additionally keeps only branches whose origin head commit is younger than the duration — useful to run a nightly deep check over all recently active branches.
|
|
Both parts combine as an intersection.
|
|
The `branches.<name>.requirePullRequest` gate stays a branch property and gates all watcher-triggered builds of that branch.
|
|
|
|
Overrides: `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`/`stderrLog`, and the docker image keys (`image`, `dockerfile`, `context`, `env`).
|
|
A definition has no `docker.enabled`/`docker.network` and no `requirePullRequest` — those are pinned branch properties, see [the branch layer](#the-branch-layer-a-branch-describes-its-own-ci).
|
|
The effective settings of one build on one branch merge in this order: defaults → `branches.default` → `branches.<branch>` → the branch's committed `.gittally.yml` → the build definition's overrides.
|
|
Unset keys fall back; the definition wins last because it is the job.
|
|
Definitions themselves are part of the branch layer: a branch may add its own and override those from the project config, for its own builds only.
|
|
|
|
The implicit `default` build (`onPush: true`, all branches) preserves the behavior without any definitions; defining other builds does not disable it, `builds.default.onPush: false` does.
|
|
The `default` build records under the plain branch name; every other build records under `<branch>@<name>` with its own row in the branches view (sorted after its branch), its own `retentionPerBranch` count, latest status, and permanent latest-green artifact link.
|
|
The URL key is the sanitized pool name — `master@pitest` is served as `/branches/master_pitest/…`.
|
|
The pools live as long as the underlying branch exists on origin.
|
|
Restart, `gittally retry`, and the startup recovery re-run a build under its recorded definition, resolving the settings from the current configuration — the job definition is the source of truth, not the historical run.
|
|
The builds still run in their branch's worktree, one build per branch at a time, and the Gitea commit status is reported per commit in the shared status context (the last build of a commit wins there).
|
|
|
|
`branches.<name>.autoBuild` (`enabled` + `times`) is the deprecated pre-ADR-0007 schedule, kept for compatibility: it rebuilds the branch's own pool with its regular command and logs a deprecation warning.
|
|
`autoBuild.times` entries carrying their own `buildCommand`/`name` (a short-lived v0.9.13 syntax) are no longer supported — use a build definition.
|
|
The concurrency limit that used to live in this section moved to `executor.maxConcurrent` without an alias.
|
|
A leftover `builds.maxConcurrent` key (or any other scalar where a definition belongs) is ignored with a warning, not a startup failure — a committed config cannot always be changed right away.
|
|
|
|
### Notes on `watcher.fastForwardLocalRefs`
|
|
|
|
Builds run in worktrees that share the primary checkout's `.git`, so a build tool can read `refs/heads/*` there.
|
|
GitTally itself never needs those refs to be current — it builds the commit `refs/remotes/origin/<branch>` points at — but build tools do.
|
|
A common case is a check that refuses to run when the local main branch differs from its origin counterpart; without this key it would fail on every build once origin moved on, because nothing would ever advance the local ref.
|
|
|
|
The fast-forward runs at the end of the poll cycle, after the due branches were enqueued.
|
|
That order is required, not cosmetic: a local ref lagging behind origin is exactly how the watcher recognizes new commits, so a ref kept in sync earlier — by this key, a cron job, or a mirroring fetch refspec (`+refs/heads/*:refs/heads/*`) — would silently stop the branch from ever being built.
|
|
|
|
Only fast-forwards are applied, as a compare-and-swap against the commit just read.
|
|
A local branch that diverged from origin or is ahead of it stays untouched, so local work in the primary checkout is never lost.
|
|
The branch checked out in the primary checkout is advanced with `git merge --ff-only`, which refuses to overwrite conflicting uncommitted changes; a refusal is logged and the cycle continues.
|
|
|
|
### Notes on `branches.<name>.docker`
|
|
|
|
With `docker.enabled`, GitTally shells out to the `docker` CLI; the `docker` command must be on the `PATH`.
|
|
When `dockerfile` is set, the image is (re)built whenever the Dockerfile content, its path, or the context path changed.
|
|
Staleness is tracked via the image label `org.gittally.build-inputs-sha256`.
|
|
A Gradle cache volume `gittally-gradle-<repo-key>` is created per repository and mounted as `GRADLE_USER_HOME`.
|
|
The build worktree is bind-mounted into the container; after each command the ownership of `build/` and `.gradle/` is repaired to the host user.
|
|
Git works inside the container: the primary repository's `.git` is mounted read-only (so build steps can run read-only git commands like `git log` or `git describe`), with `.git/gittally/` masked by an empty tmpfs so the build can never read the machine config (`git.token`) or the control token.
|
|
Note that the rest of `.git` — including `.git/config` — is visible to builds; GitTally never stores credentials there, and neither should you.
|
|
The Docker socket is mounted into the container and `DOCKER_HOST`/`TESTCONTAINERS_*` variables are set, so Testcontainers-based builds work inside the container.
|
|
All GitTally containers carry `org.hoennig.gittally` labels; stale build containers of the repository are removed before the first Docker build after a restart.
|
|
|
|
## `.git/gittally/.gittally.yml` (not committed)
|
|
|
|
```yaml
|
|
# Machine- or user-specific overrides and secrets. Keys here win over .gittally.yml.
|
|
git:
|
|
account: my-user # technical username for git HTTPS authentication
|
|
token: glpat-xxxxxxxxxxxxxxxxxxxx # Gitea API token — never commit this
|
|
```
|