werkator build succeeded after 01:41
Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #59
146 lines
9.3 KiB
Markdown
146 lines
9.3 KiB
Markdown
> **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
|
|
|
|
The History page shows no history.
|
|
|
|
On the instance serving this repository it listed three builds on 2026-09-12, all of them from that same afternoon, and nothing before them.
|
|
The page was right: `GET /api/builds/history` returned exactly those three entries, so nothing was lost in the rendering.
|
|
What was lost were the builds themselves, and two rules of retention did it together.
|
|
|
|
`artifacts.retentionPerBranch` was 3, and three merges into `main` in one afternoon (PR#56, PR#57, PR#58) were enough to push the morning out of the list.
|
|
|
|
The second rule is the one that matters.
|
|
`FileBuildResultRepository.prune` dropped every result whose `branch` is no longer on origin.
|
|
A merged pull request's branch is deleted right after the squash-merge, so its build — the build that decided whether the merge was safe — disappeared within one poll cycle.
|
|
The record of a branch was tied to the existence of that branch, which leaves a "history" that can only ever show work in progress.
|
|
|
|
The two rules compound: a repository that merges through short-lived branches keeps only the builds of the branches it happens to have open right now, plus the last three of `main`.
|
|
|
|
## Non-Goals
|
|
|
|
- No new configuration key: the existing `retentionPerBranch` and `retentionMaxAge` govern the surviving entries too, decided as option A of the three offered.
|
|
- No keeping of artifacts or logs beyond the branch: the stored directory still goes when the branch does, so the disk footprint of this change is the JSON entry alone.
|
|
- No unbounded history: an instance that merges branches forever needs `retentionMaxAge`, and this repository now sets `90d`.
|
|
- No change to the branches view, to the watcher's enqueue decisions, to worktree pruning, or to the sizing advice.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: a build result outlives the branch it was built from
|
|
|
|
#### Background
|
|
|
|
- A *pool* is one branch and one build definition (`BuildDefinition.poolName`); retention counts per pool name.
|
|
- A branch is *gone* when it is not among the origin branches the watcher passes to `prune`.
|
|
- A gone branch's entries are *marked* with `BuildResult.branchGoneAt`, the time retention first found the branch gone.
|
|
|
|
#### Scenario#59.01: the build of a merged branch stays in the history
|
|
|
|
So that the History page can answer what was built last week, not only what is open right now.
|
|
|
|
- **Given** a `SUCCESS` build of branch `feature/x`
|
|
- **and** `feature/x` has been deleted from origin after its merge
|
|
- **When** a poll cycle prunes retention
|
|
- **Then** the entry is still in `history()`, with its status, commit, times and duration
|
|
- **and** it is marked with `branchGoneAt`
|
|
- **and** `latestFor`, `latestGreenFor` and `latestPerName` do not return it, so the branches view and the repository overview are unchanged
|
|
|
|
##### Verified by
|
|
|
|
- [FileBuildResultRepositoryTest — prune marks a named pool's entries once its underlying branch is gone from origin, and keeps them](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
- [FileBuildResultRepositoryTest — latestFor, latestGreenFor and latestPerName skip the entries of gone branches, history keeps them](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
- [WatcherTest — poll keeps the results of branches gone from origin, marked, and prunes their artifacts and worktrees](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt)
|
|
|
|
#### Scenario#59.02: a branch that comes back loses the mark
|
|
|
|
So that a branch pushed again under the same name is an ordinary branch, not a half-deleted one.
|
|
|
|
- **Given** a marked entry of branch `feature/x`
|
|
- **When** `feature/x` is on origin again and a poll cycle prunes retention
|
|
- **Then** the entry's `branchGoneAt` is cleared
|
|
- **and** the entry counts as the pool's latest again
|
|
|
|
##### Verified by
|
|
|
|
- [FileBuildResultRepositoryTest — prune clears the mark when the branch is back on origin](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
|
|
#### Scenario#59.03: a gone branch's entries live by the count and the age alone
|
|
|
|
So that the history stays bounded, and the two exemptions that only make sense for a living branch expire with it.
|
|
|
|
- **Given** marked entries of a branch gone from origin
|
|
- **When** retention applies `retentionPerBranch` and `retentionMaxAge`
|
|
- **Then** entries beyond the count are dropped as for any other pool
|
|
- **and** even the newest of them is dropped once it is older than the cutoff, because a gone branch has no current status to keep
|
|
- **and** `keepLatestGreen` keeps none of them, because the permanent `/branches/<branch-key>/…` link it exists for serves artifacts that are gone
|
|
|
|
##### Verified by
|
|
|
|
- [FileBuildResultRepositoryTest — prune keeps the entries of branches missing from origin within the count, marked as gone](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
- [FileBuildResultRepositoryTest — prune age-prunes even the newest entry of a gone branch](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
- [FileBuildResultRepositoryTest — prune with keepLatestGreen does not keep a gone branch's green build beyond the count](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
|
|
#### Scenario#59.04: the row of a gone branch offers nothing that would 404
|
|
|
|
So that a surviving row never promises a page that cannot exist.
|
|
|
|
- **Given** a history row whose branch is gone from origin
|
|
- **When** the page renders it, server-side or from `GET /api/builds/history`
|
|
- **Then** it shows status, branch name, commit, start and duration as before
|
|
- **and** the branch name is plain text, not a link to a Gitea branch page that is gone
|
|
- **and** the artifacts column shows `n/a`, titled "Branch deleted from origin — artifacts dropped"
|
|
- **and** the commit still links to Gitea, because the commit survives the branch
|
|
|
|
##### Verified by
|
|
|
|
- [UiViewsTest — a row whose branch is gone from origin says so and drops the branch link](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
|
|
|
|
#### Scenario#59.05: a queued or running build is never marked
|
|
|
|
So that a branch deleted while its last build is still in flight does not lose that build from the views that drive it.
|
|
|
|
- **Given** a `RUNNING` build of a branch already deleted from origin
|
|
- **When** retention runs
|
|
- **Then** the entry is neither marked nor removed
|
|
- **and** it is still the pool's latest
|
|
|
|
##### Verified by
|
|
|
|
- [FileBuildResultRepositoryTest — prune never removes queued or running results, even of branches gone from origin](../../src/test/kotlin/de/hoennig/werkator/build/FileBuildResultRepositoryTest.kt)
|
|
|
|
## The Solution
|
|
|
|
`BuildResult` gains `branchGoneAt`, and `prune` marks instead of dropping.
|
|
|
|
The mark is set on the first pass that finds the branch missing from origin, cleared on the first pass that finds it back, and never applied to a `PENDING` or `RUNNING` entry.
|
|
Everything else follows from the mark being read in exactly three places:
|
|
|
|
- `latestFor`, `latestGreenFor` and `latestPerName` skip marked entries, so the branches view, the repository overviews, the watcher's recovery and the permanent latest-green links behave exactly as before.
|
|
`history()` is the one accessor that returns them — which is the point.
|
|
- The artifact store is pruned to `history().withArtifacts()`, the entries of living branches, in the watcher's cycle and in `DELETE /api/builds/{key}` alike.
|
|
The stored directory and the log of a gone branch are therefore deleted on the same cycle that marks its entries, as before this change.
|
|
- `BuildResultDto.branchGone` carries the fact to the browser, and `BuildRowView` drops the Gitea branch link for such a row.
|
|
`builds.html` and `werkator.js` render the same `n/a` with the same title.
|
|
|
|
`prune` was rewritten to work on positions rather than on a set of values, because two runs of one build can be equal in every field and a value set would keep or drop them together.
|
|
The returned "removed" list reports the entries as they were stored: an entry dropped in the very pass that would have marked it never carried the mark.
|
|
|
|
The default `retentionPerBranch` goes from 3 to 5 — three was one afternoon of merges — in the data class, the `init` templates, `docs/configuration.md` and `docs/llms.md`.
|
|
This repository's own `.werkator.yml` sets `retentionPerBranch: 5` and, because it merges through short-lived branches, `retentionMaxAge: 90d`.
|
|
|
|
## Open Questions
|
|
|
|
- The build-duration trend (`GET /api/builds/durations`) now sees the builds of merged branches, which used to vanish. This widens the sample of every definition rather than distorting it, and the trend is grouped by definition, so the behaviour is left as it is.
|
|
- Amber rows are not offered a way to delete a marked entry by hand; the `DELETE` route still works for anyone who wants one gone early.
|
|
|
|
## Additional Changes
|
|
|
|
- `AGENTS.md` and the architecture skill state the new retention invariant.
|
|
- `BuildResultRepository`'s Kdoc says which accessor returns marked entries and which do not.
|
|
|
|
## Follow-up PRs
|
|
|
|
- Once the History page reaches beyond a few days, it will need paging or a range selector; it currently renders every retained entry in one table.
|