The repository's config.example.yaml ships the release version this code was cut
from (upstream bumps it in the release commit: v1.7.21's config.example says
v1.7.21), while the live config.yaml is operator data that no merge ever touches.
So after an update the header badge - and the ?v= on every static asset - kept
saying the version the installation was born with while the tree already ran
newer code.
An update, an adoption, or a rollback now keeps the two together: after the merge
the update reads the version the new code carries (config.example.yaml) and writes
it into the live config's version field - that one line, temp file plus atomic
rename, everything else byte-for-byte preserved, and only when the value actually
differs. The job log records the change (the page shows the step), and the state
file records both numbers so a rollback puts the old version back with the old
code; a number hand-edited after the update is the operator's and is left alone.
The side effect worth naming: the version bump also changes the ?v= query on
static assets, so a browser stops serving the pre-update JavaScript.
Trees without a live config or without a shipped version are left untouched -
a source checkout has nothing to sync, and nothing is ever guessed.
internal/config gains the two focused helpers (FileVersion / WriteVersion, line
level like the tarball upgrade path always did); tests pin the replace, the
insert-when-absent, byte preservation, the mode, the malformed-value refusal, and
the update/adopt/rollback/leave-alone behaviours end to end.