Files
stalwart-migrator/internal/preflight/release.go
T
jcoffey-dev 719a945d64 Initial commit: stalwart-migrator design and scaffolding
In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with
checkpointed rollback and post-migration validation. Design stage; see
ARCHITECTURE.md.
2026-08-22 18:17:17 -07:00

75 lines
2.2 KiB
Go

package preflight
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// githubAPIBase is a var, not a const, so tests can point it at an
// httptest server instead of hitting the real GitHub API.
var githubAPIBase = "https://api.github.com/repos/stalwartlabs/stalwart/releases"
type ReleaseAsset struct {
Name string `json:"name"`
DownloadURL string `json:"browser_download_url"`
SizeBytes int64 `json:"size"`
}
type Release struct {
TagName string `json:"tag_name"`
Assets []ReleaseAsset `json:"assets"`
}
// ResolveRelease looks up a Stalwart release from the public GitHub API.
// version is either an exact tag like "0.16.14" (a "v" prefix is added if
// missing) or "latest".
func ResolveRelease(ctx context.Context, httpClient *http.Client, version string) (*Release, error) {
url := githubAPIBase + "/latest"
if version != "" && version != "latest" {
tag := version
if !strings.HasPrefix(tag, "v") {
tag = "v" + tag
}
url = githubAPIBase + "/tags/" + tag
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
if httpClient == nil {
httpClient = &http.Client{Timeout: 30 * time.Second}
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("preflight: fetch release %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("preflight: fetch release %s: unexpected status %s", url, resp.Status)
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return nil, fmt.Errorf("preflight: parse release response from %s: %w", url, err)
}
return &rel, nil
}
// ChecksumAsset returns the release asset that looks like a published
// checksum manifest, if any. Its presence isn't guaranteed by Stalwart's
// release process, so callers must treat a nil result as "no independently
// published checksum to verify the download against", not an error.
func ChecksumAsset(rel *Release) *ReleaseAsset {
for i := range rel.Assets {
name := strings.ToLower(rel.Assets[i].Name)
if strings.Contains(name, "sha256") || strings.Contains(name, "checksum") {
return &rel.Assets[i]
}
}
return nil
}