Finishes the small items of the security audit in docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7. The three mutating endpoints of BuildsApiController no longer accept the control token as a `token` query parameter — only the X-GitTally-Token header, which the bundled UI has always used. URLs end up in access logs, proxy logs, browser history and Referer headers, and the token never expires, so a historical log capture would yield a valid credential. `config:print` masks git.token as `***` on both the raw and the --full path and names the new --show-secrets flag in a leading YAML comment, so the output stays parseable when piped. The setup script points at --show-secrets where it used to steer the operator to the plain token. `server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the API authenticates read access, so reaching GitTally should require the host's reverse proxy. Existing .gittally.yml files keep their explicit value; the managed nginx container needs `0.0.0.0` set deliberately, which is noted in the release notes, docs/configuration.md and docs/deployment.md. Released as v0.9.9, which also carries the previous two commits. Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
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 |
| Build worktree config | .gittally.yml of the built commit |
Yes | Per-branch build settings (build layer only) |
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.
Per-branch build settings from the worktree
When a branch builds, its build config is resolved with an extra layer: the .gittally.yml
committed on the branch being built (read from its build worktree) overrides the two layers
above, giving the precedence worktree > repo install > project. So a branch can change its
own buildCommand, cleanCommand, artifactDirs, log file names, autoBuild, and
docker.image/dockerfile/context/env.
This layer applies only to the build itself. A pinned set is always taken from the repo install/project config and can never be set from the worktree:
- secrets and server-side settings: the whole
git,gitea, andserversections; - the container sandbox policy:
docker.enabledanddocker.network.
This keeps a branch from disabling its own build container, changing its network mode, or
reaching credentials. Watcher decisions that happen before a build exists — autoBuild
scheduling and the requirePullRequest gate — are still read from the repo install/project
config, because there is no worktree at that point.
Inspect the Effective Config
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.
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.
builds:
# How many branches may build 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
# 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
# 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
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: {}
main:
autoBuild:
enabled: true
master:
autoBuild:
enabled: true
release:
buildCommand: ./gradlew --console=plain --no-daemon --no-build-cache test jacocoReport
autoBuild:
enabled: true
times:
- "04:00"
Notes on server.bindAddress
The default is 127.0.0.1.
Neither the web UI nor the JSON API authenticates read access, and every page carries the control token that unlocks the build controls, 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.
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:
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 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)
# 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