A branch's committed .gittally.yml describes that branch's CI, so it wins over the project and repo-install config — the `builds` section included. Pinning it was wrong: a new build definition can only be tried out by committing it on a branch, and pinned it neither took effect at build time nor existed for the watcher, so the job silently never ran. The watcher now decides per branch from that branch's own definitions, reading its committed config via `git show` and caching it by head commit, so the read happens only when the branch moved; an unreadable config falls back to the primary definitions instead of failing the poll cycle. A branch's definitions are evaluated for that branch alone, so a definition committed on one branch can never trigger builds of another. The pinned set is reduced to what does not describe this branch's build: secrets (`git`), the host and repository sections (`server`, `gitea`, `executor`, `watcher`), the sandbox policy (`docker.enabled`/`network`), and the trust gate (`requirePullRequest`). Letting a branch set its own build command through a definition grants no new power — `branches.*. buildCommand` always allowed exactly that — while the sandbox and the gate decide whether untrusted branch code runs on the host at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
325 lines
20 KiB
Markdown
325 lines
20 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.
|
|
|
|
### 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
|
|
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 (default: none)
|
|
# 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 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 is rejected as an invalid build definition.
|
|
|
|
### 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
|
|
```
|