Skip to content

feat: updater + version display - #9

Merged
viveknathani merged 3 commits into
masterfrom
feat/updater
Mar 17, 2026
Merged

feat: updater + version display#9
viveknathani merged 3 commits into
masterfrom
feat/updater

Conversation

@viveknathani

@viveknathani viveknathani commented Mar 17, 2026

Copy link
Copy Markdown
Owner

closes #6, closes #7

@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds an auto-update workflow and version reporting to the dbtree CLI: a new updater package queries GitHub releases, downloads platform-specific archives (tar.gz or zip), extracts the dbtree binary, and atomically replaces the running executable. New version and update subcommands are wired into the CLI.

Changes

Cohort / File(s) Summary
CLI Subcommand Integration
cmd/dbtree/main.go
Introduces version and update subcommands, defines version/commit/date metadata, imports updater, and performs early branching for these commands before normal config parsing.
Auto-Update Implementation
updater/updater.go
New package implementing Update(currentVersion string) error. Queries GitHub releases API, compares versions, selects platform-specific archive (tar.gz for Unix-like, zip for Windows), downloads and extracts the dbtree binary, and atomically replaces the running executable with Windows and cross-device handling. Includes helpers for HTTP fetch, archive extraction, and safe file replacement.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as dbtree CLI
    participant GitHub as GitHub API
    participant FS as File System

    User->>CLI: dbtree update
    activate CLI
    CLI->>GitHub: GET /repos/.../releases/latest
    activate GitHub
    GitHub-->>CLI: {tag_name: "vX.Y.Z", ...}
    deactivate GitHub

    Note over CLI: Compare currentVersion vs latestVersion

    alt Newer version available
        CLI->>GitHub: GET /archive/.../dbtree_<ver>_<os>_<arch>.(tar.gz|zip)
        activate GitHub
        GitHub-->>CLI: Archive bytes
        deactivate GitHub

        CLI->>FS: Save archive to temp dir
        FS-->>CLI: Archive saved

        CLI->>FS: Extract archive → locate `dbtree` binary
        FS-->>CLI: Extracted binary path

        CLI->>FS: Atomically replace running executable (win/unix flows)
        FS-->>CLI: Replacement complete

        CLI->>User: Update successful
    else Already latest
        CLI->>User: Already up-to-date
    end
    deactivate CLI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the releases, a shiny new treat,
Pulled tar.gz or zip — a bunny-run feat.
Extracted the binary, swapped it in place,
dbtree now hops forward with nimble grace. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: updater + version display' clearly and concisely summarizes the main changes: adding update and version functionality to the CLI.
Description check ✅ Passed The description 'closes #6, closes #7' directly relates to the changeset by referencing the two linked issues that the PR addresses.
Linked Issues check ✅ Passed The PR implements both linked issue requirements: Issue #6 (dbtree update command) and Issue #7 (dbtree version command) with full auto-update workflow and version display functionality.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the update command and version display as specified in issues #6 and #7; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/updater
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
updater/updater.go (3)

76-97: HTTP requests lack timeout and User-Agent header.

http.Get uses the default client with no timeout, which could hang indefinitely. Additionally, GitHub API may rate-limit requests without a User-Agent header more aggressively.

