Skip to content

Add download progress for lets self upgrade#381

Merged
kindermax merged 1 commit into
masterfrom
progress-bar-for-self-updates
Jul 8, 2026
Merged

Add download progress for lets self upgrade#381
kindermax merged 1 commit into
masterfrom
progress-bar-for-self-updates

Conversation

@kindermax

@kindermax kindermax commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Implements #367

Wire a fetch.ProgressObserver through the registry download so the tar.gz stream reports byte progress while extracting, and disable HTTP compression so Content-Length stays usable for the bar.

The upgrade command builds its own observer from cobra's stderr stream (cmd.ErrOrStderr) and the already-loaded settings value, rather than threading the observer down from the composition root. A fetch.NopObserver replaces nil as the no-progress default, removing nil checks across the download path.

Unify terminal detection behind util.IsTerminalWriter (charmbracelet/x/term), used by cli, the upgrade command, and downloadprogress. This drops the direct mattn/go-isatty dependency; Cygwin/MSYS terminals are no longer treated as interactive, matching what logging and error rendering already did.

Summary by Sourcery

Add interactive download progress reporting to the self-upgrade flow and centralize terminal detection for progress rendering.

New Features:

  • Report byte-level download progress for self-upgrade binary downloads using the shared fetch progress observer.
  • Expose a configurable progress observer in the binary upgrader, wired from the upgrade command and user settings.

Enhancements:

  • Disable HTTP compression for GitHub registry downloads so Content-Length can be used for accurate progress reporting.
  • Unify terminal detection behind util.IsTerminalWriter and reuse it across CLI, upgrade, and download progress rendering.
  • Initialize a no-op progress observer by default to avoid nil checks in download paths.

Documentation:

  • Document the new self-upgrade download progress and unified terminal detection behavior in the changelog.

Tests:

  • Add coverage for GitHub registry downloads to verify progress events and correct extraction of the upgraded binary.
  • Update self and CLI command tests to pass settings into self-related commands and accommodate the new upgrader factory signature.

@sourcery-ai

sourcery-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds progress reporting to the self-upgrade binary download, introduces a no-op progress observer, wires progress through the registry and upgrader layers, and unifies terminal detection via a shared utility while updating commands/tests and dependencies accordingly.

Sequence diagram for self-upgrade download progress

sequenceDiagram
    actor User
    participant Cobra as CobraCommand_upgrade
    participant UpCmd as upgradeProgress
    participant Upgrader as BinaryUpgrader
    participant Reg as GithubRegistry
    participant Obs as fetch.ProgressObserver
    participant PRC as progressReadCloser
    participant Ext as extract.Gz

    User->>Cobra: run "lets self upgrade"
    Cobra->>Cobra: initUpgradeCommand(version, appSettings)
    Cobra->>UpCmd: upgradeProgress(cmd.ErrOrStderr, appSettings)
    UpCmd->>UpCmd: util.IsTerminalWriter(stderr)
    alt terminal
        UpCmd-->>Cobra: downloadprogress.New(...)
    else non_terminal
        UpCmd-->>Cobra: fetch.NopObserver{}
    end
    Cobra->>Upgrader: NewBinaryUpgrader(registry.NewGithubRegistry(), version, WithProgress(progress))
    User->>Cobra: execute upgrade command
    Cobra->>Upgrader: Upgrade(ctx)
    Upgrader->>Reg: DownloadReleaseBinary(ctx, packageName, latestVersion, downloadPath, up.progress)
    Reg->>Obs: Start(fetch.ProgressInfo{SourceSelfUpdate, URL, ContentLength})
    Obs-->>Reg: fetch.ProgressTracker
    Reg->>PRC: progressReadCloser{ReadCloser: resp.Body, tracker: tracker}
    Reg->>Ext: extract.Gz(ctx, PRC, dstDir, nil)
    loop while reading
        Ext->>PRC: Read(p)
        PRC->>PRC: ReadCloser.Read(p)
        PRC->>Obs: tracker.Add(int64(n))
    end
    Reg->>Obs: tracker.Done(err)
    Reg-->>Upgrader: error or nil
    Upgrader-->>Cobra: error or nil
    Cobra-->>User: upgrade result
Loading

File-Level Changes

Change Details Files
Add byte-level download progress reporting to GitHub registry self-update downloads and verify via tests.
  • Extend RepoRegistry.DownloadReleaseBinary to accept a fetch.ProgressObserver parameter.
  • Wrap the HTTP response body in a progressReadCloser that forwards read byte counts to a ProgressTracker and calls Done after extraction.
  • Disable HTTP compression on the GitHubRegistry HTTP client transport so Content-Length reflects the uncompressed tarball size.
  • Add TestGithubRegistryDownloadReleaseBinaryReportsProgress with a gzip+tar test archive, recordingProgress helper, and releaseArchive builder to assert correct progress info and successful extraction.
