Files
CyberStrikeAI/internal/update/version.go
T
Sycun 0d931569be feat: carry the release version across an update, so the header matches the code
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.
2026-10-08 13:57:11 +08:00

105 lines
3.5 KiB
Go

package update
import (
"cyberstrike-ai/internal/config"
"fmt"
"path/filepath"
)
// The release version travels with the code: config.example.yaml ships in the repository
// and upstream bumps its version field in the release commit, but the live config.yaml is
// the operator's file and no merge ever touches it. So without the steps below an updated
// installation keeps showing the version it was born with - the header badge and the
// static-asset cache buster would both say v-old while the tree runs v-new. The fix is
// deliberately small: read the version the new code carries, write it into the live
// config's version line (byte-for-byte otherwise untouched, atomic rename), and let a
// rollback put the old number back with the old code.
// liveConfigFile names the operator's configuration inside an install tree: the
// conventional config.yaml, then config.yml. Empty when neither exists (a source
// checkout, where there is nothing to sync).
func liveConfigFile(root string) string {
for _, name := range []string{"config.yaml", "config.yml"} {
path := filepath.Join(root, name)
if fileExists(path) {
return path
}
}
return ""
}
// installVersion is the release version the code sitting in root carries, read from the
// shipped config.example.yaml; empty when the tree does not say.
func installVersion(root string) string {
version, err := config.FileVersion(filepath.Join(root, "config.example.yaml"))
if err != nil {
return ""
}
return version
}
// syncVersionToConfig writes the code's release version into the live configuration and
// returns (before, after, error) - both empty when there was nothing to do. A tree
// without a live config, or without a shipped version, is left alone; a current value
// that already matches is not rewritten.
func syncVersionToConfig(root string) (before, after string, err error) {
path := liveConfigFile(root)
if path == "" {
return "", "", nil
}
target := installVersion(root)
if target == "" {
return "", "", nil
}
current, err := config.FileVersion(path)
if err != nil {
return "", "", err
}
if current == target {
return "", "", nil
}
changed, err := config.WriteVersion(path, target)
if err != nil {
return current, "", err
}
if !changed {
return "", "", nil
}
return current, target, nil
}
// versionStep renders the sync outcome as one progress line; no lines for "nothing to
// do", a warning when the write failed (the update itself still stands).
func versionStep(step func(string, string), before, after string, err error) {
if err != nil {
step("version", "版本号写入 config.yaml 失败(不影响本次更新):"+err.Error())
return
}
if after == "" {
return
}
if before == "" {
step("version", fmt.Sprintf("版本号随新代码写入 config.yaml:%s(重启后页头生效)", after))
return
}
step("version", fmt.Sprintf("版本号已随新代码更新:%s → %s(config.yaml,重启后页头生效)", before, after))
}
// restoreVersionAfterRollback puts the old number back with the old code - but only while
// the live config still carries exactly what the update wrote: a version someone edited by
// hand after the update belongs to them, not to the rollback.
func restoreVersionAfterRollback(root string, st State) {
if st.VersionAfter == "" {
return
}
path := liveConfigFile(root)
if path == "" {
return
}
current, err := config.FileVersion(path)
if err != nil || current != st.VersionAfter {
return
}
_, _ = config.WriteVersion(path, st.VersionBefore)
}