Let a project name the versions worth keeping (fixes #127) #140

Merged
qwc merged 2 commits from feature/version-keep-pattern into main 2026-08-30 20:06:48 +02:00
Owner

Fixes #127. The issue had no body, so here's how I read it — say the word if you meant something else.

Interpretation

introduce an regex for every project for versions to keep and the retention for versions which do not match

Retention already had exactly this shape, with the keep-rule hardcoded: semver tags are kept, everything else expires after N days. So this replaces the hardcoded half with a per-project regex and leaves the rest as it was.

Admin > Projects > Edit now shows:

Field Meaning
Versions to Keep (regex) Tags matching this are kept indefinitely
Retention for Other Versions (days) Everything else expires after this (renamed from "Non-Semver Retention")

An empty pattern means the old rule, so nothing changes for existing projects. ^v\d+\.\d+\.\d+$ keeps releases and lets v1.1.0-rc1 expire like any other build — the case the old rule couldn't express, since it kept every semver-shaped tag including prereleases.

Safety

Deletion here is irreversible — files, database row, and search index entries — so the failure modes lean one way:

  • A pattern that can't compile is refused at save time, and a rejected save doesn't overwrite the stored pattern.
  • If a bad pattern reaches the database another way, retention falls back to the semver rule rather than matching nothing and treating every version as expendable. There's a test for this.
  • Patterns are capped at 200 characters. Go's regexp is RE2, so there's no catastrophic backtracking to worry about; the cap just keeps the field a readable rule.

Validation lives in projects.ValidateVersionKeepPattern, next to ValidateVisibility, so the form and the service agree.

Tests

internal/handler/version_keep_pattern_test.go runs real retention over backdated versions: a pattern keeping v1.0.0 while v1.1.0-rc1 and a branch build expire and a recent build survives on age; the no-pattern case behaving exactly as before; the invalid-pattern fallback; and the admin form storing, rejecting, and clearing a pattern.

Full suite green.

One thing I found and did not change

Retention does not exempt a pinned version. Pin a build as latest, give the project a pattern that doesn't match it, and retention will delete it out from under the pin. That's pre-existing, not introduced here, and fixing it changes deletion behaviour beyond what #127 asked for — so I documented the sharp edge in how-to/version-retention.md and filed it separately rather than folding it in. Happy to fix it if you'd like the guard.

Docs

New how-to/version-retention.md explaining both fields together, with a table of worked patterns and a note that anchoring matters. reference/configuration.md and the docs index link to it. Worth a Deploy Built-in Docs after merging.


Assisted by Claude Opus 5.

Fixes #127. The issue had no body, so here's how I read it — say the word if you meant something else. ## Interpretation > introduce an regex for every project for versions to keep and the retention for versions which do not match Retention already had exactly this shape, with the keep-rule hardcoded: **semver tags are kept, everything else expires after N days**. So this replaces the hardcoded half with a per-project regex and leaves the rest as it was. **Admin > Projects > Edit** now shows: | Field | Meaning | |---|---| | **Versions to Keep (regex)** | Tags matching this are kept indefinitely | | **Retention for Other Versions (days)** | Everything else expires after this (renamed from "Non-Semver Retention") | An empty pattern means the old rule, so nothing changes for existing projects. `^v\d+\.\d+\.\d+$` keeps releases and lets `v1.1.0-rc1` expire like any other build — the case the old rule couldn't express, since it kept every semver-shaped tag including prereleases. ## Safety Deletion here is irreversible — files, database row, and search index entries — so the failure modes lean one way: - **A pattern that can't compile is refused at save time**, and a rejected save doesn't overwrite the stored pattern. - **If a bad pattern reaches the database another way, retention falls back to the semver rule** rather than matching nothing and treating every version as expendable. There's a test for this. - Patterns are capped at 200 characters. Go's `regexp` is RE2, so there's no catastrophic backtracking to worry about; the cap just keeps the field a readable rule. Validation lives in `projects.ValidateVersionKeepPattern`, next to `ValidateVisibility`, so the form and the service agree. ## Tests `internal/handler/version_keep_pattern_test.go` runs real retention over backdated versions: a pattern keeping `v1.0.0` while `v1.1.0-rc1` and a branch build expire and a recent build survives on age; the no-pattern case behaving exactly as before; the invalid-pattern fallback; and the admin form storing, rejecting, and clearing a pattern. Full suite green. ## One thing I found and did not change **Retention does not exempt a pinned version.** Pin a build as latest, give the project a pattern that doesn't match it, and retention will delete it out from under the pin. That's pre-existing, not introduced here, and fixing it changes deletion behaviour beyond what #127 asked for — so I documented the sharp edge in `how-to/version-retention.md` and filed it separately rather than folding it in. Happy to fix it if you'd like the guard. ## Docs New `how-to/version-retention.md` explaining both fields together, with a table of worked patterns and a note that anchoring matters. `reference/configuration.md` and the docs index link to it. Worth a **Deploy Built-in Docs** after merging. --- Assisted by Claude Opus 5.
Let a project name the versions worth keeping (fixes #127)
All checks were successful
CI / test (pull_request) Successful in 1m21s
CI / build (pull_request) Successful in 45s
CI / docker (pull_request) Has been skipped
8ceeab4e76
Retention kept semver-looking tags and expired everything else, which
is the wrong split for projects that tag releases their own way, or
that want release candidates cleaned up like any other build.

Projects gain a version keep pattern: a regular expression matched
against the version tag. Versions that match are kept indefinitely;
the rest expire after the project's retention period. An empty pattern
keeps the old rule, so existing projects are unaffected.

The pattern is validated where visibility already is, in the projects
package, so the form and the service agree on what may be saved. A
pattern that cannot compile is refused at save time, and if one reaches
the database another way, retention falls back to the semver rule
rather than treating every version as expendable — deletion is
irreversible, so a broken rule must keep more, not less.

Assisted-by: Claude Opus 5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
Default to keeping release numbers, instance-wide
All checks were successful
CI / test (pull_request) Successful in 1m23s
CI / build (pull_request) Successful in 46s
CI / docker (pull_request) Has been skipped
a6bcab6d27
Projects without a pattern of their own now follow retention.keep_pattern,
which ships as "^v?\d+\.\d+\.\d+$" — a release number with an optional v
prefix. A project that tags differently overrides it in the admin UI, and
an operator whose whole instance tags differently changes one config value.

This narrows what retention keeps compared with the previous fallback.
docs.IsSemver matches anything starting with an optional v and a digit,
so v1.2, v1.2.3-rc1 and dated builds like 2026-01-01 counted as keepers
and now expire. It only bites where retention is actually enabled:
nonsemver_days still defaults to 0, which deletes nothing.

A config pattern that cannot compile falls back to the old permissive
rule, for the same reason a broken project pattern does — deletion is
irreversible, so an unusable rule must keep more, not less.

Assisted-by: Claude Opus 5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
Author
Owner

Added a second commit (a6bcab6) making the keep pattern default instance-wide, per your request — the description above covers only the first commit.

What changed

New config option retention.keep_pattern, shipping as ^v?\d+\.\d+\.\d+$. Projects without their own pattern follow it, so "real versions" are kept everywhere by default and a project only sets its own field when it tags differently. An operator whose whole instance tags differently changes one value instead of editing every project.

Worth knowing before merging

This narrows what retention keeps. The old fallback was docs.IsSemver, whose regex is ^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)$ — the trailing .* means it matches anything starting with an optional v and a digit. So these counted as keepers before and expire under the new default:

Tag Before Now
v1.2.3 kept kept
v1.2, v2 kept expires
v1.2.3-rc1 kept expires
2026-01-01 (dated build) kept expires

Two things limit the blast radius: nonsemver_days still defaults to 0, so an instance that never enabled retention deletes nothing regardless; and any project can be exempted by giving it its own pattern.

If your projects tag two-component releases, widen the instance default to ^v?\d+(\.\d+)*$ in config.yaml — that keeps v1.2 and v2 while still expiring RCs and branch builds. There's a test pinning that widening works, and config.yaml.example spells out the option.

An unparseable config pattern falls back to the old permissive rule, same as a broken project pattern — deletion is irreversible, so an unusable rule keeps more rather than less.

PR #143 (stacked on this branch) fixes #141 on top.


Assisted by Claude Opus 5.

Added a second commit (`a6bcab6`) making the keep pattern default instance-wide, per your request — the description above covers only the first commit. ## What changed New config option `retention.keep_pattern`, shipping as `^v?\d+\.\d+\.\d+$`. Projects without their own pattern follow it, so "real versions" are kept everywhere by default and a project only sets its own field when it tags differently. An operator whose whole instance tags differently changes one value instead of editing every project. ## Worth knowing before merging **This narrows what retention keeps.** The old fallback was `docs.IsSemver`, whose regex is `^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)$` — the trailing `.*` means it matches anything starting with an optional `v` and a digit. So these counted as keepers before and expire under the new default: | Tag | Before | Now | |---|---|---| | `v1.2.3` | kept | kept | | `v1.2`, `v2` | kept | expires | | `v1.2.3-rc1` | kept | expires | | `2026-01-01` (dated build) | kept | expires | Two things limit the blast radius: `nonsemver_days` still defaults to `0`, so an instance that never enabled retention deletes nothing regardless; and any project can be exempted by giving it its own pattern. If your projects tag two-component releases, widen the instance default to `^v?\d+(\.\d+)*$` in `config.yaml` — that keeps `v1.2` and `v2` while still expiring RCs and branch builds. There's a test pinning that widening works, and `config.yaml.example` spells out the option. An unparseable config pattern falls back to the old permissive rule, same as a broken project pattern — deletion is irreversible, so an unusable rule keeps more rather than less. PR #143 (stacked on this branch) fixes #141 on top. --- Assisted by Claude Opus 5.
qwc merged commit 0dab35af82 into main 2026-08-30 20:06:48 +02:00
qwc deleted branch feature/version-keep-pattern 2026-08-30 20:06:48 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
qwc-open/asiakirjat!140
No description provided.