♻️ Proposed fix with timeout and User-Agent
 func getLatestVersion() (string, error) {
-	resp, err := http.Get("https://api.github.com/repos/viveknathani/dbtree/releases/latest")
+	client := &http.Client{Timeout: 30 * time.Second}
+	req, err := http.NewRequest("GET", "https://api.github.com/repos/viveknathani/dbtree/releases/latest", nil)
+	if err != nil {
+		return "", err
+	}
+	req.Header.Set("User-Agent", "dbtree-updater")
+	resp, err := client.Do(req)
 	if err != nil {
 		return "", err
 	}

Don't forget to add "time" to imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 76 - 97, The getLatestVersion function uses
http.Get with no timeout or headers; replace it by creating an http.Client with
a reasonable Timeout (e.g., a few seconds), build the request via
http.NewRequest("GET", "...", nil), set a User-Agent header (e.g.,
"dbtree-updater" or similar) on the request, and call client.Do(req) instead of
http.Get; keep existing resp.Body.Close and JSON decoding/error checks, and add
the "time" import for the timeout configuration.

29-32: Version comparison uses string equality — may fail for semantic versions.

String comparison works for exact matches but doesn't handle semantic versioning edge cases (e.g., 1.10.0 vs 1.9.0 would incorrectly compare as strings). Consider using a semver library like golang.org/x/mod/semver or github.com/Masterminds/semver for robust comparison.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 29 - 32, The code currently compares
versions using string equality between currentVersion and latestClean
(strings.TrimPrefix(latest, "v")), which fails for semantic-version ordering;
replace the string equality check with a semantic-version comparison using a
semver library (e.g., import "golang.org/x/mod/semver" or
"github.com/Masterminds/semver"), validate/normalize both currentVersion and
latestClean (add leading "v" if required by the chosen lib), and use the
library's Compare/Equal functions to determine if currentVersion == latestClean
or if an update is needed before printing "Already up to date" in the updater
logic.

99-118: Same timeout concern applies to download requests.

Large file downloads without timeout could hang indefinitely. Consider using the same client pattern with timeout as suggested for getLatestVersion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 99 - 118, The downloadFile function uses
http.Get which can hang for large downloads; replace it with an http.Client
configured with a timeout (the same pattern used in getLatestVersion) and use
client.Do on a GET request (or accept a context) so the request is bounded;
update downloadFile to create an http.Client{Timeout: <appropriate duration>},
build an http.NewRequest("GET", url, nil), call client.Do(req), handle
resp.StatusCode as before, and ensure resp.Body and file are properly closed and
errors propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@updater/updater.go`:
- Line 40: The archive name currently always uses ".tar.gz" (archiveName :=
fmt.Sprintf("dbtree_%s_%s_%s.tar.gz", ...)), which breaks Windows updates;
change the extension to ".zip" when runtime.GOOS == "windows" and build
archiveName accordingly, add a new extractBinaryFromZip(archivePath, destDir
string) (returning the extracted binary path and error) that opens the zip,
finds "dbtree.exe", copies it with executable perms into destDir, and then,
where extraction is performed (the existing tar.gz extraction call/site),
conditionally call extractBinaryFromZip for Windows and the existing tar.gz
extractor for other platforms so Windows downloads and extraction succeed.
- Line 143: The check for the dbtree binary only matches "dbtree" and misses
Windows "dbtree.exe"; update the condition that uses header and
filepath.Base(header.Name) so it normalizes the filename by trimming a possible
".exe" suffix before comparing to "dbtree" (e.g. name :=
filepath.Base(header.Name); if header.Typeflag == tar.TypeReg &&
strings.TrimSuffix(name, ".exe") == "dbtree" { ... }), and add the required
import for the strings package.
- Around line 161-195: replaceBinary currently fails on Windows because
executables are locked; implement a Windows-specific two-process swap or use a
library that does (e.g., inconshreveable/go-update) to perform a safe replace
while the app is running. Update the replaceBinary flow: detect runtime.GOOS ==
"windows" in replaceBinary, spawn a short-lived helper process (or call
MoveFileEx via syscall with MOVEFILE_REPLACE_EXISTING after the main process
exits) that waits for the parent to exit, then atomically moves the new binary
into place and removes the old one; ensure the helper returns exit codes on
failure and that file permissions are preserved similarly to the non-Windows
path. Also add a brief comment in replaceBinary explaining the Windows lock
behavior and the chosen approach.

---

Nitpick comments:
In `@updater/updater.go`:
- Around line 76-97: The getLatestVersion function uses http.Get with no timeout
or headers; replace it by creating an http.Client with a reasonable Timeout
(e.g., a few seconds), build the request via http.NewRequest("GET", "...", nil),
set a User-Agent header (e.g., "dbtree-updater" or similar) on the request, and
call client.Do(req) instead of http.Get; keep existing resp.Body.Close and JSON
decoding/error checks, and add the "time" import for the timeout configuration.
- Around line 29-32: The code currently compares versions using string equality
between currentVersion and latestClean (strings.TrimPrefix(latest, "v")), which
fails for semantic-version ordering; replace the string equality check with a
semantic-version comparison using a semver library (e.g., import
"golang.org/x/mod/semver" or "github.com/Masterminds/semver"),
validate/normalize both currentVersion and latestClean (add leading "v" if
required by the chosen lib), and use the library's Compare/Equal functions to
determine if currentVersion == latestClean or if an update is needed before
printing "Already up to date" in the updater logic.
- Around line 99-118: The downloadFile function uses http.Get which can hang for
large downloads; replace it with an http.Client configured with a timeout (the
same pattern used in getLatestVersion) and use client.Do on a GET request (or
accept a context) so the request is bounded; update downloadFile to create an
http.Client{Timeout: <appropriate duration>}, build an http.NewRequest("GET",
url, nil), call client.Do(req), handle resp.StatusCode as before, and ensure
resp.Body and file are properly closed and errors propagated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9279926c-f7b2-480a-b26f-35a402077a35

📥 Commits

Reviewing files that changed from the base of the PR and between c158e4d and 4c18c9c.

📒 Files selected for processing (2)
  • cmd/dbtree/main.go
  • updater/updater.go

Comment thread updater/updater.go Outdated
Comment thread updater/updater.go Outdated
Comment thread updater/updater.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
updater/updater.go (1)

217-228: ⚠️ Potential issue | 🟠 Major

Windows in-process binary swap can still fail on a running .exe.

Line 220 renames the currently running executable in-process. On Windows, this path is brittle and can fail with file-lock/access errors. Prefer a helper-process swap (main exits, helper replaces, then relaunches), or a proven self-update library flow.

In Go on Windows, can a running executable reliably be renamed/replaced by the same process, and what self-update pattern is recommended?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 217 - 228, The Windows branch in updater.go
currently attempts an in-process os.Rename of the running executable (using
target + ".old" and moveFile), which can fail due to Windows file locks; replace
this with a helper-process swap flow: write the new binary to a temporary path,
spawn a short-lived updater helper (or use a proven self-update library) passing
the current target, temp-path and old-path, then have the main process exit
immediately so the helper can wait for the process to terminate, rename/move
files atomically (rename target→old, temp→target) and finally relaunch the app;
update the code paths around the existing moveFile call and the Windows-specific
block in updater.go to invoke the helper (or library API) and handle its
error/exit codes instead of attempting an in-process os.Rename.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@updater/updater.go`:
- Around line 54-57: After downloadFile writes archivePath (using tmpDir,
archiveName, downloadURL) you must verify the artifact integrity before any
extraction/replacement: compute the SHA-256 (or configured hash) of archivePath
and compare it to the expected checksum retrieved from the signed checksums
asset (download or load the checksums file and verify its signature) and fail
fast if they differ; add a helper like verifyArchiveChecksum(archivePath,
expectedChecksum) and a routine verifyChecksumsSignature(checksumsBlob,
signature) to validate the signed checksums, and invoke these checks immediately
after the downloadFile call and before any extraction/replacement logic,
returning an error if verification fails.
- Around line 87-117: The two http.Get calls (the one fetching the GitHub latest
release and the one in downloadFile) use the default client without timeouts;
replace them with an http.Client that has a sensible Timeout (e.g., 10-30s) and
use client.Get instead, or create a package-level client with a configured
Timeout and reuse it in both the release-checking function and downloadFile to
prevent indefinite hangs; ensure you close resp.Body as before and propagate
errors the same way.

---

Duplicate comments:
In `@updater/updater.go`:
- Around line 217-228: The Windows branch in updater.go currently attempts an
in-process os.Rename of the running executable (using target + ".old" and
moveFile), which can fail due to Windows file locks; replace this with a
helper-process swap flow: write the new binary to a temporary path, spawn a
short-lived updater helper (or use a proven self-update library) passing the
current target, temp-path and old-path, then have the main process exit
immediately so the helper can wait for the process to terminate, rename/move
files atomically (rename target→old, temp→target) and finally relaunch the app;
update the code paths around the existing moveFile call and the Windows-specific
block in updater.go to invoke the helper (or library API) and handle its
error/exit codes instead of attempting an in-process os.Rename.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84275d38-2c50-42a9-a51b-14c226268ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 4c18c9c and acbdb67.

📒 Files selected for processing (1)
  • updater/updater.go

Comment thread updater/updater.go
Comment thread updater/updater.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
updater/updater.go (2)

33-37: Version comparison doesn't handle semantic versioning or edge cases.

The simple string equality comparison works for normal version checks, but has edge cases:

  1. Pre-release or development builds (e.g., 1.1.0-dev) will always be considered different from release versions
  2. If a user has a newer local build than the latest release, it would attempt a "downgrade"

Consider using semantic version comparison (e.g., github.com/Masterminds/semver) or at minimum document that dev builds should not use the updater.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 33 - 37, The current comparison using
latestClean and currentVersion only checks string equality and fails for semver
edge cases (pre-releases, build metadata, newer local builds); update the logic
in updater.go to parse and compare semantic versions instead of string equality:
use a semver library (e.g., github.com/Masterminds/semver) to parse
currentVersion and latest (after TrimPrefix) into Version objects, handle parse
errors (fall back to logging and skipping automatic update), and compare with
Version.Compare/Compare to detect older/newer/pre-release cases so you avoid
downgrades and correctly treat dev builds; include a clear log message when
versions are incomparable and document that dev builds are not supported by the
updater.

123-130: Consider explicit sync before close for robustness.

While typically fine, adding f.Sync() before close ensures the downloaded content is flushed to disk, making the operation more robust against unexpected system interruptions during large downloads.

♻️ Proposed fix
 	_, err = io.Copy(f, resp.Body)
+	if err != nil {
+		return err
+	}
+	if err := f.Sync(); err != nil {
+		return err
+	}
 	return err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@updater/updater.go` around lines 123 - 130, The file currently creates a file
with os.Create (variable f), copies resp.Body into it with io.Copy, and defers
f.Close() without flushing; modify the write sequence in the function containing
this snippet so that after io.Copy returns you call f.Sync() and check its error
(return it if non-nil) before closing the file, and keep the deferred f.Close()
for cleanup; reference the os.Create / f variable, the io.Copy call, and the
deferred f.Close() when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@updater/updater.go`:
- Around line 33-37: The current comparison using latestClean and currentVersion
only checks string equality and fails for semver edge cases (pre-releases, build
metadata, newer local builds); update the logic in updater.go to parse and
compare semantic versions instead of string equality: use a semver library
(e.g., github.com/Masterminds/semver) to parse currentVersion and latest (after
TrimPrefix) into Version objects, handle parse errors (fall back to logging and
skipping automatic update), and compare with Version.Compare/Compare to detect
older/newer/pre-release cases so you avoid downgrades and correctly treat dev
builds; include a clear log message when versions are incomparable and document
that dev builds are not supported by the updater.
- Around line 123-130: The file currently creates a file with os.Create
(variable f), copies resp.Body into it with io.Copy, and defers f.Close()
without flushing; modify the write sequence in the function containing this
snippet so that after io.Copy returns you call f.Sync() and check its error
(return it if non-nil) before closing the file, and keep the deferred f.Close()
for cleanup; reference the os.Create / f variable, the io.Copy call, and the
deferred f.Close() when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e3cf749-b20e-4bdf-9e79-0056482e96af

📥 Commits

Reviewing files that changed from the base of the PR and between acbdb67 and 4e8cf7c.

📒 Files selected for processing (1)
  • updater/updater.go

@viveknathani
viveknathani merged commit 3daa642 into master Mar 17, 2026
2 checks passed
@viveknathani
viveknathani deleted the feat/updater branch March 17, 2026 14:23
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.

Display version number Need an easy way to update the CLI

1 participant