internal/upgrade/registry/registry.go
internal/upgrade/registry/registry_test.go
Propagate progress observation through the upgrader and CLI upgrade command using a configurable observer with sensible defaults.
  • Extend BinaryUpgrader to hold a fetch.ProgressObserver, defaulting to fetch.NopObserver, and accept functional options including WithProgress.
  • Update BinaryUpgrader.Upgrade to pass the observer through to RepoRegistry.DownloadReleaseBinary.
  • Change upgraderFactory to accept *cobra.Command so initUpgradeCommand can build a ProgressObserver from cmd.ErrOrStderr and settings.
  • Add upgradeProgress helper that chooses downloadprogress.New when stderr is an interactive terminal and falls back to fetch.NopObserver otherwise, and wire it into initUpgradeCommand and InitSelfCmd.
  • Adjust tests and mocks to the new BinaryUpgrader and RepoRegistry.DownloadReleaseBinary signatures and factory shape.
internal/upgrade/upgrade.go
internal/upgrade/upgrade_test.go
internal/upgrade/notifier_test.go
internal/cmd/upgrade.go
internal/cmd/self.go
internal/cmd/root_test.go
internal/cmd/self_skills_test.go
internal/cli/cli_test.go
Introduce a no-op progress implementation and a dedicated "self update" source kind for progress metadata.
  • Add SourceSelfUpdate to fetch.SourceKind and use it when starting progress for self-upgrade downloads.
  • Implement fetch.NopObserver and fetch.NopTracker as no-op implementations of ProgressObserver and ProgressTracker to replace nil usage and avoid nil checks.
internal/fetch/fetch.go
internal/upgrade/registry/registry.go
internal/upgrade/registry/registry_test.go
Unify terminal detection logic across the CLI and download progress code behind a shared utility.
  • Add util.IsTerminalWriter, using charmbracelet/x/term to detect whether an io.Writer is attached to a terminal.
  • Refactor isInteractiveStderr and downloadprogress.isTerminal to delegate to util.IsTerminalWriter instead of using package-local logic or mattn/go-isatty.
  • Use util.IsTerminalWriter in upgradeProgress to decide whether to show the progress UI.
internal/util/terminal.go
internal/cli/cli.go
internal/downloadprogress/progress.go
internal/cmd/upgrade.go
Update wiring and dependencies to reflect new terminal util and progress behavior.
  • Pass settings.Settings into InitSelfCmd and initUpgradeCommand from CLI setup and tests so upgradeProgress can honor color and theme settings.
  • Adjust go.mod to promote bubbletea as a direct dependency and move mattn/go-isatty to indirect while dropping its direct usage.
  • Document the new self-upgrade progress behavior and unified terminal detection in the changelog.
internal/cli/cli.go
internal/cli/cli_test.go
internal/cmd/help_golden_test.go
internal/cmd/root_test.go
internal/cmd/self_skills_test.go
internal/cmd/self.go
docs/docs/changelog.md
go.mod

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/upgrade/registry/registry.go" line_range="52-53" />
<code_context>
 }

 func NewGithubRegistry() *GithubRegistry {
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.DisableCompression = true
+
 	client := &http.Client{
</code_context>
<issue_to_address>
**issue (bug_risk):** The direct type assertion on http.DefaultTransport can panic; consider a safer assertion or constructing a new transport explicitly.

This relies on `http.DefaultTransport` always being a `*http.Transport`, which may not hold if it’s overridden (e.g. in tests or by other code), causing a panic. Prefer a safe type assertion with a fallback, e.g.:

```go
base, ok := http.DefaultTransport.(*http.Transport)
if !ok {
    base = &http.Transport{}
}
transport := base.Clone()
transport.DisableCompression = true
```

or construct a new `http.Transport` directly and configure it as needed.
</issue_to_address>

### Comment 2
<location path="internal/upgrade/upgrade.go" line_range="34-36" />
<code_context>
-func NewBinaryUpgrader(reg registry.RepoRegistry, currentVersion string) (*BinaryUpgrader, error) {
+type BinaryUpgraderOption func(*BinaryUpgrader)
+
+func WithProgress(progress fetch.ProgressObserver) BinaryUpgraderOption {
+	return func(upgrader *BinaryUpgrader) {
+		upgrader.progress = progress
+	}
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Defensively handle a nil ProgressObserver in WithProgress to avoid potential nil dereferences.

`BinaryUpgrader` starts with a non-nil `NopObserver`, but `WithProgress` can overwrite it with `nil`, leading to a panic when `up.progress.Start(...)` is called. Consider normalizing `nil` to a no-op observer:

```go
func WithProgress(progress fetch.ProgressObserver) BinaryUpgraderOption {
    return func(upgrader *BinaryUpgrader) {
        if progress == nil {
            upgrader.progress = fetch.NopObserver{}
            return
        }
        upgrader.progress = progress
    }
}
```
</issue_to_address>

### Comment 3
<location path="internal/upgrade/registry/registry_test.go" line_range="85-94" />
<code_context>
+func TestGithubRegistryDownloadReleaseBinaryReportsProgress(t *testing.T) {
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen progress assertions by checking total bytes added matches Content-Length

The test currently only checks that `len(progress.adds) > 0`, which would still pass if bytes were under‑ or over‑reported. To better validate the progress wiring, assert that `sum(progress.adds)` equals `int64(len(archive))` so changes to `progressReadCloser` that mis-account bytes are caught.

Suggested implementation:

```golang
	var totalAdded int64
	for _, n := range progress.adds {
		totalAdded += n
	}

	if totalAdded != int64(len(archive)) {
		t.Fatalf("expected total progress bytes %d, got %d", len(archive), totalAdded)
	}

```

1. Ensure this assertion block appears after the code that triggers the download and populates `progress.adds` in `TestGithubRegistryDownloadReleaseBinaryReportsProgress`.
2. If the existing assertion message or condition differs slightly (e.g. different wording or using `require.NotEmpty`), adjust the SEARCH block accordingly to exactly match the current assertion and replace it with the summing logic above.
3. If `progress.adds` is not of type `[]int64`, adapt the `totalAdded` type and the summation loop to match the actual element type, converting to `int64` for the comparison if needed.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +52 to +53
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DisableCompression = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The direct type assertion on http.DefaultTransport can panic; consider a safer assertion or constructing a new transport explicitly.

This relies on http.DefaultTransport always being a *http.Transport, which may not hold if it’s overridden (e.g. in tests or by other code), causing a panic. Prefer a safe type assertion with a fallback, e.g.:

base, ok := http.DefaultTransport.(*http.Transport)
if !ok {
    base = &http.Transport{}
}
transport := base.Clone()
transport.DisableCompression = true

or construct a new http.Transport directly and configure it as needed.

Comment on lines +34 to +36
func WithProgress(progress fetch.ProgressObserver) BinaryUpgraderOption {
return func(upgrader *BinaryUpgrader) {
upgrader.progress = progress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Defensively handle a nil ProgressObserver in WithProgress to avoid potential nil dereferences.

BinaryUpgrader starts with a non-nil NopObserver, but WithProgress can overwrite it with nil, leading to a panic when up.progress.Start(...) is called. Consider normalizing nil to a no-op observer:

func WithProgress(progress fetch.ProgressObserver) BinaryUpgraderOption {
    return func(upgrader *BinaryUpgrader) {
        if progress == nil {
            upgrader.progress = fetch.NopObserver{}
            return
        }
        upgrader.progress = progress
    }
}

Comment on lines +85 to +94
func TestGithubRegistryDownloadReleaseBinaryReportsProgress(t *testing.T) {
archive := releaseArchive(t, "updated binary")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Path; got != "/releases/download/v0.0.2/lets_Test_x86_64.tar.gz" {
t.Fatalf("unexpected path %q", got)
}

w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Length", strconv.Itoa(len(archive)))
_, _ = w.Write(archive)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen progress assertions by checking total bytes added matches Content-Length

The test currently only checks that len(progress.adds) > 0, which would still pass if bytes were under‑ or over‑reported. To better validate the progress wiring, assert that sum(progress.adds) equals int64(len(archive)) so changes to progressReadCloser that mis-account bytes are caught.

Suggested implementation:

	var totalAdded int64
	for _, n := range progress.adds {
		totalAdded += n
	}

	if totalAdded != int64(len(archive)) {
		t.Fatalf("expected total progress bytes %d, got %d", len(archive), totalAdded)
	}
  1. Ensure this assertion block appears after the code that triggers the download and populates progress.adds in TestGithubRegistryDownloadReleaseBinaryReportsProgress.
  2. If the existing assertion message or condition differs slightly (e.g. different wording or using require.NotEmpty), adjust the SEARCH block accordingly to exactly match the current assertion and replace it with the summing logic above.
  3. If progress.adds is not of type []int64, adapt the totalAdded type and the summation loop to match the actual element type, converting to int64 for the comparison if needed.

Wire a fetch.ProgressObserver through the registry download so the
tar.gz stream reports byte progress while extracting, and disable
HTTP compression so Content-Length stays usable for the bar.

The upgrade command builds its own observer from cobra's stderr stream
(cmd.ErrOrStderr) and the already-loaded settings value, rather than
threading the observer down from the composition root. A fetch.NopObserver
replaces nil as the no-progress default, removing nil checks across the
download path.

Unify terminal detection behind util.IsTerminalWriter (charmbracelet/x/term),
used by cli, the upgrade command, and downloadprogress. This drops the
direct mattn/go-isatty dependency; Cygwin/MSYS terminals are no longer
treated as interactive, matching what logging and error rendering already did.
@kindermax kindermax force-pushed the progress-bar-for-self-updates branch from 1a59f3e to 7fd90c2 Compare July 8, 2026 18:54
@kindermax kindermax merged commit 40bf1ba into master Jul 8, 2026
5 checks passed
@kindermax kindermax deleted the progress-bar-for-self-updates branch July 8, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant