Skip to content

Commit e2a7abb

Browse files
jai-deepsourcesourya-deepsourceclaude
authored
Add self-updater for CLI binary (#274)
* Add self-updater for CLI binary - Two-phase update: check manifest, apply on next run - Verify archive checksum before replacing binary - Skip in CI and dev builds, add config opt-out - Bump version to 2.0.45 * Fix archive download URL to include /build/ path segment The CDN serves archives under /build/, but the URL was constructed without it, causing 404 errors during self-update downloads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix DeepSource review issues in self-updater - Handle os.UserHomeDir() error to prevent state file in relative paths - Restrict config dir permissions from 0o755 to 0o750 - Limit download size with io.LimitReader (50MB cap) - Replace unused parameter `r` with `_` in test handler - Refactor replaceBinary into testable replaceBinaryAt function - Rewrite tests to call actual functions instead of reimplementing logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix remaining DeepSource issues: test logic and coverage gaps - Rewrite TestCheckForUpdate_AlreadyUpToDate to call CheckForUpdate with a mock HTTP server instead of reimplementing logic - Add tests for FetchManifest, downloadFile, and error paths (HTTP errors, network errors, checksum mismatch, missing platform, nil build info, zip archive path) - Add parseSemver test cases for invalid minor/patch and pre-release - Add ClearBuildInfo helper for test setup - Update package coverage from 47.5% to ~80% Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io> Co-authored-by: Sourya Vatsyayan <sourya@deepsource.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 51594ab commit e2a7abb

File tree

9 files changed

+1206
-5
lines changed

9 files changed

+1206
-5
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.43
1+
2.0.45

buildinfo/version.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ var buildInfo *BuildInfo
99

1010
// App identity variables. Defaults are prod values; overridden in main.go for dev builds.
1111
var (
12-
AppName = "deepsource" // binary name / display name
13-
ConfigDirName = ".deepsource" // ~/<this>/
12+
AppName = "deepsource" // binary name / display name
13+
ConfigDirName = ".deepsource" // ~/<this>/
14+
BaseURL = "https://cli.deepsource.com" // CDN base for manifest and archives
1415
)
1516

1617
// BuildInfo describes the compile time information.
@@ -37,6 +38,10 @@ func GetBuildInfo() *BuildInfo {
3738
return buildInfo
3839
}
3940

41+
func ClearBuildInfo() {
42+
buildInfo = nil
43+
}
44+
4045
func (bi BuildInfo) String() string {
4146
s := fmt.Sprintf("Version: v%s", bi.Version)
4247
if bi.BuildMode == "dev" && !bi.Date.IsZero() {

cmd/deepsource/main.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import (
44
"errors"
55
"fmt"
66
"log"
7+
"net/http"
78
"os"
89
"strings"
910
"time"
1011

1112
v "github.com/deepsourcelabs/cli/buildinfo"
1213
"github.com/deepsourcelabs/cli/command"
1314
"github.com/deepsourcelabs/cli/internal/cli/style"
15+
"github.com/deepsourcelabs/cli/internal/debug"
1416
clierrors "github.com/deepsourcelabs/cli/internal/errors"
17+
"github.com/deepsourcelabs/cli/internal/update"
1518
"github.com/getsentry/sentry-go"
1619
)
1720

@@ -40,6 +43,7 @@ func mainRun() (exitCode int) {
4043
if buildMode == "dev" {
4144
v.AppName = "deepsource-dev"
4245
v.ConfigDirName = ".deepsource-dev"
46+
v.BaseURL = "https://cli.deepsource.one"
4347
}
4448

4549
// Init sentry
@@ -67,6 +71,32 @@ func mainRun() (exitCode int) {
6771
func run() int {
6872
v.SetBuildInfo(version, Date, buildMode)
6973

74+
// Two-phase auto-update: apply pending update or check for new one
75+
if update.ShouldAutoUpdate() {
76+
state, err := update.ReadUpdateState()
77+
if err != nil {
78+
debug.Log("update: %v", err)
79+
}
80+
81+
if state != nil {
82+
// Phase 2: a previous run found a newer version — apply it now
83+
client := &http.Client{Timeout: 30 * time.Second}
84+
newVer, err := update.ApplyUpdate(client)
85+
if err != nil {
86+
debug.Log("update: %v", err)
87+
} else if newVer != "" {
88+
fmt.Fprintf(os.Stderr, "%s\n", style.Yellow("Updated DeepSource CLI to v%s", newVer))
89+
}
90+
} else {
91+
// Phase 1: check manifest and write state file for next run
92+
client := &http.Client{Timeout: 3 * time.Second}
93+
if err := update.CheckForUpdate(client); err != nil {
94+
debug.Log("update: %v", err)
95+
}
96+
}
97+
}
98+
99+
exitCode := 0
70100
if err := command.Execute(); err != nil {
71101
var cliErr *clierrors.CLIError
72102
if errors.As(err, &cliErr) {
@@ -78,7 +108,8 @@ func run() int {
78108
sentry.CaptureException(err)
79109
}
80110
sentry.Flush(2 * time.Second)
81-
return 1
111+
exitCode = 1
82112
}
83-
return 0
113+
114+
return exitCode
84115
}

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type CLIConfig struct {
1616
User string `toml:"user"`
1717
Token string `toml:"token"`
1818
TokenExpiresIn time.Time `toml:"token_expires_in,omitempty"`
19+
AutoUpdate *bool `toml:"auto_update,omitempty"`
1920
SkipTLSVerify bool `toml:"skip_tls_verify,omitempty"`
2021
TokenFromEnv bool `toml:"-"`
2122
}

internal/update/manifest.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package update
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"runtime"
9+
10+
"github.com/deepsourcelabs/cli/buildinfo"
11+
)
12+
13+
// Manifest represents the CLI release manifest served by the CDN.
14+
type Manifest struct {
15+
Version string `json:"version"`
16+
BuildTime string `json:"buildTime"`
17+
Platforms map[string]PlatformInfo `json:"platforms"`
18+
}
19+
20+
// PlatformInfo holds the archive filename and checksum for a platform.
21+
type PlatformInfo struct {
22+
Archive string `json:"archive"`
23+
SHA256 string `json:"sha256"`
24+
}
25+
26+
// FetchManifest downloads and parses the release manifest.
27+
func FetchManifest(client *http.Client) (*Manifest, error) {
28+
resp, err := client.Get(buildinfo.BaseURL + "/manifest.json")
29+
if err != nil {
30+
return nil, fmt.Errorf("fetching manifest: %w", err)
31+
}
32+
defer resp.Body.Close()
33+
34+
if resp.StatusCode != http.StatusOK {
35+
return nil, fmt.Errorf("manifest returned HTTP %d", resp.StatusCode)
36+
}
37+
38+
body, err := io.ReadAll(resp.Body)
39+
if err != nil {
40+
return nil, fmt.Errorf("reading manifest body: %w", err)
41+
}
42+
43+
var m Manifest
44+
if err := json.Unmarshal(body, &m); err != nil {
45+
return nil, fmt.Errorf("parsing manifest JSON: %w", err)
46+
}
47+
return &m, nil
48+
}
49+
50+
// PlatformKey returns the manifest key for the current OS/arch.
51+
func PlatformKey() string {
52+
return runtime.GOOS + "_" + runtime.GOARCH
53+
}

0 commit comments

Comments
 (0)