mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-04 13:52:24 +00:00
fix(updates): require exact server archive for update download
Release metadata contains agent, MCP and multiple architecture archives. An incomplete release must not offer an unrelated tarball or advertise an update that cannot be applied. Retain server archive names only, require the exact tag and runtime architecture, and record this installability boundary with regression coverage. Change-source: pulse-maintainer
This commit is contained in:
parent
e02eeb8fa3
commit
ab0aa1ef02
4 changed files with 90 additions and 16 deletions
|
|
@ -4041,6 +4041,13 @@ tracks what the updater keeps rather than the response size. Buffering the
|
|||
whole response behind a small bound is a release-blocking regression: it made
|
||||
every stable-channel check fail on main and left 6.4.3-rc.1, which has the
|
||||
bound without the fallback, unable to discover any update (#1881, #2282).
|
||||
The release-list path must retain only server-shaped Linux archives, not
|
||||
`pulse-agent-*` or `pulse-mcp-*` components. It may advertise an update as
|
||||
available only when that release contains the exact server archive for the
|
||||
running architecture and tag. A missing exact archive leaves the download URL
|
||||
empty and the update unavailable; it must not fall back to another component,
|
||||
architecture or version. This keeps an incomplete release visible as metadata
|
||||
without presenting an unusable or wrong installation path.
|
||||
Malformed metadata stays a hard error and only the typed over-limit condition
|
||||
may fall through to the feed. Proof:
|
||||
`internal/updates/issue2282_release_metadata_stream_test.go`.
|
||||
|
|
|
|||
|
|
@ -205,3 +205,64 @@ func TestIssue2282StreamedDecodeKeepsTypedByteBound(t *testing.T) {
|
|||
t.Fatalf("decodeReleaseList error = %v, want typed response size rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue2282OnlyServerArchivesAreRetained(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"pulse-v6.4.5-linux-amd64.tar.gz", true},
|
||||
{"pulse-v6.4.5-rc.4-linux-arm64.tar.gz", true},
|
||||
{"pulse-agent-v6.4.5-linux-amd64.tar.gz", false},
|
||||
{"pulse-mcp-v6.4.5-linux-amd64.tar.gz", false},
|
||||
{"pulse-agent-helper-v6.4.5-linux-amd64.tar.gz", false},
|
||||
{"pulse-v6.4.5-darwin-arm64.tar.gz", false},
|
||||
{"pulse-v6.4.5-linux-amd64.tar.gz.sshsig", false},
|
||||
} {
|
||||
if got := isRuntimeReleaseAssetName(tc.name); got != tc.want {
|
||||
t.Errorf("isRuntimeReleaseAssetName(%q) = %t, want %t", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue2282UpdateCheckRequiresExactServerArchive(t *testing.T) {
|
||||
withBuildVersion(t, "6.4.0")
|
||||
exact, ok := updateReleaseAssetForRuntime("v99.0.0")
|
||||
if !ok {
|
||||
t.Skip("no release archive for this architecture")
|
||||
}
|
||||
otherArch := "amd64"
|
||||
if strings.HasSuffix(exact.Name, "-amd64.tar.gz") {
|
||||
otherArch = "arm64"
|
||||
}
|
||||
sameArch := strings.TrimSuffix(strings.TrimPrefix(exact.Name, "pulse-v99.0.0-linux-"), ".tar.gz")
|
||||
decoys := []ReleaseAsset{
|
||||
{Name: "pulse-agent-v99.0.0-linux-" + sameArch + ".tar.gz", BrowserDownloadURL: "https://example.invalid/agent"},
|
||||
{Name: "pulse-mcp-v99.0.0-linux-" + sameArch + ".tar.gz", BrowserDownloadURL: "https://example.invalid/mcp"},
|
||||
{Name: "pulse-v99.0.0-linux-" + otherArch + ".tar.gz", BrowserDownloadURL: "https://example.invalid/wrong-arch"},
|
||||
{Name: "pulse-v98.0.0-linux-" + sameArch + ".tar.gz", BrowserDownloadURL: "https://example.invalid/wrong-version"},
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, wantURL string
|
||||
wantAvailable bool
|
||||
assets []ReleaseAsset
|
||||
}{
|
||||
{"missing exact archive", "", false, decoys},
|
||||
{"exact archive after decoys", exact.BrowserDownloadURL, true, append(append([]ReleaseAsset{}, decoys...), exact)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := newReleaseServer(t, []ReleaseInfo{{TagName: "v99.0.0", Assets: tc.assets}}, nil)
|
||||
defer server.Close()
|
||||
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
||||
|
||||
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
||||
info, err := manager.CheckForUpdatesWithChannel(context.Background(), "stable")
|
||||
if err != nil {
|
||||
t.Fatalf("check update: %v", err)
|
||||
}
|
||||
if info.Available != tc.wantAvailable || info.DownloadURL != tc.wantURL {
|
||||
t.Fatalf("update = %+v, want available=%t with download URL %q", info, tc.wantAvailable, tc.wantURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,16 +509,9 @@ func (m *Manager) CheckForUpdatesWithOptions(ctx context.Context, options Update
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to any pulse tarball if exact match not found
|
||||
if downloadURL == "" {
|
||||
for _, asset := range release.Assets {
|
||||
if isRuntimeReleaseAssetName(asset.Name) {
|
||||
downloadURL = asset.BrowserDownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// A missing archive must not fall back to another architecture or to an
|
||||
// agent/MCP tarball, nor advertise an update that cannot be applied.
|
||||
available := latestVer.IsNewerThan(currentVer) && downloadURL != ""
|
||||
|
||||
isMajorUpgrade := latestVer.Major > currentVer.Major
|
||||
// Derive prerelease from the parsed version tag (not GitHub metadata) so the
|
||||
|
|
@ -526,7 +519,7 @@ func (m *Manager) CheckForUpdatesWithOptions(ctx context.Context, options Update
|
|||
isPrerelease := release.Prerelease || latestVer.IsPrerelease()
|
||||
|
||||
info := &UpdateInfo{
|
||||
Available: latestVer.IsNewerThan(currentVer),
|
||||
Available: available,
|
||||
CurrentVersion: currentInfo.Version,
|
||||
LatestVersion: strings.TrimPrefix(release.TagName, "v"),
|
||||
ReleaseNotes: release.Body,
|
||||
|
|
|
|||
|
|
@ -63,12 +63,25 @@ func decodeSingleRelease(resp *http.Response) (ReleaseInfo, error) {
|
|||
return release, nil
|
||||
}
|
||||
|
||||
// isRuntimeReleaseAssetName matches the Pulse server archives the update check
|
||||
// can offer: the exact runtime asset and the any-linux-tarball fallback.
|
||||
// isRuntimeReleaseAssetName keeps server archives out of the much larger list
|
||||
// of agent, MCP and other release artifacts. The update check still requires
|
||||
// the exact release and architecture before offering an archive to the user.
|
||||
func isRuntimeReleaseAssetName(name string) bool {
|
||||
return strings.HasPrefix(name, "pulse-") &&
|
||||
strings.Contains(name, "linux") &&
|
||||
strings.HasSuffix(name, ".tar.gz")
|
||||
if !strings.HasPrefix(name, "pulse-v") {
|
||||
return false
|
||||
}
|
||||
for _, suffix := range [...]string{
|
||||
"-linux-amd64.tar.gz",
|
||||
"-linux-arm64.tar.gz",
|
||||
"-linux-armv7.tar.gz",
|
||||
"-linux-armv6.tar.gz",
|
||||
"-linux-386.tar.gz",
|
||||
} {
|
||||
if strings.HasSuffix(name, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeReleaseObject(dec *json.Decoder, keepAsset func(string) bool) (ReleaseInfo, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue