@@ -7,57 +7,135 @@ import (
77 "compress/gzip"
88 "crypto/sha256"
99 "encoding/hex"
10+ "encoding/json"
11+ "errors"
1012 "fmt"
1113 "io"
1214 "net/http"
1315 "os"
1416 "path/filepath"
1517 "runtime"
1618 "strings"
19+ "time"
1720
1821 "github.com/deepsourcelabs/cli/buildinfo"
1922 "github.com/deepsourcelabs/cli/config"
2023 "github.com/deepsourcelabs/cli/internal/debug"
2124)
2225
23- // Update checks for a newer CLI version and replaces the current binary.
24- // Returns the new version string if an update was applied, or "" if already
25- // up to date. Errors are non-fatal — callers should log and move on.
26- func Update (client * http.Client ) (string , error ) {
26+ // UpdateState is the on-disk state written by CheckForUpdate and consumed by ApplyUpdate.
27+ type UpdateState struct {
28+ Version string `json:"version"`
29+ ArchiveURL string `json:"archive_url"`
30+ SHA256 string `json:"sha256"`
31+ CheckedAt time.Time `json:"checked_at"`
32+ }
33+
34+ // updateStatePath returns the path to the update state file (~/.deepsource/update.json).
35+ func updateStatePath () string {
36+ home , _ := os .UserHomeDir ()
37+ return filepath .Join (home , buildinfo .ConfigDirName , "update.json" )
38+ }
39+
40+ // ReadUpdateState reads the update state file. Returns nil if the file does not exist.
41+ func ReadUpdateState () (* UpdateState , error ) {
42+ data , err := os .ReadFile (updateStatePath ())
43+ if err != nil {
44+ if errors .Is (err , os .ErrNotExist ) {
45+ return nil , nil
46+ }
47+ return nil , fmt .Errorf ("reading update state: %w" , err )
48+ }
49+ var s UpdateState
50+ if err := json .Unmarshal (data , & s ); err != nil {
51+ return nil , fmt .Errorf ("parsing update state: %w" , err )
52+ }
53+ return & s , nil
54+ }
55+
56+ func writeUpdateState (s * UpdateState ) error {
57+ data , err := json .MarshalIndent (s , "" , " " )
58+ if err != nil {
59+ return fmt .Errorf ("marshaling update state: %w" , err )
60+ }
61+ p := updateStatePath ()
62+ if err := os .MkdirAll (filepath .Dir (p ), 0o755 ); err != nil {
63+ return fmt .Errorf ("creating config dir: %w" , err )
64+ }
65+ if err := os .WriteFile (p , data , 0o644 ); err != nil {
66+ return fmt .Errorf ("writing update state: %w" , err )
67+ }
68+ return nil
69+ }
70+
71+ func clearUpdateState () {
72+ _ = os .Remove (updateStatePath ())
73+ }
74+
75+ // CheckForUpdate fetches the manifest, compares versions, and writes a state
76+ // file if a newer version is available. This is meant to be fast (~100-200ms).
77+ func CheckForUpdate (client * http.Client ) error {
2778 bi := buildinfo .GetBuildInfo ()
2879 if bi == nil {
29- return "" , fmt .Errorf ("build info not set" )
80+ return fmt .Errorf ("build info not set" )
3081 }
3182
3283 manifest , err := FetchManifest (client )
3384 if err != nil {
34- return "" , err
85+ return err
3586 }
3687
3788 newer , err := IsNewer (bi .Version , manifest .Version )
3889 if err != nil {
39- return "" , err
90+ return err
4091 }
4192 if ! newer {
4293 debug .Log ("update: already up to date (current=%s, remote=%s)" , bi .Version , manifest .Version )
43- return "" , nil
94+ return nil
4495 }
4596
4697 key := PlatformKey ()
4798 platform , ok := manifest .Platforms [key ]
4899 if ! ok {
49- return "" , fmt .Errorf ("no release for platform %s" , key )
100+ return fmt .Errorf ("no release for platform %s" , key )
101+ }
102+
103+ state := & UpdateState {
104+ Version : manifest .Version ,
105+ ArchiveURL : "https://cli.deepsource.com/" + platform .Archive ,
106+ SHA256 : platform .SHA256 ,
107+ CheckedAt : time .Now ().UTC (),
108+ }
109+
110+ debug .Log ("update: newer version %s available, writing state file" , manifest .Version )
111+ return writeUpdateState (state )
112+ }
113+
114+ // ApplyUpdate reads the state file, downloads the archive, verifies, extracts,
115+ // and replaces the binary. Returns the new version string on success.
116+ // Clears the state file regardless of outcome so we don't retry broken updates forever.
117+ func ApplyUpdate (client * http.Client ) (string , error ) {
118+ state , err := ReadUpdateState ()
119+ if err != nil {
120+ clearUpdateState ()
121+ return "" , err
122+ }
123+ if state == nil {
124+ return "" , nil
50125 }
51126
52- debug .Log ("update: downloading %s" , platform .Archive )
127+ // Clear state file up front so a failed update doesn't retry forever.
128+ // The next run will do a fresh CheckForUpdate instead.
129+ clearUpdateState ()
130+
131+ debug .Log ("update: applying update to v%s" , state .Version )
53132
54- archiveURL := "https://cli.deepsource.com/" + platform .Archive
55- data , err := downloadFile (client , archiveURL )
133+ data , err := downloadFile (client , state .ArchiveURL )
56134 if err != nil {
57135 return "" , err
58136 }
59137
60- if err := verifyChecksum (data , platform .SHA256 ); err != nil {
138+ if err := verifyChecksum (data , state .SHA256 ); err != nil {
61139 return "" , err
62140 }
63141
@@ -67,7 +145,7 @@ func Update(client *http.Client) (string, error) {
67145 }
68146
69147 var binaryData []byte
70- if strings .HasSuffix (platform . Archive , ".zip" ) {
148+ if strings .HasSuffix (state . ArchiveURL , ".zip" ) {
71149 binaryData , err = extractFromZip (data , binaryName )
72150 } else {
73151 binaryData , err = extractFromTarGz (data , binaryName )
@@ -80,8 +158,8 @@ func Update(client *http.Client) (string, error) {
80158 return "" , err
81159 }
82160
83- debug .Log ("update: updated to v%s" , manifest .Version )
84- return manifest .Version , nil
161+ debug .Log ("update: updated to v%s" , state .Version )
162+ return state .Version , nil
85163}
86164
87165// ShouldAutoUpdate reports whether the auto-updater should run.
0 commit comments