1package agent
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "os"
9 "strings"
10 "time"
11)
12
13// ChangelogInfo is what FetchChangelog returns. Body is the markdown
14// from the GitHub release page; URL points back to that page so the
15// dialog can offer "open in browser".
16type ChangelogInfo struct {
17 Version string
18 Body string
19 URL string
20}
21
22// FetchChangelog hits the GitHub releases API for the given version
23// (must already include the leading "v") and returns the release
24// notes body. Returns an empty ChangelogInfo on any failure or when
25// the body is empty; the caller treats either as "skip silently".
26//
27// Honours $GITHUB_TOKEN for private-repo access. Times out at 4s so
28// startup never blocks on a flaky network.
29func FetchChangelog(ctx context.Context, version string) (ChangelogInfo, error) {
30 if version == "" || version == "dev" || version == "0.0.0" {
31 return ChangelogInfo{}, nil
32 }
33 tag := version
34 if !strings.HasPrefix(tag, "v") {
35 tag = "v" + tag
36 }
37 url := fmt.Sprintf("https://api.github.com/repos/patriceckhart/zot/releases/tags/%s", tag)
38 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
39 if err != nil {
40 return ChangelogInfo{}, err
41 }
42 req.Header.Set("accept", "application/vnd.github+json")
43 req.Header.Set("x-github-api-version", "2022-11-28")
44 if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
45 req.Header.Set("authorization", "Bearer "+tok)
46 }
47
48 client := &http.Client{Timeout: 4 * time.Second}
49 resp, err := client.Do(req)
50 if err != nil {
51 return ChangelogInfo{}, err
52 }
53 defer resp.Body.Close()
54 if resp.StatusCode != 200 {
55 return ChangelogInfo{}, fmt.Errorf("github api %d", resp.StatusCode)
56 }
57
58 var body struct {
59 TagName string `json:"tag_name"`
60 HTMLURL string `json:"html_url"`
61 Body string `json:"body"`
62 }
63 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
64 return ChangelogInfo{}, err
65 }
66 body.Body = strings.TrimSpace(body.Body)
67 if body.Body == "" {
68 return ChangelogInfo{}, nil
69 }
70 return ChangelogInfo{
71 Version: strings.TrimPrefix(body.TagName, "v"),
72 Body: body.Body,
73 URL: body.HTMLURL,
74 }, nil
75}
76
77// FetchChangelogAsync runs FetchChangelog on a goroutine and delivers
78// the result on the returned channel. Channel always closes.
79func FetchChangelogAsync(version string) <-chan ChangelogInfo {
80 ch := make(chan ChangelogInfo, 1)
81 go func() {
82 defer close(ch)
83 ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
84 defer cancel()
85 info, _ := FetchChangelog(ctx, version)
86 ch <- info
87 }()
88 return ch
89}
90
91// ShouldShowChangelog reports whether the running binary version
92// differs from the last version whose changelog the user dismissed.
93// Returns false on dev builds (version "" / "dev" / "0.0.0") and on
94// the first-ever launch (no LastChangelogShown stored — we don't
95// dump release notes at someone who just installed).
96func ShouldShowChangelog(currentVersion string, cfg Config) bool {
97 if currentVersion == "" || currentVersion == "dev" || currentVersion == "0.0.0" {
98 return false
99 }
100 if cfg.LastChangelogShown == "" {
101 return false
102 }
103 return cfg.LastChangelogShown != currentVersion
104}
105
106// MarkChangelogShown persists the version whose changelog the user
107// just dismissed. Idempotent; safe to call when the dialog wasn't
108// actually shown (e.g. fetch failed) so we don't keep retrying.
109func MarkChangelogShown(version string) error {
110 cfg, _ := LoadConfig()
111 if cfg.LastChangelogShown == version {
112 return nil
113 }
114 cfg.LastChangelogShown = version
115 return SaveConfig(cfg)
116}
117
118// SeedChangelogVersion sets LastChangelogShown if it's currently
119// empty. Called once on first-ever launch so future upgrades
120// correctly trigger the dialog while THIS launch (which is also
121// "first-ever") doesn't.
122func SeedChangelogVersion(version string) {
123 if version == "" || version == "dev" || version == "0.0.0" {
124 return
125 }
126 cfg, err := LoadConfig()
127 if err != nil {
128 return
129 }
130 if cfg.LastChangelogShown != "" {
131 return
132 }
133 cfg.LastChangelogShown = version
134 _ = SaveConfig(cfg)
135}