diff --git a/cmd/felis/update.go b/cmd/felis/update.go index 866719f..e1341e4 100644 --- a/cmd/felis/update.go +++ b/cmd/felis/update.go @@ -50,10 +50,13 @@ type updateTarget struct { // and even on the bootstrap path it re-images felis-api from the binary setup is already // running (FELIS_BOOTSTRAP_BINARY), which looks like an update and changes nothing. // -// The URL is the same one-liner both READMEs hand out. While the repo is private it +// The URL is the one-liner both READMEs hand out, read at a tag rather than main: the +// script's release channel installs the newest release's binary, and main can carry +// installer changes that binary was never tested with. installerRef picks the tag and +// renderApplyGuidance substitutes it for {ref}. While the repo is private the URL // answers 404 (raw.githubusercontent.com hides private repos), which is why the trailer // below points at the README's token'd form for that case. -const installerRerun = "curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash" +const installerRerun = "curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/{ref}/deploy/bootstrap.sh | sudo bash" // updateTargets is the selector table. panel and plugins both resolve to felis-api // because they are not separately versioned: the panel is compiled into the felis @@ -258,7 +261,7 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo // component, and reinstalling the current release is a valid repair action. fmt.Fprintf(&b, " note: cannot tell whether %s is current — its latest version could not be discovered (see above); this reinstalls it either way\n", t.component) } - fmt.Fprintf(&b, " run: %s\n", t.command) + fmt.Fprintf(&b, " run: %s\n", strings.ReplaceAll(t.command, "{ref}", installerRef(byComponent))) offeredCommand = true } // Only explain the command when one was actually offered; a --mc-only run has @@ -277,3 +280,31 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo } return b.String() } + +// installerRef is the git ref the installer re-run reads bootstrap.sh from: the newest +// stable felis release when the feed answered, which is the release that script then +// installs; else the release this host runs; main only when neither is a release tag. +func installerRef(byComponent map[string]updates.Action) string { + a, ok := byComponent["felis-api"] + if !ok { + return "main" + } + if a.LatestKnown && isReleaseTag(a.Latest) { + return a.Latest.String() + } + if isReleaseTag(a.Current) { + return a.Current.String() + } + return "main" +} + +// isReleaseTag reports whether v was read from a stable vX.Y.Z tag, the only refs +// release.yml publishes a binary for. +func isReleaseTag(v updates.Version) bool { + s := v.String() + if !strings.HasPrefix(s, "v") || v.IsPrerelease() { + return false + } + _, err := updates.Parse(s) + return err == nil +} diff --git a/cmd/felis/update_test.go b/cmd/felis/update_test.go index 1d0f11b..b077deb 100644 --- a/cmd/felis/update_test.go +++ b/cmd/felis/update_test.go @@ -167,3 +167,35 @@ func TestApplyGuidancePointsEveryComponentAtTheInstaller(t *testing.T) { t.Fatalf("--mc offers no command; the trailer is a non-sequitur:\n%s", mc) } } + +// The re-run reads bootstrap.sh at the tag whose binary it installs. main can carry +// installer changes no release was tested with. +func TestApplyGuidanceReadsTheInstallerAtTheReleaseTag(t *testing.T) { + v := func(s string) updates.Version { + t.Helper() + out, err := updates.Parse(s) + if err != nil { + t.Fatal(err) + } + return out + } + cases := []struct { + name string + api []updates.Action + want string + }{ + {"latest known", []updates.Action{{Component: "felis-api", Kind: updates.ActionNotify, Current: v("v1.3.0"), Latest: v("v1.4.0"), LatestKnown: true}}, "/FelisMC/Felis/v1.4.0/deploy/bootstrap.sh"}, + {"latest unknown", []updates.Action{{Component: "felis-api", Kind: updates.ActionNone, Current: v("v1.3.0")}}, "/FelisMC/Felis/v1.3.0/deploy/bootstrap.sh"}, + {"prerelease latest", []updates.Action{{Component: "felis-api", Kind: updates.ActionNone, Current: v("v1.3.0"), Latest: v("v1.4.0-rc.1"), LatestKnown: true}}, "/FelisMC/Felis/v1.3.0/deploy/bootstrap.sh"}, + {"nothing known", nil, "/FelisMC/Felis/main/deploy/bootstrap.sh"}, + } + for _, c := range cases { + out := renderApplyGuidance(planResult(c.api), map[string]bool{"velocity": true, "panel": true}, true) + if !strings.Contains(out, c.want) { + t.Errorf("%s: want %q in:\n%s", c.name, c.want, out) + } + if strings.Contains(out, "{ref}") { + t.Errorf("%s: placeholder left in:\n%s", c.name, out) + } + } +}