diff --git a/cmd/felis/update.go b/cmd/felis/update.go index 7c30e78..e7405c6 100644 --- a/cmd/felis/update.go +++ b/cmd/felis/update.go @@ -219,6 +219,7 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo var b strings.Builder var offeredCommand bool + var offeredFelisAPI bool for _, t := range updateTargets { if !selected[t.selector] { continue @@ -247,11 +248,22 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo } fmt.Fprintf(&b, " run: %s\n", t.command) offeredCommand = true + offeredFelisAPI = offeredFelisAPI || t.component == "felis-api" } // Only explain the command when one was actually offered; a --mc-only run has // nothing to run and the trailer would be a non-sequitur. if offeredCommand { b.WriteString("\nfelis setup is idempotent and re-runs the installer that owns these components;\nit does not reinstall what is already current. Restart game servers afterwards.\n") } + // Scoped to felis-api because it is the only component setup cannot move forward. + // velocity is fine: install_velocity re-resolves the newest build of the pinned minor + // on every run. But setup hands deploy/bootstrap.sh the binary it is itself running + // (FELIS_BOOTSTRAP_BINARY), and that arm skips the release lookup entirely, so it + // rebuilds the image and rolls the deployment from the SAME binary -- a run that looks + // like a successful update and leaves the version unchanged. Only the installer, which + // resolves and downloads a release, actually moves felis-api. + if offeredFelisAPI { + b.WriteString("\nfelis-api (panel, plugins) is the exception: setup re-images it from the felis binary\nalready on this host, so it cannot install a NEWER felis-api. To move to a newer\nrelease, re-run the bootstrap installer (see README).\n") + } return b.String() } diff --git a/cmd/felis/update_test.go b/cmd/felis/update_test.go index 7375f48..01ee167 100644 --- a/cmd/felis/update_test.go +++ b/cmd/felis/update_test.go @@ -132,3 +132,36 @@ func TestUpdateTargetsMatchTopology(t *testing.T) { } } } + +// `sudo felis setup` is the right answer for velocity and the wrong one for felis-api, +// so the caveat has to be scoped rather than appended to every run. setup hands +// bootstrap the binary it is already running, and that arm skips the release lookup: +// the run rebuilds the image and rolls the deployment off the SAME binary, which looks +// like a successful update and changes nothing. install_velocity, by contrast, really +// does re-resolve the newest build on every run. +func TestApplyGuidanceScopesTheFelisAPICaveat(t *testing.T) { + const caveat = "cannot install a NEWER felis-api" + + api := renderApplyGuidance( + planResult([]updates.Action{{Component: "felis-api", Kind: updates.ActionNotify, LatestKnown: true}}), + map[string]bool{"panel": true}, false) + if !strings.Contains(api, caveat) { + t.Fatalf("--panel resolves to felis-api and must carry the caveat:\n%s", api) + } + + vel := renderApplyGuidance( + planResult([]updates.Action{{Component: "velocity", Kind: updates.ActionNotify, LatestKnown: true}}), + map[string]bool{"velocity": true}, false) + if strings.Contains(vel, caveat) { + t.Fatalf("velocity IS fixed by setup; the caveat would misdirect the operator:\n%s", vel) + } + if !strings.Contains(vel, "felis setup is idempotent") { + t.Fatalf("velocity still wants the ordinary trailer:\n%s", vel) + } + + // --mc offers no command at all, so neither trailer belongs. + mc := renderApplyGuidance(planResult(nil), map[string]bool{"mc": true}, true) + if strings.Contains(mc, caveat) { + t.Fatalf("--mc offers no command; the caveat is a non-sequitur:\n%s", mc) + } +}