added age-based build retention: artifacts.retentionMaxAge (e.g. 30d, empty = no limit) drops builds older than the given age; combines with retentionPerBranch as independent caps — a build is kept only while it satisfies both limits; a branch's newest build is never age-pruned and keepLatestGreen now shields the latest green build from both limits, keeping the permanent /branches/... links valid

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Michael Hoennig
2026-07-08 22:35:31 +02:00
co-authored by Claude Fable 5
parent b104eeee05
commit d0b38c557a
10 changed files with 246 additions and 9 deletions
+6 -1
View File
@@ -77,7 +77,12 @@ artifacts:
rootDir: ""
# number of builds to keep per branch
retentionPerBranch: 3
# Keep each branch's latest green (successful) build even beyond retentionPerBranch.
# 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.
+1 -1
View File
@@ -27,7 +27,7 @@ Branch-level keys below live under `branches.<name>`; use `branches.default` for
| `GITTALLY_ARTIFACT_SERVER_PORT` | `server.port` |
| `GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS` | `server.bindAddress` |
| `GITTALLY_ARTIFACT_PUBLIC_BASE_URL` | `server.publicBaseUrl` |
| `GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH` | `artifacts.retentionPerBranch` — build count only; the legacy age suffix (`h`/`d`) is not supported |
| `GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH` | `artifacts.retentionPerBranch` for a count, `artifacts.retentionMaxAge` for a legacy age value (`h`/`d` suffix); unlike legacy, both limits can be combined |
| `GITTALLY_IMPRESSUM_URL` | `server.impressumUrl` |
| `GITTALLY_AUTO_BUILD_BRANCHES` | `branches.<name>.autoBuild.enabled: true` per branch instead of a branch list |
| `GITTALLY_AUTO_BUILD_TIMES` | `branches.<name>.autoBuild.times` — YAML list, per branch |
@@ -0,0 +1,105 @@
> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
Retention is count-based only: `artifacts.retentionPerBranch` keeps the newest N builds per branch, no matter how old they are.
The legacy script also supported an age limit (`GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH=3d`), which the rewrite dropped — the migration doc declared the age suffix unsupported.
Without an age limit there is no way to bound how long stale build logs and reports stay on disk, e.g. on branches that build rarely.
## Non-Goals
- No either/or single config value like legacy (`3` *or* `3d`); count and age are separate keys that can be combined.
- No age pruning of a branch's newest build — a dormant branch keeps its last status and artifacts.
- No change to the existing pruning of branches deleted from origin.
- No size-based (bytes) retention.
## The Scenarios
### Feature: age-based build retention combinable with count-based retention
#### Background
- `artifacts.retentionMaxAge` takes the shared duration format (`s`/`m`/`h`/`d` suffix, like `watcher.newBranchMaxAge`); empty (the default) means no age limit.
- Pruning runs in the watcher poll cycle; artifact directories follow the surviving build results.
#### Scenario#000.01: Builds older than retentionMaxAge are pruned
So that stale logs and reports do not stay on disk indefinitely.
- **Given** `artifacts.retentionMaxAge: 30d`
- **and** a branch with builds within the retention count, some older than 30 days
- **When** the watcher prunes results and artifacts
- **Then** the builds older than 30 days are dropped together with their artifacts
- **and** the younger builds are kept.
##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt)
- [WatcherTest](../../src/test/kotlin/de/hoennig/gittally/watcher/WatcherTest.kt)
#### Scenario#000.02: Count and age combine as independent caps
So that one config can bound disk usage by count and staleness by age at the same time — which legacy could not.
- **Given** `artifacts.retentionPerBranch: 2` and `artifacts.retentionMaxAge: 30d`
- **When** the watcher prunes
- **Then** a build is kept only while it is among the branch's newest 2 builds **and** younger than 30 days
- **and** a build violating either limit is dropped.
##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.03: A branch's newest build is never age-pruned
So that a dormant branch keeps its last build status visible, matching the legacy age-retention behavior.
- **Given** a branch whose newest build is older than `retentionMaxAge`
- **When** the watcher prunes
- **Then** that newest build is kept regardless of its age.
##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.04: keepLatestGreen shields the latest green build from the age limit
So that the permanent `/branches/<branch-key>/…` artifact links stay valid while newer builds fail, even under an age limit.
- **Given** `artifacts.keepLatestGreen: true` (the default)
- **and** a branch whose latest green build is older than `retentionMaxAge`, followed by newer failed builds
- **When** the watcher prunes
- **Then** the latest green build and its artifacts are kept.
##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt)
#### Scenario#000.05: The default keeps existing behavior unchanged
So that existing installations are unaffected by the new key.
- **Given** `artifacts.retentionMaxAge` is unset or empty
- **When** the watcher prunes
- **Then** only the count-based retention applies, exactly as before this PR.
##### Verified by
- [FileBuildResultRepositoryTest](../../src/test/kotlin/de/hoennig/gittally/build/FileBuildResultRepositoryTest.kt) (pre-existing count-only prune tests)
## The Solution
- New config key `artifacts.retentionMaxAge`, parsed with the existing `DurationParser`; all three config sync points are updated (`GitTallyConfig`, the `init` templates, `docs/configuration.md`).
- `BuildResultRepository.prune` takes an optional `retentionCutoff: Instant?`; entries started before the cutoff are dropped, except each branch's newest entry and — with `keepLatestGreen` — the newest green entry.
- The `Watcher` computes the cutoff from its injected `Clock` on every poll cycle, so the repository stays clock-free and deterministic in tests, and config changes apply without restart like the other retention keys.
- Artifact directories need no separate handling: the artifact store already prunes by the surviving build results.
## Open Questions
- An invalid `retentionMaxAge` value (e.g. `30x`) surfaces as a poll-cycle error in the log and the watcher state, not as a startup failure — consistent with how `watcher.newBranchMaxAge` is handled.
## Additional Changes
- `docs/migration-from-legacy.md` now maps a legacy age value of `GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH` to `artifacts.retentionMaxAge` instead of declaring the age suffix unsupported.